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.
@@ -1,422 +1,468 @@
1
- import enum
2
- import json
3
- import logging
4
- import mimetypes
5
- import pathlib
6
- import pathspec # type: ignore
7
-
8
- from collections import OrderedDict
9
- from typing import List, Dict, Optional, Tuple, Any
10
-
11
- from dsw_tdk.consts import VERSION, DEFAULT_ENCODING, METAMODEL_VERSION, PATHSPEC_FACTORY
12
-
13
- mimetypes.init()
14
-
15
-
16
- class TemplateFileType(enum.Enum):
17
- asset = 'asset'
18
- file = 'file'
19
-
20
-
21
- class PackageFilter:
22
-
23
- def __init__(self, *, organization_id=None, km_id=None, min_version=None,
24
- max_version=None):
25
- self.organization_id = organization_id # type: Optional[str]
26
- self.km_id = km_id # type: Optional[str]
27
- self.min_version = min_version # type: Optional[str]
28
- self.max_version = max_version # type: Optional[str]
29
-
30
- @classmethod
31
- def load(cls, data):
32
- return PackageFilter(
33
- organization_id=data.get('orgId', None),
34
- km_id=data.get('kmId', None),
35
- min_version=data.get('minVersion', None),
36
- max_version=data.get('maxVersion', None),
37
- )
38
-
39
- def serialize(self):
40
- return {
41
- 'orgId': self.organization_id,
42
- 'kmId': self.km_id,
43
- 'minVersion': self.min_version,
44
- 'maxVersion': self.max_version,
45
- }
46
-
47
-
48
- class Step:
49
-
50
- def __init__(self, *, name=None, options=None):
51
- self.name = name # type: str
52
- self.options = options or dict() # type: Dict[str, str]
53
-
54
- @classmethod
55
- def load(cls, data):
56
- return Step(
57
- name=data.get('name', None),
58
- options=data.get('options', None),
59
- )
60
-
61
- def serialize(self):
62
- return {
63
- 'name': self.name,
64
- 'options': self.options,
65
- }
66
-
67
-
68
- class Format:
69
-
70
- DEFAULT_ICON = 'fas fa-file'
71
- DEFAULT_COLOR = '#f15722'
72
-
73
- def __init__(self, *, uuid=None, name=None, short_name=None, icon=None, color=None):
74
- self.uuid = uuid # type: str
75
- self.name = name # type: str
76
- self.short_name = short_name or name # type: str
77
- self.icon = icon or self.DEFAULT_ICON # type: str
78
- self.color = color or self.DEFAULT_COLOR # type: str
79
- self.steps = [] # type: List[Step]
80
-
81
- @classmethod
82
- def load(cls, data):
83
- format_spec = Format(
84
- uuid=data.get('uuid', None),
85
- name=data.get('name', None),
86
- short_name=data.get('shortName', None),
87
- icon=data.get('icon', None),
88
- color=data.get('color', None),
89
- )
90
- for s_data in data.get('steps', []):
91
- format_spec.steps.append(Step.load(s_data))
92
- return format_spec
93
-
94
- def serialize(self):
95
- return {
96
- 'uuid': self.uuid,
97
- 'name': self.name,
98
- 'shortName': self.short_name,
99
- 'icon': self.icon,
100
- 'color': self.color,
101
- 'steps': [step.serialize() for step in self.steps]
102
- }
103
-
104
-
105
- class TDKConfig:
106
-
107
- DEFAULT_README = 'README.md'
108
- DEFAULT_FILES = ['*']
109
-
110
- def __init__(self, *, version=None, readme_file=None, files=None):
111
- self.version = version or VERSION # type: str
112
- self.readme_file = readme_file or self.DEFAULT_README # type: Optional[pathlib.Path]
113
- self.files = files or [] # type: List[str]
114
-
115
- @classmethod
116
- def load(cls, data):
117
- return TDKConfig(
118
- version=data.get('version', VERSION),
119
- readme_file=data.get('readmeFile', cls.DEFAULT_README),
120
- files=data.get('files', cls.DEFAULT_FILES),
121
- )
122
-
123
- def serialize(self):
124
- return {
125
- 'version': self.version,
126
- 'readmeFile': self.readme_file,
127
- 'files': self.files,
128
- }
129
-
130
-
131
- class TemplateFile:
132
-
133
- DEFAULT_CONTENT_TYPE = 'application/octet-stream'
134
- TEMPLATE_EXTENSIONS = ('.j2', '.jinja', '.jinja2', '.jnj')
135
-
136
- def __init__(self, *, remote_id=None, remote_type=None, filename=None,
137
- content_type=None, content=None):
138
- self.remote_id = remote_id # type: Optional[str]
139
- self.filename = filename # type: pathlib.Path
140
- self.content = content # type: bytes
141
- self.remote_type = remote_type
142
- self.content_type = content_type or self.guess_type() # type: str
143
- self.remote_type = remote_type or self.guess_tfile_type() # type: TemplateFileType
144
-
145
- def guess_tfile_type(self):
146
- return TemplateFileType.file if self.is_text else TemplateFileType.asset
147
-
148
- def guess_type(self) -> str:
149
- # TODO: add own map of file extensions
150
- filename = self.filename.name
151
- for ext in self.TEMPLATE_EXTENSIONS:
152
- if filename.endswith(ext):
153
- return 'text/jinja2'
154
- guessed_type = mimetypes.guess_type(filename, strict=False)
155
- if guessed_type is None or guessed_type[0] is None:
156
- return self.DEFAULT_CONTENT_TYPE
157
- return guessed_type[0]
158
-
159
- @property
160
- def is_text(self):
161
- # TODO: custom mapping (also some starting with "application" are textual)
162
- return self.remote_type == TemplateFileType.file or self.content_type.startswith('text')
163
-
164
- @property
165
- def has_remote_id(self):
166
- return self.remote_id is not None
167
-
168
-
169
- class Template:
170
-
171
- def __init__(self, *, template_id=None, organization_id=None, version=None, name=None,
172
- description=None, readme=None, recommended_package_id=None, template_license=None,
173
- metamodel_version=None, tdk_config=None, loaded_json=None):
174
- self.template_id = template_id # type: str
175
- self.organization_id = organization_id # type: str
176
- self.version = version # type: str
177
- self.name = name # type: str
178
- self.description = description # type: str
179
- self.readme = readme # type: str
180
- self.recommended_package_id = recommended_package_id # type: str
181
- self.license = template_license # type: str
182
- self.metamodel_version = metamodel_version or METAMODEL_VERSION # type: int
183
- self.allowed_packages = [] # type: List[PackageFilter]
184
- self.formats = [] # type: List[Format]
185
- self.files = {} # type: Dict[str, TemplateFile]
186
- self.extras = [] # type: List[TemplateFile]
187
- self.tdk_config = tdk_config or TDKConfig() # type: TDKConfig
188
- self.loaded_json = loaded_json or OrderedDict() # type: OrderedDict
189
-
190
- @property
191
- def id(self):
192
- return f'{self.organization_id}:{self.template_id}:{self.version}'
193
-
194
- @classmethod
195
- def _common_load(cls, data):
196
- if 'id' in data.keys():
197
- composite_id = data['id'] # type: str
198
- if composite_id.count(':') != 2:
199
- raise RuntimeError(f'Invalid template ID: {composite_id}')
200
- org_id, tmp_id, version = composite_id.split(':')
201
- else:
202
- try:
203
- org_id = data['organizationId']
204
- tmp_id = data['templateId']
205
- version = data['version']
206
- except KeyError:
207
- raise RuntimeError('Cannot retrieve template ID')
208
- template = Template(
209
- template_id=tmp_id,
210
- organization_id=org_id,
211
- version=version,
212
- name=data.get('name', 'Uknown template'),
213
- description=data.get('description', ''),
214
- template_license=data.get('license', 'nolicense'),
215
- metamodel_version=data.get('metamodelVersion', METAMODEL_VERSION),
216
- recommended_package_id=data.get('recommendedPackageId', ''),
217
- readme=data.get('readme', ''),
218
- )
219
- for ap_data in data.get('allowedPackages', []):
220
- template.allowed_packages.append(PackageFilter.load(ap_data))
221
- for f_data in data.get('formats', []):
222
- template.formats.append(Format.load(f_data))
223
- return template
224
-
225
- @classmethod
226
- def load_local(cls, data: OrderedDict):
227
- template = cls._common_load(data)
228
- if '_tdk' in data.keys():
229
- template.tdk_config = TDKConfig.load(data['_tdk'])
230
- template.loaded_json = data
231
- return template
232
-
233
- @classmethod
234
- def load_remote(cls, data: dict):
235
- return cls._common_load(data)
236
-
237
- def serialize_local(self) -> OrderedDict:
238
- self.loaded_json['templateId'] = self.template_id
239
- self.loaded_json['organizationId'] = self.organization_id
240
- self.loaded_json['version'] = self.version
241
- self.loaded_json['name'] = self.name
242
- self.loaded_json['description'] = self.description
243
- self.loaded_json['license'] = self.license
244
- self.loaded_json['metamodelVersion'] = self.metamodel_version
245
- self.loaded_json['recommendedPackageId'] = self.recommended_package_id
246
- # self.loaded_json['readme'] = self.readme
247
- self.loaded_json['allowedPackages'] = [ap.serialize() for ap in self.allowed_packages]
248
- self.loaded_json['formats'] = [f.serialize() for f in self.formats]
249
- self.loaded_json['_tdk'] = self.tdk_config.serialize()
250
- return self.loaded_json
251
-
252
- def serialize_remote(self) -> Dict[str, Any]:
253
- return {
254
- 'id': self.id,
255
- 'templateId': self.template_id,
256
- 'organizationId': self.organization_id,
257
- 'version': self.version,
258
- 'name': self.name,
259
- 'description': self.description,
260
- 'license': self.license,
261
- 'metamodelVersion': self.metamodel_version,
262
- 'recommendedPackageId': self.recommended_package_id,
263
- 'readme': self.readme,
264
- 'allowedPackages': [ap.serialize() for ap in self.allowed_packages],
265
- 'formats': [f.serialize() for f in self.formats],
266
- }
267
-
268
-
269
- def _to_ordered_dict(tuples: List[Tuple[str, Any]]) -> OrderedDict:
270
- return OrderedDict(tuples)
271
-
272
-
273
- class TemplateProject:
274
-
275
- TEMPLATE_FILE = 'template.json'
276
- DEFAULT_PATTERNS = ['!**/.git/**/*', '!template.json', '!template.zip']
277
-
278
- json_decoder = json.JSONDecoder(object_pairs_hook=_to_ordered_dict)
279
-
280
- def __init__(self, template_dir, logger):
281
- self.template_dir = pathlib.Path(template_dir) # type: pathlib.Path
282
- self.descriptor_path = self.template_dir / self.TEMPLATE_FILE
283
- self.template = None # type: Optional[Template]
284
- self.used_readme = None # type: Optional[pathlib.Path]
285
- self._logger = logger # type: logging.Logger
286
-
287
- @property
288
- def logger(self) -> logging.Logger:
289
- return self._logger
290
-
291
- @property
292
- def safe_template(self) -> Template:
293
- if self.template is None:
294
- raise RuntimeError('Template is not loaded')
295
- return self.template
296
-
297
- def load_descriptor(self):
298
- if not self.descriptor_path.is_file():
299
- raise RuntimeError(f'Template file does not exist: {self.descriptor_path.as_posix()}')
300
- try:
301
- content = self.descriptor_path.read_text(encoding=DEFAULT_ENCODING)
302
- self.template = Template.load_local(self.json_decoder.decode(content))
303
- except Exception:
304
- raise RuntimeError(f'Unable to load template using {self.descriptor_path}.')
305
-
306
- def load_readme(self):
307
- readme = self.safe_template.tdk_config.readme_file
308
- if readme is not None:
309
- try:
310
- self.used_readme = self.template_dir / readme
311
- self.safe_template.readme = self.used_readme.read_text(encoding=DEFAULT_ENCODING)
312
- except Exception as e:
313
- raise RuntimeWarning(f'README file "{readme}" cannot be loaded: {e}')
314
-
315
- def load_file(self, filepath: pathlib.Path) -> TemplateFile:
316
- try:
317
- if filepath.is_absolute():
318
- filepath = filepath.relative_to(self.template_dir)
319
- tfile = TemplateFile(filename=filepath)
320
- with open(self.template_dir / filepath, mode='rb') as f:
321
- tfile.content = f.read()
322
- self.safe_template.files[filepath.as_posix()] = tfile
323
- return tfile
324
- except Exception as e:
325
- raise RuntimeWarning(f'Failed to load template file {filepath}: {e}')
326
-
327
- def load_files(self):
328
- self.safe_template.files.clear()
329
- for f in self.list_files():
330
- self.load_file(f)
331
-
332
- @property
333
- def files_pathspec(self) -> pathspec.PathSpec:
334
- # TODO: make this more efficient (reload only when tdk_config changes, otherwise cache)
335
- patterns = self.safe_template.tdk_config.files + self.DEFAULT_PATTERNS
336
- return pathspec.PathSpec.from_lines(PATHSPEC_FACTORY, patterns)
337
-
338
- def list_files(self) -> List[pathlib.Path]:
339
- files = (pathlib.Path(p) for p in self.files_pathspec.match_tree_files(str(self.template_dir)))
340
- if self.used_readme is not None:
341
- return list(p for p in files if p != self.used_readme.relative_to(self.template_dir))
342
- return list(files)
343
-
344
- def _relative_paths_eq(self, filepath1: Optional[pathlib.Path], filepath2: Optional[pathlib.Path]) -> bool:
345
- if filepath1 is None or filepath2 is None:
346
- return False
347
- return filepath1.relative_to(self.template_dir) == filepath2.relative_to(self.template_dir)
348
-
349
- def is_template_file(self, filepath: pathlib.Path, include_descriptor: bool = False, include_readme: bool = False):
350
- if include_readme and self._relative_paths_eq(filepath, self.used_readme):
351
- return True
352
- if include_descriptor and self._relative_paths_eq(filepath, self.descriptor_path):
353
- return True
354
- return self.files_pathspec.match_file(filepath.relative_to(self.template_dir))
355
-
356
- def load(self):
357
- self.load_descriptor()
358
- self.load_readme()
359
- self.load_files()
360
-
361
- def remove_template_file(self, filepath: pathlib.Path):
362
- if filepath.is_absolute():
363
- filepath = filepath.relative_to(self.template_dir)
364
- filename = filepath.as_posix()
365
- if filename in self.safe_template.files:
366
- del self.safe_template.files[filename]
367
-
368
- def update_template_file(self, tfile: TemplateFile):
369
- filename = tfile.filename.as_posix()
370
- self.safe_template.files[filename] = tfile
371
-
372
- def get_template_file(self, filepath: pathlib.Path) -> Optional[TemplateFile]:
373
- if filepath.is_absolute():
374
- filepath = filepath.relative_to(self.template_dir)
375
- return self.safe_template.files.get(filepath.as_posix(), None)
376
-
377
- def _write_file(self, filepath: pathlib.Path, contents: bytes, force: bool):
378
- if filepath.exists() and not force:
379
- self.logger.warning(f'Skipping file {filepath} (not forced)')
380
- return
381
- try:
382
- filepath.parent.mkdir(parents=True, exist_ok=True)
383
- filepath.write_bytes(contents)
384
- self.logger.debug(f'Stored file {filepath}')
385
- except Exception as e:
386
- self.logger.error(f'Unable to write file {filepath}: {e}')
387
-
388
- def store_descriptor(self, force: bool):
389
- self._write_file(
390
- filepath=self.descriptor_path,
391
- contents=json.dumps(self.safe_template.serialize_local(), indent=4).encode(encoding=DEFAULT_ENCODING),
392
- force=force,
393
- )
394
-
395
- def store_readme(self, force: bool):
396
- if self.safe_template.tdk_config.readme_file is None:
397
- self.logger.warning('No README file specified for the template')
398
- return
399
- self._write_file(
400
- filepath=self.template_dir / self.safe_template.tdk_config.readme_file,
401
- contents=self.safe_template.readme.encode(encoding=DEFAULT_ENCODING),
402
- force=force,
403
- )
404
-
405
- def store_files(self, force: bool):
406
- for tfile in self.safe_template.files.values():
407
- self._write_file(
408
- filepath=self.template_dir / tfile.filename,
409
- contents=tfile.content,
410
- force=force,
411
- )
412
-
413
- def store(self, force: bool):
414
- self.logger.debug(f'Ensuring directory {self.template_dir.as_posix()}')
415
- self.template_dir.mkdir(parents=True, exist_ok=True)
416
- self.logger.debug(f'Storing {self.TEMPLATE_FILE} descriptor')
417
- self.store_descriptor(force=force)
418
- self.logger.debug('Storing README file')
419
- self.store_readme(force=force)
420
- self.logger.debug('Storing template files')
421
- self.store_files(force=force)
422
- self.logger.debug('Storing finished')
1
+ import collections
2
+ import enum
3
+ import json
4
+ import logging
5
+ import mimetypes
6
+ import pathlib
7
+ import typing
8
+
9
+ import pathspec
10
+
11
+ from . import consts
12
+
13
+
14
+ mimetypes.init()
15
+
16
+
17
+ class TemplateFileType(enum.Enum):
18
+ ASSET = 'asset'
19
+ FILE = 'file'
20
+
21
+
22
+ class PackageFilter:
23
+
24
+ def __init__(self, *, organization_id: str | None = None, km_id: str | None = None,
25
+ min_version: str | None = None, max_version: str | None = None):
26
+ self.organization_id = organization_id
27
+ self.km_id = km_id
28
+ self.min_version = min_version
29
+ self.max_version = max_version
30
+
31
+ @classmethod
32
+ def load(cls, data):
33
+ return PackageFilter(
34
+ organization_id=data.get('orgId', None),
35
+ km_id=data.get('kmId', None),
36
+ min_version=data.get('minVersion', None),
37
+ max_version=data.get('maxVersion', None),
38
+ )
39
+
40
+ def serialize(self):
41
+ return {
42
+ 'orgId': self.organization_id,
43
+ 'kmId': self.km_id,
44
+ 'minVersion': self.min_version,
45
+ 'maxVersion': self.max_version,
46
+ }
47
+
48
+
49
+ class Step:
50
+
51
+ def __init__(self, *, name: str | None = None,
52
+ options: dict[str, str] | None = None):
53
+ self.name = name
54
+ self.options = options or {}
55
+
56
+ @classmethod
57
+ def load(cls, data):
58
+ return Step(
59
+ name=data.get('name', None),
60
+ options=data.get('options', None),
61
+ )
62
+
63
+ def serialize(self):
64
+ return {
65
+ 'name': self.name,
66
+ 'options': self.options,
67
+ }
68
+
69
+
70
+ class Format:
71
+
72
+ DEFAULT_ICON = 'fas fa-file'
73
+
74
+ def __init__(self, *, uuid: str | None = None, name: str | None = None,
75
+ icon: str | None = None):
76
+ self.uuid = uuid
77
+ self.name = name
78
+ self.icon: str = icon or self.DEFAULT_ICON
79
+ self.steps: list[Step] = []
80
+
81
+ @classmethod
82
+ def load(cls, data):
83
+ format_spec = Format(
84
+ uuid=data.get('uuid', None),
85
+ name=data.get('name', None),
86
+ icon=data.get('icon', None),
87
+ )
88
+ for s_data in data.get('steps', []):
89
+ format_spec.steps.append(Step.load(s_data))
90
+ return format_spec
91
+
92
+ def serialize(self):
93
+ return {
94
+ 'uuid': self.uuid,
95
+ 'name': self.name,
96
+ 'icon': self.icon,
97
+ 'steps': [step.serialize() for step in self.steps],
98
+ }
99
+
100
+
101
+ class TDKConfig:
102
+
103
+ DEFAULT_README = 'README.md'
104
+ DEFAULT_FILES = ['*', '!.git/', '!.env']
105
+
106
+ def __init__(self, *, version: str | None = None, readme_file: str | None = None,
107
+ files: list[str] | None = None):
108
+ self.version: str = version or consts.VERSION
109
+ readme_file_str: str = readme_file or self.DEFAULT_README
110
+ self.readme_file: pathlib.Path = pathlib.Path(readme_file_str)
111
+ self.files: list[str] = files or []
112
+
113
+ @classmethod
114
+ def load(cls, data):
115
+ return TDKConfig(
116
+ version=data.get('version', consts.VERSION),
117
+ readme_file=data.get('readmeFile', cls.DEFAULT_README),
118
+ files=data.get('files', cls.DEFAULT_FILES),
119
+ )
120
+
121
+ def use_default_files(self):
122
+ self.files = self.DEFAULT_FILES
123
+
124
+ def serialize(self):
125
+ return {
126
+ 'version': self.version,
127
+ 'readmeFile': str(self.readme_file),
128
+ 'files': self.files,
129
+ }
130
+
131
+
132
+ class TemplateFile:
133
+
134
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'
135
+ TEMPLATE_EXTENSIONS = ('.j2', '.jinja', '.jinja2', '.jnj')
136
+
137
+ def __init__(self, *, filename: pathlib.Path,
138
+ remote_id: str | None = None, remote_type: TemplateFileType | None = None,
139
+ content_type: str | None = None, content: bytes = b''):
140
+ self.remote_id = remote_id
141
+ self.filename = filename
142
+ self.content = content
143
+ self.content_type: str = content_type or self.guess_type()
144
+ self.remote_type: TemplateFileType = remote_type or self.guess_template_file_type()
145
+
146
+ def guess_template_file_type(self):
147
+ return TemplateFileType.FILE if self.is_text else TemplateFileType.ASSET
148
+
149
+ def guess_type(self) -> str:
150
+ filename = self.filename.name
151
+ for ext in self.TEMPLATE_EXTENSIONS:
152
+ if filename.endswith(ext):
153
+ return 'text/jinja2'
154
+ guessed_type = mimetypes.guess_type(filename, strict=False)
155
+ if guessed_type is None or guessed_type[0] is None:
156
+ return self.DEFAULT_CONTENT_TYPE
157
+ return guessed_type[0]
158
+
159
+ @property
160
+ def is_text(self):
161
+ if getattr(self, 'remote_type', None) == TemplateFileType.FILE:
162
+ return True
163
+ return self.content_type.startswith('text')
164
+
165
+ @property
166
+ def has_remote_id(self):
167
+ return self.remote_id is not None
168
+
169
+
170
+ class Template:
171
+
172
+ # pylint: disable-next=too-many-arguments
173
+ def __init__(self, *, template_id=None, organization_id=None, version=None, name=None,
174
+ description=None, readme=None, template_license=None,
175
+ metamodel_version=None, tdk_config=None, loaded_json=None):
176
+ self.template_id: str | None = template_id
177
+ self.organization_id: str | None = organization_id
178
+ self.version: str | None = version
179
+ self.name: str | None = name
180
+ self.description: str | None = description
181
+ self.readme: str | None = readme
182
+ self.license: str | None = template_license
183
+ self.metamodel_version: str = metamodel_version or consts.METAMODEL_VERSION
184
+ self.allowed_packages: list[PackageFilter] = []
185
+ self.formats: list[Format] = []
186
+ self.files: dict[str, TemplateFile] = {}
187
+ self.extras: list[TemplateFile] = []
188
+ self.tdk_config: TDKConfig = tdk_config or TDKConfig()
189
+ self.loaded_json: collections.OrderedDict = loaded_json or collections.OrderedDict()
190
+
191
+ @property
192
+ def id(self) -> str:
193
+ return f'{self.organization_id}:{self.template_id}:{self.version}'
194
+
195
+ def id_with_org(self, organization_id: str) -> str:
196
+ return f'{organization_id}:{self.template_id}:{self.version}'
197
+
198
+ @classmethod
199
+ def _common_load(cls, data):
200
+ if 'id' in data:
201
+ composite_id = data['id'] # type: str
202
+ if composite_id.count(':') != 2:
203
+ raise RuntimeError(f'Invalid template ID: {composite_id}')
204
+ org_id, tmp_id, version = composite_id.split(':')
205
+ else:
206
+ try:
207
+ org_id = data['organizationId']
208
+ tmp_id = data['templateId']
209
+ version = data['version']
210
+ except KeyError as e:
211
+ raise RuntimeError('Cannot retrieve template ID') from e
212
+ template = Template(
213
+ template_id=tmp_id,
214
+ organization_id=org_id,
215
+ version=version,
216
+ name=data.get('name', 'Unknown template'),
217
+ description=data.get('description', ''),
218
+ template_license=data.get('license', 'no-license'),
219
+ metamodel_version=data.get('metamodelVersion', consts.METAMODEL_VERSION),
220
+ readme=data.get('readme', ''),
221
+ )
222
+ for ap_data in data.get('allowedPackages', []):
223
+ template.allowed_packages.append(PackageFilter.load(ap_data))
224
+ for f_data in data.get('formats', []):
225
+ template.formats.append(Format.load(f_data))
226
+ return template
227
+
228
+ @classmethod
229
+ def load_local(cls, data: collections.OrderedDict):
230
+ template = cls._common_load(data)
231
+ if '_tdk' in data:
232
+ template.tdk_config = TDKConfig.load(data['_tdk'])
233
+ template.loaded_json = data
234
+ return template
235
+
236
+ @classmethod
237
+ def load_remote(cls, data: dict):
238
+ return cls._common_load(data)
239
+
240
+ def serialize_local(self) -> collections.OrderedDict:
241
+ self.loaded_json['templateId'] = self.template_id
242
+ self.loaded_json['organizationId'] = self.organization_id
243
+ self.loaded_json['version'] = self.version
244
+ self.loaded_json['name'] = self.name
245
+ self.loaded_json['description'] = self.description
246
+ self.loaded_json['license'] = self.license
247
+ self.loaded_json['metamodelVersion'] = self.metamodel_version
248
+ self.loaded_json['allowedPackages'] = [ap.serialize() for ap in self.allowed_packages]
249
+ self.loaded_json['formats'] = [f.serialize() for f in self.formats]
250
+ self.loaded_json['_tdk'] = self.tdk_config.serialize()
251
+ return self.loaded_json
252
+
253
+ def serialize_remote(self) -> dict[str, typing.Any]:
254
+ return {
255
+ 'id': self.id,
256
+ 'templateId': self.template_id,
257
+ 'organizationId': self.organization_id,
258
+ 'version': self.version,
259
+ 'name': self.name,
260
+ 'description': self.description,
261
+ 'license': self.license,
262
+ 'metamodelVersion': self.metamodel_version,
263
+ 'readme': self.readme,
264
+ 'allowedPackages': [ap.serialize() for ap in self.allowed_packages],
265
+ 'formats': [f.serialize() for f in self.formats],
266
+ 'phase': 'DraftDocumentTemplatePhase',
267
+ }
268
+
269
+ def serialize_for_update(self) -> dict[str, typing.Any]:
270
+ return {
271
+ 'templateId': self.template_id,
272
+ 'version': self.version,
273
+ 'name': self.name,
274
+ 'description': self.description,
275
+ 'license': self.license,
276
+ 'metamodelVersion': self.metamodel_version,
277
+ 'readme': self.readme,
278
+ 'allowedPackages': [ap.serialize() for ap in self.allowed_packages],
279
+ 'formats': [f.serialize() for f in self.formats],
280
+ 'phase': 'DraftDocumentTemplatePhase',
281
+ }
282
+
283
+ def serialize_for_create(self, based_on: str | None = None) -> dict[str, typing.Any]:
284
+ return {
285
+ 'basedOn': based_on,
286
+ 'name': self.name,
287
+ 'templateId': self.template_id,
288
+ 'version': self.version,
289
+ }
290
+
291
+ def serialize_local_new(self) -> dict[str, typing.Any]:
292
+ return {
293
+ 'templateId': self.template_id,
294
+ 'organizationId': self.organization_id,
295
+ 'version': self.version,
296
+ 'name': self.name,
297
+ 'description': self.description,
298
+ 'license': self.license,
299
+ 'metamodelVersion': self.metamodel_version,
300
+ 'allowedPackages': [ap.serialize() for ap in self.allowed_packages],
301
+ 'formats': [f.serialize() for f in self.formats],
302
+ '_tdk': self.tdk_config.serialize(),
303
+ }
304
+
305
+
306
+ def _to_ordered_dict(tuples: list[tuple[str, typing.Any]]) -> collections.OrderedDict:
307
+ return collections.OrderedDict(tuples)
308
+
309
+
310
+ class TemplateProject:
311
+
312
+ TEMPLATE_FILE = 'template.json'
313
+ DEFAULT_PATTERNS = ['!**/.*', '!**/.*/', '!**/~*', '!**/~*/',
314
+ '!template.json', '!template.zip']
315
+
316
+ json_decoder = json.JSONDecoder(object_pairs_hook=_to_ordered_dict)
317
+
318
+ def __init__(self, template_dir: pathlib.Path, logger: logging.Logger):
319
+ self.template_dir = pathlib.Path(template_dir)
320
+ self.descriptor_path = self.template_dir / self.TEMPLATE_FILE
321
+ self.template: Template | None = None
322
+ self.used_readme: pathlib.Path | None = None
323
+ self._logger = logger
324
+
325
+ @property
326
+ def logger(self) -> logging.Logger:
327
+ return self._logger
328
+
329
+ @property
330
+ def safe_template(self) -> Template:
331
+ if self.template is None:
332
+ raise RuntimeError('Template is not loaded')
333
+ return self.template
334
+
335
+ def load_descriptor(self):
336
+ if not self.descriptor_path.is_file():
337
+ raise RuntimeError(f'Template file does not exist: {self.descriptor_path.as_posix()}')
338
+ try:
339
+ content = self.descriptor_path.read_text(encoding=consts.DEFAULT_ENCODING)
340
+ self.template = Template.load_local(self.json_decoder.decode(content))
341
+ except Exception as e:
342
+ raise RuntimeError(f'Unable to load template using {self.descriptor_path}.') from e
343
+
344
+ def load_readme(self):
345
+ readme = self.safe_template.tdk_config.readme_file
346
+ if readme is not None:
347
+ try:
348
+ self.used_readme = self.template_dir / readme
349
+ readme_text = self.used_readme.read_text(encoding=consts.DEFAULT_ENCODING)
350
+ self.safe_template.readme = readme_text
351
+ except Exception as e:
352
+ raise RuntimeWarning(f'README file "{readme}" cannot be loaded: {e}') from e
353
+
354
+ def load_file(self, filepath: pathlib.Path) -> TemplateFile:
355
+ try:
356
+ if filepath.is_absolute():
357
+ filepath = filepath.relative_to(self.template_dir)
358
+ template_file = TemplateFile(filename=filepath)
359
+ template_file.content = (self.template_dir / filepath).read_bytes()
360
+ self.safe_template.files[filepath.as_posix()] = template_file
361
+ return template_file
362
+ except Exception as e:
363
+ raise RuntimeWarning(f'Failed to load template file {filepath}: {e}') from e
364
+
365
+ def load_files(self):
366
+ self.safe_template.files.clear()
367
+ for f in self.list_files():
368
+ self.load_file(f)
369
+
370
+ @property
371
+ def files_pathspec(self) -> pathspec.PathSpec:
372
+ patterns = self.safe_template.tdk_config.files + self.DEFAULT_PATTERNS
373
+ return pathspec.PathSpec.from_lines('gitignore', patterns)
374
+
375
+ def list_files(self) -> list[pathlib.Path]:
376
+ files = (pathlib.Path(p)
377
+ for p in self.files_pathspec.match_tree_files(str(self.template_dir)))
378
+ if self.used_readme is not None:
379
+ return [p for p in files if p != self.used_readme.relative_to(self.template_dir)]
380
+ return list(files)
381
+
382
+ def _relative_paths_eq(self, filepath1: pathlib.Path | None,
383
+ filepath2: pathlib.Path | None) -> bool:
384
+ if filepath1 is None or filepath2 is None:
385
+ return False
386
+ return filepath1.relative_to(self.template_dir) == filepath2.relative_to(self.template_dir)
387
+
388
+ def is_template_file(self, filepath: pathlib.Path, include_descriptor: bool = False,
389
+ include_readme: bool = False):
390
+ if include_readme and self._relative_paths_eq(filepath, self.used_readme):
391
+ return True
392
+ if include_descriptor and self._relative_paths_eq(filepath, self.descriptor_path):
393
+ return True
394
+ return self.files_pathspec.match_file(filepath.relative_to(self.template_dir))
395
+
396
+ def load(self):
397
+ self.load_descriptor()
398
+ self.load_readme()
399
+ self.load_files()
400
+
401
+ def remove_template_file(self, filepath: pathlib.Path):
402
+ if filepath.is_absolute():
403
+ filepath = filepath.relative_to(self.template_dir)
404
+ filename = filepath.as_posix()
405
+ if filename in self.safe_template.files:
406
+ del self.safe_template.files[filename]
407
+
408
+ def update_template_file(self, template_file: TemplateFile):
409
+ filename = template_file.filename.as_posix()
410
+ self.safe_template.files[filename] = template_file
411
+
412
+ def get_template_file(self, filepath: pathlib.Path) -> TemplateFile | None:
413
+ if filepath.is_absolute():
414
+ filepath = filepath.relative_to(self.template_dir)
415
+ return self.safe_template.files.get(filepath.as_posix(), None)
416
+
417
+ def _write_file(self, filepath: pathlib.Path, contents: bytes, force: bool):
418
+ if filepath.exists() and not force:
419
+ self.logger.warning('Skipping file %s (not forced)', filepath.as_posix())
420
+ return
421
+ try:
422
+ filepath.parent.mkdir(parents=True, exist_ok=True)
423
+ filepath.write_bytes(contents)
424
+ self.logger.debug('Stored file %s', filepath.as_posix())
425
+ except Exception as e:
426
+ self.logger.error('Unable to write file %s: %s', filepath.as_posix(), e)
427
+
428
+ def store_descriptor(self, force: bool):
429
+ self._write_file(
430
+ filepath=self.descriptor_path,
431
+ contents=json.dumps(
432
+ obj=self.safe_template.serialize_local(),
433
+ indent=4,
434
+ ).encode(encoding=consts.DEFAULT_ENCODING),
435
+ force=force,
436
+ )
437
+
438
+ def store_readme(self, force: bool):
439
+ if self.safe_template.tdk_config.readme_file is None:
440
+ self.logger.warning('No README file specified for the template')
441
+ return
442
+ if self.safe_template.readme is None:
443
+ self.logger.warning('No README content specified for the template')
444
+ return
445
+ self._write_file(
446
+ filepath=self.template_dir / self.safe_template.tdk_config.readme_file,
447
+ contents=self.safe_template.readme.encode(encoding=consts.DEFAULT_ENCODING),
448
+ force=force,
449
+ )
450
+
451
+ def store_files(self, force: bool):
452
+ for template_file in self.safe_template.files.values():
453
+ self._write_file(
454
+ filepath=self.template_dir / template_file.filename,
455
+ contents=template_file.content,
456
+ force=force,
457
+ )
458
+
459
+ def store(self, force: bool):
460
+ self.logger.debug('Ensuring directory %s', self.template_dir.as_posix())
461
+ self.template_dir.mkdir(parents=True, exist_ok=True)
462
+ self.logger.debug('Storing %s descriptor', self.TEMPLATE_FILE)
463
+ self.store_descriptor(force=force)
464
+ self.logger.debug('Storing README file')
465
+ self.store_readme(force=force)
466
+ self.logger.debug('Storing template files')
467
+ self.store_files(force=force)
468
+ self.logger.debug('Storing finished')