backlogops-cli 0.3__tar.gz → 0.5__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 (44) hide show
  1. {backlogops_cli-0.3 → backlogops_cli-0.5}/PKG-INFO +17 -5
  2. {backlogops_cli-0.3 → backlogops_cli-0.5}/README_pypi.md +14 -2
  3. {backlogops_cli-0.3 → backlogops_cli-0.5}/backlogops_cli.egg-info/PKG-INFO +17 -5
  4. {backlogops_cli-0.3 → backlogops_cli-0.5}/backlogops_cli.egg-info/SOURCES.txt +1 -0
  5. {backlogops_cli-0.3 → backlogops_cli-0.5}/backlogops_cli.egg-info/requires.txt +1 -1
  6. {backlogops_cli-0.3 → backlogops_cli-0.5}/pyproject.toml +2 -3
  7. {backlogops_cli-0.3 → backlogops_cli-0.5}/setup.py +2 -2
  8. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/_command_io.py +96 -1
  9. backlogops_cli-0.5/src/backlogops_cli/_wizard_io.py +126 -0
  10. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/add_releases_to_jira.py +23 -54
  11. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/add_to_jira.py +21 -54
  12. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/config_wizard.py +16 -5
  13. backlogops_cli-0.5/src/backlogops_cli/encrypt_token_file.py +121 -0
  14. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/jira_fields.py +4 -12
  15. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/order_by_deps.py +2 -3
  16. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/order_by_keys.py +2 -3
  17. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/order_by_release.py +2 -3
  18. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/order_releases.py +2 -3
  19. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/order_releases_in_jira.py +6 -16
  20. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/preset_wizard.py +20 -5
  21. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/rank_in_jira.py +6 -16
  22. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/read_jira.py +2 -8
  23. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/rename_releases_in_jira.py +6 -16
  24. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/update_backlog_in_jira.py +6 -18
  25. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/update_releases_in_jira.py +6 -17
  26. backlogops_cli-0.3/src/backlogops_cli/_wizard_io.py +0 -92
  27. {backlogops_cli-0.3 → backlogops_cli-0.5}/LICENSE.txt +0 -0
  28. {backlogops_cli-0.3 → backlogops_cli-0.5}/backlogops_cli.egg-info/dependency_links.txt +0 -0
  29. {backlogops_cli-0.3 → backlogops_cli-0.5}/backlogops_cli.egg-info/top_level.txt +0 -0
  30. {backlogops_cli-0.3 → backlogops_cli-0.5}/setup.cfg +0 -0
  31. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/__init__.py +0 -0
  32. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/__main__.py +0 -0
  33. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/_migrate_warn.py +0 -0
  34. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/adjust_release_content.py +0 -0
  35. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/bloc_version_reporter.py +0 -0
  36. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/convert.py +0 -0
  37. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/demo_backlog.py +0 -0
  38. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/estimate_ready_date.py +0 -0
  39. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/extract_keys.py +0 -0
  40. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/list.py +0 -0
  41. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/migrate_cfg.py +0 -0
  42. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/plan_release_dates.py +0 -0
  43. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/py.typed +0 -0
  44. {backlogops_cli-0.3 → backlogops_cli-0.5}/src/backlogops_cli/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops-cli
3
- Version: 0.3
3
+ Version: 0.5
4
4
  Summary: Command line interface for backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Development Status :: 3 - Alpha
17
+ Classifier: Development Status :: 4 - Beta
18
18
  Classifier: Environment :: Console
19
19
  Classifier: Intended Audience :: Developers
20
20
  Classifier: Intended Audience :: End Users/Desktop
@@ -25,7 +25,7 @@ Requires-Python: >=3.12
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE.txt
27
27
  Requires-Dist: argcomplete>=3.7.0
28
- Requires-Dist: backlogops>=0.3
28
+ Requires-Dist: backlogops>=0.5
29
29
  Requires-Dist: versionreporter>=0.4
30
30
  Dynamic: author
31
31
  Dynamic: license-file
