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/api_client.py
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import aiohttp
|
|
2
|
-
import aiohttp.client_exceptions
|
|
3
|
-
import functools
|
|
4
|
-
import multidict
|
|
5
|
-
import pathlib
|
|
6
|
-
import urllib.parse
|
|
7
|
-
|
|
8
|
-
from typing import List, Optional
|
|
9
|
-
|
|
10
|
-
from dsw_tdk.consts import DEFAULT_ENCODING, APP, VERSION
|
|
11
|
-
from dsw_tdk.model import Template, TemplateFile, TemplateFileType
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class DSWCommunicationError(RuntimeError):
|
|
15
|
-
|
|
16
|
-
def __init__(self, reason: str, message: str):
|
|
17
|
-
"""
|
|
18
|
-
Exception representing communication error with DSW.
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
reason (str): Cause of the error.
|
|
22
|
-
message (str): Additional information about the error.
|
|
23
|
-
"""
|
|
24
|
-
self.reason = reason
|
|
25
|
-
self.message = message
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def handle_client_errors(func):
|
|
29
|
-
@functools.wraps(func)
|
|
30
|
-
async def handled_client_call(job, *args, **kwargs):
|
|
31
|
-
try:
|
|
32
|
-
return await func(job, *args, **kwargs)
|
|
33
|
-
except DSWCommunicationError as e:
|
|
34
|
-
# Already DSWCommunicationError (re-raise)
|
|
35
|
-
raise e
|
|
36
|
-
except aiohttp.client_exceptions.ContentTypeError as e:
|
|
37
|
-
raise DSWCommunicationError(
|
|
38
|
-
reason='Unexpected response type',
|
|
39
|
-
message=e.message
|
|
40
|
-
)
|
|
41
|
-
except aiohttp.client_exceptions.ClientResponseError as e:
|
|
42
|
-
raise DSWCommunicationError(
|
|
43
|
-
reason='Error response status',
|
|
44
|
-
message=f'Server responded with error HTTP status {e.status}: {e.message}'
|
|
45
|
-
)
|
|
46
|
-
except aiohttp.client_exceptions.InvalidURL as e:
|
|
47
|
-
raise DSWCommunicationError(
|
|
48
|
-
reason='Invalid URL',
|
|
49
|
-
message=f'Provided API URL seems invalid: {e.url}'
|
|
50
|
-
)
|
|
51
|
-
except aiohttp.client_exceptions.ClientConnectorError as e:
|
|
52
|
-
raise DSWCommunicationError(
|
|
53
|
-
reason='Server unreachable',
|
|
54
|
-
message=f'Desired server is not reachable (errno {e.os_error.errno})'
|
|
55
|
-
)
|
|
56
|
-
except Exception as e:
|
|
57
|
-
raise DSWCommunicationError(
|
|
58
|
-
reason='Communication error',
|
|
59
|
-
message=f'Communication with server failed ({e})'
|
|
60
|
-
)
|
|
61
|
-
return handled_client_call
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class DSWAPIClient:
|
|
65
|
-
|
|
66
|
-
def _headers(self, extra=None):
|
|
67
|
-
headers = {
|
|
68
|
-
'Authorization': f'Bearer {self.token}',
|
|
69
|
-
'User-Agent': f'{APP}/{VERSION}'
|
|
70
|
-
}
|
|
71
|
-
if extra is not None:
|
|
72
|
-
headers.update(extra)
|
|
73
|
-
return headers
|
|
74
|
-
|
|
75
|
-
@staticmethod
|
|
76
|
-
def _check_status(r: aiohttp.ClientResponse, expected_status):
|
|
77
|
-
r.raise_for_status()
|
|
78
|
-
if r.status != expected_status:
|
|
79
|
-
raise DSWCommunicationError(
|
|
80
|
-
reason='Unexpected response status',
|
|
81
|
-
message=f'Server responded with unexpected HTTP status {r.status}: '
|
|
82
|
-
f'{r.reason} (expecting {expected_status})'
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
def __init__(self, api_url: str, session=None):
|
|
86
|
-
"""
|
|
87
|
-
Exception representing communication error with DSW.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
api_url (str): URL of DSW API for HTTP communication.
|
|
91
|
-
session (aiohttp.ClientSession): Optional custom session for HTTP communication.
|
|
92
|
-
"""
|
|
93
|
-
self.api_url = api_url
|
|
94
|
-
self.token = None
|
|
95
|
-
self.session = session or aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False))
|
|
96
|
-
|
|
97
|
-
async def close(self):
|
|
98
|
-
await self.session.close()
|
|
99
|
-
|
|
100
|
-
async def safe_close(self) -> bool:
|
|
101
|
-
try:
|
|
102
|
-
await self.session.close()
|
|
103
|
-
return True
|
|
104
|
-
except Exception:
|
|
105
|
-
return False
|
|
106
|
-
|
|
107
|
-
async def _post_json(self, endpoint, json) -> dict:
|
|
108
|
-
async with self.session.post(f'{self.api_url}{endpoint}', json=json, headers=self._headers()) as r:
|
|
109
|
-
self._check_status(r, expected_status=201)
|
|
110
|
-
return await r.json()
|
|
111
|
-
|
|
112
|
-
async def _put_json(self, endpoint, json) -> dict:
|
|
113
|
-
async with self.session.put(f'{self.api_url}{endpoint}', json=json, headers=self._headers()) as r:
|
|
114
|
-
self._check_status(r, expected_status=200)
|
|
115
|
-
return await r.json()
|
|
116
|
-
|
|
117
|
-
async def _get_json(self, endpoint) -> dict:
|
|
118
|
-
async with self.session.get(f'{self.api_url}{endpoint}', headers=self._headers()) as r:
|
|
119
|
-
self._check_status(r, expected_status=200)
|
|
120
|
-
return await r.json()
|
|
121
|
-
|
|
122
|
-
async def _get_bytes(self, endpoint) -> bytes:
|
|
123
|
-
async with self.session.get(f'{self.api_url}{endpoint}', headers=self._headers()) as r:
|
|
124
|
-
self._check_status(r, expected_status=200)
|
|
125
|
-
return await r.read()
|
|
126
|
-
|
|
127
|
-
async def _delete(self, endpoint) -> bool:
|
|
128
|
-
async with self.session.delete(f'{self.api_url}{endpoint}', headers=self._headers()) as r:
|
|
129
|
-
return r.status == 204
|
|
130
|
-
|
|
131
|
-
@handle_client_errors
|
|
132
|
-
async def login(self, email: str, password: str) -> Optional[str]:
|
|
133
|
-
req = {'email': email, 'password': password}
|
|
134
|
-
body = await self._post_json('/tokens', json=req)
|
|
135
|
-
self.token = body.get('token', None)
|
|
136
|
-
return self.token
|
|
137
|
-
|
|
138
|
-
@handle_client_errors
|
|
139
|
-
async def check_template_exists(self, template_id: str) -> bool:
|
|
140
|
-
async with self.session.get(f'{self.api_url}/templates/{template_id}', headers=self._headers()) as r:
|
|
141
|
-
if r.status == 404:
|
|
142
|
-
return False
|
|
143
|
-
self._check_status(r, expected_status=200)
|
|
144
|
-
return True
|
|
145
|
-
|
|
146
|
-
@handle_client_errors
|
|
147
|
-
async def get_templates(self) -> List[Template]:
|
|
148
|
-
body = await self._get_json('/templates/all')
|
|
149
|
-
return list(map(_load_remote_template, body))
|
|
150
|
-
|
|
151
|
-
@handle_client_errors
|
|
152
|
-
async def get_template(self, template_id: str) -> Template:
|
|
153
|
-
body = await self._get_json(f'/templates/{template_id}')
|
|
154
|
-
return _load_remote_template(body)
|
|
155
|
-
|
|
156
|
-
@handle_client_errors
|
|
157
|
-
async def get_template_files(self, template_id: str) -> List[TemplateFile]:
|
|
158
|
-
body = await self._get_json(f'/templates/{template_id}/files')
|
|
159
|
-
return list(map(_load_remote_file, body))
|
|
160
|
-
|
|
161
|
-
@handle_client_errors
|
|
162
|
-
async def get_template_file(self, template_id: str, file_id: str) -> TemplateFile:
|
|
163
|
-
body = await self._get_json(f'/templates/{template_id}/files/{file_id}')
|
|
164
|
-
return _load_remote_file(body)
|
|
165
|
-
|
|
166
|
-
@handle_client_errors
|
|
167
|
-
async def get_template_assets(self, template_id: str) -> List[TemplateFile]:
|
|
168
|
-
body = await self._get_json(f'/templates/{template_id}/assets')
|
|
169
|
-
result = []
|
|
170
|
-
for file_body in body:
|
|
171
|
-
asset_id = file_body['uuid']
|
|
172
|
-
content = await self._get_bytes(f'/templates/{template_id}/assets/{asset_id}/content')
|
|
173
|
-
result.append(_load_remote_asset(file_body, content))
|
|
174
|
-
return result
|
|
175
|
-
|
|
176
|
-
@handle_client_errors
|
|
177
|
-
async def get_template_asset(self, template_id: str, asset_id: str) -> TemplateFile:
|
|
178
|
-
body = await self._get_json(f'/templates/{template_id}/assets/{asset_id}')
|
|
179
|
-
content = await self._get_bytes(f'/templates/{template_id}/assets/{asset_id}/content')
|
|
180
|
-
return _load_remote_asset(body, content)
|
|
181
|
-
|
|
182
|
-
@handle_client_errors
|
|
183
|
-
async def post_template(self, template: Template) -> Template:
|
|
184
|
-
body = await self._post_json(
|
|
185
|
-
endpoint='/templates',
|
|
186
|
-
json=template.serialize_remote(),
|
|
187
|
-
)
|
|
188
|
-
return _load_remote_template(body)
|
|
189
|
-
|
|
190
|
-
@handle_client_errors
|
|
191
|
-
async def put_template(self, template: Template) -> Template:
|
|
192
|
-
body = await self._put_json(
|
|
193
|
-
endpoint=f'/templates/{template.id}',
|
|
194
|
-
json=template.serialize_remote(),
|
|
195
|
-
)
|
|
196
|
-
return _load_remote_template(body)
|
|
197
|
-
|
|
198
|
-
@handle_client_errors
|
|
199
|
-
async def post_template_file(self, template_id: str, tfile: TemplateFile):
|
|
200
|
-
data = await self._post_json(
|
|
201
|
-
endpoint=f'/templates/{template_id}/files',
|
|
202
|
-
json={
|
|
203
|
-
'fileName': tfile.filename.as_posix(),
|
|
204
|
-
'content': tfile.content.decode(DEFAULT_ENCODING)
|
|
205
|
-
}
|
|
206
|
-
)
|
|
207
|
-
return _load_remote_file(data)
|
|
208
|
-
|
|
209
|
-
@handle_client_errors
|
|
210
|
-
async def post_template_asset(self, template_id: str, tfile: TemplateFile):
|
|
211
|
-
headers = multidict.CIMultiDict() # type: multidict.CIMultiDict
|
|
212
|
-
headers[aiohttp.hdrs.CONTENT_TYPE] = tfile.content_type
|
|
213
|
-
headers[aiohttp.hdrs.CONTENT_DISPOSITION] = f'form-data; name="file"; ' \
|
|
214
|
-
f'filename="{tfile.filename.as_posix()}"'
|
|
215
|
-
with aiohttp.MultipartWriter('form-data', boundary='tdk-asset-boundary') as mpwriter:
|
|
216
|
-
mpwriter.append(tfile.content, headers=headers)
|
|
217
|
-
async with self.session.post(
|
|
218
|
-
f'{self.api_url}/templates/{template_id}/assets',
|
|
219
|
-
data=mpwriter,
|
|
220
|
-
headers=self._headers()
|
|
221
|
-
) as r:
|
|
222
|
-
self._check_status(r, expected_status=201)
|
|
223
|
-
body = await r.json()
|
|
224
|
-
return _load_remote_asset(body, tfile.content)
|
|
225
|
-
|
|
226
|
-
@handle_client_errors
|
|
227
|
-
async def delete_template(self, template_id: str) -> bool:
|
|
228
|
-
return await self._delete(f'/templates/{template_id}')
|
|
229
|
-
|
|
230
|
-
@handle_client_errors
|
|
231
|
-
async def delete_template_file(self, template_id: str, file_id: str) -> bool:
|
|
232
|
-
if file_id is None:
|
|
233
|
-
raise RuntimeWarning('Tried to delete file without ID (None)')
|
|
234
|
-
return await self._delete(f'/templates/{template_id}/files/{file_id}')
|
|
235
|
-
|
|
236
|
-
@handle_client_errors
|
|
237
|
-
async def delete_template_asset(self, template_id: str, asset_id: str) -> bool:
|
|
238
|
-
if asset_id is None:
|
|
239
|
-
raise RuntimeWarning('Tried to delete asset without ID (None)')
|
|
240
|
-
return await self._delete(f'/templates/{template_id}/assets/{asset_id}')
|
|
241
|
-
|
|
242
|
-
@handle_client_errors
|
|
243
|
-
async def get_api_version(self) -> str:
|
|
244
|
-
body = await self._get_json('/')
|
|
245
|
-
return body['version']
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def _load_remote_file(data: dict) -> TemplateFile:
|
|
249
|
-
content = data.get('content', None) # type: str
|
|
250
|
-
filename = str(data.get('fileName', '')) # type: str
|
|
251
|
-
template_file = TemplateFile(
|
|
252
|
-
remote_id=data.get('uuid', None),
|
|
253
|
-
remote_type=TemplateFileType.file,
|
|
254
|
-
filename=pathlib.Path(urllib.parse.unquote(filename)),
|
|
255
|
-
content=content.encode(encoding=DEFAULT_ENCODING),
|
|
256
|
-
)
|
|
257
|
-
return template_file
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def _load_remote_asset(data: dict, content: bytes) -> TemplateFile:
|
|
261
|
-
filename = str(data.get('fileName', ''))
|
|
262
|
-
template_file = TemplateFile(
|
|
263
|
-
remote_id=data.get('uuid', None),
|
|
264
|
-
remote_type=TemplateFileType.asset,
|
|
265
|
-
filename=pathlib.Path(urllib.parse.unquote(filename)),
|
|
266
|
-
content_type=data.get('contentType', None),
|
|
267
|
-
content=content,
|
|
268
|
-
)
|
|
269
|
-
return template_file
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def _load_remote_template(data: dict) -> Template:
|
|
273
|
-
return Template.load_remote(data)
|
dsw_tdk/cli.py
DELETED
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import click # type: ignore
|
|
3
|
-
import datetime
|
|
4
|
-
import dotenv
|
|
5
|
-
import humanize # type: ignore
|
|
6
|
-
import logging
|
|
7
|
-
import mimetypes
|
|
8
|
-
import pathlib
|
|
9
|
-
import slugify
|
|
10
|
-
import watchgod # type: ignore
|
|
11
|
-
|
|
12
|
-
from typing import Dict
|
|
13
|
-
|
|
14
|
-
from dsw_tdk.api_client import DSWCommunicationError
|
|
15
|
-
from dsw_tdk.core import TDKCore, TDKProcessingError
|
|
16
|
-
from dsw_tdk.consts import VERSION, DEFAULT_LIST_FORMAT
|
|
17
|
-
from dsw_tdk.model import Template
|
|
18
|
-
from dsw_tdk.utils import TemplateBuilder, FormatSpec
|
|
19
|
-
from dsw_tdk.validation import ValidationError
|
|
20
|
-
|
|
21
|
-
CURRENT_DIR = pathlib.Path.cwd()
|
|
22
|
-
DIR_TYPE = click.Path(exists=True, dir_okay=True, file_okay=False, resolve_path=True,
|
|
23
|
-
readable=True, writable=True)
|
|
24
|
-
NEW_DIR_TYPE = click.Path(dir_okay=True, file_okay=False, resolve_path=True,
|
|
25
|
-
readable=True, writable=True)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class ClickPrinter:
|
|
29
|
-
|
|
30
|
-
CHANGE_SIGNS = {
|
|
31
|
-
watchgod.Change.added: click.style('+', fg='green'),
|
|
32
|
-
watchgod.Change.modified: click.style('*', fg='yellow'),
|
|
33
|
-
watchgod.Change.deleted: click.style('-', fg='red'),
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
@staticmethod
|
|
37
|
-
def error(message: str, **kwargs):
|
|
38
|
-
click.secho(message=message, err=True, fg='red', **kwargs)
|
|
39
|
-
|
|
40
|
-
@staticmethod
|
|
41
|
-
def success(message: str):
|
|
42
|
-
click.secho('SUCCESS', fg='green', bold=True, nl=False)
|
|
43
|
-
click.echo(f': {message}')
|
|
44
|
-
|
|
45
|
-
@staticmethod
|
|
46
|
-
def failure(message: str):
|
|
47
|
-
click.secho('FAILURE', fg='red', bold=True, nl=False)
|
|
48
|
-
click.echo(f': {message}')
|
|
49
|
-
|
|
50
|
-
@staticmethod
|
|
51
|
-
def watch(message: str):
|
|
52
|
-
click.secho('WATCH', fg='blue', bold=True, nl=False)
|
|
53
|
-
click.echo(f': {message}')
|
|
54
|
-
|
|
55
|
-
@classmethod
|
|
56
|
-
def watch_change(cls, change_type: watchgod.Change, filepath: pathlib.Path, root: pathlib.Path):
|
|
57
|
-
timestamp = datetime.datetime.now().isoformat(timespec='milliseconds')
|
|
58
|
-
sign = cls.CHANGE_SIGNS[change_type]
|
|
59
|
-
click.secho('WATCH', fg='blue', bold=True, nl=False)
|
|
60
|
-
click.echo(f'@{timestamp} {sign} {filepath.relative_to(root)}')
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def prompt_fill(text: str, obj, attr, **kwargs):
|
|
64
|
-
while True:
|
|
65
|
-
try:
|
|
66
|
-
setattr(obj, attr, click.prompt(text, **kwargs).strip())
|
|
67
|
-
break
|
|
68
|
-
except ValidationError as e:
|
|
69
|
-
ClickPrinter.error(e.message)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def print_template_info(template: Template):
|
|
73
|
-
click.echo(f'Template ID: {template.id}')
|
|
74
|
-
click.echo(f'Name: {template.name}')
|
|
75
|
-
click.echo(f'License: {template.license}')
|
|
76
|
-
click.echo(f'Description: {template.description}')
|
|
77
|
-
click.echo('Formats:')
|
|
78
|
-
for format_spec in template.formats:
|
|
79
|
-
click.echo(f' - {format_spec.name}')
|
|
80
|
-
click.echo('Files:')
|
|
81
|
-
for tfile in template.files.values():
|
|
82
|
-
filesize = humanize.naturalsize(len(tfile.content))
|
|
83
|
-
click.echo(f' - {tfile.filename.as_posix()} [{filesize}]')
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class ClickLogger(logging.Logger):
|
|
87
|
-
|
|
88
|
-
NAME = 'DSW-TDK-CLI'
|
|
89
|
-
LEVEL_STYLES = {
|
|
90
|
-
logging.CRITICAL: lambda x: click.style(x, fg='red', bold=True),
|
|
91
|
-
logging.ERROR: lambda x: click.style(x, fg='red'),
|
|
92
|
-
logging.WARNING: lambda x: click.style(x, fg='yellow'),
|
|
93
|
-
logging.INFO: lambda x: click.style(x, fg='cyan'),
|
|
94
|
-
logging.DEBUG: lambda x: click.style(x, fg='magenta'),
|
|
95
|
-
}
|
|
96
|
-
LEVELS = [
|
|
97
|
-
logging.getLevelName(logging.CRITICAL),
|
|
98
|
-
logging.getLevelName(logging.ERROR),
|
|
99
|
-
logging.getLevelName(logging.WARNING),
|
|
100
|
-
logging.getLevelName(logging.INFO),
|
|
101
|
-
logging.getLevelName(logging.DEBUG),
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
def __init__(self, show_timestamp: bool = False, show_level: bool = True, colors: bool = True):
|
|
105
|
-
super().__init__(name=self.NAME)
|
|
106
|
-
self.show_timestamp = show_timestamp
|
|
107
|
-
self.show_level = show_level
|
|
108
|
-
self.colors = colors
|
|
109
|
-
self.muted = False
|
|
110
|
-
|
|
111
|
-
def _format_level(self, level, justify=False):
|
|
112
|
-
name = logging.getLevelName(level) # type: str
|
|
113
|
-
if justify:
|
|
114
|
-
name = name.ljust(8, ' ')
|
|
115
|
-
if self.colors and level in self.LEVEL_STYLES.keys():
|
|
116
|
-
name = self.LEVEL_STYLES[level](name)
|
|
117
|
-
return name
|
|
118
|
-
|
|
119
|
-
def _print_message(self, level, message):
|
|
120
|
-
if self.show_timestamp:
|
|
121
|
-
timestamp = datetime.datetime.now().isoformat(timespec='milliseconds')
|
|
122
|
-
click.echo(timestamp + ' | ', nl=False)
|
|
123
|
-
if self.show_level:
|
|
124
|
-
sep = ' | ' if self.show_timestamp else ': '
|
|
125
|
-
click.echo(self._format_level(level, justify=self.show_timestamp) + sep, nl=False)
|
|
126
|
-
click.echo(message)
|
|
127
|
-
|
|
128
|
-
def _log(self, level, msg, *args, **kwargs):
|
|
129
|
-
if not self.muted:
|
|
130
|
-
# super()._log(level, msg, args, exc_info, extra, stack_info, stacklevel)
|
|
131
|
-
self._print_message(level, msg)
|
|
132
|
-
|
|
133
|
-
@staticmethod
|
|
134
|
-
def default():
|
|
135
|
-
logger = ClickLogger()
|
|
136
|
-
logger.setLevel('INFO')
|
|
137
|
-
return logger
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class AliasedGroup(click.Group):
|
|
141
|
-
|
|
142
|
-
def get_command(self, ctx, cmd_name):
|
|
143
|
-
rv = click.Group.get_command(self, ctx, cmd_name)
|
|
144
|
-
if rv is not None:
|
|
145
|
-
return rv
|
|
146
|
-
matches = [x for x in self.list_commands(ctx)
|
|
147
|
-
if x.startswith(cmd_name)]
|
|
148
|
-
if not matches:
|
|
149
|
-
return None
|
|
150
|
-
elif len(matches) == 1:
|
|
151
|
-
return click.Group.get_command(self, ctx, matches[0])
|
|
152
|
-
ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class CLIContext:
|
|
156
|
-
|
|
157
|
-
def __init__(self):
|
|
158
|
-
self.logger = ClickLogger.default()
|
|
159
|
-
|
|
160
|
-
def debug_mode(self):
|
|
161
|
-
self.logger.show_timestamp = True
|
|
162
|
-
self.logger.setLevel(level=logging.DEBUG)
|
|
163
|
-
|
|
164
|
-
def quiet_mode(self):
|
|
165
|
-
self.logger.muted = True
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def interact_formats() -> Dict[str, FormatSpec]:
|
|
169
|
-
add_format = click.confirm('Do you want to add a format?', default=True)
|
|
170
|
-
formats = dict() # type: Dict[str, FormatSpec]
|
|
171
|
-
while add_format:
|
|
172
|
-
format_spec = FormatSpec()
|
|
173
|
-
prompt_fill('Format name', obj=format_spec, attr='name', default='HTML')
|
|
174
|
-
if format_spec.name not in formats.keys() or click.confirm(
|
|
175
|
-
'There is already a format with this name. Do you want to change it?'
|
|
176
|
-
):
|
|
177
|
-
prompt_fill('File extension', obj=format_spec, attr='file_extension',
|
|
178
|
-
default=format_spec.name.lower() if ' ' not in format_spec.name else None)
|
|
179
|
-
prompt_fill('Content type', obj=format_spec, attr='content_type',
|
|
180
|
-
default=mimetypes.types_map.get(f'.{format_spec.file_extension}', None))
|
|
181
|
-
default_filename = str(pathlib.Path('src') / f'template.{format_spec.file_extension}.j2')
|
|
182
|
-
prompt_fill('Jinja2 filename', obj=format_spec, attr='filename', default=default_filename)
|
|
183
|
-
formats[format_spec.name] = format_spec
|
|
184
|
-
click.echo('=' * 60)
|
|
185
|
-
add_format = click.confirm('Do you want to add yet another format?', default=False)
|
|
186
|
-
return formats
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def interact_builder(builder: TemplateBuilder):
|
|
190
|
-
prompt_fill('Template name', obj=builder, attr='name')
|
|
191
|
-
prompt_fill('Organization ID', obj=builder, attr='organization_id')
|
|
192
|
-
prompt_fill('Template ID', obj=builder, attr='template_id', default=slugify.slugify(builder.name))
|
|
193
|
-
prompt_fill('Version', obj=builder, attr='version', default='0.1.0')
|
|
194
|
-
prompt_fill('Description', obj=builder, attr='description', default='My custom template')
|
|
195
|
-
prompt_fill('License', obj=builder, attr='license', default='CC0')
|
|
196
|
-
click.echo('=' * 60)
|
|
197
|
-
formats = interact_formats()
|
|
198
|
-
for format_spec in formats.values():
|
|
199
|
-
builder.add_format(format_spec)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def load_local(tdk: TDKCore, template_dir: pathlib.Path):
|
|
203
|
-
try:
|
|
204
|
-
tdk.load_local(template_dir=template_dir)
|
|
205
|
-
except Exception as e:
|
|
206
|
-
ClickPrinter.failure('Could not load local template')
|
|
207
|
-
ClickPrinter.error(f'> {e}')
|
|
208
|
-
exit(1)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def dir_from_id(template_id: str) -> pathlib.Path:
|
|
212
|
-
return pathlib.Path.cwd() / template_id.replace(':', '_')
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
#############################################################################################################
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
@click.group(cls=AliasedGroup)
|
|
219
|
-
@click.option('-e', '--dot-env', default='.env', required=False, show_default=True,
|
|
220
|
-
type=click.Path(file_okay=True, dir_okay=False),
|
|
221
|
-
help='Provide file with environment variables.')
|
|
222
|
-
@click.option('-q', '--quiet', is_flag=True,
|
|
223
|
-
help='Hide additional information logs.')
|
|
224
|
-
@click.option('--debug', is_flag=True,
|
|
225
|
-
help='Enable debug logging.')
|
|
226
|
-
@click.version_option(version=VERSION)
|
|
227
|
-
@click.pass_context
|
|
228
|
-
def main(ctx, quiet, debug, dot_env):
|
|
229
|
-
if pathlib.Path(dot_env).exists():
|
|
230
|
-
dotenv.load_dotenv(dotenv_path=dot_env)
|
|
231
|
-
ctx.ensure_object(CLIContext)
|
|
232
|
-
if quiet:
|
|
233
|
-
ctx.obj.quiet_mode()
|
|
234
|
-
if debug:
|
|
235
|
-
ctx.obj.debug_mode()
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
@main.command(help='Create a new DSW template project.', name='new')
|
|
239
|
-
@click.argument('TEMPLATE-DIR', type=NEW_DIR_TYPE, default=None, required=False)
|
|
240
|
-
@click.option('-f', '--force', is_flag=True, help='Overwrite any matching files.')
|
|
241
|
-
@click.pass_context
|
|
242
|
-
def new_template(ctx, template_dir, force):
|
|
243
|
-
builder = TemplateBuilder()
|
|
244
|
-
try:
|
|
245
|
-
interact_builder(builder)
|
|
246
|
-
except Exception:
|
|
247
|
-
click.echo('')
|
|
248
|
-
ClickPrinter.failure('Exited...')
|
|
249
|
-
exit(1)
|
|
250
|
-
tdk = TDKCore(template=builder.build(), logger=ctx.obj.logger)
|
|
251
|
-
template_dir = template_dir or dir_from_id(tdk.template.id)
|
|
252
|
-
tdk.prepare_local(template_dir=template_dir)
|
|
253
|
-
try:
|
|
254
|
-
tdk.store_local(force=force)
|
|
255
|
-
ClickPrinter.success(f'Template project created: {template_dir}')
|
|
256
|
-
except Exception as e:
|
|
257
|
-
ClickPrinter.failure('Could not create new template project')
|
|
258
|
-
ClickPrinter.error(f'> {e}')
|
|
259
|
-
exit(1)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
@main.command(help='Download template from DSW.', name='get')
|
|
263
|
-
@click.argument('TEMPLATE-ID')
|
|
264
|
-
@click.argument('TEMPLATE-DIR', type=NEW_DIR_TYPE, default=None, required=False)
|
|
265
|
-
@click.option('-s', '--api-server', metavar='API-URL', envvar='DSW_API',
|
|
266
|
-
prompt='URL of DSW API', help='URL of DSW server API.')
|
|
267
|
-
@click.option('-u', '--username', envvar='DSW_USERNAME', prompt='Username', hide_input=False,
|
|
268
|
-
metavar='EMAIL', help='Admin username (email) for DSW instance.')
|
|
269
|
-
@click.option('-p', '--password', envvar='DSW_PASSWORD', prompt='Email', hide_input=True,
|
|
270
|
-
metavar='PASSWORD', help='Admin password for DSW instance.')
|
|
271
|
-
@click.option('-f', '--force', is_flag=True, help='Overwrite any existing files.')
|
|
272
|
-
@click.pass_context
|
|
273
|
-
def get_template(ctx, api_server, template_id, template_dir, username, password, force):
|
|
274
|
-
template_dir = template_dir or dir_from_id(template_id)
|
|
275
|
-
|
|
276
|
-
async def main_routine():
|
|
277
|
-
tdk = TDKCore(logger=ctx.obj.logger)
|
|
278
|
-
try:
|
|
279
|
-
await tdk.init_client(api_url=api_server, username=username, password=password)
|
|
280
|
-
await tdk.load_remote(template_id=template_id)
|
|
281
|
-
await tdk.client.close()
|
|
282
|
-
except DSWCommunicationError as e:
|
|
283
|
-
ClickPrinter.error('Could not get template:', bold=True)
|
|
284
|
-
ClickPrinter.error(f'> {e.reason}\n> {e.message}')
|
|
285
|
-
exit(1)
|
|
286
|
-
await tdk.client.safe_close()
|
|
287
|
-
tdk.prepare_local(template_dir=template_dir)
|
|
288
|
-
try:
|
|
289
|
-
tdk.store_local(force=force)
|
|
290
|
-
ClickPrinter.success(f'Template {template_id} downloaded to {template_dir}')
|
|
291
|
-
except Exception as e:
|
|
292
|
-
ClickPrinter.failure('Could not store template locally')
|
|
293
|
-
ClickPrinter.error(f'> {e}')
|
|
294
|
-
exit(1)
|
|
295
|
-
|
|
296
|
-
loop = asyncio.get_event_loop()
|
|
297
|
-
loop.run_until_complete(main_routine())
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
@main.command(help='Upload template to DSW.', name='put')
|
|
301
|
-
@click.argument('TEMPLATE-DIR', type=DIR_TYPE, default=CURRENT_DIR, required=False)
|
|
302
|
-
@click.option('-s', '--api-server', metavar='API-URL', envvar='DSW_API',
|
|
303
|
-
prompt='URL of DSW API', help='URL of DSW server API.')
|
|
304
|
-
@click.option('-u', '--username', envvar='DSW_USERNAME', prompt='Username', hide_input=False,
|
|
305
|
-
metavar='USERNAME', help='Admin username (email address) for DSW instance.')
|
|
306
|
-
@click.option('-p', '--password', envvar='DSW_PASSWORD', prompt='Password', hide_input=True,
|
|
307
|
-
metavar='PASSWORD', help='Admin password for DSW instance.')
|
|
308
|
-
@click.option('-f', '--force', is_flag=True, help='Delete template if already exists.')
|
|
309
|
-
@click.option('-w', '--watch', is_flag=True, help='Enter watch mode to continually upload changes.')
|
|
310
|
-
@click.pass_context
|
|
311
|
-
def put_template(ctx, api_server, template_dir, username, password, force, watch):
|
|
312
|
-
tdk = TDKCore(logger=ctx.obj.logger)
|
|
313
|
-
|
|
314
|
-
async def watch_callback(changes):
|
|
315
|
-
changes = list(changes)
|
|
316
|
-
for change in changes:
|
|
317
|
-
ClickPrinter.watch_change(*change, root=tdk.project.template_dir)
|
|
318
|
-
if len(changes) > 0:
|
|
319
|
-
await tdk.process_changes(changes, force=force)
|
|
320
|
-
|
|
321
|
-
async def main_routine():
|
|
322
|
-
load_local(tdk, template_dir)
|
|
323
|
-
try:
|
|
324
|
-
await tdk.init_client(api_server, username, password)
|
|
325
|
-
await tdk.store_remote(force=force)
|
|
326
|
-
ClickPrinter.success(f'Template {tdk.project.template.id} uploaded to {api_server}')
|
|
327
|
-
|
|
328
|
-
if watch:
|
|
329
|
-
ClickPrinter.watch('Entering watch mode... (press Ctrl+C to abort)')
|
|
330
|
-
await tdk.watch_project(watch_callback)
|
|
331
|
-
|
|
332
|
-
await tdk.client.close()
|
|
333
|
-
except TDKProcessingError as e:
|
|
334
|
-
ClickPrinter.failure('Could not upload template')
|
|
335
|
-
ClickPrinter.error(f'> {e.message}\n> {e.hint}')
|
|
336
|
-
await tdk.client.safe_close()
|
|
337
|
-
exit(1)
|
|
338
|
-
except DSWCommunicationError as e:
|
|
339
|
-
ClickPrinter.failure('Could not upload template')
|
|
340
|
-
ClickPrinter.error(f'> {e.reason}\n> {e.message}')
|
|
341
|
-
ClickPrinter.error('> Probably incorrect API URL, metamodel version, '
|
|
342
|
-
'or template already exists...')
|
|
343
|
-
ClickPrinter.error('> Check if you are using the matching version')
|
|
344
|
-
await tdk.client.safe_close()
|
|
345
|
-
exit(1)
|
|
346
|
-
|
|
347
|
-
loop = asyncio.get_event_loop()
|
|
348
|
-
loop.run_until_complete(main_routine())
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
@main.command(help='Create ZIP package for DSW template.', name='package')
|
|
352
|
-
@click.argument('TEMPLATE-DIR', type=DIR_TYPE, default=CURRENT_DIR, required=False)
|
|
353
|
-
@click.option('-o', '--output', default='template.zip', type=click.Path(writable=True),
|
|
354
|
-
show_default=True, help='Target package file.')
|
|
355
|
-
@click.option('-f', '--force', is_flag=True, help='Delete package if already exists.')
|
|
356
|
-
@click.pass_context
|
|
357
|
-
def create_package(ctx, template_dir, output, force):
|
|
358
|
-
tdk = TDKCore(logger=ctx.obj.logger)
|
|
359
|
-
load_local(tdk, template_dir)
|
|
360
|
-
try:
|
|
361
|
-
tdk.create_package(output=pathlib.Path(output), force=force)
|
|
362
|
-
except Exception as e:
|
|
363
|
-
ClickPrinter.failure('Failed to create the package')
|
|
364
|
-
ClickPrinter.error(f'> {e}')
|
|
365
|
-
exit(1)
|
|
366
|
-
filename = click.style(output, bold=True)
|
|
367
|
-
ClickPrinter.success(f'Package {filename} created')
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
@main.command(help='List templates from DSW via API.', name='list')
|
|
371
|
-
@click.option('-s', '--api-server', metavar='API-URL', envvar='DSW_API', prompt=True,
|
|
372
|
-
help='URL of DSW server API.')
|
|
373
|
-
@click.option('-u', '--username', envvar='DSW_USERNAME', prompt=True, hide_input=False,
|
|
374
|
-
metavar='EMAIL', help='Admin username (email) for DSW instance.')
|
|
375
|
-
@click.option('-p', '--password', envvar='DSW_PASSWORD', prompt=True, hide_input=True,
|
|
376
|
-
metavar='PASSWORD', help='Admin password for DSW instance.')
|
|
377
|
-
@click.option('--output-format', default=DEFAULT_LIST_FORMAT,
|
|
378
|
-
metavar='FORMAT', help='Entry format string for printing.')
|
|
379
|
-
@click.pass_context
|
|
380
|
-
def list_templates(ctx, api_server, username, password, output_format):
|
|
381
|
-
async def main_routine():
|
|
382
|
-
tdk = TDKCore(logger=ctx.obj.logger)
|
|
383
|
-
try:
|
|
384
|
-
await tdk.init_client(api_server, username, password)
|
|
385
|
-
templates = await tdk.list_remote()
|
|
386
|
-
for template in templates:
|
|
387
|
-
click.echo(output_format.format(template=template))
|
|
388
|
-
except DSWCommunicationError as e:
|
|
389
|
-
ClickPrinter.failure('Failed to get list of templates')
|
|
390
|
-
ClickPrinter.error(f'> {e.reason}\n> {e.message}')
|
|
391
|
-
exit(1)
|
|
392
|
-
await tdk.client.safe_close()
|
|
393
|
-
|
|
394
|
-
loop = asyncio.get_event_loop()
|
|
395
|
-
loop.run_until_complete(main_routine())
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
@main.command(help='Verify DSW template project.', name='verify')
|
|
399
|
-
@click.argument('TEMPLATE-DIR', type=DIR_TYPE, default=CURRENT_DIR, required=False)
|
|
400
|
-
@click.pass_context
|
|
401
|
-
def verify_template(ctx, template_dir):
|
|
402
|
-
tdk = TDKCore(logger=ctx.obj.logger)
|
|
403
|
-
load_local(tdk, template_dir)
|
|
404
|
-
errors = tdk.verify()
|
|
405
|
-
if len(errors) == 0:
|
|
406
|
-
ClickPrinter.success('The template is valid!')
|
|
407
|
-
print_template_info(template=tdk.project.template)
|
|
408
|
-
else:
|
|
409
|
-
ClickPrinter.failure('The template is invalid!')
|
|
410
|
-
click.echo('Found violations:')
|
|
411
|
-
for err in errors:
|
|
412
|
-
click.echo(f' - {err.field_name}: {err.message}')
|