backlogops-cli 0.3__tar.gz → 0.4__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.4}/PKG-INFO +20 -5
  2. {backlogops_cli-0.3 → backlogops_cli-0.4}/README_pypi.md +17 -2
  3. {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/PKG-INFO +20 -5
  4. {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/SOURCES.txt +1 -0
  5. {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/requires.txt +1 -1
  6. {backlogops_cli-0.3 → backlogops_cli-0.4}/pyproject.toml +2 -3
  7. {backlogops_cli-0.3 → backlogops_cli-0.4}/setup.py +2 -2
  8. backlogops_cli-0.4/src/backlogops_cli/_wizard_io.py +126 -0
  9. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/config_wizard.py +16 -5
  10. backlogops_cli-0.4/src/backlogops_cli/encrypt_token_file.py +120 -0
  11. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/preset_wizard.py +20 -5
  12. backlogops_cli-0.3/src/backlogops_cli/_wizard_io.py +0 -92
  13. {backlogops_cli-0.3 → backlogops_cli-0.4}/LICENSE.txt +0 -0
  14. {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/dependency_links.txt +0 -0
  15. {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/top_level.txt +0 -0
  16. {backlogops_cli-0.3 → backlogops_cli-0.4}/setup.cfg +0 -0
  17. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/__init__.py +0 -0
  18. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/__main__.py +0 -0
  19. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/_command_io.py +0 -0
  20. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/_migrate_warn.py +0 -0
  21. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/add_releases_to_jira.py +0 -0
  22. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/add_to_jira.py +0 -0
  23. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/adjust_release_content.py +0 -0
  24. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/bloc_version_reporter.py +0 -0
  25. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/convert.py +0 -0
  26. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/demo_backlog.py +0 -0
  27. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/estimate_ready_date.py +0 -0
  28. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/extract_keys.py +0 -0
  29. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/jira_fields.py +0 -0
  30. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/list.py +0 -0
  31. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/migrate_cfg.py +0 -0
  32. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_by_deps.py +0 -0
  33. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_by_keys.py +0 -0
  34. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_by_release.py +0 -0
  35. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_releases.py +0 -0
  36. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_releases_in_jira.py +0 -0
  37. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/plan_release_dates.py +0 -0
  38. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/py.typed +0 -0
  39. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/rank_in_jira.py +0 -0
  40. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/read_jira.py +0 -0
  41. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/rename_releases_in_jira.py +0 -0
  42. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/update_backlog_in_jira.py +0 -0
  43. {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/update_releases_in_jira.py +0 -0
  44. {backlogops_cli-0.3 → backlogops_cli-0.4}/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.4
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.4
29
29
  Requires-Dist: versionreporter>=0.4
30
30
  Dynamic: author
31
31
  Dynamic: license-file
@@ -48,6 +48,18 @@ 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
+ backlogops is in beta. The algorithms intended for its initial scope are in
54
+ place, so the main work from here is on the user experience rather than on new
55
+ core functionality. The three packages share a version number and are released
56
+ together.
57
+
58
+ The library is not meant to ever be "finished": using it for real planning keeps
59
+ surfacing new ideas. Some of those will be added to backlogops itself, and
60
+ others will fit better in separate packages built on top of backlogops. Early
61
+ adopters are very welcome, and their feedback shapes what comes next.
62
+
51
63
  ## Available functionality
52
64
 
53
65
  The following functionality is available in all 3 packages:
@@ -249,6 +261,9 @@ backlogops_cli serves 2 purposes:
249
261
  python3 -m backlogops_cli.demo_backlog
250
262
  Write a demonstration backlog and releases to a file
251
263
 
264
+ python3 -m backlogops_cli.encrypt_token_file
265
+ Encrypt a Jira API token file with a pass phrase.
266
+
252
267
  python3 -m backlogops_cli.estimate_ready_date
253
268
  Estimate ready dates for the backlog items
254
269
 
@@ -307,9 +322,9 @@ backlogops_cli serves 2 purposes:
307
322
 
308
323
  ## Test summary
309
324
 
310
- - Test result: 2284 passed, 7 deselected in 44s
325
+ - Test result: 2638 passed, 7 deselected in 51s
311
326
  - No flake8 warnings.
312
327
  - No mypy errors found.
313
328
  - No python layout warnings.
314
- - Built version(s): 0.3
329
+ - Built version(s): 0.4
315
330
  - Build and test using Python 3.14.6
@@ -14,6 +14,18 @@ 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
+ backlogops is in beta. The algorithms intended for its initial scope are in
20
+ place, so the main work from here is on the user experience rather than on new
21
+ core functionality. The three packages share a version number and are released
22
+ together.
23
+
24
+ The library is not meant to ever be "finished": using it for real planning keeps
25
+ surfacing new ideas. Some of those will be added to backlogops itself, and
26
+ others will fit better in separate packages built on top of backlogops. Early
27
+ adopters are very welcome, and their feedback shapes what comes next.
28
+
17
29
  ## Available functionality
18
30
 
19
31
  The following functionality is available in all 3 packages:
@@ -215,6 +227,9 @@ backlogops_cli serves 2 purposes:
215
227
  python3 -m backlogops_cli.demo_backlog
216
228
  Write a demonstration backlog and releases to a file
217
229
 
230
+ python3 -m backlogops_cli.encrypt_token_file
231
+ Encrypt a Jira API token file with a pass phrase.
232
+
218
233
  python3 -m backlogops_cli.estimate_ready_date
219
234
  Estimate ready dates for the backlog items
220
235
 
@@ -273,9 +288,9 @@ backlogops_cli serves 2 purposes:
273
288
 
274
289
  ## Test summary
275
290
 
276
- - Test result: 2284 passed, 7 deselected in 44s
291
+ - Test result: 2638 passed, 7 deselected in 51s
277
292
  - No flake8 warnings.
278
293
  - No mypy errors found.
279
294
  - No python layout warnings.
280
- - Built version(s): 0.3
295
+ - Built version(s): 0.4
281
296
  - 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.4
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.4
29
29
  Requires-Dist: versionreporter>=0.4
30
30
  Dynamic: author
31
31
  Dynamic: license-file
@@ -48,6 +48,18 @@ 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
+ backlogops is in beta. The algorithms intended for its initial scope are in
54
+ place, so the main work from here is on the user experience rather than on new
55
+ core functionality. The three packages share a version number and are released
56
+ together.
57
+
58
+ The library is not meant to ever be "finished": using it for real planning keeps
59
+ surfacing new ideas. Some of those will be added to backlogops itself, and
60
+ others will fit better in separate packages built on top of backlogops. Early
61
+ adopters are very welcome, and their feedback shapes what comes next.
62
+
51
63
  ## Available functionality
52
64
 
53
65
  The following functionality is available in all 3 packages:
@@ -249,6 +261,9 @@ backlogops_cli serves 2 purposes:
249
261
  python3 -m backlogops_cli.demo_backlog
250
262
  Write a demonstration backlog and releases to a file
251
263
 
264
+ python3 -m backlogops_cli.encrypt_token_file
265
+ Encrypt a Jira API token file with a pass phrase.
266
+
252
267
  python3 -m backlogops_cli.estimate_ready_date
253
268
  Estimate ready dates for the backlog items
254
269
 
@@ -307,9 +322,9 @@ backlogops_cli serves 2 purposes:
307
322
 
308
323
  ## Test summary
309
324
 
310
- - Test result: 2284 passed, 7 deselected in 44s
325
+ - Test result: 2638 passed, 7 deselected in 51s
311
326
  - No flake8 warnings.
312
327
  - No mypy errors found.
313
328
  - No python layout warnings.
314
- - Built version(s): 0.3
329
+ - Built version(s): 0.4
315
330
  - 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.4
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.4',
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.4',
19
19
  'versionreporter >= 0.4'
20
20
  ]
21
21
  )
@@ -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
@@ -8,8 +8,10 @@
8
8
  import argparse
9
9
  import sys
10
10
  from typing import Optional
11
- from backlogops import backlog_ops_wizard
11
+ from backlogops import BacklogOpsConfig, backlog_ops_wizard, \
12
+ read_backlog_ops_config
12
13
  from backlogops_cli._command_io import parsed_args
14
+ from backlogops_cli._migrate_warn import CliMigrateWarnHook
13
15
  from backlogops_cli._wizard_io import build_wizard_parser, run_wizard_to_file
14
16
 
15
17
  DESCRIPTION = 'Create a backlog-ops configuration file via a wizard'
@@ -20,21 +22,30 @@ def build_parser() -> argparse.ArgumentParser:
20
22
  return build_wizard_parser(DESCRIPTION)
21
23
 
22
24
 
25
+ def _read_config(filename: str) -> BacklogOpsConfig:
26
+ """Read an existing backlog-ops configuration to pre-fill the wizard."""
27
+ return read_backlog_ops_config(filename, sys.stderr,
28
+ auto_ch_hook=CliMigrateWarnHook())
29
+
30
+
23
31
  def main(args: Optional[list[str]] = None) -> int:
24
32
  """Run the interactive wizard and write the backlog-ops configuration.
25
33
 
26
- The output filename receives the ``.cfg`` extension when it is not
27
- already present.
34
+ With ``-i`` an existing configuration file is read first and used to
35
+ pre-fill the wizard, so the user edits those values; pointing ``-i`` at
36
+ the same file as ``-o`` edits it in place after confirming the
37
+ overwrite. The output filename receives the ``.cfg`` extension when it
38
+ is not already present.
28
39
 
29
40
  Args:
30
41
  args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
31
42
 
32
43
  Returns:
33
44
  ``0`` on success, ``1`` when the entered configuration is rejected
34
- or cannot be written.
45
+ or cannot be read or written.
35
46
  """
36
47
  parsed = parsed_args(build_parser(), args)
37
- return run_wizard_to_file(parsed, backlog_ops_wizard,
48
+ return run_wizard_to_file(parsed, backlog_ops_wizard, _read_config,
38
49
  'Backlog-ops configuration')
39
50
 
40
51
 
@@ -0,0 +1,120 @@
1
+ #!/usr/local/bin/python3
2
+ """Encrypt a Jira API token to a file with a pass phrase.
3
+
4
+ Read a clear text API token from a file or from standard input, encrypt it
5
+ with a pass phrase and write the encrypted token to a file. The pass phrase
6
+ is requested on the terminal and is not echoed. The clear text token file
7
+ is not modified, unless the the encrypted token file is the same as the
8
+ clear text token file, in which case the clear text token file is overwritten
9
+ with the encrypted token. The encrypted token file is written atomically,
10
+ so that it is either the old version or the new version, and never a
11
+ half-written file.
12
+ """
13
+
14
+ # PYTHON_ARGCOMPLETE_OK
15
+ # Copyright (c) 2026, Tom Björkholm
16
+ # MIT License
17
+
18
+ import argparse
19
+ import sys
20
+ from getpass import getpass
21
+ from pathlib import Path
22
+ from typing import Optional
23
+ from backlogops import encrypt_token_to_file, encrypt_token_file
24
+ from backlogops_cli._command_io import parsed_args
25
+
26
+
27
+ DESCRIPTION = 'Encrypt a Jira API token file with a pass phrase.'
28
+
29
+
30
+ def _ok_to_overwrite(filename: Path) -> bool:
31
+ """Return True if the file can be overwritten, False otherwise."""
32
+ txt = f'The output file {filename} already exists. Overwrite? [y/n] '
33
+ while True:
34
+ answer = input(txt)
35
+ if answer.lower().strip() in ('y', 'ye', 'yes'):
36
+ return True
37
+ if answer.lower().strip() in ('n', 'no'):
38
+ return False
39
+ print('Please answer yes or no.')
40
+ return False # pragma: no cover
41
+
42
+
43
+ def build_parser() -> argparse.ArgumentParser:
44
+ """Build the command line parser for the encrypt-token-file command."""
45
+ descr = DESCRIPTION + '. The pass phrase is requested on the terminal.'
46
+ descr += ' The clear text token file is not modified, unless the output'
47
+ descr += ' file is the same as the input file, in which case the clear'
48
+ descr += ' text token file is overwritten with the encrypted token.'
49
+ parser = argparse.ArgumentParser(description=descr)
50
+ inhelp = 'The clear text token file to read from. Omit to read from stdin.'
51
+ parser.add_argument('-i', '--infile', required=False, help=inhelp)
52
+ parser.add_argument('-o', '--outfile', required=True,
53
+ help='The encrypted token file to write to.')
54
+ return parser
55
+
56
+
57
+ EXCEPTIONS = (FileNotFoundError, ValueError, FileExistsError,
58
+ NotADirectoryError, OSError)
59
+
60
+
61
+ def _run(parsed: argparse.Namespace) -> int:
62
+ """Run the encrypt-token-file command with the given parsed arguments.
63
+
64
+ Args:
65
+ parsed: The parsed command line arguments.
66
+ Returns:
67
+ 0 if the command was successful, 1 if there was an error
68
+ """
69
+ outfile = Path(parsed.outfile)
70
+ token: Optional[str] = None
71
+ pphrase1 = 'a'
72
+ phrase2 = 'b'
73
+ while pphrase1 != phrase2 or not pphrase1:
74
+ pphrase1 = getpass('Enter the pass phrase to encrypt the token with: ')
75
+ if not pphrase1:
76
+ print('No pass phrase entered. Exiting.')
77
+ return 1
78
+ phrase2 = getpass('Re-enter the pass phrase to confirm: ')
79
+ if pphrase1 != phrase2:
80
+ print('Pass phrases do not match. Please try again.')
81
+ if not parsed.infile:
82
+ print('Enter the clear text token on a single line, then press Enter.')
83
+ token = input().strip()
84
+ if not token:
85
+ print('No token entered. Exiting.')
86
+ return 1
87
+ try:
88
+ encrypt_token_to_file(token, passphrase=pphrase1, filename=outfile,
89
+ ok_to_overwrite=_ok_to_overwrite)
90
+ return 0
91
+ except EXCEPTIONS as e:
92
+ print(f'Error encrypting token: {e}')
93
+ return 1
94
+ infile = Path(parsed.infile)
95
+ try:
96
+ encrypt_token_file(clear_file=infile, encrypted_file=outfile,
97
+ passphrase=pphrase1,
98
+ ok_to_overwrite=_ok_to_overwrite)
99
+ return 0
100
+ except EXCEPTIONS as e:
101
+ print(f'Error encrypting token file: {e}')
102
+ return 1
103
+ return 2 # pragma: no cover # this should never be reached
104
+
105
+
106
+ def main(arguments: Optional[list[str]] = None) -> int:
107
+ """Encrypt a Jira API token file with a pass phrase.
108
+
109
+ Args:
110
+ arguments: The command line arguments to parse. If None, the arguments
111
+ are taken from sys.argv.
112
+ Returns:
113
+ 0 if the command was successful, 1 if there was an error.
114
+ """
115
+ parsed = parsed_args(build_parser(), arguments)
116
+ return _run(parsed)
117
+
118
+
119
+ if __name__ == '__main__': # pragma: no cover
120
+ sys.exit(main())
@@ -14,8 +14,10 @@ configuration is taken, by giving its file name.
14
14
  import argparse
15
15
  import sys
16
16
  from typing import Optional
17
- from backlogops import preset_wizard
17
+ from backlogops import InputFormatConfig, OutputFormatConfig, preset_wizard, \
18
+ read_io_preset
18
19
  from backlogops_cli._command_io import parsed_args
20
+ from backlogops_cli._migrate_warn import CliPresetMigrateWarnHook
19
21
  from backlogops_cli._wizard_io import build_wizard_parser, run_wizard_to_file
20
22
 
21
23
  DESCRIPTION = 'Create an input or output preset config file via a wizard'
@@ -26,22 +28,35 @@ def build_parser() -> argparse.ArgumentParser:
26
28
  return build_wizard_parser(DESCRIPTION)
27
29
 
28
30
 
31
+ def _read_preset(filename: str) -> InputFormatConfig | OutputFormatConfig:
32
+ """Read a stand-alone preset file, auto-detecting its direction.
33
+
34
+ The direction is detected from the file contents; the wizard still
35
+ lets the user switch direction afterwards.
36
+ """
37
+ return read_io_preset(filename, CliPresetMigrateWarnHook(), sys.stderr)
38
+
39
+
29
40
  def main(args: Optional[list[str]] = None) -> int:
30
41
  """Run the interactive IO preset wizard and write the preset file.
31
42
 
32
43
  The wizard asks whether to build an input or an output preset and then
33
- the settings for it. The output filename receives the ``.cfg``
34
- extension when it is not already present.
44
+ the settings for it. With ``-i`` an existing preset file is read first
45
+ and used to pre-fill the wizard; its direction (input or output) is
46
+ detected from the file, and pointing ``-i`` at the same file as ``-o``
47
+ edits it in place after confirming the overwrite. The output filename
48
+ receives the ``.cfg`` extension when it is not already present.
35
49
 
36
50
  Args:
37
51
  args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
38
52
 
39
53
  Returns:
40
54
  ``0`` on success, ``1`` when the wizard is abandoned or the preset
41
- cannot be written.
55
+ cannot be read or written.
42
56
  """
43
57
  parsed = parsed_args(build_parser(), args)
44
- return run_wizard_to_file(parsed, preset_wizard, 'IO preset configuration')
58
+ return run_wizard_to_file(parsed, preset_wizard, _read_preset,
59
+ 'IO preset configuration')
45
60
 
46
61
 
47
62
  if __name__ == '__main__': # pragma: no cover
@@ -1,92 +0,0 @@
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, a switch forcing the plain console interface, and a force
7
- flag), the same overwrite check, and the same run-write-report flow. That
8
- shared logic lives here; the leading underscore in the module name keeps
9
- it out of the command listing.
10
- """
11
- # PYTHON_ARGCOMPLETE_OK
12
-
13
- # Copyright (c) 2026, Tom Björkholm
14
- # MIT License
15
-
16
- import argparse
17
- import sys
18
- from collections.abc import Callable
19
- from pathlib import Path
20
- from config_as_json import Config
21
- from config_as_json.file_extension import fix_file_extension
22
- from tableio_cfg_json import WizardUiBridge, WizardUiBridgeConsole, \
23
- make_text_ui_bridge
24
- from backlogops_cli._command_io import add_force_arg, overwrite_callback
25
-
26
- CONFIG_EXTENSION = '.cfg'
27
-
28
-
29
- def build_wizard_parser(description: str) -> argparse.ArgumentParser:
30
- """Build a wizard parser with output, no-textual and force options."""
31
- parser = argparse.ArgumentParser(description=description)
32
- parser.add_argument('-o', '--output', dest='output', required=True,
33
- help='Configuration file to write; the '
34
- f'{CONFIG_EXTENSION} extension is added if missing.')
35
- parser.add_argument('--no-textual', dest='no_textual', action='store_true',
36
- help='Force the plain console interface instead of '
37
- 'the Textual full-screen interface.')
38
- add_force_arg(parser)
39
- return parser
40
-
41
-
42
- def _check_overwrite(output: str, force: bool) -> None:
43
- """Ask before overwriting an existing configuration file.
44
-
45
- The check runs before the wizard, so the user is not asked to confirm
46
- an overwrite only after entering the whole configuration.
47
- """
48
- if Path(output).exists():
49
- overwrite_callback(force)(output)
50
-
51
-
52
- def _make_bridge(no_textual: bool) -> WizardUiBridge:
53
- """Return the console bridge when forced, else the best text bridge.
54
-
55
- Without ``--no-textual`` the factory returns a Textual full-screen
56
- bridge in a real terminal and a console bridge otherwise, such as when
57
- input is redirected or under tests.
58
- """
59
- if no_textual:
60
- return WizardUiBridgeConsole(sys.stdout, sys.stdin, sys.stderr)
61
- return make_text_ui_bridge(sys.stdout, sys.stdin, sys.stderr)
62
-
63
-
64
- def run_wizard_to_file(parsed: argparse.Namespace,
65
- wizard: Callable[[WizardUiBridge], Config],
66
- label: str) -> int:
67
- """Run a wizard, write its configuration to the output file, report.
68
-
69
- The output filename receives the ``.cfg`` extension when it is not
70
- already present.
71
-
72
- Args:
73
- parsed: Parsed arguments holding ``output``, ``force`` and
74
- ``no_textual``.
75
- wizard: Wizard called with the chosen UI bridge; it returns a
76
- configuration object that knows how to write itself.
77
- label: Human-readable name of what was written, for the message.
78
-
79
- Returns:
80
- ``0`` on success, ``1`` when the wizard is abandoned or the
81
- configuration is rejected or cannot be written.
82
- """
83
- output = fix_file_extension(parsed.output, CONFIG_EXTENSION)
84
- try:
85
- _check_overwrite(output, parsed.force)
86
- config = wizard(_make_bridge(parsed.no_textual))
87
- config.write(to_json_filename=output, stderr_file=sys.stderr)
88
- except (ValueError, TypeError, KeyError, EOFError, OSError) as error:
89
- print(f'Could not create the configuration: {error}', file=sys.stderr)
90
- return 1
91
- print(f'{label} written to {output}')
92
- return 0
File without changes
File without changes