@@ -48,6 +48,14 @@ There are 3 related packages for backlog operations:
48
48
  interface to use the functions in the library. It is based on TkInter. The
49
49
  ambition is to keep it as a thin wrapper around the library.
50
50
 
51
+ ## Project status
52
+
53
+ The three packages share a version number and are released together.
54
+
55
+ The library is not meant to ever be "finished": using it for real planning keeps
56
+ surfacing new ideas. Some of those will be added to backlogops itself, and
57
+ others will fit better in separate packages built on top of backlogops.
58
+
51
59
  ## Available functionality
52
60
 
53
61
  The following functionality is available in all 3 packages:
@@ -249,6 +257,9 @@ backlogops_cli serves 2 purposes:
249
257
  python3 -m backlogops_cli.demo_backlog
250
258
  Write a demonstration backlog and releases to a file
251
259
 
260
+ python3 -m backlogops_cli.encrypt_token_file
261
+ Encrypt a Jira API token file with a pass phrase.
262
+
252
263
  python3 -m backlogops_cli.estimate_ready_date
253
264
  Estimate ready dates for the backlog items
254
265
 
@@ -307,9 +318,10 @@ backlogops_cli serves 2 purposes:
307
318
 
308
319
  ## Test summary
309
320
 
310
- - Test result: 2284 passed, 7 deselected in 44s
321
+ - Test result: 2450 passed, 9 deselected in 41s
311
322
  - No flake8 warnings.
312
323
  - No mypy errors found.
324
+ - No pylint warnings.
313
325
  - No python layout warnings.
314
- - Built version(s): 0.3
326
+ - Built version(s): 0.5
315
327
  - Build and test using Python 3.14.6
@@ -14,6 +14,14 @@ There are 3 related packages for backlog operations:
14
14
  interface to use the functions in the library. It is based on TkInter. The
15
15
  ambition is to keep it as a thin wrapper around the library.
16
16
 
17
+ ## Project status
18
+
19
+ The three packages share a version number and are released together.
20
+
21
+ The library is not meant to ever be "finished": using it for real planning keeps
22
+ surfacing new ideas. Some of those will be added to backlogops itself, and
23
+ others will fit better in separate packages built on top of backlogops.
24
+
17
25
  ## Available functionality
18
26
 
19
27
  The following functionality is available in all 3 packages:
@@ -215,6 +223,9 @@ backlogops_cli serves 2 purposes:
215
223
  python3 -m backlogops_cli.demo_backlog
216
224
  Write a demonstration backlog and releases to a file
217
225
 
226
+ python3 -m backlogops_cli.encrypt_token_file
227
+ Encrypt a Jira API token file with a pass phrase.
228
+
218
229
  python3 -m backlogops_cli.estimate_ready_date
219
230
  Estimate ready dates for the backlog items
220
231
 
@@ -273,9 +284,10 @@ backlogops_cli serves 2 purposes:
273
284
 
274
285
  ## Test summary
275
286
 
276
- - Test result: 2284 passed, 7 deselected in 44s
287
+ - Test result: 2450 passed, 9 deselected in 41s
277
288
  - No flake8 warnings.
278
289
  - No mypy errors found.
290
+ - No pylint warnings.
279
291
  - No python layout warnings.
280
- - Built version(s): 0.3
292
+ - Built version(s): 0.5
281
293
  - Build and test using Python 3.14.6
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops-cli
3
- Version: 0.3
3
+ Version: 0.5
4
4
  Summary: Command line interface for backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Development Status :: 3 - Alpha
17
+ Classifier: Development Status :: 4 - Beta
18
18
  Classifier: Environment :: Console
19
19
  Classifier: Intended Audience :: Developers
20
20
  Classifier: Intended Audience :: End Users/Desktop
@@ -25,7 +25,7 @@ Requires-Python: >=3.12
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE.txt
27
27
  Requires-Dist: argcomplete>=3.7.0
28
- Requires-Dist: backlogops>=0.3
28
+ Requires-Dist: backlogops>=0.5
29
29
  Requires-Dist: versionreporter>=0.4
