dsw-tdk 4.21.0__tar.gz → 4.22.1__tar.gz

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.
Files changed (39) hide show
  1. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/CHANGELOG.md +18 -0
  2. {dsw_tdk-4.21.0/dsw_tdk.egg-info → dsw_tdk-4.22.1}/PKG-INFO +1 -1
  3. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/api_client.py +35 -35
  4. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/build_info.py +4 -4
  5. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/cli.py +211 -54
  6. dsw_tdk-4.22.1/dsw/tdk/config.py +154 -0
  7. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/consts.py +4 -2
  8. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/core.py +127 -114
  9. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/model.py +13 -13
  10. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/templates/env.j2 +1 -0
  11. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/validation.py +33 -1
  12. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1/dsw_tdk.egg-info}/PKG-INFO +1 -1
  13. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw_tdk.egg-info/SOURCES.txt +1 -0
  14. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/pyproject.toml +1 -1
  15. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/tests/test_cmd_dot-env.py +12 -4
  16. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/tests/test_cmd_get.py +9 -9
  17. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/tests/test_cmd_list.py +8 -8
  18. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/tests/test_cmd_new.py +2 -0
  19. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/tests/test_cmd_put.py +6 -5
  20. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/tests/test_cmd_verify.py +1 -1
  21. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/LICENSE +0 -0
  22. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/MANIFEST.in +0 -0
  23. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/README.md +0 -0
  24. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/__init__.py +0 -0
  25. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/__main__.py +0 -0
  26. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/templates/LICENSE.j2 +0 -0
  27. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/templates/README.md.j2 +0 -0
  28. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/templates/starter.j2 +0 -0
  29. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw/tdk/utils.py +0 -0
  30. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw_tdk.egg-info/dependency_links.txt +0 -0
  31. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw_tdk.egg-info/entry_points.txt +0 -0
  32. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw_tdk.egg-info/not-zip-safe +0 -0
  33. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw_tdk.egg-info/requires.txt +0 -0
  34. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/dsw_tdk.egg-info/top_level.txt +0 -0
  35. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/setup.cfg +0 -0
  36. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/setup.py +0 -0
  37. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/tests/test_basic.py +0 -0
  38. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/tests/test_cmd_package.py +0 -0
  39. {dsw_tdk-4.21.0 → dsw_tdk-4.22.1}/tests/test_cmd_unpackage.py +0 -0
@@ -8,6 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [4.22.1]
12
+
13
+ ### Fixed
14
+
15
+ - Fixed checking metamodel and API version in TDK
16
+
17
+ ## [4.22.0]
18
+
19
+ ### Added
20
+
21
+ - Support shared TDK config across document template projects
22
+
23
+ ### Changed
24
+
25
+ - Switch to use semver for document template metamodel versioning
26
+
11
27
  ## [4.21.0]
12
28
 
13
29
  Released for version consistency with other DSW tools.
