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