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/cli.py
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
# pylint: disable=too-many-positional-arguments
|
|
2
|
+
import asyncio
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
import mimetypes
|
|
6
|
+
import pathlib
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import humanize
|
|
12
|
+
import slugify
|
|
13
|
+
import watchfiles
|
|
14
|
+
|
|
15
|
+
from . import consts
|
|
16
|
+
from .api_client import WizardCommunicationError
|
|
17
|
+
from .config import CONFIG
|
|
18
|
+
from .core import TDKCore, TDKProcessingError
|
|
19
|
+
from .model import Template
|
|
20
|
+
from .utils import FormatSpec, TemplateBuilder, create_dot_env, safe_utf8
|
|
21
|
+
from .validation import ValidationError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
CURRENT_DIR = pathlib.Path.cwd()
|
|
25
|
+
DIR_TYPE = click.Path(exists=True, dir_okay=True, file_okay=False, resolve_path=True,
|
|
26
|
+
readable=True, writable=True)
|
|
27
|
+
FILE_READ_TYPE = click.Path(exists=True, dir_okay=False, file_okay=True, resolve_path=True,
|
|
28
|
+
readable=True)
|
|
29
|
+
NEW_DIR_TYPE = click.Path(dir_okay=True, file_okay=False, resolve_path=True,
|
|
30
|
+
readable=True, writable=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _now() -> datetime.datetime:
|
|
34
|
+
return datetime.datetime.now(tz=datetime.UTC)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ClickPrinter:
|
|
38
|
+
|
|
39
|
+
CHANGE_SIGNS = {
|
|
40
|
+
watchfiles.Change.added: click.style('+', fg='green'),
|
|
41
|
+
watchfiles.Change.modified: click.style('*', fg='yellow'),
|
|
42
|
+
watchfiles.Change.deleted: click.style('-', fg='red'),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def error(message: str, **kwargs):
|
|
47
|
+
click.secho(message=message, err=True, fg='red', **kwargs)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def warning(message: str, **kwargs):
|
|
51
|
+
click.secho('WARNING', fg='yellow', bold=True, nl=False, **kwargs)
|
|
52
|
+
click.echo(f': {message}')
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def success(message: str):
|
|
56
|
+
click.secho('SUCCESS', fg='green', bold=True, nl=False)
|
|
57
|
+
click.echo(f': {message}')
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def failure(message: str):
|
|
61
|
+
click.secho('FAILURE', fg='red', bold=True, nl=False)
|
|
62
|
+
click.echo(f': {message}')
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def watch(message: str):
|
|
66
|
+
click.secho('WATCH', fg='blue', bold=True, nl=False)
|
|
67
|
+
click.echo(f': {message}')
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def watch_change(cls, change_type: watchfiles.Change, filepath: pathlib.Path,
|
|
71
|
+
root: pathlib.Path):
|
|
72
|
+
timestamp = _now().isoformat(timespec='milliseconds')
|
|
73
|
+
sign = cls.CHANGE_SIGNS[change_type]
|
|
74
|
+
click.secho('WATCH', fg='blue', bold=True, nl=False)
|
|
75
|
+
click.echo(f'@{timestamp} {sign} {filepath.relative_to(root)}')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def prompt_fill(text: str, obj, attr, **kwargs):
|
|
79
|
+
while True:
|
|
80
|
+
try:
|
|
81
|
+
value = safe_utf8(click.prompt(text, **kwargs).strip())
|
|
82
|
+
setattr(obj, attr, value)
|
|
83
|
+
break
|
|
84
|
+
except ValidationError as e:
|
|
85
|
+
ClickPrinter.error(e.message)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def print_template_info(template: Template):
|
|
89
|
+
click.echo(f'Template ID: {template.id}')
|
|
90
|
+
click.echo(f'Name: {template.name}')
|
|
91
|
+
click.echo(f'License: {template.license}')
|
|
92
|
+
click.echo(f'Description: {template.description}')
|
|
93
|
+
click.echo('Formats:')
|
|
94
|
+
for format_spec in template.formats:
|
|
95
|
+
click.echo(f' - {format_spec.name}')
|
|
96
|
+
click.echo('Files:')
|
|
97
|
+
for template_file in template.files.values():
|
|
98
|
+
filesize = humanize.naturalsize(len(template_file.content))
|
|
99
|
+
click.echo(f' - {template_file.filename.as_posix()} [{filesize}]')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def ensure_api_config(api_url: str | None, api_key: str | None):
|
|
103
|
+
if api_url is not None:
|
|
104
|
+
CONFIG.use_local_env()
|
|
105
|
+
CONFIG.set_api_url(api_url)
|
|
106
|
+
if not CONFIG.has_api_url:
|
|
107
|
+
CONFIG.set_api_url(
|
|
108
|
+
api_url=click.prompt('API URL'),
|
|
109
|
+
)
|
|
110
|
+
if api_key is not None:
|
|
111
|
+
CONFIG.use_local_env()
|
|
112
|
+
CONFIG.set_api_key(api_key)
|
|
113
|
+
if not CONFIG.has_api_key:
|
|
114
|
+
CONFIG.set_api_key(
|
|
115
|
+
api_key=click.prompt('API Key', hide_input=True),
|
|
116
|
+
)
|
|
117
|
+
if not CONFIG.has_api_url or not CONFIG.has_api_key:
|
|
118
|
+
ClickPrinter.error('API URL and API Key are required to proceed.')
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ClickLogger(logging.Logger):
|
|
123
|
+
|
|
124
|
+
NAME = 'DSW-TDK-CLI'
|
|
125
|
+
LEVEL_STYLES = {
|
|
126
|
+
logging.CRITICAL: lambda x: click.style(x, fg='red', bold=True),
|
|
127
|
+
logging.ERROR: lambda x: click.style(x, fg='red'),
|
|
128
|
+
logging.WARNING: lambda x: click.style(x, fg='yellow'),
|
|
129
|
+
logging.INFO: lambda x: click.style(x, fg='cyan'),
|
|
130
|
+
logging.DEBUG: lambda x: click.style(x, fg='magenta'),
|
|
131
|
+
}
|
|
132
|
+
LEVELS = [
|
|
133
|
+
logging.getLevelName(logging.CRITICAL),
|
|
134
|
+
logging.getLevelName(logging.ERROR),
|
|
135
|
+
logging.getLevelName(logging.WARNING),
|
|
136
|
+
logging.getLevelName(logging.INFO),
|
|
137
|
+
logging.getLevelName(logging.DEBUG),
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
def __init__(self, show_timestamp: bool = False, show_level: bool = True, colors: bool = True):
|
|
141
|
+
super().__init__(name=self.NAME)
|
|
142
|
+
self.show_timestamp = show_timestamp
|
|
143
|
+
self.show_level = show_level
|
|
144
|
+
self.colors = colors
|
|
145
|
+
self.muted = False
|
|
146
|
+
|
|
147
|
+
def _format_level(self, level, justify=False):
|
|
148
|
+
name = logging.getLevelName(level) # type: str
|
|
149
|
+
if justify:
|
|
150
|
+
name = name.ljust(8, ' ')
|
|
151
|
+
if self.colors and level in self.LEVEL_STYLES:
|
|
152
|
+
name = self.LEVEL_STYLES[level](name)
|
|
153
|
+
return name
|
|
154
|
+
|
|
155
|
+
def _print_message(self, level, message):
|
|
156
|
+
if self.show_timestamp:
|
|
157
|
+
timestamp = _now().isoformat(timespec='milliseconds')
|
|
158
|
+
click.echo(timestamp + ' | ', nl=False)
|
|
159
|
+
if self.show_level:
|
|
160
|
+
sep = ' | ' if self.show_timestamp else ': '
|
|
161
|
+
click.echo(self._format_level(level, justify=self.show_timestamp) + sep, nl=False)
|
|
162
|
+
click.echo(message)
|
|
163
|
+
|
|
164
|
+
# pylint: disable-next=unused-argument
|
|
165
|
+
def _log(self, level, msg, args, *other, **kwargs):
|
|
166
|
+
if not self.muted and isinstance(msg, str):
|
|
167
|
+
self._print_message(level, msg % args)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def default():
|
|
171
|
+
logger = ClickLogger()
|
|
172
|
+
logger.setLevel('INFO')
|
|
173
|
+
return logger
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class AliasedGroup(click.Group):
|
|
177
|
+
|
|
178
|
+
def get_command(self, ctx, cmd_name):
|
|
179
|
+
rv = click.Group.get_command(self, ctx, cmd_name)
|
|
180
|
+
if rv is not None:
|
|
181
|
+
return rv
|
|
182
|
+
matches = [x for x in self.list_commands(ctx)
|
|
183
|
+
if x.startswith(cmd_name)]
|
|
184
|
+
if not matches:
|
|
185
|
+
return None
|
|
186
|
+
if len(matches) == 1:
|
|
187
|
+
return click.Group.get_command(self, ctx, matches[0])
|
|
188
|
+
return ctx.fail(f'Too many matches: {", ".join(sorted(matches))}')
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class CLIContext:
|
|
192
|
+
|
|
193
|
+
def __init__(self):
|
|
194
|
+
self.logger = ClickLogger.default()
|
|
195
|
+
self.dot_env_file = None
|
|
196
|
+
|
|
197
|
+
def debug_mode(self):
|
|
198
|
+
self.logger.show_timestamp = True
|
|
199
|
+
self.logger.setLevel(level=logging.DEBUG)
|
|
200
|
+
|
|
201
|
+
def quiet_mode(self):
|
|
202
|
+
self.logger.muted = True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def interact_formats() -> dict[str, FormatSpec]:
|
|
206
|
+
add_format = click.confirm('Do you want to add a format?', default=True)
|
|
207
|
+
formats: dict[str, FormatSpec] = {}
|
|
208
|
+
while add_format:
|
|
209
|
+
format_spec = FormatSpec()
|
|
210
|
+
prompt_fill('Format name', obj=format_spec, attr='name', default='HTML')
|
|
211
|
+
if format_spec.name not in formats or click.confirm(
|
|
212
|
+
'There is already a format with this name. Do you want to change it?',
|
|
213
|
+
):
|
|
214
|
+
prompt_fill('File extension', obj=format_spec, attr='file_extension',
|
|
215
|
+
default=format_spec.name.lower() if ' ' not in format_spec.name else None)
|
|
216
|
+
prompt_fill('Content type', obj=format_spec, attr='content_type',
|
|
217
|
+
default=mimetypes.types_map.get(f'.{format_spec.file_extension}', None))
|
|
218
|
+
t_path = pathlib.Path('src') / f'template.{format_spec.file_extension}.j2'
|
|
219
|
+
prompt_fill(
|
|
220
|
+
text='Jinja2 filename',
|
|
221
|
+
obj=format_spec,
|
|
222
|
+
attr='filename',
|
|
223
|
+
default=str(t_path),
|
|
224
|
+
)
|
|
225
|
+
formats[format_spec.name] = format_spec
|
|
226
|
+
click.echo('=' * 60)
|
|
227
|
+
add_format = click.confirm('Do you want to add yet another format?', default=False)
|
|
228
|
+
return formats
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def interact_builder(builder: TemplateBuilder):
|
|
232
|
+
prompt_fill('Template name', obj=builder, attr='name')
|
|
233
|
+
prompt_fill('Organization ID', obj=builder, attr='organization_id')
|
|
234
|
+
prompt_fill('Template ID', obj=builder, attr='template_id',
|
|
235
|
+
default=slugify.slugify(builder.name))
|
|
236
|
+
prompt_fill('Version', obj=builder, attr='version',
|
|
237
|
+
default='0.1.0')
|
|
238
|
+
prompt_fill('Description', obj=builder, attr='description',
|
|
239
|
+
default='My custom template')
|
|
240
|
+
prompt_fill('License', obj=builder, attr='license',
|
|
241
|
+
default='CC0')
|
|
242
|
+
click.echo('=' * 60)
|
|
243
|
+
formats = interact_formats()
|
|
244
|
+
for format_spec in formats.values():
|
|
245
|
+
builder.add_format(format_spec)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def load_local(tdk: TDKCore, template_dir: pathlib.Path):
|
|
249
|
+
try:
|
|
250
|
+
tdk.load_local(template_dir=template_dir)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
ClickPrinter.failure('Could not load local template')
|
|
253
|
+
ClickPrinter.error(f'> {e}')
|
|
254
|
+
sys.exit(1)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def dir_from_id(template_id: str) -> pathlib.Path:
|
|
258
|
+
return pathlib.Path.cwd() / template_id.replace(':', '_')
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@click.group(cls=AliasedGroup)
|
|
262
|
+
@click.option('-d', '--dot-env', default='.env', required=False,
|
|
263
|
+
show_default=True, type=click.Path(file_okay=True, dir_okay=False),
|
|
264
|
+
help='File path to dot-env file with environment variables.')
|
|
265
|
+
@click.option('-e', '--environment', default=None, required=False,
|
|
266
|
+
help='Configuration environment name.')
|
|
267
|
+
@click.option('--no-dot-env', is_flag=True, default=False,
|
|
268
|
+
help='Do not load .env file, use only environment variables.')
|
|
269
|
+
@click.option('--no-config', is_flag=True, default=False,
|
|
270
|
+
help='Do not load shared configuration, use only environment variables.')
|
|
271
|
+
@click.option('-q', '--quiet', is_flag=True,
|
|
272
|
+
help='Hide additional information logs.')
|
|
273
|
+
@click.option('--debug', is_flag=True,
|
|
274
|
+
help='Enable debug logging.')
|
|
275
|
+
@click.version_option(version=consts.VERSION)
|
|
276
|
+
@click.pass_context
|
|
277
|
+
def main(ctx, quiet, debug, dot_env, environment, no_dot_env, no_config):
|
|
278
|
+
if not no_config:
|
|
279
|
+
try:
|
|
280
|
+
CONFIG.load_home_config()
|
|
281
|
+
except Exception as e:
|
|
282
|
+
ClickPrinter.warning('Failed to load shared configuration')
|
|
283
|
+
ClickPrinter.warning(f'> {e}')
|
|
284
|
+
if not no_dot_env and dot_env is not None:
|
|
285
|
+
dot_env_path = pathlib.Path(dot_env)
|
|
286
|
+
if dot_env_path.exists():
|
|
287
|
+
CONFIG.load_dotenv(path=dot_env_path)
|
|
288
|
+
try:
|
|
289
|
+
if environment is not None:
|
|
290
|
+
CONFIG.switch_current_env(environment)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
ClickPrinter.warning('Failed to set config environment')
|
|
293
|
+
ClickPrinter.warning(f'> {e}')
|
|
294
|
+
ctx.ensure_object(CLIContext)
|
|
295
|
+
ctx.obj.dot_env_file = dot_env
|
|
296
|
+
if quiet:
|
|
297
|
+
ctx.obj.quiet_mode()
|
|
298
|
+
if debug:
|
|
299
|
+
ctx.obj.debug_mode()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@main.command(help='Create a new template project.', name='new')
|
|
303
|
+
@click.argument('TEMPLATE-DIR', type=NEW_DIR_TYPE, default=None, required=False)
|
|
304
|
+
@click.option('-f', '--force', is_flag=True, help='Overwrite any matching files.')
|
|
305
|
+
@click.pass_context
|
|
306
|
+
def new_template(ctx, template_dir, force):
|
|
307
|
+
builder = TemplateBuilder()
|
|
308
|
+
try:
|
|
309
|
+
interact_builder(builder)
|
|
310
|
+
except Exception:
|
|
311
|
+
click.echo('')
|
|
312
|
+
ClickPrinter.failure('Exited...')
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
tdk = TDKCore(template=builder.build(), logger=ctx.obj.logger)
|
|
315
|
+
template_dir = template_dir or dir_from_id(tdk.safe_template.id)
|
|
316
|
+
tdk.prepare_local(template_dir=template_dir)
|
|
317
|
+
try:
|
|
318
|
+
tdk.store_local(force=force)
|
|
319
|
+
ClickPrinter.success(f'Template project created: {template_dir}')
|
|
320
|
+
except Exception as e:
|
|
321
|
+
ClickPrinter.failure('Could not create new template project')
|
|
322
|
+
ClickPrinter.error(f'> {e}')
|
|
323
|
+
sys.exit(1)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@main.command(help='Download template from Wizard.', name='get')
|
|
327
|
+
@click.argument('TEMPLATE-ID')
|
|
328
|
+
@click.argument('TEMPLATE-DIR', type=NEW_DIR_TYPE, default=None, required=False)
|
|
329
|
+
@click.option('-u', '--api-url', metavar='API-URL', envvar='DSW_API_URL',
|
|
330
|
+
help='URL of Wizard server API.')
|
|
331
|
+
@click.option('-k', '--api-key', metavar='API-KEY', envvar='DSW_API_KEY',
|
|
332
|
+
help='API key for Wizard instance.')
|
|
333
|
+
@click.option('-f', '--force', is_flag=True, help='Overwrite any existing files.')
|
|
334
|
+
@click.pass_context
|
|
335
|
+
def get_template(ctx, template_id, template_dir, api_url, api_key, force):
|
|
336
|
+
ensure_api_config(api_url, api_key)
|
|
337
|
+
template_dir = pathlib.Path(template_dir or dir_from_id(template_id))
|
|
338
|
+
|
|
339
|
+
async def main_routine():
|
|
340
|
+
tdk = TDKCore(logger=ctx.obj.logger)
|
|
341
|
+
template_type = 'unknown'
|
|
342
|
+
zip_data = None
|
|
343
|
+
try:
|
|
344
|
+
await tdk.init_client(
|
|
345
|
+
api_url=CONFIG.env.api_url,
|
|
346
|
+
api_key=CONFIG.env.api_key,
|
|
347
|
+
)
|
|
348
|
+
try:
|
|
349
|
+
await tdk.load_remote(template_id=template_id)
|
|
350
|
+
template_type = 'draft'
|
|
351
|
+
except Exception:
|
|
352
|
+
zip_data = await tdk.download_bundle(template_id=template_id)
|
|
353
|
+
template_type = 'bundle'
|
|
354
|
+
await tdk.safe_client.close()
|
|
355
|
+
except WizardCommunicationError as e:
|
|
356
|
+
ClickPrinter.error('Could not get template:', bold=True)
|
|
357
|
+
ClickPrinter.error(f'> {e.reason}\n> {e.message}')
|
|
358
|
+
await tdk.safe_client.close()
|
|
359
|
+
sys.exit(1)
|
|
360
|
+
await tdk.safe_client.safe_close()
|
|
361
|
+
if template_type == 'draft':
|
|
362
|
+
tdk.prepare_local(template_dir=template_dir)
|
|
363
|
+
try:
|
|
364
|
+
tdk.store_local(force=force)
|
|
365
|
+
ClickPrinter.success(f'Template draft {template_id} '
|
|
366
|
+
f'downloaded to {template_dir}')
|
|
367
|
+
except Exception as e:
|
|
368
|
+
ClickPrinter.failure('Could not store template locally')
|
|
369
|
+
ClickPrinter.error(f'> {e}')
|
|
370
|
+
await tdk.safe_client.close()
|
|
371
|
+
sys.exit(1)
|
|
372
|
+
elif template_type == 'bundle' and zip_data is not None:
|
|
373
|
+
try:
|
|
374
|
+
tdk.extract_package(zip_data=zip_data, template_dir=template_dir, force=force)
|
|
375
|
+
ClickPrinter.success(f'Template {template_id} (released) '
|
|
376
|
+
f'downloaded to {template_dir}')
|
|
377
|
+
except Exception as e:
|
|
378
|
+
ClickPrinter.failure('Could not store template locally')
|
|
379
|
+
ClickPrinter.error(f'> {e}')
|
|
380
|
+
await tdk.safe_client.close()
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
else:
|
|
383
|
+
ClickPrinter.failure(f'{template_id} is not released nor draft of a document template')
|
|
384
|
+
sys.exit(1)
|
|
385
|
+
|
|
386
|
+
loop = asyncio.get_event_loop()
|
|
387
|
+
loop.run_until_complete(main_routine())
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@main.command(help='Upload template to Wizard.', name='put')
|
|
391
|
+
@click.argument('TEMPLATE-DIR', type=DIR_TYPE, default=CURRENT_DIR, required=False)
|
|
392
|
+
@click.option('-u', '--api-url', metavar='API-URL', envvar='DSW_API_URL',
|
|
393
|
+
help='URL of Wizard server API.')
|
|
394
|
+
@click.option('-k', '--api-key', metavar='API-KEY', envvar='DSW_API_KEY',
|
|
395
|
+
help='API key for Wizard instance.')
|
|
396
|
+
@click.option('-f', '--force', is_flag=True,
|
|
397
|
+
help='Delete template if already exists.')
|
|
398
|
+
@click.option('-w', '--watch', is_flag=True,
|
|
399
|
+
help='Enter watch mode to continually upload changes.')
|
|
400
|
+
@click.pass_context
|
|
401
|
+
def put_template(ctx, template_dir, api_url, api_key, force, watch):
|
|
402
|
+
ensure_api_config(api_url, api_key)
|
|
403
|
+
tdk = TDKCore(logger=ctx.obj.logger)
|
|
404
|
+
stop_event = asyncio.Event()
|
|
405
|
+
|
|
406
|
+
async def watch_callback(changes):
|
|
407
|
+
changes = list(changes)
|
|
408
|
+
for change in changes:
|
|
409
|
+
ClickPrinter.watch_change(
|
|
410
|
+
change_type=change[0],
|
|
411
|
+
filepath=change[1],
|
|
412
|
+
root=tdk.safe_project.template_dir,
|
|
413
|
+
)
|
|
414
|
+
if len(changes) > 0:
|
|
415
|
+
await tdk.process_changes(changes, force=force)
|
|
416
|
+
|
|
417
|
+
async def main_routine():
|
|
418
|
+
load_local(tdk, template_dir)
|
|
419
|
+
try:
|
|
420
|
+
await tdk.init_client(
|
|
421
|
+
api_url=CONFIG.env.api_url,
|
|
422
|
+
api_key=CONFIG.env.api_key,
|
|
423
|
+
)
|
|
424
|
+
await tdk.store_remote(force=force)
|
|
425
|
+
ClickPrinter.success(f'Template {tdk.safe_project.safe_template.id} '
|
|
426
|
+
f'uploaded to {CONFIG.env.api_url}')
|
|
427
|
+
|
|
428
|
+
if watch:
|
|
429
|
+
ClickPrinter.watch('Entering watch mode... (press Ctrl+C to abort)')
|
|
430
|
+
await tdk.watch_project(watch_callback, stop_event)
|
|
431
|
+
|
|
432
|
+
await tdk.safe_client.close()
|
|
433
|
+
except TDKProcessingError as e:
|
|
434
|
+
ClickPrinter.failure('Could not upload template')
|
|
435
|
+
ClickPrinter.error(f'> {e.message}\n> {e.hint}')
|
|
436
|
+
await tdk.safe_client.safe_close()
|
|
437
|
+
sys.exit(1)
|
|
438
|
+
except WizardCommunicationError as e:
|
|
439
|
+
ClickPrinter.failure('Could not upload template')
|
|
440
|
+
ClickPrinter.error(f'> {e.reason}\n> {e.message}')
|
|
441
|
+
ClickPrinter.error('> Probably incorrect API URL, metamodel version, '
|
|
442
|
+
'or template already exists...')
|
|
443
|
+
ClickPrinter.error('> Check if you are using the matching version')
|
|
444
|
+
await tdk.safe_client.safe_close()
|
|
445
|
+
sys.exit(1)
|
|
446
|
+
|
|
447
|
+
# pylint: disable-next=unused-argument
|
|
448
|
+
def set_stop_event(signal_num, frame):
|
|
449
|
+
signal_name = signal.Signals(signal_num).name
|
|
450
|
+
ClickPrinter.warning(f'Got {signal_name}, finishing... Bye!')
|
|
451
|
+
stop_event.set()
|
|
452
|
+
|
|
453
|
+
signal.signal(signal.SIGINT, set_stop_event)
|
|
454
|
+
signal.signal(signal.SIGABRT, set_stop_event)
|
|
455
|
+
|
|
456
|
+
loop = asyncio.get_event_loop()
|
|
457
|
+
main_task = asyncio.ensure_future(main_routine())
|
|
458
|
+
loop.run_until_complete(main_task)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@main.command(help='Create ZIP package for a template.', name='package')
|
|
462
|
+
@click.argument('TEMPLATE-DIR', type=DIR_TYPE, default=CURRENT_DIR, required=False)
|
|
463
|
+
@click.option('-o', '--output', default='template.zip', type=click.Path(writable=True),
|
|
464
|
+
show_default=True, help='Target package file.')
|
|
465
|
+
@click.option('-f', '--force', is_flag=True, help='Delete package if already exists.')
|
|
466
|
+
@click.pass_context
|
|
467
|
+
def create_package(ctx, template_dir, output, force: bool):
|
|
468
|
+
tdk = TDKCore(logger=ctx.obj.logger)
|
|
469
|
+
load_local(tdk, template_dir)
|
|
470
|
+
try:
|
|
471
|
+
tdk.create_package(output=pathlib.Path(output), force=force)
|
|
472
|
+
except Exception as e:
|
|
473
|
+
ClickPrinter.failure('Failed to create the package')
|
|
474
|
+
ClickPrinter.error(f'> {e}')
|
|
475
|
+
sys.exit(1)
|
|
476
|
+
filename = click.style(output, bold=True)
|
|
477
|
+
ClickPrinter.success(f'Package {filename} created')
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@main.command(help='Extract a template from ZIP package', name='unpackage')
|
|
481
|
+
@click.argument('TEMPLATE-PACKAGE', type=FILE_READ_TYPE, required=False)
|
|
482
|
+
@click.option('-o', '--output', type=NEW_DIR_TYPE, default=None, required=False,
|
|
483
|
+
help='Target package file.')
|
|
484
|
+
@click.option('-f', '--force', is_flag=True, help='Overwrite folder if already exists.')
|
|
485
|
+
@click.pass_context
|
|
486
|
+
def extract_package(ctx, template_package, output, force: bool):
|
|
487
|
+
tdk = TDKCore(logger=ctx.obj.logger)
|
|
488
|
+
try:
|
|
489
|
+
data = pathlib.Path(template_package).read_bytes()
|
|
490
|
+
tdk.extract_package(
|
|
491
|
+
zip_data=data,
|
|
492
|
+
template_dir=pathlib.Path(output) if output is not None else output,
|
|
493
|
+
force=force,
|
|
494
|
+
)
|
|
495
|
+
except Exception as e:
|
|
496
|
+
ClickPrinter.failure('Failed to extract the package')
|
|
497
|
+
ClickPrinter.error(f'> {e}')
|
|
498
|
+
sys.exit(1)
|
|
499
|
+
ClickPrinter.success(f'Package {template_package} extracted')
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@main.command(help='List templates from Wizard via API.', name='list')
|
|
503
|
+
@click.option('-u', '--api-url', metavar='API-URL', envvar='DSW_API_URL',
|
|
504
|
+
help='URL of Wizard server API.')
|
|
505
|
+
@click.option('-k', '--api-key', metavar='API-KEY', envvar='DSW_API_KEY',
|
|
506
|
+
help='API key for Wizard instance.')
|
|
507
|
+
@click.option('--output-format', default=consts.DEFAULT_LIST_FORMAT,
|
|
508
|
+
metavar='FORMAT', help='Entry format string for printing.')
|
|
509
|
+
@click.option('-r', '--released-only', is_flag=True, help='List only released templates')
|
|
510
|
+
@click.option('-d', '--drafts-only', is_flag=True, help='List only template drafts')
|
|
511
|
+
@click.pass_context
|
|
512
|
+
def list_templates(ctx, api_url, api_key, output_format: str,
|
|
513
|
+
released_only: bool, drafts_only: bool):
|
|
514
|
+
ensure_api_config(api_url, api_key)
|
|
515
|
+
|
|
516
|
+
async def main_routine():
|
|
517
|
+
tdk = TDKCore(logger=ctx.obj.logger)
|
|
518
|
+
try:
|
|
519
|
+
await tdk.init_client(
|
|
520
|
+
api_url=CONFIG.env.api_url,
|
|
521
|
+
api_key=CONFIG.env.api_key,
|
|
522
|
+
)
|
|
523
|
+
if released_only:
|
|
524
|
+
templates = await tdk.list_remote_templates()
|
|
525
|
+
for template in templates:
|
|
526
|
+
click.echo(output_format.format(template=template))
|
|
527
|
+
elif drafts_only:
|
|
528
|
+
drafts = await tdk.list_remote_drafts()
|
|
529
|
+
for template in drafts:
|
|
530
|
+
click.echo(output_format.format(template=template))
|
|
531
|
+
else:
|
|
532
|
+
click.echo('Document Templates (released)')
|
|
533
|
+
templates = await tdk.list_remote_templates()
|
|
534
|
+
for template in templates:
|
|
535
|
+
click.echo(output_format.format(template=template))
|
|
536
|
+
click.echo('\nDocument Templates Drafts')
|
|
537
|
+
drafts = await tdk.list_remote_drafts()
|
|
538
|
+
for template in drafts:
|
|
539
|
+
click.echo(output_format.format(template=template))
|
|
540
|
+
await tdk.safe_client.safe_close()
|
|
541
|
+
|
|
542
|
+
except WizardCommunicationError as e:
|
|
543
|
+
ClickPrinter.failure('Failed to get list of templates')
|
|
544
|
+
ClickPrinter.error(f'> {e.reason}\n> {e.message}')
|
|
545
|
+
await tdk.safe_client.safe_close()
|
|
546
|
+
sys.exit(1)
|
|
547
|
+
|
|
548
|
+
loop = asyncio.get_event_loop()
|
|
549
|
+
loop.run_until_complete(main_routine())
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
@main.command(help='Verify a template project.', name='verify')
|
|
553
|
+
@click.argument('TEMPLATE-DIR', type=DIR_TYPE, default=CURRENT_DIR, required=False)
|
|
554
|
+
@click.pass_context
|
|
555
|
+
def verify_template(ctx, template_dir):
|
|
556
|
+
tdk = TDKCore(logger=ctx.obj.logger)
|
|
557
|
+
load_local(tdk, template_dir)
|
|
558
|
+
errors = tdk.verify()
|
|
559
|
+
if len(errors) == 0:
|
|
560
|
+
ClickPrinter.success('The template is valid!')
|
|
561
|
+
print_template_info(template=tdk.safe_project.safe_template)
|
|
562
|
+
else:
|
|
563
|
+
ClickPrinter.failure('The template is invalid!')
|
|
564
|
+
click.echo('Found violations:')
|
|
565
|
+
for err in errors:
|
|
566
|
+
click.echo(f' - {err.field_name}: {err.message}')
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@main.group(help='Manage shared user configuration (~/.dsw-tdk).', name='config')
|
|
570
|
+
@click.pass_context
|
|
571
|
+
# pylint: disable-next=unused-argument
|
|
572
|
+
def config(ctx):
|
|
573
|
+
pass
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@config.command(name='init', help='Initialize the shared user configuration (~/.dsw-tdk).')
|
|
577
|
+
@click.option('-f', '--force', is_flag=True, help='Overwrite file if already exists.')
|
|
578
|
+
def config_init(force):
|
|
579
|
+
if CONFIG.HOME_CONFIG.exists() and not force:
|
|
580
|
+
ClickPrinter.error('Configuration file already exists. Use `--force` to overwrite it.')
|
|
581
|
+
sys.exit(1)
|
|
582
|
+
CONFIG.shared_envs.clear()
|
|
583
|
+
CONFIG.default_env_name = None
|
|
584
|
+
|
|
585
|
+
click.echo('You need to specify your first environment name (default one).')
|
|
586
|
+
click.echo('Recommendation: use short and lowercase name, e.g. "production"')
|
|
587
|
+
environment = click.prompt('Environment name', default='production')
|
|
588
|
+
api_url = click.prompt('API URL')
|
|
589
|
+
api_key = click.prompt('API Key', hide_input=True)
|
|
590
|
+
try:
|
|
591
|
+
CONFIG.add_shared_env(
|
|
592
|
+
name=environment,
|
|
593
|
+
api_url=api_url,
|
|
594
|
+
api_key=api_key,
|
|
595
|
+
)
|
|
596
|
+
CONFIG.default_env_name = environment
|
|
597
|
+
except Exception as e:
|
|
598
|
+
ClickPrinter.failure('Failed to add environment')
|
|
599
|
+
ClickPrinter.error(f'> {e}')
|
|
600
|
+
sys.exit(1)
|
|
601
|
+
while True:
|
|
602
|
+
add_another = click.confirm('Do you want to add another environment?', default=False)
|
|
603
|
+
if not add_another:
|
|
604
|
+
break
|
|
605
|
+
environment = click.prompt('Environment name')
|
|
606
|
+
api_url = click.prompt('API URL')
|
|
607
|
+
api_key = click.prompt('API Key', hide_input=True)
|
|
608
|
+
try:
|
|
609
|
+
CONFIG.add_shared_env(
|
|
610
|
+
name=environment,
|
|
611
|
+
api_url=api_url,
|
|
612
|
+
api_key=api_key,
|
|
613
|
+
)
|
|
614
|
+
except Exception as e:
|
|
615
|
+
ClickPrinter.failure('Failed to add environment (skipping)')
|
|
616
|
+
ClickPrinter.warning(f'> {e}')
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
CONFIG.persist(force=force)
|
|
620
|
+
ClickPrinter.success('Configuration initialized successfully.')
|
|
621
|
+
except Exception as e:
|
|
622
|
+
ClickPrinter.failure('Failed to initialize configuration')
|
|
623
|
+
ClickPrinter.error(f'> {e}')
|
|
624
|
+
sys.exit(1)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@config.command(name='edit', help='Edit the shared user configuration (~/.dsw-tdk).')
|
|
628
|
+
@click.option('-f', '--force', is_flag=True, help='Create file if does not exist.')
|
|
629
|
+
def config_edit(force):
|
|
630
|
+
if not CONFIG.HOME_CONFIG.exists():
|
|
631
|
+
if force:
|
|
632
|
+
CONFIG.HOME_CONFIG.parent.mkdir(parents=True, exist_ok=True)
|
|
633
|
+
CONFIG.HOME_CONFIG.touch()
|
|
634
|
+
else:
|
|
635
|
+
ClickPrinter.error('Configuration file does not exist. Use `init` command '
|
|
636
|
+
'or `--force` flag to create it.')
|
|
637
|
+
sys.exit(1)
|
|
638
|
+
click.edit(
|
|
639
|
+
filename=str(CONFIG.HOME_CONFIG),
|
|
640
|
+
extension='.cfg',
|
|
641
|
+
require_save=True,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
@config.command(name='check', help='Check the current configuration that can be loaded.')
|
|
646
|
+
def config_check():
|
|
647
|
+
hidden = click.style('(hidden)', fg='red', bold=True)
|
|
648
|
+
not_set = click.style('(not set)', fg='yellow', bold=True)
|
|
649
|
+
click.secho('Shared configuration (~/.dsw-tdk):', bold=True)
|
|
650
|
+
for env_name, env in CONFIG.shared_envs.items():
|
|
651
|
+
if env_name == CONFIG.current_env_name:
|
|
652
|
+
env_out = click.style(env_name, fg='green', bold=True)
|
|
653
|
+
click.echo(f'{env_out} (current)')
|
|
654
|
+
elif env_name == CONFIG.default_env_name:
|
|
655
|
+
env_out = click.style(env_name, fg='cyan', bold=True)
|
|
656
|
+
click.echo(f'{env_out} (default)')
|
|
657
|
+
else:
|
|
658
|
+
env_out = click.style(env_name, fg='blue', bold=True)
|
|
659
|
+
click.echo(env_out)
|
|
660
|
+
click.echo(f' API URL: {env.api_url or not_set}')
|
|
661
|
+
click.echo(f' API Key: {hidden if env.api_key else not_set}')
|
|
662
|
+
click.echo('')
|
|
663
|
+
click.secho('Project-local configuration:', bold=True)
|
|
664
|
+
if CONFIG.current_env_name == CONFIG.LOCAL_CONFIG:
|
|
665
|
+
env_out = click.style(CONFIG.LOCAL_CONFIG, fg='green', bold=True)
|
|
666
|
+
click.echo(f'{env_out} (current)')
|
|
667
|
+
else:
|
|
668
|
+
env_out = click.style('local', fg='blue', bold=True)
|
|
669
|
+
click.echo(env_out)
|
|
670
|
+
if CONFIG.local_env.api_url:
|
|
671
|
+
click.echo(f' API URL: {CONFIG.local_env.api_url}')
|
|
672
|
+
else:
|
|
673
|
+
click.echo(f' API URL: {not_set}')
|
|
674
|
+
if CONFIG.local_env.api_key:
|
|
675
|
+
click.echo(f' API Key: {hidden}')
|
|
676
|
+
else:
|
|
677
|
+
click.echo(f' API Key: {not_set}')
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@config.command(name='dot-env', help='Create a .env file with API configuration.')
|
|
681
|
+
@click.argument('TEMPLATE-DIR', type=DIR_TYPE, default=CURRENT_DIR, required=False)
|
|
682
|
+
@click.option('-u', '--api-url', metavar='API-URL', envvar='DSW_API_URL',
|
|
683
|
+
help='URL of Wizard server API.')
|
|
684
|
+
@click.option('-k', '--api-key', metavar='API-KEY', envvar='DSW_API_KEY',
|
|
685
|
+
help='API key for Wizard instance.')
|
|
686
|
+
@click.option('-f', '--force', is_flag=True, help='Overwrite file if already exists.')
|
|
687
|
+
@click.pass_context
|
|
688
|
+
def config_create_dotenv(ctx, template_dir, api_url, api_key, force):
|
|
689
|
+
ensure_api_config(api_url, api_key)
|
|
690
|
+
filename = ctx.obj.dot_env_file or '.env'
|
|
691
|
+
output = pathlib.Path(template_dir) / filename
|
|
692
|
+
try:
|
|
693
|
+
if output.exists():
|
|
694
|
+
if force:
|
|
695
|
+
ClickPrinter.warning(f'Overwriting {output.as_posix()} (forced)')
|
|
696
|
+
else:
|
|
697
|
+
raise FileExistsError(f'File {output.as_posix()} already exists (not forced)')
|
|
698
|
+
output.write_text(
|
|
699
|
+
data=create_dot_env(
|
|
700
|
+
api_url=CONFIG.env.api_url,
|
|
701
|
+
api_key=CONFIG.env.api_key,
|
|
702
|
+
),
|
|
703
|
+
encoding=consts.DEFAULT_ENCODING,
|
|
704
|
+
)
|
|
705
|
+
except Exception as e:
|
|
706
|
+
ClickPrinter.failure('Failed to create dot-env file')
|
|
707
|
+
ClickPrinter.error(f'> {e}')
|
|
708
|
+
sys.exit(1)
|