@@ -589,3 +605,5 @@ Initial DSW Template Development Kit (versioned as part of the [DSW platform](ht
589
605
  [4.20.0]: /../../tree/v4.20.0
590
606
  [4.20.1]: /../../tree/v4.20.1
591
607
  [4.21.0]: /../../tree/v4.21.0
608
+ [4.22.0]: /../../tree/v4.22.0
609
+ [4.22.1]: /../../tree/v4.22.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsw-tdk
3
- Version: 4.21.0
3
+ Version: 4.22.1
4
4
  Summary: Data Stewardship Wizard Template Development Toolkit
5
5
  Author-email: Marek Suchánek <marek.suchanek@ds-wizard.org>
6
6
  License: Apache License 2.0
@@ -9,7 +9,7 @@ from .consts import DEFAULT_ENCODING, APP, VERSION
9
9
  from .model import Template, TemplateFile, TemplateFileType
10
10
 
11
11
 
12
- class DSWCommunicationError(RuntimeError):
12
+ class WizardCommunicationError(RuntimeError):
13
13
 
14
14
  def __init__(self, reason: str, message: str):
15
15
  """
@@ -28,31 +28,31 @@ def handle_client_errors(func):
28
28
  async def handled_client_call(job, *args, **kwargs):
29
29
  try:
30
30
  return await func(job, *args, **kwargs)
31
- except DSWCommunicationError as e:
31
+ except WizardCommunicationError as e:
32
32
  # Already DSWCommunicationError (re-raise)
33
33
  raise e
34
34
  except aiohttp.client_exceptions.ContentTypeError as e:
35
- raise DSWCommunicationError(
35
+ raise WizardCommunicationError(
36
36
  reason='Unexpected response type',
37
37
  message=e.message
38
38
  ) from e
39
39
  except aiohttp.client_exceptions.ClientResponseError as e:
40
- raise DSWCommunicationError(
40
+ raise WizardCommunicationError(
41
41
  reason='Error response status',
42
42
  message=f'Server responded with error HTTP status {e.status}: {e.message}'
43
43
  ) from e
44
44
  except aiohttp.client_exceptions.InvalidURL as e:
45
- raise DSWCommunicationError(
45
+ raise WizardCommunicationError(
46
46
  reason='Invalid URL',
47
47
  message=f'Provided API URL seems invalid: {e.url}'
48
48
  ) from e
49
49
  except aiohttp.client_exceptions.ClientConnectorError as e:
50
- raise DSWCommunicationError(
50
+ raise WizardCommunicationError(
51
51
  reason='Server unreachable',
52
52
  message=f'Desired server is not reachable (errno {e.os_error.errno})'
53
53
  ) from e
54
54
  except Exception as e:
55
- raise DSWCommunicationError(
55
+ raise WizardCommunicationError(
56
56
  reason='Communication error',
57
57
  message=f'Communication with server failed ({e})'
58
58
  ) from e
@@ -60,7 +60,7 @@ def handle_client_errors(func):
60
60
 
61
61
 
62
62
  # pylint: disable-next=too-many-public-methods
63
- class DSWAPIClient:
63
+ class WizardAPIClient:
64
64
 
65
65
  def _headers(self, extra=None):
66
66
  headers = {
@@ -75,7 +75,7 @@ class DSWAPIClient:
75
75
  def _check_status(r: aiohttp.ClientResponse, expected_status):
76
76
  r.raise_for_status()
77
77
  if r.status != expected_status:
78
- raise DSWCommunicationError(
78
+ raise WizardCommunicationError(
79
79
  reason='Unexpected response status',
80
80
  message=f'Server responded with unexpected HTTP status {r.status}: '
81
81
  f'{r.reason} (expecting {expected_status})'
@@ -160,7 +160,7 @@ class DSWAPIClient:
160
160
  body = await self._post_json('/tokens', json=req)
161
161
  token_value = body.get('token', None)
162
162
  if not isinstance(token_value, str):
163
- raise DSWCommunicationError(
163
+ raise WizardCommunicationError(
164
164
  reason='Invalid response',
165
165
  message='Server did not return a valid token'
166
166
  )
@@ -226,8 +226,8 @@ class DSWAPIClient:
226
226
  result = []
227
227
  for file_body in body:
228
228
  file_id = file_body['uuid']
229
- template_file = await self.get_template_draft_file(remote_id, file_id)
230
- result.append(template_file)
229
+ file = await self.get_template_draft_file(remote_id, file_id)
230
+ result.append(file)
231
231
  return result
232
232
 
233
233
  @handle_client_errors
@@ -276,23 +276,23 @@ class DSWAPIClient:
276
276
  return _load_remote_template(body)
277
277
 
278
278
  @handle_client_errors
279
- async def post_template_draft_file(self, remote_id: str, tfile: TemplateFile):
279
+ async def post_template_draft_file(self, remote_id: str, file: TemplateFile):
280
280
  data = await self._post_json(
281
281
  endpoint=f'/document-template-drafts/{remote_id}/files',
282
282
  json={
283
- 'fileName': tfile.filename.as_posix(),
284
- 'content': tfile.content.decode(DEFAULT_ENCODING)
283
+ 'fileName': file.filename.as_posix(),
284
+ 'content': file.content.decode(DEFAULT_ENCODING)
285
285
  }
286
286
  )
287
287
  return _load_remote_file(data)
288
288
 
289
289
  @handle_client_errors
290
- async def put_template_draft_file_content(self, remote_id: str, tfile: TemplateFile):
290
+ async def put_template_draft_file_content(self, remote_id: str, file: TemplateFile):
291
291
  self.session.headers.update(self._headers())
292
292
  async with self.session.put(
293
293
  f'{self.api_url}/document-template-drafts/{remote_id}'
294
- f'/files/{tfile.remote_id}/content',
295
- data=tfile.content,
294
+ f'/files/{file.remote_id}/content',
295
+ data=file.content,
296
296
  headers={'Content-Type': 'text/plain;charset=UTF-8'},
297
297
  ) as r:
298
298
  self._check_status(r, expected_status=200)
@@ -300,17 +300,17 @@ class DSWAPIClient:
300
300
  return _load_remote_file(body)
301
301
 
302
302
  @handle_client_errors
303
- async def post_template_draft_asset(self, remote_id: str, tfile: TemplateFile):
303
+ async def post_template_draft_asset(self, remote_id: str, file: TemplateFile):
304
304
  data = aiohttp.FormData()
305
305
  data.add_field(
306
306
  name='file',
307
- value=tfile.content,
308
- filename=tfile.filename.as_posix(),
309
- content_type=tfile.content_type,
307
+ value=file.content,
308
+ filename=file.filename.as_posix(),
309
+ content_type=file.content_type,
310
310
  )
311
311
  data.add_field(
312
312
  name='fileName',
313
- value=tfile.filename.as_posix(),
313
+ value=file.filename.as_posix(),
314
314
  )
315
315
  async with self.session.post(
316
316
  f'{self.api_url}/document-template-drafts/{remote_id}/assets',
@@ -319,30 +319,30 @@ class DSWAPIClient:
319
319
  ) as r:
320
320
  self._check_status(r, expected_status=201)
321
321
  body = await r.json()
322
- return _load_remote_asset(body, tfile.content)
322
+ return _load_remote_asset(body, file.content)
323
323
 
324
324
  @handle_client_errors
325
- async def put_template_draft_asset_content(self, remote_id: str, tfile: TemplateFile):
325
+ async def put_template_draft_asset_content(self, remote_id: str, file: TemplateFile):
326
326
  data = aiohttp.FormData()
327
327
  data.add_field(
328
328
  name='file',
329
- value=tfile.content,
330
- filename=tfile.filename.as_posix(),
331
- content_type=tfile.content_type,
329
+ value=file.content,
330
+ filename=file.filename.as_posix(),
331
+ content_type=file.content_type,
332
332
  )
333
333
  data.add_field(
334
334
  name='fileName',
335
- value=tfile.filename.as_posix(),
335
+ value=file.filename.as_posix(),
336
336
  )
337
337
  async with self.session.put(
338
338
  f'{self.api_url}/document-template-drafts/{remote_id}'
339
- f'/assets/{tfile.remote_id}/content',
339
+ f'/assets/{file.remote_id}/content',
340
340
  data=data,
341
341
  headers=self._headers()
342
342
  ) as r:
343
343
  self._check_status(r, expected_status=200)
344
344
  body = await r.json()
345
- return _load_remote_asset(body, tfile.content)
345
+ return _load_remote_asset(body, file.content)
346
346
 
347
347
  @handle_client_errors
348
348
  async def delete_template_draft(self, remote_id: str) -> bool:
@@ -374,25 +374,25 @@ class DSWAPIClient:
374
374
  def _load_remote_file(data: dict) -> TemplateFile:
375
375
  content: str = data.get('content', '')
376
376
  filename: str = str(data.get('fileName', ''))
377
- template_file = TemplateFile(
377
+ file = TemplateFile(
378
378
  remote_id=data.get('uuid', None),
379
379
  remote_type=TemplateFileType.FILE,
380
380
  filename=pathlib.Path(urllib.parse.unquote(filename)),
381
381
  content=content.encode(encoding=DEFAULT_ENCODING),
382
382
  )
383
- return template_file
383
+ return file
384
384
 
385
385
 
386
386
  def _load_remote_asset(data: dict, content: bytes) -> TemplateFile:
387
387
  filename = str(data.get('fileName', ''))
388
- template_file = TemplateFile(
388
+ asset = TemplateFile(
389
389
  remote_id=data.get('uuid', None),
390
390
  remote_type=TemplateFileType.ASSET,
391
391
  filename=pathlib.Path(urllib.parse.unquote(filename)),
392
392
  content_type=data.get('contentType', None),
393
393
  content=content,
394
394
  )
395
- return template_file
395
+ return asset
396
396
 
397
397
 
398
398
  def _load_remote_template(data: dict) -> Template:
@@ -9,9 +9,9 @@ BuildInfo = namedtuple(
9
9
  )
10
10
 
11
11
  BUILD_INFO = BuildInfo(
12
- version='v4.21.0~12713f1',
13
- built_at='2025-08-05 09:20:48Z',
14
- sha='12713f1988c2a33d5c40dd2cb899cdac7be35668',
12
+ version='v4.22.1~13699ca',
13
+ built_at='2025-09-04 06:55:59Z',
14
+ sha='13699ca0f74ff51a24bd76e242a89ef434b74658',
15
15
  branch='HEAD',
16
- tag='v4.21.0',
16
+ tag='v4.22.1',
17
17
  )
@@ -8,16 +8,16 @@ import signal
8
8
  import sys
9
9
 
10
10
  import click
11
- import dotenv
12
11
  import humanize
13
12
  import slugify
14
13
  import watchfiles
15
14
 
16
- from .api_client import DSWCommunicationError
15
+ from .api_client import WizardCommunicationError
16
+ from .config import CONFIG
17
+ from .consts import VERSION, DEFAULT_LIST_FORMAT, DEFAULT_ENCODING
17
18
  from .core import TDKCore, TDKProcessingError
18
- from .consts import VERSION, DEFAULT_LIST_FORMAT
19
19
  from .model import Template
20
- from .utils import TemplateBuilder, FormatSpec, safe_utf8
20
+ from .utils import TemplateBuilder, FormatSpec, safe_utf8, create_dot_env
21
21
  from .validation import ValidationError
22
22
 
23
23
  CURRENT_DIR = pathlib.Path.cwd()
@@ -89,19 +89,29 @@ def print_template_info(template: Template):
89
89
  for format_spec in template.formats:
90
90
  click.echo(f' - {format_spec.name}')
91
91
  click.echo('Files:')
92
- for tfile in template.files.values():
93
- filesize = humanize.naturalsize(len(tfile.content))
94
- click.echo(f' - {tfile.filename.as_posix()} [{filesize}]')
95
-
96
-
97
- # pylint: disable-next=unused-argument
98
- def rectify_url(ctx, param, value) -> str:
99
- return value.rstrip('/')
100
-
101
-
102
- # pylint: disable-next=unused-argument
103
- def rectify_key(ctx, param, value) -> str:
104
- return value.strip()
92
+ for template_file in template.files.values():
93
+ filesize = humanize.naturalsize(len(template_file.content))
94
+ click.echo(f' - {template_file.filename.as_posix()} [{filesize}]')
95
+
96
+
97
+ def ensure_api_config(api_url: str | None, api_key: str | None):
98
+ if api_url is not None:
99
+ CONFIG.use_local_env()
100
+ CONFIG.set_api_url(api_url)
101
+ if not CONFIG.has_api_url:
102
+ CONFIG.set_api_url(
103
+ api_url=click.prompt('API URL'),
104
+ )
105
+ if api_key is not None:
106
+ CONFIG.use_local_env()
107
+ CONFIG.set_api_key(api_key)
108
+ if not CONFIG.has_api_key:
109
+ CONFIG.set_api_key(
110
+ api_key=click.prompt('API Key', hide_input=True),
111
+ )
112
+ if not CONFIG.has_api_url or not CONFIG.has_api_key:
113
+ ClickPrinter.error('API URL and API Key are required to proceed.')
114
+ sys.exit(1)
105
115
 
106
116
 
107
117
  class ClickLogger(logging.Logger):
@@ -244,18 +254,38 @@ def dir_from_id(template_id: str) -> pathlib.Path:
244
254
 
245
255
 
246
256
  @click.group(cls=AliasedGroup)
247
- @click.option('-e', '--dot-env', default='.env', required=False,
257
+ @click.option('-d', '--dot-env', default='.env', required=False,
248
258
  show_default=True, type=click.Path(file_okay=True, dir_okay=False),
249
- help='Provide file with environment variables.')
259
+ help='File path to dot-env file with environment variables.')
260
+ @click.option('-e', '--environment', default=None, required=False,
261
+ help='Configuration environment name.')
262
+ @click.option('--no-dot-env', is_flag=True, default=False,
263
+ help='Do not load .env file, use only environment variables.')
264
+ @click.option('--no-config', is_flag=True, default=False,
265
+ help='Do not load shared configuration, use only environment variables.')
250
266
  @click.option('-q', '--quiet', is_flag=True,
251
267
  help='Hide additional information logs.')
252
268
  @click.option('--debug', is_flag=True,
253
269
  help='Enable debug logging.')
254
270
  @click.version_option(version=VERSION)
255
271
  @click.pass_context
256
- def main(ctx, quiet, debug, dot_env):
257
- if pathlib.Path(dot_env).exists():
258
- dotenv.load_dotenv(dotenv_path=dot_env)
272
+ def main(ctx, quiet, debug, dot_env, environment, no_dot_env, no_config):
273
+ if not no_config:
274
+ try:
275
+ CONFIG.load_home_config()
276
+ except Exception as e:
277
+ ClickPrinter.warning('Failed to load shared configuration')
278
+ ClickPrinter.warning(f'> {e}')
279
+ if not no_dot_env and dot_env is not None:
280
+ dot_env_path = pathlib.Path(dot_env)
281
+ if dot_env_path.exists():
282
+ CONFIG.load_dotenv(path=dot_env_path)
283
+ try:
284
+ if environment is not None:
285
+ CONFIG.switch_current_env(environment)
286
+ except Exception as e:
287
+ ClickPrinter.warning('Failed to set config environment')
288
+ ClickPrinter.warning(f'> {e}')
259
289
  ctx.ensure_object(CLIContext)
260
290
  ctx.obj.dot_env_file = dot_env
261
291
  if quiet:
@@ -292,13 +322,13 @@ def new_template(ctx, template_dir, force):
292
322
  @click.argument('TEMPLATE-ID')
293
323
  @click.argument('TEMPLATE-DIR', type=NEW_DIR_TYPE, default=None, required=False)
294
324
  @click.option('-u', '--api-url', metavar='API-URL', envvar='DSW_API_URL',
295
- prompt='API URL', help='URL of Wizard server API.', callback=rectify_url)
325
+ help='URL of Wizard server API.')
296
326
  @click.option('-k', '--api-key', metavar='API-KEY', envvar='DSW_API_KEY',
297
- prompt='API Key', help='API key for Wizard instance.', callback=rectify_key,
298
- hide_input=True)
327
+ help='API key for Wizard instance.')
299
328
  @click.option('-f', '--force', is_flag=True, help='Overwrite any existing files.')
300
329
  @click.pass_context
301
- def get_template(ctx, api_url, template_id, template_dir, api_key, force):
330
+ def get_template(ctx, template_id, template_dir, api_url, api_key, force):
331
+ ensure_api_config(api_url, api_key)
302
332
  template_dir = pathlib.Path(template_dir or dir_from_id(template_id))
303
333
 
304
334
  async def main_routine():
@@ -306,7 +336,10 @@ def get_template(ctx, api_url, template_id, template_dir, api_key, force):
306
336
  template_type = 'unknown'
307
337
  zip_data = None
308
338
  try:
309
- await tdk.init_client(api_url=api_url, api_key=api_key)
339
+ await tdk.init_client(
340
+ api_url=CONFIG.env.api_url,
341
+ api_key=CONFIG.env.api_key,
342
+ )
310
343
  try:
311
344
  await tdk.load_remote(template_id=template_id)
312
345
  template_type = 'draft'
@@ -314,7 +347,7 @@ def get_template(ctx, api_url, template_id, template_dir, api_key, force):
314
347
  zip_data = await tdk.download_bundle(template_id=template_id)
315
348
  template_type = 'bundle'
316
349
  await tdk.safe_client.close()
317
- except DSWCommunicationError as e:
350
+ except WizardCommunicationError as e:
318
351
  ClickPrinter.error('Could not get template:', bold=True)
319
352
  ClickPrinter.error(f'> {e.reason}\n> {e.message}')
320
353
  await tdk.safe_client.close()
@@ -352,16 +385,16 @@ def get_template(ctx, api_url, template_id, template_dir, api_key, force):
352
385
  @main.command(help='Upload template to Wizard.', name='put')
353
386
  @click.argument('TEMPLATE-DIR', type=DIR_TYPE, default=CURRENT_DIR, required=False)
354
387
  @click.option('-u', '--api-url', metavar='API-URL', envvar='DSW_API_URL',
355
- prompt='API URL', help='URL of Wizard server API.', callback=rectify_url)
388
+ help='URL of Wizard server API.')
356
389
  @click.option('-k', '--api-key', metavar='API-KEY', envvar='DSW_API_KEY',
357
- prompt='API Key', help='API key for Wizard instance.', callback=rectify_key,
358
- hide_input=True)
390
+ help='API key for Wizard instance.')
359
391
  @click.option('-f', '--force', is_flag=True,
360
392
  help='Delete template if already exists.')
361
393
  @click.option('-w', '--watch', is_flag=True,
362
394
  help='Enter watch mode to continually upload changes.')
363
395
  @click.pass_context
364
- def put_template(ctx, api_url, template_dir, api_key, force, watch):
396
+ def put_template(ctx, template_dir, api_url, api_key, force, watch):
397
+ ensure_api_config(api_url, api_key)
365
398
  tdk = TDKCore(logger=ctx.obj.logger)
366
399
  stop_event = asyncio.Event()
367
400
 
@@ -379,10 +412,13 @@ def put_template(ctx, api_url, template_dir, api_key, force, watch):
379
412
  async def main_routine():
380
413
  load_local(tdk, template_dir)
381
414
  try:
382
- await tdk.init_client(api_url=api_url, api_key=api_key)
415
+ await tdk.init_client(
416
+ api_url=CONFIG.env.api_url,
417
+ api_key=CONFIG.env.api_key,
418
+ )
383
419
  await tdk.store_remote(force=force)
384
420
  ClickPrinter.success(f'Template {tdk.safe_project.safe_template.id} '
385
- f'uploaded to {api_url}')
421
+ f'uploaded to {CONFIG.env.api_url}')
386
422
 
387
423
  if watch:
388
424
  ClickPrinter.watch('Entering watch mode... (press Ctrl+C to abort)')
@@ -394,7 +430,7 @@ def put_template(ctx, api_url, template_dir, api_key, force, watch):
394
430
  ClickPrinter.error(f'> {e.message}\n> {e.hint}')
395
431
  await tdk.safe_client.safe_close()
396
432
  sys.exit(1)
397
- except DSWCommunicationError as e:
433
+ except WizardCommunicationError as e:
398
434
  ClickPrinter.failure('Could not upload template')
399
435
  ClickPrinter.error(f'> {e.reason}\n> {e.message}')
400
436
  ClickPrinter.error('> Probably incorrect API URL, metamodel version, '
@@ -404,9 +440,9 @@ def put_template(ctx, api_url, template_dir, api_key, force, watch):
404
440
  sys.exit(1)
405
441
 
406
442
  # pylint: disable-next=unused-argument
407
- def set_stop_event(signum, frame):
408
- signame = signal.Signals(signum).name
409
- ClickPrinter.warning(f'Got {signame}, finishing... Bye!')
443
+ def set_stop_event(signal_num, frame):
444
+ signal_name = signal.Signals(signal_num).name
445
+ ClickPrinter.warning(f'Got {signal_name}, finishing... Bye!')
410
446
  stop_event.set()
411
447
 
412
448
  signal.signal(signal.SIGINT, set_stop_event)
@@ -460,10 +496,9 @@ def extract_package(ctx, template_package, output, force: bool):
460
496
 
461
497
  @main.command(help='List templates from Wizard via API.', name='list')
462
498
  @click.option('-u', '--api-url', metavar='API-URL', envvar='DSW_API_URL',
463
- prompt='API URL', help='URL of Wizard server API.', callback=rectify_url)
499
+ help='URL of Wizard server API.')
464
500
  @click.option('-k', '--api-key', metavar='API-KEY', envvar='DSW_API_KEY',
465
- prompt='API Key', help='API key for Wizard instance.', callback=rectify_key,
466
- hide_input=True)
501
+ help='API key for Wizard instance.')
467
502
  @click.option('--output-format', default=DEFAULT_LIST_FORMAT,
468
503
  metavar='FORMAT', help='Entry format string for printing.')
469
504
  @click.option('-r', '--released-only', is_flag=True, help='List only released templates')
@@ -471,10 +506,15 @@ def extract_package(ctx, template_package, output, force: bool):
471
506
  @click.pass_context
472
507
  def list_templates(ctx, api_url, api_key, output_format: str,
473
508
  released_only: bool, drafts_only: bool):
509
+ ensure_api_config(api_url, api_key)
510
+
474
511
  async def main_routine():
475
512
  tdk = TDKCore(logger=ctx.obj.logger)
476
513
  try:
477
- await tdk.init_client(api_url=api_url, api_key=api_key)
514
+ await tdk.init_client(
515
+ api_url=CONFIG.env.api_url,
516
+ api_key=CONFIG.env.api_key,
517
+ )
478
518
  if released_only:
479
519
  templates = await tdk.list_remote_templates()
480
520
  for template in templates:
@@ -494,7 +534,7 @@ def list_templates(ctx, api_url, api_key, output_format: str,
494
534
  click.echo(output_format.format(template=template))
495
535
  await tdk.safe_client.safe_close()
496
536
 
497
- except DSWCommunicationError as e:
537
+ except WizardCommunicationError as e:
498
538
  ClickPrinter.failure('Failed to get list of templates')
499
539
  ClickPrinter.error(f'> {e.reason}\n> {e.message}')
500
540
  await tdk.safe_client.safe_close()
@@ -521,24 +561,141 @@ def verify_template(ctx, template_dir):
521
561
  click.echo(f' - {err.field_name}: {err.message}')
522
562
 
523
563
 
524
- @main.command(help='Create a .env file.', name='dot-env')
564
+ @main.group(help='Manage shared user configuration (~/.dsw-tdk).', name='config')
565
+ @click.pass_context
566
+ # pylint: disable-next=unused-argument
567
+ def config(ctx):
568
+ pass
569
+
570
+
571
+ @config.command(name='init', help='Initialize the shared user configuration (~/.dsw-tdk).')
572
+ @click.option('-f', '--force', is_flag=True, help='Overwrite file if already exists.')
573
+ def config_init(force):
574
+ if CONFIG.HOME_CONFIG.exists() and not force:
575
+ ClickPrinter.error('Configuration file already exists. Use `--force` to overwrite it.')
576
+ sys.exit(1)
577
+ CONFIG.shared_envs.clear()
578
+ CONFIG.default_env_name = None
579
+
580
+ click.echo('You need to specify your first environment name (default one).')
581
+ click.echo('Recommendation: use short and lowercase name, e.g. "production"')
582
+ environment = click.prompt('Environment name', default='production')
583
+ api_url = click.prompt('API URL')
584
+ api_key = click.prompt('API Key', hide_input=True)
585
+ try:
586
+ CONFIG.add_shared_env(
587
+ name=environment,
588
+ api_url=api_url,
589
+ api_key=api_key,
590
+ )
591
+ CONFIG.default_env_name = environment
592
+ except Exception as e:
593
+ ClickPrinter.failure('Failed to add environment')
594
+ ClickPrinter.error(f'> {e}')
595
+ sys.exit(1)
596
+ while True:
597
+ add_another = click.confirm('Do you want to add another environment?', default=False)
598
+ if not add_another:
599
+ break
600
+ environment = click.prompt('Environment name')
601
+ api_url = click.prompt('API URL')
602
+ api_key = click.prompt('API Key', hide_input=True)
603
+ try:
604
+ CONFIG.add_shared_env(
605
+ name=environment,
606
+ api_url=api_url,
607
+ api_key=api_key,
608
+ )
609
+ except Exception as e:
610
+ ClickPrinter.failure('Failed to add environment (skipping)')
611
+ ClickPrinter.warning(f'> {e}')
612
+
613
+ try:
614
+ CONFIG.persist(force=force)
615
+ ClickPrinter.success('Configuration initialized successfully.')
616
+ except Exception as e:
617
+ ClickPrinter.failure('Failed to initialize configuration')
618
+ ClickPrinter.error(f'> {e}')
619
+ sys.exit(1)
620
+
621
+
622
+ @config.command(name='edit', help='Edit the shared user configuration (~/.dsw-tdk).')
623
+ @click.option('-f', '--force', is_flag=True, help='Create file if does not exist.')
624
+ def config_edit(force):
625
+ if not CONFIG.HOME_CONFIG.exists():
626
+ if force:
627
+ CONFIG.HOME_CONFIG.parent.mkdir(parents=True, exist_ok=True)
628
+ CONFIG.HOME_CONFIG.touch()
629
+ else:
630
+ ClickPrinter.error('Configuration file does not exist. Use `init` command '
631
+ 'or `--force` flag to create it.')
632
+ sys.exit(1)
633
+ click.edit(
634
+ filename=str(CONFIG.HOME_CONFIG),
635
+ extension='.cfg',
636
+ require_save=True,
637
+ )
638
+
639
+
640
+ @config.command(name='check', help='Check the current configuration that can be loaded.')
641
+ def config_check():
642
+ hidden = click.style('(hidden)', fg='red', bold=True)
643
+ not_set = click.style('(not set)', fg='yellow', bold=True)
644
+ click.secho('Shared configuration (~/.dsw-tdk):', bold=True)
645
+ for env_name, env in CONFIG.shared_envs.items():
646
+ if env_name == CONFIG.current_env_name:
647
+ env_out = click.style(env_name, fg='green', bold=True)
648
+ click.echo(f'{env_out} (current)')
649
+ elif env_name == CONFIG.default_env_name:
650
+ env_out = click.style(env_name, fg='cyan', bold=True)
651
+ click.echo(f'{env_out} (default)')
652
+ else:
653
+ env_out = click.style(env_name, fg='blue', bold=True)
654
+ click.echo(env_out)
655
+ click.echo(f' API URL: {env.api_url if env.api_url else not_set}')
656
+ click.echo(f' API Key: {hidden if env.api_key else not_set}')
657
+ click.echo('')
658
+ click.secho('Project-local configuration:', bold=True)
659
+ if CONFIG.current_env_name == CONFIG.LOCAL_CONFIG:
660
+ env_out = click.style(CONFIG.LOCAL_CONFIG, fg='green', bold=True)
661
+ click.echo(f'{env_out} (current)')
662
+ else:
663
+ env_out = click.style('local', fg='blue', bold=True)
664
+ click.echo(env_out)
665
+ if CONFIG.local_env.api_url:
666
+ click.echo(f' API URL: {CONFIG.local_env.api_url}')
667
+ else:
668
+ click.echo(f' API URL: {not_set}')
669
+ if CONFIG.local_env.api_key:
670
+ click.echo(f' API Key: {hidden}')
671
+ else:
672
+ click.echo(f' API Key: {not_set}')
673
+
674
+
675
+ @config.command(name='dot-env', help='Create a .env file with API configuration.')
525
676
  @click.argument('TEMPLATE-DIR', type=DIR_TYPE, default=CURRENT_DIR, required=False)
526
677
  @click.option('-u', '--api-url', metavar='API-URL', envvar='DSW_API_URL',
527
- prompt='API URL', help='URL of Wizard server API.', callback=rectify_url)
678
+ help='URL of Wizard server API.')
528
679
  @click.option('-k', '--api-key', metavar='API-KEY', envvar='DSW_API_KEY',
529
- prompt='API Key', help='API key for Wizard instance.', callback=rectify_key,
530
- hide_input=True)
680
+ help='API key for Wizard instance.')
531
681
  @click.option('-f', '--force', is_flag=True, help='Overwrite file if already exists.')
532
682
  @click.pass_context
533
- def create_dot_env(ctx, template_dir, api_url, api_key, force):
683
+ def config_create_dotenv(ctx, template_dir, api_url, api_key, force):
684
+ ensure_api_config(api_url, api_key)
534
685
  filename = ctx.obj.dot_env_file or '.env'
535
- tdk = TDKCore(logger=ctx.obj.logger)
686
+ output = pathlib.Path(template_dir) / filename
536
687
  try:
537
- tdk.create_dot_env(
538
- output=pathlib.Path(template_dir) / filename,
539
- force=force,
540
- api_url=api_url,
541
- api_key=api_key,
688
+ if output.exists():
689
+ if force:
690
+ ClickPrinter.warning(f'Overwriting {output.as_posix()} (forced)', )
691
+ else:
692
+ raise FileExistsError(f'File {output.as_posix()} already exists (not forced)')
693
+ output.write_text(
694
+ data=create_dot_env(
695
+ api_url=CONFIG.env.api_url,
696
+ api_key=CONFIG.env.api_key,
697
+ ),
698
+ encoding=DEFAULT_ENCODING,
542
699
  )
543
700
  except Exception as e:
544
701
  ClickPrinter.failure('Failed to create dot-env file')