30
30
  Dynamic: author
31
31
  Dynamic: license-file
@@ -48,6 +48,14 @@ There are 3 related packages for backlog operations:
48
48
  interface to use the functions in the library. It is based on TkInter. The
49
49
  ambition is to keep it as a thin wrapper around the library.
50
50
 
51
+ ## Project status
52
+
53
+ The three packages share a version number and are released together.
54
+
55
+ The library is not meant to ever be "finished": using it for real planning keeps
56
+ surfacing new ideas. Some of those will be added to backlogops itself, and
57
+ others will fit better in separate packages built on top of backlogops.
58
+
51
59
  ## Available functionality
52
60
 
53
61
  The following functionality is available in all 3 packages:
@@ -249,6 +257,9 @@ backlogops_cli serves 2 purposes:
249
257
  python3 -m backlogops_cli.demo_backlog
250
258
  Write a demonstration backlog and releases to a file
251
259
 
260
+ python3 -m backlogops_cli.encrypt_token_file
261
+ Encrypt a Jira API token file with a pass phrase.
262
+
252
263
  python3 -m backlogops_cli.estimate_ready_date
253
264
  Estimate ready dates for the backlog items
254
265
 
@@ -307,9 +318,10 @@ backlogops_cli serves 2 purposes:
307
318
 
308
319
  ## Test summary
309
320
 
310
- - Test result: 2284 passed, 7 deselected in 44s
321
+ - Test result: 2450 passed, 9 deselected in 41s
311
322
  - No flake8 warnings.
312
323
  - No mypy errors found.
324
+ - No pylint warnings.
313
325
  - No python layout warnings.
314
- - Built version(s): 0.3
326
+ - Built version(s): 0.5
315
327
  - Build and test using Python 3.14.6
@@ -19,6 +19,7 @@ src/backlogops_cli/bloc_version_reporter.py
19
19
  src/backlogops_cli/config_wizard.py
20
20
  src/backlogops_cli/convert.py
21
21
  src/backlogops_cli/demo_backlog.py
22
+ src/backlogops_cli/encrypt_token_file.py
22
23
  src/backlogops_cli/estimate_ready_date.py
23
24
  src/backlogops_cli/extract_keys.py
24
25
  src/backlogops_cli/jira_fields.py
@@ -1,3 +1,3 @@
1
1
  argcomplete>=3.7.0
2
- backlogops>=0.3
2
+ backlogops>=0.5
3
3
  versionreporter>=0.4
@@ -4,7 +4,6 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "backlogops-cli"
7
- version = "0.3"
8
7
  authors = [
9
8
  { name="Tom Björkholm", email="klausuler_linnet0q@icloud.com" },
10
9
  ]
