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 → dsw/tdk}/__init__.py
RENAMED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
"""DSW Template Development Kit
|
|
2
|
-
|
|
3
|
-
Template Development Kit for `Data Stewardship Wizard`_.
|
|
4
|
-
|
|
5
|
-
.. _Data Stewardship Wizard:
|
|
6
|
-
https://ds-wizard.org
|
|
7
|
-
|
|
8
|
-
"""
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
"""DSW Template Development Kit
|
|
2
|
+
|
|
3
|
+
Template Development Kit for `Data Stewardship Wizard`_.
|
|
4
|
+
|
|
5
|
+
.. _Data Stewardship Wizard:
|
|
6
|
+
https://ds-wizard.org
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
from . import consts
|
|
10
|
+
from .cli import main
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__app__ = consts.APP
|
|
14
|
+
__version__ = consts.VERSION
|
|
15
|
+
|
|
16
|
+
__all__ = ['__app__', '__version__', 'main']
|
dsw/tdk/__main__.py
ADDED
dsw/tdk/api_client.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import pathlib
|
|
3
|
+
import urllib.parse
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
import aiohttp.client_exceptions
|
|
7
|
+
|
|
8
|
+
from . import consts
|
|
9
|
+
from .model import Template, TemplateFile, TemplateFileType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WizardCommunicationError(RuntimeError):
|
|
13
|
+
|
|
14
|
+
def __init__(self, reason: str, message: str):
|
|
15
|
+
"""
|
|
16
|
+
Exception representing communication error with DSW.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
reason (str): Cause of the error.
|
|
20
|
+
message (str): Additional information about the error.
|
|
21
|
+
"""
|
|
22
|
+
self.reason = reason
|
|
23
|
+
self.message = message
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def handle_client_errors(func):
|
|
27
|
+
@functools.wraps(func)
|
|
28
|
+
async def handled_client_call(job, *args, **kwargs):
|
|
29
|
+
try:
|
|
30
|
+
return await func(job, *args, **kwargs)
|
|
31
|
+
except WizardCommunicationError as e:
|
|
32
|
+
# Already DSWCommunicationError (re-raise)
|
|
33
|
+
raise e
|
|
34
|
+
except aiohttp.client_exceptions.ContentTypeError as e:
|
|
35
|
+
raise WizardCommunicationError(
|
|
36
|
+
reason='Unexpected response type',
|
|
37
|
+
message=e.message,
|
|
38
|
+
) from e
|
|
39
|
+
except aiohttp.client_exceptions.ClientResponseError as e:
|
|
40
|
+
raise WizardCommunicationError(
|
|
41
|
+
reason='Error response status',
|
|
42
|
+
message=f'Server responded with error HTTP status {e.status}: {e.message}',
|
|
43
|
+
) from e
|
|
44
|
+
except aiohttp.client_exceptions.InvalidURL as e:
|
|
45
|
+
raise WizardCommunicationError(
|
|
46
|
+
reason='Invalid URL',
|
|
47
|
+
message=f'Provided API URL seems invalid: {e.url}',
|
|
48
|
+
) from e
|
|
49
|
+
except aiohttp.client_exceptions.ClientConnectorError as e:
|
|
50
|
+
raise WizardCommunicationError(
|
|
51
|
+
reason='Server unreachable',
|
|
52
|
+
message=f'Desired server is not reachable (errno {e.os_error.errno})',
|
|
53
|
+
) from e
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise WizardCommunicationError(
|
|
56
|
+
reason='Communication error',
|
|
57
|
+
message=f'Communication with server failed ({e})',
|
|
58
|
+
) from e
|
|
59
|
+
return handled_client_call
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# pylint: disable-next=too-many-public-methods
|
|
63
|
+
class WizardAPIClient:
|
|
64
|
+
|
|
65
|
+
def _headers(self, extra=None):
|
|
66
|
+
headers = {
|
|
67
|
+
'Authorization': f'Bearer {self.token}',
|
|
68
|
+
'User-Agent': f'{consts.APP}/{consts.VERSION}',
|
|
69
|
+
}
|
|
70
|
+
if extra is not None:
|
|
71
|
+
headers.update(extra)
|
|
72
|
+
return headers
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _check_status(r: aiohttp.ClientResponse, expected_status):
|
|
76
|
+
r.raise_for_status()
|
|
77
|
+
if r.status != expected_status:
|
|
78
|
+
raise WizardCommunicationError(
|
|
79
|
+
reason='Unexpected response status',
|
|
80
|
+
message=f'Server responded with unexpected HTTP status {r.status}: '
|
|
81
|
+
f'{r.reason} (expecting {expected_status})',
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def __init__(self, api_url: str, api_key: str, session=None):
|
|
85
|
+
"""
|
|
86
|
+
Exception representing communication error with DSW.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
api_url (str): URL of DSW API for HTTP communication.
|
|
90
|
+
session (aiohttp.ClientSession): Optional custom session for HTTP communication.
|
|
91
|
+
"""
|
|
92
|
+
self.api_url = api_url
|
|
93
|
+
self.token = api_key
|
|
94
|
+
self.session = session or aiohttp.ClientSession(
|
|
95
|
+
connector=aiohttp.TCPConnector(ssl=False),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def templates_endpoint(self):
|
|
100
|
+
return f'{self.api_url}/document-templates'
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def drafts_endpoint(self):
|
|
104
|
+
return f'{self.api_url}/document-template-drafts'
|
|
105
|
+
|
|
106
|
+
async def close(self):
|
|
107
|
+
await self.session.close()
|
|
108
|
+
|
|
109
|
+
async def safe_close(self) -> bool:
|
|
110
|
+
try:
|
|
111
|
+
await self.session.close()
|
|
112
|
+
return True
|
|
113
|
+
except Exception:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
async def _post_json(self, endpoint, json) -> dict:
|
|
117
|
+
async with self.session.post(
|
|
118
|
+
url=f'{self.api_url}{endpoint}',
|
|
119
|
+
json=json,
|
|
120
|
+
headers=self._headers(),
|
|
121
|
+
) as r:
|
|
122
|
+
self._check_status(r, expected_status=201)
|
|
123
|
+
return await r.json()
|
|
124
|
+
|
|
125
|
+
async def _put_json(self, endpoint, json) -> dict:
|
|
126
|
+
async with self.session.put(
|
|
127
|
+
url=f'{self.api_url}{endpoint}',
|
|
128
|
+
json=json,
|
|
129
|
+
headers=self._headers(),
|
|
130
|
+
) as r:
|
|
131
|
+
self._check_status(r, expected_status=200)
|
|
132
|
+
return await r.json()
|
|
133
|
+
|
|
134
|
+
async def _get_json(self, endpoint) -> dict:
|
|
135
|
+
async with self.session.get(
|
|
136
|
+
url=f'{self.api_url}{endpoint}',
|
|
137
|
+
headers=self._headers(),
|
|
138
|
+
) as r:
|
|
139
|
+
self._check_status(r, expected_status=200)
|
|
140
|
+
return await r.json()
|
|
141
|
+
|
|
142
|
+
async def _get_bytes(self, endpoint) -> bytes:
|
|
143
|
+
async with self.session.get(
|
|
144
|
+
url=f'{self.api_url}{endpoint}',
|
|
145
|
+
headers=self._headers(),
|
|
146
|
+
) as r:
|
|
147
|
+
self._check_status(r, expected_status=200)
|
|
148
|
+
return await r.read()
|
|
149
|
+
|
|
150
|
+
async def _delete(self, endpoint) -> bool:
|
|
151
|
+
async with self.session.delete(
|
|
152
|
+
url=f'{self.api_url}{endpoint}',
|
|
153
|
+
headers=self._headers(),
|
|
154
|
+
) as r:
|
|
155
|
+
return r.status == 204
|
|
156
|
+
|
|
157
|
+
@handle_client_errors
|
|
158
|
+
async def login(self, email: str, password: str) -> str | None:
|
|
159
|
+
req = {'email': email, 'password': password}
|
|
160
|
+
body = await self._post_json('/tokens', json=req)
|
|
161
|
+
token_value = body.get('token')
|
|
162
|
+
if not isinstance(token_value, str):
|
|
163
|
+
raise WizardCommunicationError(
|
|
164
|
+
reason='Invalid response',
|
|
165
|
+
message='Server did not return a valid token',
|
|
166
|
+
)
|
|
167
|
+
self.token = token_value
|
|
168
|
+
return self.token
|
|
169
|
+
|
|
170
|
+
@handle_client_errors
|
|
171
|
+
async def get_current_user(self) -> dict:
|
|
172
|
+
return await self._get_json('/users/current')
|
|
173
|
+
|
|
174
|
+
@handle_client_errors
|
|
175
|
+
async def check_template_exists(self, remote_id: str) -> bool:
|
|
176
|
+
async with self.session.get(
|
|
177
|
+
url=f'{self.templates_endpoint}/{remote_id}',
|
|
178
|
+
headers=self._headers(),
|
|
179
|
+
) as r:
|
|
180
|
+
if r.status == 404:
|
|
181
|
+
return False
|
|
182
|
+
self._check_status(r, expected_status=200)
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
@handle_client_errors
|
|
186
|
+
async def check_draft_exists(self, remote_id: str) -> bool:
|
|
187
|
+
async with self.session.get(
|
|
188
|
+
url=f'{self.drafts_endpoint}/{remote_id}',
|
|
189
|
+
headers=self._headers(),
|
|
190
|
+
) as r:
|
|
191
|
+
if r.status == 404:
|
|
192
|
+
return False
|
|
193
|
+
self._check_status(r, expected_status=200)
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
@handle_client_errors
|
|
197
|
+
async def get_templates(self) -> list[Template]:
|
|
198
|
+
body = await self._get_json('/document-templates/all')
|
|
199
|
+
return list(map(_load_remote_template, body))
|
|
200
|
+
|
|
201
|
+
@handle_client_errors
|
|
202
|
+
async def get_drafts(self) -> list[Template]:
|
|
203
|
+
body = await self._get_json('/document-template-drafts?size=10000')
|
|
204
|
+
drafts = body.get('_embedded', {}).get('documentTemplateDrafts', [])
|
|
205
|
+
return list(map(_load_remote_template, drafts))
|
|
206
|
+
|
|
207
|
+
@handle_client_errors
|
|
208
|
+
async def get_template_bundle(self, remote_id: str) -> bytes:
|
|
209
|
+
remote_file_descriptor = await self._get_json(
|
|
210
|
+
endpoint=f'/document-templates/{remote_id}/bundle'
|
|
211
|
+
f'?Authorization=Bearer%20{self.token}',
|
|
212
|
+
)
|
|
213
|
+
s3_url = remote_file_descriptor['url']
|
|
214
|
+
async with self.session.get(url=s3_url) as r:
|
|
215
|
+
self._check_status(r, expected_status=200)
|
|
216
|
+
return await r.content.read()
|
|
217
|
+
|
|
218
|
+
@handle_client_errors
|
|
219
|
+
async def get_template_draft(self, remote_id: str) -> Template:
|
|
220
|
+
body = await self._get_json(f'/document-template-drafts/{remote_id}')
|
|
221
|
+
return _load_remote_template(body)
|
|
222
|
+
|
|
223
|
+
@handle_client_errors
|
|
224
|
+
async def get_template_draft_files(self, remote_id: str) -> list[TemplateFile]:
|
|
225
|
+
body = await self._get_json(f'/document-template-drafts/{remote_id}/files')
|
|
226
|
+
result = []
|
|
227
|
+
for file_body in body:
|
|
228
|
+
file_id = file_body['uuid']
|
|
229
|
+
file = await self.get_template_draft_file(remote_id, file_id)
|
|
230
|
+
result.append(file)
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
@handle_client_errors
|
|
234
|
+
async def get_template_draft_file(self, remote_id: str, file_id: str) -> TemplateFile:
|
|
235
|
+
body = await self._get_json(f'/document-template-drafts/{remote_id}/files/{file_id}')
|
|
236
|
+
return _load_remote_file(body)
|
|
237
|
+
|
|
238
|
+
@handle_client_errors
|
|
239
|
+
async def get_template_draft_assets(self, remote_id: str) -> list[TemplateFile]:
|
|
240
|
+
body = await self._get_json(f'/document-template-drafts/{remote_id}/assets')
|
|
241
|
+
result = []
|
|
242
|
+
for file_body in body:
|
|
243
|
+
asset_id = file_body['uuid']
|
|
244
|
+
template_asset = await self.get_template_draft_asset(remote_id, asset_id)
|
|
245
|
+
result.append(template_asset)
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
@handle_client_errors
|
|
249
|
+
async def get_template_draft_asset(self, remote_id: str, asset_id: str) -> TemplateFile:
|
|
250
|
+
body = await self._get_json(f'/document-template-drafts/{remote_id}'
|
|
251
|
+
f'/assets/{asset_id}')
|
|
252
|
+
content = await self._get_bytes(f'/document-template-drafts/{remote_id}'
|
|
253
|
+
f'/assets/{asset_id}/content')
|
|
254
|
+
return _load_remote_asset(body, content)
|
|
255
|
+
|
|
256
|
+
@handle_client_errors
|
|
257
|
+
async def create_new_template_draft(self, template: Template, remote_id: str) -> Template:
|
|
258
|
+
created_template = await self._post_json(
|
|
259
|
+
endpoint='/document-template-drafts',
|
|
260
|
+
json=template.serialize_for_create(),
|
|
261
|
+
)
|
|
262
|
+
if created_template['id'] != remote_id:
|
|
263
|
+
raise RuntimeError('Organization ID changed during the process')
|
|
264
|
+
body = await self._put_json(
|
|
265
|
+
endpoint=f'/document-template-drafts/{remote_id}',
|
|
266
|
+
json=template.serialize_for_update(),
|
|
267
|
+
)
|
|
268
|
+
return _load_remote_template(body)
|
|
269
|
+
|
|
270
|
+
@handle_client_errors
|
|
271
|
+
async def update_template_draft(self, template: Template, remote_id: str) -> Template:
|
|
272
|
+
body = await self._put_json(
|
|
273
|
+
endpoint=f'/document-template-drafts/{remote_id}',
|
|
274
|
+
json=template.serialize_for_update(),
|
|
275
|
+
)
|
|
276
|
+
return _load_remote_template(body)
|
|
277
|
+
|
|
278
|
+
@handle_client_errors
|
|
279
|
+
async def post_template_draft_file(self, remote_id: str, file: TemplateFile):
|
|
280
|
+
data = await self._post_json(
|
|
281
|
+
endpoint=f'/document-template-drafts/{remote_id}/files',
|
|
282
|
+
json={
|
|
283
|
+
'fileName': file.filename.as_posix(),
|
|
284
|
+
'content': file.content.decode(consts.DEFAULT_ENCODING),
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
return _load_remote_file(data)
|
|
288
|
+
|
|
289
|
+
@handle_client_errors
|
|
290
|
+
async def put_template_draft_file_content(self, remote_id: str, file: TemplateFile):
|
|
291
|
+
self.session.headers.update(self._headers())
|
|
292
|
+
async with self.session.put(
|
|
293
|
+
f'{self.api_url}/document-template-drafts/{remote_id}'
|
|
294
|
+
f'/files/{file.remote_id}/content',
|
|
295
|
+
data=file.content,
|
|
296
|
+
headers={'Content-Type': 'text/plain;charset=UTF-8'},
|
|
297
|
+
) as r:
|
|
298
|
+
self._check_status(r, expected_status=200)
|
|
299
|
+
body = await r.json()
|
|
300
|
+
return _load_remote_file(body)
|
|
301
|
+
|
|
302
|
+
@handle_client_errors
|
|
303
|
+
async def post_template_draft_asset(self, remote_id: str, file: TemplateFile):
|
|
304
|
+
data = aiohttp.FormData()
|
|
305
|
+
data.add_field(
|
|
306
|
+
name='file',
|
|
307
|
+
value=file.content,
|
|
308
|
+
filename=file.filename.as_posix(),
|
|
309
|
+
content_type=file.content_type,
|
|
310
|
+
)
|
|
311
|
+
data.add_field(
|
|
312
|
+
name='fileName',
|
|
313
|
+
value=file.filename.as_posix(),
|
|
314
|
+
)
|
|
315
|
+
async with self.session.post(
|
|
316
|
+
f'{self.api_url}/document-template-drafts/{remote_id}/assets',
|
|
317
|
+
data=data,
|
|
318
|
+
headers=self._headers(),
|
|
319
|
+
) as r:
|
|
320
|
+
self._check_status(r, expected_status=201)
|
|
321
|
+
body = await r.json()
|
|
322
|
+
return _load_remote_asset(body, file.content)
|
|
323
|
+
|
|
324
|
+
@handle_client_errors
|
|
325
|
+
async def put_template_draft_asset_content(self, remote_id: str, file: TemplateFile):
|
|
326
|
+
data = aiohttp.FormData()
|
|
327
|
+
data.add_field(
|
|
328
|
+
name='file',
|
|
329
|
+
value=file.content,
|
|
330
|
+
filename=file.filename.as_posix(),
|
|
331
|
+
content_type=file.content_type,
|
|
332
|
+
)
|
|
333
|
+
data.add_field(
|
|
334
|
+
name='fileName',
|
|
335
|
+
value=file.filename.as_posix(),
|
|
336
|
+
)
|
|
337
|
+
async with self.session.put(
|
|
338
|
+
f'{self.api_url}/document-template-drafts/{remote_id}'
|
|
339
|
+
f'/assets/{file.remote_id}/content',
|
|
340
|
+
data=data,
|
|
341
|
+
headers=self._headers(),
|
|
342
|
+
) as r:
|
|
343
|
+
self._check_status(r, expected_status=200)
|
|
344
|
+
body = await r.json()
|
|
345
|
+
return _load_remote_asset(body, file.content)
|
|
346
|
+
|
|
347
|
+
@handle_client_errors
|
|
348
|
+
async def delete_template_draft(self, remote_id: str) -> bool:
|
|
349
|
+
return await self._delete(f'/document-template-drafts/{remote_id}')
|
|
350
|
+
|
|
351
|
+
@handle_client_errors
|
|
352
|
+
async def delete_template_draft_file(self, remote_id: str, file_id: str) -> bool:
|
|
353
|
+
if file_id is None:
|
|
354
|
+
raise RuntimeWarning('Tried to delete file without ID (None)')
|
|
355
|
+
return await self._delete(f'/document-template-drafts/{remote_id}/files/{file_id}')
|
|
356
|
+
|
|
357
|
+
@handle_client_errors
|
|
358
|
+
async def delete_template_draft_asset(self, remote_id: str, asset_id: str) -> bool:
|
|
359
|
+
if asset_id is None:
|
|
360
|
+
raise RuntimeWarning('Tried to delete asset without ID (None)')
|
|
361
|
+
return await self._delete(f'/document-template-drafts/{remote_id}/assets/{asset_id}')
|
|
362
|
+
|
|
363
|
+
@handle_client_errors
|
|
364
|
+
async def get_api_version(self) -> tuple[str, str | None]:
|
|
365
|
+
body = await self._get_json('/')
|
|
366
|
+
version = body.get('version')
|
|
367
|
+
metamodel_version = None
|
|
368
|
+
for item in body.get('metamodelVersions', []):
|
|
369
|
+
if item.get('name', '') == 'Document Template':
|
|
370
|
+
metamodel_version = item.get('version')
|
|
371
|
+
if version is None:
|
|
372
|
+
raise WizardCommunicationError(
|
|
373
|
+
reason='Invalid response',
|
|
374
|
+
message='Server did not return valid API version information (incompatible TDK?)',
|
|
375
|
+
)
|
|
376
|
+
return version, metamodel_version
|
|
377
|
+
|
|
378
|
+
@handle_client_errors
|
|
379
|
+
async def get_organization_id(self) -> str:
|
|
380
|
+
body = await self._get_json('/configs/bootstrap')
|
|
381
|
+
return body['organization']['organizationId']
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _load_remote_file(data: dict) -> TemplateFile:
|
|
385
|
+
content: str = data.get('content', '')
|
|
386
|
+
filename: str = str(data.get('fileName', ''))
|
|
387
|
+
return TemplateFile(
|
|
388
|
+
remote_id=data.get('uuid'),
|
|
389
|
+
remote_type=TemplateFileType.FILE,
|
|
390
|
+
filename=pathlib.Path(urllib.parse.unquote(filename)),
|
|
391
|
+
content=content.encode(encoding=consts.DEFAULT_ENCODING),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _load_remote_asset(data: dict, content: bytes) -> TemplateFile:
|
|
396
|
+
filename = str(data.get('fileName', ''))
|
|
397
|
+
return TemplateFile(
|
|
398
|
+
remote_id=data.get('uuid'),
|
|
399
|
+
remote_type=TemplateFileType.ASSET,
|
|
400
|
+
filename=pathlib.Path(urllib.parse.unquote(filename)),
|
|
401
|
+
content_type=data.get('contentType'),
|
|
402
|
+
content=content,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _load_remote_template(data: dict) -> Template:
|
|
407
|
+
return Template.load_remote(data)
|
dsw/tdk/build_info.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated file
|
|
2
|
+
# - do not overwrite
|
|
3
|
+
# - do not include in git
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
|
|
6
|
+
BuildInfo = namedtuple(
|
|
7
|
+
'BuildInfo',
|
|
8
|
+
['version', 'built_at', 'sha', 'branch', 'tag'],
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
BUILD_INFO = BuildInfo(
|
|
12
|
+
version='v4.27.0~8ec71bd',
|
|
13
|
+
built_at='2026-02-03 08:45:12Z',
|
|
14
|
+
sha='8ec71bd85dfbea66adedb6590f7d76ae5143bbaa',
|
|
15
|
+
branch='HEAD',
|
|
16
|
+
tag='v4.27.0',
|
|
17
|
+
)
|