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.
- {backlogops_cli-0.3 → backlogops_cli-0.4}/PKG-INFO +20 -5
- {backlogops_cli-0.3 → backlogops_cli-0.4}/README_pypi.md +17 -2
- {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/PKG-INFO +20 -5
- {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/SOURCES.txt +1 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/requires.txt +1 -1
- {backlogops_cli-0.3 → backlogops_cli-0.4}/pyproject.toml +2 -3
- {backlogops_cli-0.3 → backlogops_cli-0.4}/setup.py +2 -2
- backlogops_cli-0.4/src/backlogops_cli/_wizard_io.py +126 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/config_wizard.py +16 -5
- backlogops_cli-0.4/src/backlogops_cli/encrypt_token_file.py +120 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/preset_wizard.py +20 -5
- backlogops_cli-0.3/src/backlogops_cli/_wizard_io.py +0 -92
- {backlogops_cli-0.3 → backlogops_cli-0.4}/LICENSE.txt +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/dependency_links.txt +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/backlogops_cli.egg-info/top_level.txt +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/setup.cfg +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/__init__.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/__main__.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/_command_io.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/_migrate_warn.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/add_releases_to_jira.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/add_to_jira.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/adjust_release_content.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/bloc_version_reporter.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/convert.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/demo_backlog.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/estimate_ready_date.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/extract_keys.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/jira_fields.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/list.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/migrate_cfg.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_by_deps.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_by_keys.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_by_release.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_releases.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/order_releases_in_jira.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/plan_release_dates.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/py.typed +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/rank_in_jira.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/read_jira.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/rename_releases_in_jira.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/update_backlog_in_jira.py +0 -0
- {backlogops_cli-0.3 → backlogops_cli-0.4}/src/backlogops_cli/update_releases_in_jira.py +0 -0
- {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
|
+
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 ::
|
|
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.
|
|
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:
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
+
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 ::
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
|
@@ -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 ::
|
|
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.
|
|
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.
|
|
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
|
-
|
|
27
|
-
|
|
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.
|
|
34
|
-
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|