@@ -22,7 +21,7 @@ classifiers = [
22
21
  "Programming Language :: Python :: 3.13",
23
22
  "Programming Language :: Python :: 3.14",
24
23
  "Operating System :: OS Independent",
25
- "Development Status :: 3 - Alpha",
24
+ "Development Status :: 4 - Beta",
26
25
  "Environment :: Console",
27
26
  "Intended Audience :: Developers",
28
27
  "Intended Audience :: End Users/Desktop",
@@ -30,7 +29,7 @@ classifiers = [
30
29
  "Topic :: Office/Business :: Scheduling",
31
30
  "Typing :: Typed"
32
31
  ]
33
- dynamic = ["dependencies"]
32
+ dynamic = ["dependencies", "version"]
34
33
 
35
34
  [project.urls]
36
35
  "Homepage" = "https://github.com/tom-bjorkholm/backlog-ops"
@@ -5,7 +5,7 @@ from setuptools import setup
5
5
 
6
6
  setup(
7
7
  name='backlogops-cli',
8
- version='0.3',
8
+ version='0.5',
9
9
  description='Command line interface for backlog operations.',
10
10
  author='Tom Björkholm',
11
11
  author_email='klausuler_linnet0q@icloud.com',
@@ -15,7 +15,7 @@ setup(
15
15
  package_data={'backlogops_cli': ['src/py.typed']},
16
16
  install_requires=[
17
17
  'argcomplete >= 3.7.0',
18
- 'backlogops >= 0.3',
18
+ 'backlogops >= 0.5',
19
19
  'versionreporter >= 0.4'
20
20
  ]
21
21
  )
@@ -12,9 +12,10 @@ underscore in the module name keeps it out of the command listing.
12
12
 
13
13
  import argparse
14
14
  import sys
15
+ from getpass import getpass
15
16
  from pathlib import Path
16
17
  from collections.abc import Callable
17
- from typing import Optional, TextIO
18
+ from typing import Optional, TextIO, TypeVar
18
19
  import argcomplete
19
20
  from backlogops_cli._migrate_warn import (
20
21
  CliMigrateWarnHook, CliPresetMigrateWarnHook)
@@ -27,6 +28,9 @@ from backlogops import (
27
28
  resolve_output_config, write_backlog_releases, write_content_changes,
28
29
  write_date_changes)
29
30
 
31
+ ResultT = TypeVar('ResultT')
32
+ """Result type of a Jira write, carried from the operation to the report."""
33
+
30
34
  RANK_ANCHOR_CHOICES = {
31
35
  'backlog-top': JiraRankAnchor.BACKLOG_TOP,
32
36
  'backlog-bottom': JiraRankAnchor.BACKLOG_BOTTOM,
@@ -97,6 +101,11 @@ def parsed_args(parser: argparse.ArgumentParser,
97
101
  return parser.parse_args(args)
98
102
 
99
103
 
104
+ def jira_passphrase() -> str:
105
+ """Ask for the Jira API token pass phrase on the terminal."""
106
+ return getpass('Jira API token pass phrase: ')
107
+
108
+
100
109
  def add_input_args(parser: argparse.ArgumentParser) -> None:
101
110
  """Add the input-file and input-config arguments."""
102
111
  parser.add_argument('-i', '--input', dest='input', required=True,
@@ -120,6 +129,18 @@ def add_config_arg(parser: argparse.ArgumentParser) -> None:
120
129
  '$HOME/.backlogops.cfg.')
121
130
 
122
131
 
132
+ def add_preset_arg(parser: argparse.ArgumentParser) -> None:
133
+ """Add the required ``-p``/``--preset`` Jira preset argument."""
134
+ parser.add_argument('-p', '--preset', dest='preset', required=True,
135
+ help='Name of the Jira preset in the configuration.')
136
+
137
+
138
+ def add_quiet_arg(parser: argparse.ArgumentParser) -> None:
139
+ """Add the ``-q``/``--quiet`` flag suppressing the result lists."""
140
+ parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
141
+ help='Do not print the result lists to stdout.')
142
+
143
+
123
144
  def _resolve_config(parsed: argparse.Namespace) -> BacklogOpsConfig:
124
145
  """Return the backlog-ops configuration from ``-c`` or by discovery.
125
146
 
@@ -265,6 +286,23 @@ def build_io_parser(description: str, *, with_input: bool = True,
265
286
  return parser
266
287
 
267
288
 
289
+ def build_jira_parser(description: str, *,
290
+ with_input: bool = True) -> argparse.ArgumentParser:
291
+ """Create a parser with the config, preset and optional input options.
292
+
293
+ The Jira commands all take a backlog-ops configuration and a required
294
+ ``-p``/``--preset``. Most also read an input file, so ``with_input``
295
+ adds the input-file and input-config options by default; a command
296
+ that does not read a backlog file passes ``with_input=False``.
297
+ """
298
+ parser = argparse.ArgumentParser(description=description)
299
+ if with_input:
300
+ add_input_args(parser)
301
+ add_config_arg(parser)
302
+ add_preset_arg(parser)
303
+ return parser
304
+
305
+
268
306
  def _write_output(parsed: argparse.Namespace,
269
307
  config: Optional[BacklogOpsConfig],
270
308
  data: BacklogReleases) -> None:
@@ -317,6 +355,63 @@ def run_write(parsed: argparse.Namespace,
317
355
  return 0
318
356
 
319
357
 
358
+ def run_write_command(
359
+ build_parser: Callable[[], argparse.ArgumentParser],
360
+ produce: Callable[[argparse.Namespace, Optional[BacklogOpsConfig]],
361
+ BacklogReleases],
362
+ args: Optional[list[str]]) -> int:
363
+ """Parse the command line and write the produced backlog and releases.
364
+
365
+ Folds argument parsing and :func:`run_write` so an ordering command's
366
+ ``main`` stays a single delegating call. ``produce`` receives the
367
+ parsed args and the resolved config (or None) and returns the data.
368
+ """
369
+ parsed = parsed_args(build_parser(), args)
370
+ return run_write(parsed, lambda config: produce(parsed, config))
371
+
372
+
373
+ def write_result_file(config: BacklogOpsConfig, path: str,
374
+ data: BacklogReleases, force: bool) -> None:
375
+ """Write one backlog-and-releases result file, honoring ``--force``."""
376
+ out_config = resolve_output_config(None, data_file=path,
377
+ presets=config.output_configs,
378
+ auto_ch_hook=CliPresetMigrateWarnHook())
379
+ write_backlog_releases(data, path, out_config, FormatRules(),
380
+ levels=config.get_levels(),
381
+ file_exists_callback=overwrite_callback(force))
382
+ print(f'Wrote {path}')
383
+
384
+
385
+ def run_added_to_jira(
386
+ parsed: argparse.Namespace,
387
+ add: Callable[
388
+ [argparse.Namespace, BacklogOpsConfig, BacklogReleases], ResultT],
389
+ report: Callable[[ResultT], str], exists_error: type[Exception],
390
+ fail_note: str) -> int:
391
+ """Read the input, add it to Jira, and print the added lists.
392
+
393
+ ``add`` receives the parsed args, the config and the input data, adds
394
+ to Jira, writes any ``--added``/``--existing`` files, and returns the
395
+ result. ``report`` formats that result for stdout, printed unless
396
+ ``-q``/``--quiet`` was given. ``exists_error`` is the key-exists error
397
+ meaning nothing was added; other value or OS errors are reported with
398
+ ``fail_note``.
399
+ """
400
+ try:
401
+ config = required_config(parsed)
402
+ data = read_input(parsed, config)
403
+ result = add(parsed, config, data)
404
+ except exists_error:
405
+ print('Nothing added to Jira.', file=sys.stderr)
406
+ return 1
407
+ except (ValueError, TypeError, KeyError, OSError) as error:
408
+ print(f'{fail_note}: {error}', file=sys.stderr)
409
+ return 1
410
+ if not parsed.quiet:
411
+ print(report(result))
412
+ return 0
413
+
414
+
320
415
  DEFAULT_BUFFER_DAYS = 5
321
416
  """Default slack in calendar days added when fitting dates."""
322
417
 
@@ -0,0 +1,126 @@
1
+ #! /usr/local/bin/python3
2
+ """Shared helpers for the configuration and preset wizard commands.
3
+
4
+ Both wizard commands write a JSON configuration file built interactively
5
+ through a ``WizardUiBridge``. They share the same command line shape (an
6
+ output file, an optional input file whose contents pre-fill the wizard, a
7
+ switch forcing the plain console interface, and a force flag), the same
8
+ overwrite check, the same read-run-write-report flow, and the same
9
+ crash-safe write. That shared logic lives here; the leading underscore in
10
+ the module name keeps it out of the command listing.
11
+ """
12
+ # PYTHON_ARGCOMPLETE_OK
13
+
14
+ # Copyright (c) 2026, Tom Björkholm
15
+ # MIT License
16
+
17
+ import argparse
18
+ import sys
19
+ from collections.abc import Callable
20
+ from pathlib import Path
21
+ from typing import Optional, TypeVar
22
+ from config_as_json import Config
23
+ from config_as_json.file_extension import fix_file_extension
24
+ from tableio_cfg_json import WizardUiBridge, WizardUiBridgeConsole, \
25
+ make_text_ui_bridge
26
+ from backlogops import safe_write_config
27
+ from backlogops_cli._command_io import add_force_arg, overwrite_callback
28
+
29
+ CONFIG_EXTENSION = '.cfg'
30
+ _ConfigT = TypeVar('_ConfigT', bound=Config)
31
+
32
+
33
+ def build_wizard_parser(description: str) -> argparse.ArgumentParser:
34
+ """Build a wizard parser with output, input, no-textual and force."""
35
+ parser = argparse.ArgumentParser(description=description)
36
+ parser.add_argument('-o', '--output', dest='output', required=True,
37
+ help='Configuration file to write; the '
38
+ f'{CONFIG_EXTENSION} extension is added if missing.')
39
+ parser.add_argument('-i', '--input', dest='input',
40
+ help='Existing configuration file whose contents '
41
+ 'pre-fill the wizard as the starting answers, so you '
42
+ 'edit them instead of starting empty. Use the same '
43
+ 'path as -o to edit that file in place; you are then '
44
+ f'asked to confirm overwriting it. The '
45
+ f'{CONFIG_EXTENSION} extension is assumed when the '
46
+ 'file is not found as named.')
47
+ parser.add_argument('--no-textual', dest='no_textual', action='store_true',
48
+ help='Force the plain console interface instead of '
49
+ 'the Textual full-screen interface.')
50
+ add_force_arg(parser)
51
+ return parser
52
+
53
+
54
+ def _check_overwrite(output: str, force: bool) -> None:
55
+ """Ask before overwriting an existing configuration file.
56
+
57
+ The check runs before the wizard, so the user is not asked to confirm
58
+ an overwrite only after entering the whole configuration.
59
+ """
60
+ if Path(output).exists():
61
+ overwrite_callback(force)(output)
62
+
63
+
64
+ def _make_bridge(no_textual: bool) -> WizardUiBridge:
65
+ """Return the console bridge when forced, else the best text bridge.
66
+
67
+ Without ``--no-textual`` the factory returns a Textual full-screen
68
+ bridge in a real terminal and a console bridge otherwise, such as when
69
+ input is redirected or under tests.
70
+ """
71
+ if no_textual:
72
+ return WizardUiBridgeConsole(sys.stdout, sys.stdin, sys.stderr)
73
+ return make_text_ui_bridge(sys.stdout, sys.stdin, sys.stderr)
74
+
75
+
76
+ def _read_default(input_file: Optional[str],
77
+ reader: Callable[[str], _ConfigT]) -> Optional[_ConfigT]:
78
+ """Read the pre-fill file, or return None when none is requested.
79
+
80
+ The ``.cfg`` extension is assumed when the named file is not found as
81
+ given, matching how the output filename is completed.
82
+ """
83
+ if input_file is None:
84
+ return None
85
+ path = fix_file_extension(input_file, CONFIG_EXTENSION, for_reading=True)
86
+ if not Path(path).is_file():
87
+ raise ValueError(f'File to read initial values from not found: {path}')
88
+ return reader(path)
89
+
90
+
91
+ def run_wizard_to_file(parsed: argparse.Namespace,
92
+ wizard: Callable[..., _ConfigT],
93
+ reader: Callable[[str], _ConfigT], label: str) -> int:
94
+ """Run a wizard, write its configuration to the output file, report.
95
+
96
+ When ``parsed.input`` names an existing file it is read first and used
97
+ to pre-fill the wizard, so the user edits those values instead of
98
+ starting from scratch. The output filename receives the ``.cfg``
99
+ extension when it is not already present, an existing output file is
100
+ only overwritten after confirmation (or with ``--force``), and the
101
+ result is written crash-safely through a ``.in_progress`` sibling file.
102
+
103
+ Args:
104
+ parsed: Parsed arguments holding ``output``, ``input``, ``force``
105
+ and ``no_textual``.
106
+ wizard: Wizard called with the chosen UI bridge and the pre-fill
107
+ default; it returns a configuration object that knows how to
108
+ write itself.
109
+ reader: Reads the ``input`` file into a default for the wizard.
110
+ label: Human-readable name of what was written, for the message.
111
+
112
+ Returns:
113
+ ``0`` on success, ``1`` when the wizard is abandoned or the
114
+ configuration is rejected or cannot be read or written.
115
+ """
116
+ output = fix_file_extension(parsed.output, CONFIG_EXTENSION)
117
+ try:
118
+ default = _read_default(parsed.input, reader)
119
+ _check_overwrite(output, parsed.force)
120
+ config = wizard(_make_bridge(parsed.no_textual), default=default)
121
+ safe_write_config(config, output, sys.stderr)
122
+ except (ValueError, TypeError, KeyError, EOFError, OSError) as error:
123
+ print(f'Could not create the configuration: {error}', file=sys.stderr)
124
+ return 1
125
+ print(f'{label} written to {output}')
126
+ return 0
@@ -20,27 +20,21 @@ asked on the terminal only when it is needed.
20
20
 
21
21
  import argparse
22
22
  import sys
23
- from getpass import getpass
24
23
  from typing import Optional
25
24
  from backlogops import (
26
- AddedReleasesToJira, BacklogOpsConfig, BacklogReleases, FormatRules,
27
- JiraConnections, OnExistingKey, ReleaseExistsError, add_releases_to_jira,
28
- format_release_result, resolve_output_config, write_backlog_releases)
25
+ AddedReleasesToJira, BacklogOpsConfig, BacklogReleases, JiraConnections,
26
+ OnExistingKey, ReleaseExistsError, add_releases_to_jira,
27
+ format_release_result)
29
28
  from backlogops_cli._command_io import (
30
- add_config_arg, add_input_args, overwrite_callback, parsed_args,
31
- read_input, required_config)
32
- from backlogops_cli._migrate_warn import CliPresetMigrateWarnHook
29
+ add_force_arg, add_quiet_arg, build_jira_parser, jira_passphrase,
30
+ parsed_args, run_added_to_jira, write_result_file)
33
31
 
34
32
  DESCRIPTION = 'Add releases to Jira, creating a new version per release'
35
33
 
36
34
 
37
35
  def build_parser() -> argparse.ArgumentParser:
38
36
  """Build the command line parser for the add-releases command."""
39
- parser = argparse.ArgumentParser(description=DESCRIPTION)
40
- add_input_args(parser)
41
- add_config_arg(parser)
42
- parser.add_argument('-p', '--preset', dest='preset', required=True,
43
- help='Name of the Jira preset in the configuration.')
37
+ parser = build_jira_parser(DESCRIPTION)
44
38
  parser.add_argument('--skip-existing', dest='skip_existing',
45
39
  action='store_true',
46
40
  help='Skip releases whose name already exists in Jira '
@@ -52,26 +46,17 @@ def build_parser() -> argparse.ArgumentParser:
52
46
  metavar='FILE',
53
47
  help='Write the releases already in Jira, with the '
54
48
  'input backlog, to this file. Omit to not write it.')
55
- parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
56
- help='Do not print the added and already-present '
57
- 'lists to stdout.')
58
- parser.add_argument('-f', '--force', dest='force', action='store_true',
59
- help='Overwrite existing output files without '
60
- 'asking.')
49
+ add_quiet_arg(parser)
50
+ add_force_arg(parser)
61
51
  return parser
62
52
 
63
53
 
64
- def _passphrase() -> str:
65
- """Ask for the Jira token pass phrase on the terminal."""
66
- return getpass('Jira API token pass phrase: ')
67
-
68
-
69
54
  def _add(parsed: argparse.Namespace, config: BacklogOpsConfig,
70
55
  data: BacklogReleases) -> AddedReleasesToJira:
71
56
  """Add the input releases to Jira using the named preset."""
72
57
  print(f"Adding releases to Jira using preset '{parsed.preset}'...",
73
58
  file=sys.stderr)
74
- connections = JiraConnections(config.get_jira_config(), _passphrase)
59
+ connections = JiraConnections(config.get_jira_config(), jira_passphrase)
75
60
  mode = (OnExistingKey.SKIP if parsed.skip_existing
76
61
  else OnExistingKey.RAISE)
77
62
  result = add_releases_to_jira(connections, parsed.preset, data.releases,
@@ -82,18 +67,6 @@ def _add(parsed: argparse.Namespace, config: BacklogOpsConfig,
82
67
  return result
83
68
 
84
69
 
85
- def _write_file(config: BacklogOpsConfig, path: str, data: BacklogReleases,
86
- force: bool) -> None:
87
- """Write one backlog-and-releases result to a file."""
88
- out_config = resolve_output_config(None, data_file=path,
89
- presets=config.output_configs,
90
- auto_ch_hook=CliPresetMigrateWarnHook())
91
- write_backlog_releases(data, path, out_config, FormatRules(),
92
- levels=config.get_levels(),
93
- file_exists_callback=overwrite_callback(force))
94
- print(f'Wrote {path}')
95
-
96
-
97
70
  def _write_result_files(parsed: argparse.Namespace, config: BacklogOpsConfig,
98
71
  data: BacklogReleases,
99
72
  result: AddedReleasesToJira) -> None:
@@ -101,29 +74,26 @@ def _write_result_files(parsed: argparse.Namespace, config: BacklogOpsConfig,
101
74
  backlog = list(data.backlog)
102
75
  if parsed.added_file is not None:
103
76
  added = BacklogReleases(backlog=backlog, releases=result.stored)
104
- _write_file(config, parsed.added_file, added, parsed.force)
77
+ write_result_file(config, parsed.added_file, added, parsed.force)
105
78
  if parsed.existing_file is not None:
106
79
  present = BacklogReleases(backlog=backlog,
107
80
  releases=result.already_present)
108
- _write_file(config, parsed.existing_file, present, parsed.force)
81
+ write_result_file(config, parsed.existing_file, present, parsed.force)
82
+
83
+
84
+ def _add_and_write(parsed: argparse.Namespace, config: BacklogOpsConfig,
85
+ data: BacklogReleases) -> AddedReleasesToJira:
86
+ """Add the releases to Jira and write any requested result files."""
87
+ result = _add(parsed, config, data)
88
+ _write_result_files(parsed, config, data, result)
89
+ return result
109
90
 
110
91
 
111
92
  def _run(parsed: argparse.Namespace) -> int:
112
93
  """Read the input, add the releases, write files and print the lists."""
113
- try:
114
- config = required_config(parsed)
115
- data = read_input(parsed, config)
116
- result = _add(parsed, config, data)
117
- _write_result_files(parsed, config, data, result)
118
- except ReleaseExistsError:
119
- print('Nothing added to Jira.', file=sys.stderr)
120
- return 1
121
- except (ValueError, TypeError, KeyError, OSError) as error:
122
- print(f'Could not add releases to Jira: {error}', file=sys.stderr)
123
- return 1
124
- if not parsed.quiet:
125
- print(format_release_result(result))
126
- return 0
94
+ return run_added_to_jira(parsed, _add_and_write, format_release_result,
95
+ ReleaseExistsError,
96
+ 'Could not add releases to Jira')
127
97
 
128
98
 
129
99
  def main(args: Optional[list[str]] = None) -> int:
@@ -136,8 +106,7 @@ def main(args: Optional[list[str]] = None) -> int:
136
106
  ``0`` on success, ``1`` when the releases cannot be added or a name
137
107
  already exists in Jira without ``--skip-existing``.
138
108
  """
139
- parsed = parsed_args(build_parser(), args)
140
- return _run(parsed)
109
+ return _run(parsed_args(build_parser(), args))
141
110
 
142
111
 
143
112
  if __name__ == '__main__': # pragma: no cover