backlogops-cli 0.1__py3-none-any.whl → 0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backlogops_cli/_command_io.py +245 -69
- backlogops_cli/_migrate_warn.py +56 -0
- backlogops_cli/_wizard_io.py +92 -0
- backlogops_cli/adjust_release_content.py +4 -3
- backlogops_cli/bloc_version_reporter.py +25 -0
- backlogops_cli/config_wizard.py +42 -0
- backlogops_cli/convert.py +4 -7
- backlogops_cli/demo_backlog.py +5 -6
- backlogops_cli/estimate_ready_date.py +18 -28
- backlogops_cli/extract_keys.py +13 -10
- backlogops_cli/list.py +4 -1
- backlogops_cli/migrate_cfg.py +82 -0
- backlogops_cli/order_by_deps.py +7 -8
- backlogops_cli/order_by_keys.py +7 -8
- backlogops_cli/order_by_release.py +71 -0
- backlogops_cli/order_releases.py +61 -0
- backlogops_cli/plan_release_dates.py +4 -3
- backlogops_cli/preset_wizard.py +48 -0
- backlogops_cli/version.py +20 -0
- {backlogops_cli-0.1.dist-info → backlogops_cli-0.2.dist-info}/METADATA +78 -31
- backlogops_cli-0.2.dist-info/RECORD +27 -0
- backlogops_cli/teams_wizard.py +0 -55
- backlogops_cli-0.1.dist-info/RECORD +0 -19
- {backlogops_cli-0.1.dist-info → backlogops_cli-0.2.dist-info}/WHEEL +0 -0
- {backlogops_cli-0.1.dist-info → backlogops_cli-0.2.dist-info}/licenses/LICENSE.txt +0 -0
- {backlogops_cli-0.1.dist-info → backlogops_cli-0.2.dist-info}/top_level.txt +0 -0
backlogops_cli/_command_io.py
CHANGED
|
@@ -12,21 +12,60 @@ underscore in the module name keeps it out of the command listing.
|
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
14
|
import sys
|
|
15
|
+
from pathlib import Path
|
|
15
16
|
from collections.abc import Callable
|
|
16
|
-
from typing import Optional
|
|
17
|
+
from typing import Optional, TextIO
|
|
17
18
|
import argcomplete
|
|
19
|
+
from backlogops_cli._migrate_warn import (
|
|
20
|
+
CliMigrateWarnHook, CliPresetMigrateWarnHook)
|
|
21
|
+
from backlogops_cli.bloc_version_reporter import BloCliVersionReporter
|
|
18
22
|
from backlogops import (
|
|
19
|
-
BacklogReleases,
|
|
20
|
-
ReleaseChanges, ReleaseDateChanges,
|
|
21
|
-
|
|
22
|
-
resolve_input_config, resolve_output_config,
|
|
23
|
-
write_content_changes, write_date_changes)
|
|
23
|
+
BacklogOpsConfig, BacklogReleases, FileExistsCb, FormatRules, Levels,
|
|
24
|
+
ReleaseChanges, ReleaseDateChanges, allow_overwrite,
|
|
25
|
+
format_content_changes, format_date_changes, get_backlog_ops_config,
|
|
26
|
+
read_backlog_releases, resolve_input_config, resolve_output_config,
|
|
27
|
+
write_backlog_releases, write_content_changes, write_date_changes)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def overwrite_callback(force: bool, in_stream: Optional[TextIO] = None,
|
|
31
|
+
out_stream: Optional[TextIO] = None) -> FileExistsCb:
|
|
32
|
+
"""Return a file-exists callback for writing CLI output files.
|
|
33
|
+
|
|
34
|
+
A writer calls the returned callback only when the target file
|
|
35
|
+
already exists. With ``force`` the overwrite is allowed silently.
|
|
36
|
+
Otherwise the user is asked on ``out_stream``/``in_stream`` and the
|
|
37
|
+
overwrite is allowed only on an explicit yes; any other answer, an
|
|
38
|
+
empty answer, or end of input refuses it with ``FileExistsError``.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
force: Allow the overwrite without asking when True.
|
|
42
|
+
in_stream: Stream the answer is read from, or None for stdin.
|
|
43
|
+
out_stream: Stream the prompt is written to, or None for stdout.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A callback suitable as ``file_exists_callback`` for the writers.
|
|
47
|
+
"""
|
|
48
|
+
if force:
|
|
49
|
+
return allow_overwrite
|
|
50
|
+
reader = sys.stdin if in_stream is None else in_stream
|
|
51
|
+
writer = sys.stdout if out_stream is None else out_stream
|
|
52
|
+
|
|
53
|
+
def ask(file_name: str) -> None:
|
|
54
|
+
"""Ask whether to overwrite ``file_name``; raise when refused."""
|
|
55
|
+
prompt = f'Output file {file_name} already exists. Overwrite? [y/N]: '
|
|
56
|
+
print(prompt, end='', file=writer)
|
|
57
|
+
writer.flush()
|
|
58
|
+
if reader.readline().strip().lower() in ('y', 'yes'):
|
|
59
|
+
return
|
|
60
|
+
raise FileExistsError(f'Did not overwrite existing file {file_name}.')
|
|
61
|
+
return ask
|
|
24
62
|
|
|
25
63
|
|
|
26
64
|
def parsed_args(parser: argparse.ArgumentParser,
|
|
27
65
|
args: Optional[list[str]]) -> argparse.Namespace:
|
|
28
66
|
"""Enable shell completion and parse the command line arguments."""
|
|
29
67
|
argcomplete.autocomplete(parser)
|
|
68
|
+
BloCliVersionReporter().check_if_unsupported_python()
|
|
30
69
|
return parser.parse_args(args)
|
|
31
70
|
|
|
32
71
|
|
|
@@ -38,85 +77,205 @@ def add_input_args(parser: argparse.ArgumentParser) -> None:
|
|
|
38
77
|
help='Input format: a config file or a preset name.')
|
|
39
78
|
|
|
40
79
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
80
|
+
def add_config_arg(parser: argparse.ArgumentParser) -> None:
|
|
81
|
+
"""Add the ``-c``/``--config`` backlog-ops configuration argument.
|
|
82
|
+
|
|
83
|
+
The configuration file holds the workforce, the named input and output
|
|
84
|
+
presets, the levels and the global status map. Without ``-c`` the file
|
|
85
|
+
is discovered the same way as the GUI.
|
|
86
|
+
"""
|
|
87
|
+
parser.add_argument('-c', '--config', dest='config',
|
|
88
|
+
help='Backlog-ops configuration file (workforce, '
|
|
89
|
+
'named presets, levels, status map). Without -c the '
|
|
90
|
+
'file is found from $BACKLOGOPS_CFG, else '
|
|
91
|
+
'backlogops.cfg in $BACKLOGOPS_DIR, else '
|
|
92
|
+
'$HOME/.backlogops.cfg.')
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _resolve_config(parsed: argparse.Namespace) -> BacklogOpsConfig:
|
|
96
|
+
"""Return the backlog-ops configuration from ``-c`` or by discovery.
|
|
97
|
+
|
|
98
|
+
With ``-c`` the named file is read; an old file triggers a migration
|
|
99
|
+
warning. Without ``-c`` the file is discovered the same way as the GUI
|
|
100
|
+
(``$BACKLOGOPS_CFG``, then ``backlogops.cfg`` in ``$BACKLOGOPS_DIR``,
|
|
101
|
+
then ``$HOME/.backlogops.cfg``).
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If ``-c`` names a file that does not exist.
|
|
105
|
+
RuntimeError: If no ``-c`` is given and no file is discovered.
|
|
106
|
+
"""
|
|
107
|
+
config_file = parsed.config
|
|
108
|
+
if config_file is not None:
|
|
109
|
+
if not Path(config_file).is_file():
|
|
110
|
+
raise ValueError(f'Configuration file not found: {config_file}')
|
|
111
|
+
return get_backlog_ops_config(config_file, sys.stderr,
|
|
112
|
+
auto_ch_hook=CliMigrateWarnHook())
|
|
113
|
+
return get_backlog_ops_config(None, sys.stderr,
|
|
114
|
+
auto_ch_hook=CliMigrateWarnHook())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def required_config(parsed: argparse.Namespace) -> BacklogOpsConfig:
|
|
118
|
+
"""Return the configuration, reporting a missing one as a ValueError.
|
|
119
|
+
|
|
120
|
+
Used by commands that cannot work without a configuration, such as the
|
|
121
|
+
estimate command, which needs the workforce.
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
return _resolve_config(parsed)
|
|
125
|
+
except RuntimeError as error:
|
|
126
|
+
raise ValueError(str(error)) from error
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def optional_config(parsed: argparse.Namespace) -> Optional[BacklogOpsConfig]:
|
|
130
|
+
"""Return the configuration, or None with a note when none is found.
|
|
131
|
+
|
|
132
|
+
Used by commands that fall back to the built-in defaults (formats
|
|
133
|
+
inferred from the file name, no presets) when no configuration file is
|
|
134
|
+
available.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
return _resolve_config(parsed)
|
|
138
|
+
except RuntimeError:
|
|
139
|
+
print('No backlog-ops configuration file found; using built-in '
|
|
140
|
+
'defaults.', file=sys.stderr)
|
|
45
141
|
return None
|
|
46
|
-
return read_available_teams(io_config, sys.stderr).input_configs
|
|
47
142
|
|
|
48
143
|
|
|
49
|
-
def
|
|
144
|
+
def io_levels(config: Optional[BacklogOpsConfig]) -> Optional[Levels]:
|
|
145
|
+
"""Return the configured levels from ``config``, or None.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
config: The resolved backlog-ops configuration, or None to use the
|
|
149
|
+
default levels.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The levels configured in ``config``, or None when no configuration
|
|
153
|
+
is given.
|
|
154
|
+
"""
|
|
155
|
+
return config.get_levels() if config is not None else None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_input(parsed: argparse.Namespace,
|
|
159
|
+
config: Optional[BacklogOpsConfig]) -> BacklogReleases:
|
|
50
160
|
"""Read and validate the backlog and releases from the input file.
|
|
51
161
|
|
|
52
162
|
The input format is resolved from the ``--input-config`` value, which
|
|
53
163
|
may be empty (inferred from the file name), a preset name looked up in
|
|
54
|
-
|
|
164
|
+
``config``, or a config file path. When ``config`` is given its levels
|
|
165
|
+
and its library-wide status input map are honoured while reading the
|
|
166
|
+
items; the input configuration's own status map overrides the global
|
|
167
|
+
one per name.
|
|
55
168
|
|
|
56
169
|
Args:
|
|
57
170
|
parsed: Parsed command line arguments holding the input options
|
|
58
|
-
added by :func:`add_input_args
|
|
59
|
-
|
|
171
|
+
added by :func:`add_input_args`.
|
|
172
|
+
config: The resolved backlog-ops configuration, or None to use the
|
|
173
|
+
built-in defaults.
|
|
60
174
|
|
|
61
175
|
Returns:
|
|
62
176
|
The validated backlog and releases read from the input file.
|
|
63
177
|
"""
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
178
|
+
presets = config.input_configs if config is not None else None
|
|
179
|
+
levels = config.get_levels() if config is not None else None
|
|
180
|
+
status_map = (config.get_status_input_map() if config is not None
|
|
181
|
+
else None)
|
|
182
|
+
in_config = resolve_input_config(parsed.input_config,
|
|
183
|
+
data_file=parsed.input, presets=presets,
|
|
184
|
+
auto_ch_hook=CliPresetMigrateWarnHook())
|
|
185
|
+
data = read_backlog_releases(parsed.input, in_config, levels, status_map)
|
|
69
186
|
data.check_consistency(sys.stderr)
|
|
70
187
|
return data
|
|
71
188
|
|
|
72
189
|
|
|
190
|
+
def add_force_arg(parser: argparse.ArgumentParser) -> None:
|
|
191
|
+
"""Add the force flag that overwrites output files without asking."""
|
|
192
|
+
parser.add_argument('-f', '--force', dest='force', action='store_true',
|
|
193
|
+
help='Overwrite existing output files without '
|
|
194
|
+
'asking.')
|
|
195
|
+
|
|
196
|
+
|
|
73
197
|
def add_output_args(parser: argparse.ArgumentParser) -> None:
|
|
74
198
|
"""Add the output-file, output-config and ordering arguments."""
|
|
75
199
|
parser.add_argument('-o', '--output', dest='output', required=True,
|
|
76
200
|
help='Output data file to create.')
|
|
77
201
|
parser.add_argument('-O', '--output-config', dest='output_config',
|
|
78
202
|
help='Output format: a config file or a preset name.')
|
|
79
|
-
parser.add_argument('--io-config', dest='io_config',
|
|
80
|
-
help='Configuration file holding the named presets '
|
|
81
|
-
'(by default the teams configuration file).')
|
|
82
203
|
parser.add_argument('--releases-first', dest='releases_first',
|
|
83
204
|
action='store_true',
|
|
84
205
|
help='Write the releases before the backlog.')
|
|
206
|
+
add_force_arg(parser)
|
|
85
207
|
|
|
86
208
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
209
|
+
def build_io_parser(description: str, *, with_input: bool = True,
|
|
210
|
+
with_config: bool = True,
|
|
211
|
+
with_output: bool = True) -> argparse.ArgumentParser:
|
|
212
|
+
"""Create a parser with the common input, config and output options.
|
|
213
|
+
|
|
214
|
+
Most data commands read a file, take the backlog-ops configuration,
|
|
215
|
+
and write a file, so this builds the parser with those option groups
|
|
216
|
+
already added. A command adds only its own extra options to the
|
|
217
|
+
returned parser. A group is left out when its flag is False, for a
|
|
218
|
+
command that does not read (or does not write) a backlog file.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
description: The command description shown in the help text.
|
|
222
|
+
with_input: Add the input-file and input-config options.
|
|
223
|
+
with_config: Add the ``-c`` backlog-ops configuration option.
|
|
224
|
+
with_output: Add the output-file, output-config, ordering and
|
|
225
|
+
force options.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The parser with the requested common options added.
|
|
229
|
+
"""
|
|
230
|
+
parser = argparse.ArgumentParser(description=description)
|
|
231
|
+
if with_input:
|
|
232
|
+
add_input_args(parser)
|
|
233
|
+
if with_config:
|
|
234
|
+
add_config_arg(parser)
|
|
235
|
+
if with_output:
|
|
236
|
+
add_output_args(parser)
|
|
237
|
+
return parser
|
|
93
238
|
|
|
94
239
|
|
|
95
|
-
def _write_output(parsed: argparse.Namespace,
|
|
240
|
+
def _write_output(parsed: argparse.Namespace,
|
|
241
|
+
config: Optional[BacklogOpsConfig],
|
|
242
|
+
data: BacklogReleases) -> None:
|
|
96
243
|
"""Write the backlog and releases to the configured output file."""
|
|
97
|
-
presets =
|
|
98
|
-
|
|
99
|
-
|
|
244
|
+
presets = config.output_configs if config is not None else None
|
|
245
|
+
out_config = resolve_output_config(parsed.output_config,
|
|
246
|
+
data_file=parsed.output,
|
|
247
|
+
presets=presets,
|
|
248
|
+
auto_ch_hook=CliPresetMigrateWarnHook())
|
|
100
249
|
rules = FormatRules(backlog_first=not parsed.releases_first)
|
|
101
|
-
write_backlog_releases(data, parsed.output,
|
|
250
|
+
write_backlog_releases(data, parsed.output, out_config, rules,
|
|
251
|
+
levels=io_levels(config),
|
|
252
|
+
file_exists_callback=overwrite_callback(
|
|
253
|
+
parsed.force))
|
|
102
254
|
|
|
103
255
|
|
|
104
256
|
def run_write(parsed: argparse.Namespace,
|
|
105
|
-
data_source: Callable[[],
|
|
257
|
+
data_source: Callable[[Optional[BacklogOpsConfig]],
|
|
258
|
+
BacklogReleases]) -> int:
|
|
106
259
|
"""Build the data, write it to the output file, and report the result.
|
|
107
260
|
|
|
261
|
+
The configuration is resolved once from ``-c`` or by discovery, falling
|
|
262
|
+
back to the built-in defaults when none is found.
|
|
263
|
+
|
|
108
264
|
Args:
|
|
109
265
|
parsed: Parsed command line arguments holding the output options
|
|
110
|
-
added by :func:`add_output_args
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
266
|
+
added by :func:`add_output_args` and the ``-c`` option added by
|
|
267
|
+
:func:`add_config_arg`.
|
|
268
|
+
data_source: Callable that receives the resolved configuration (or
|
|
269
|
+
None) and returns the backlog and releases to write. It is
|
|
270
|
+
called inside the error handling so that reading failures are
|
|
271
|
+
reported like writing failures.
|
|
114
272
|
|
|
115
273
|
Returns:
|
|
116
274
|
``0`` on success, ``1`` when the data cannot be built or written.
|
|
117
275
|
"""
|
|
118
276
|
try:
|
|
119
|
-
|
|
277
|
+
config = optional_config(parsed)
|
|
278
|
+
_write_output(parsed, config, data_source(config))
|
|
120
279
|
except (ValueError, TypeError, KeyError, OSError) as error:
|
|
121
280
|
print(f'Could not write {parsed.output}: {error}', file=sys.stderr)
|
|
122
281
|
return 1
|
|
@@ -146,55 +305,72 @@ def add_changes_arg(parser: argparse.ArgumentParser) -> None:
|
|
|
146
305
|
|
|
147
306
|
|
|
148
307
|
def build_change_parser(description: str) -> argparse.ArgumentParser:
|
|
149
|
-
"""Build a parser with input,
|
|
150
|
-
parser =
|
|
151
|
-
add_input_args(parser)
|
|
308
|
+
"""Build a parser with input, config, output, buffer and changes."""
|
|
309
|
+
parser = build_io_parser(description)
|
|
152
310
|
add_buffer_arg(parser)
|
|
153
|
-
add_output_args(parser)
|
|
154
311
|
add_changes_arg(parser)
|
|
155
312
|
return parser
|
|
156
313
|
|
|
157
314
|
|
|
158
|
-
def date_report(changes: ReleaseDateChanges
|
|
315
|
+
def date_report(changes: ReleaseDateChanges, file_exists_cb: FileExistsCb
|
|
159
316
|
) -> tuple[str, Optional[Callable[[str], None]]]:
|
|
160
|
-
"""Return the date change listing and a writer, None when empty.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
317
|
+
"""Return the date change listing and a writer, None when empty.
|
|
318
|
+
|
|
319
|
+
The writer overwrites an existing changes file as decided by
|
|
320
|
+
``file_exists_cb``.
|
|
321
|
+
"""
|
|
322
|
+
def write(path: str) -> None:
|
|
323
|
+
"""Write the date changes, overwriting as the callback decides."""
|
|
324
|
+
write_date_changes(changes, path, file_exists_callback=file_exists_cb)
|
|
325
|
+
return format_date_changes(changes), (write if changes else None)
|
|
164
326
|
|
|
165
327
|
|
|
166
|
-
def content_report(changes: ReleaseChanges
|
|
328
|
+
def content_report(changes: ReleaseChanges, file_exists_cb: FileExistsCb
|
|
167
329
|
) -> tuple[str, Optional[Callable[[str], None]]]:
|
|
168
|
-
"""Return the content change listing and a writer, None when empty.
|
|
169
|
-
writer = None if not changes else \
|
|
170
|
-
(lambda path: write_content_changes(changes, path))
|
|
171
|
-
return format_content_changes(changes), writer
|
|
172
|
-
|
|
330
|
+
"""Return the content change listing and a writer, None when empty.
|
|
173
331
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
332
|
+
The writer overwrites an existing changes file as decided by
|
|
333
|
+
``file_exists_cb``.
|
|
334
|
+
"""
|
|
335
|
+
def write(path: str) -> None:
|
|
336
|
+
"""Write the content changes, overwriting as the callback decides."""
|
|
337
|
+
write_content_changes(changes, path,
|
|
338
|
+
file_exists_callback=file_exists_cb)
|
|
339
|
+
return format_content_changes(changes), (write if changes else None)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def run_change_command(
|
|
343
|
+
parsed: argparse.Namespace,
|
|
344
|
+
produce: Callable[[Optional[BacklogOpsConfig], BacklogReleases],
|
|
345
|
+
tuple[str, Optional[Callable[[str], None]]]],
|
|
346
|
+
require_config: bool = False) -> int:
|
|
177
347
|
"""Read, change, write the data, and emit the list of changes.
|
|
178
348
|
|
|
179
|
-
The
|
|
180
|
-
returns the change listing as text
|
|
181
|
-
writes the same changes to a file. The
|
|
182
|
-
output file, the listing is printed to
|
|
183
|
-
``--changes-file`` is given, the changes are also
|
|
349
|
+
The configuration is resolved once, the input is read and validated,
|
|
350
|
+
``produce`` changes it in place and returns the change listing as text
|
|
351
|
+
together with a callback that writes the same changes to a file. The
|
|
352
|
+
changed data is written to the output file, the listing is printed to
|
|
353
|
+
stdout, and, when ``--changes-file`` is given, the changes are also
|
|
354
|
+
written to that file.
|
|
184
355
|
|
|
185
356
|
Args:
|
|
186
|
-
parsed: Parsed command line arguments holding the input, output
|
|
187
|
-
and ``--changes-file`` options.
|
|
188
|
-
produce: Callable that
|
|
357
|
+
parsed: Parsed command line arguments holding the input, output,
|
|
358
|
+
``-c`` and ``--changes-file`` options.
|
|
359
|
+
produce: Callable that receives the resolved configuration (or
|
|
360
|
+
None) and the data, changes the data, and returns the change
|
|
189
361
|
listing text and a writer for the change file.
|
|
362
|
+
require_config: When True a missing configuration is reported as an
|
|
363
|
+
error instead of falling back to the built-in defaults.
|
|
190
364
|
|
|
191
365
|
Returns:
|
|
192
366
|
``0`` on success, ``1`` when any step fails.
|
|
193
367
|
"""
|
|
194
368
|
try:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
369
|
+
config = (required_config(parsed) if require_config
|
|
370
|
+
else optional_config(parsed))
|
|
371
|
+
data = read_input(parsed, config)
|
|
372
|
+
listing, write_changes = produce(config, data)
|
|
373
|
+
_write_output(parsed, config, data)
|
|
198
374
|
print(f'Wrote {parsed.output}')
|
|
199
375
|
print(listing)
|
|
200
376
|
_save_changes(parsed, write_changes)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Backward-compatibility warning hooks for the command line interface.
|
|
3
|
+
|
|
4
|
+
When a command reads a file that needed backward-compatible normalization
|
|
5
|
+
(Reading an Old Configuration File), one of these hooks prints the
|
|
6
|
+
standard migration warning followed by command-specific instructions.
|
|
7
|
+
``CliMigrateWarnHook`` is used when the old file is the backlog-ops
|
|
8
|
+
configuration file and shows the ``migrate_cfg`` command for the default
|
|
9
|
+
config kind. ``CliPresetMigrateWarnHook`` is used when the old file is a
|
|
10
|
+
stand-alone input or output preset file and shows the ``migrate_cfg``
|
|
11
|
+
command with the ``--kind`` option, because that option selects how a
|
|
12
|
+
preset file is migrated. The leading underscore in the module name keeps
|
|
13
|
+
it out of the command listing.
|
|
14
|
+
"""
|
|
15
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
16
|
+
|
|
17
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
18
|
+
# MIT License
|
|
19
|
+
|
|
20
|
+
from config_as_json import MigrateCfgWarnHook
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CliMigrateWarnHook(MigrateCfgWarnHook):
|
|
24
|
+
"""Tell the user to migrate an old config file with ``migrate_cfg``."""
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def migrate_instructions(cls) -> str:
|
|
28
|
+
"""Return the command line migration instructions.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Text that points the user at the ``migrate_cfg`` command to
|
|
32
|
+
rewrite the configuration file in the current format.
|
|
33
|
+
"""
|
|
34
|
+
txt = 'Run the migrate_cfg command to write the configuration file '
|
|
35
|
+
txt += 'in the\ncurrent format, for example:\n'
|
|
36
|
+
txt += ' python3 -m backlogops_cli.migrate_cfg -i OLD.cfg '
|
|
37
|
+
txt += '-o NEW.cfg\n\n'
|
|
38
|
+
return txt
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CliPresetMigrateWarnHook(MigrateCfgWarnHook):
|
|
42
|
+
"""Tell the user to migrate an old preset file with ``migrate_cfg``."""
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def migrate_instructions(cls) -> str:
|
|
46
|
+
"""Return the command line preset migration instructions.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Text that points the user at the ``migrate_cfg`` command with
|
|
50
|
+
the ``--kind`` option for input or output preset files.
|
|
51
|
+
"""
|
|
52
|
+
txt = 'Run the migrate_cfg command with -k input or -k output to '
|
|
53
|
+
txt += 'write the\npreset file in the current format, for example:\n'
|
|
54
|
+
txt += ' python3 -m backlogops_cli.migrate_cfg -k input '
|
|
55
|
+
txt += '-i OLD.cfg -o NEW.cfg\n\n'
|
|
56
|
+
return txt
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
|
@@ -20,7 +20,8 @@ from datetime import timedelta
|
|
|
20
20
|
from typing import Callable, Optional
|
|
21
21
|
from backlogops import BacklogReleases
|
|
22
22
|
from backlogops_cli._command_io import (
|
|
23
|
-
build_change_parser, content_report, parsed_args,
|
|
23
|
+
build_change_parser, content_report, overwrite_callback, parsed_args,
|
|
24
|
+
run_change_command)
|
|
24
25
|
|
|
25
26
|
DESCRIPTION = 'Adjust release content to fit the planned release dates'
|
|
26
27
|
|
|
@@ -34,7 +35,7 @@ def _adjust(parsed: argparse.Namespace, data: BacklogReleases
|
|
|
34
35
|
) -> tuple[str, Optional[Callable[[str], None]]]:
|
|
35
36
|
"""Adjust the release content and return the change report."""
|
|
36
37
|
changes = data.adjust_release_content(timedelta(days=parsed.buffer_days))
|
|
37
|
-
return content_report(changes)
|
|
38
|
+
return content_report(changes, overwrite_callback(parsed.force))
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def main(args: Optional[list[str]] = None) -> int:
|
|
@@ -48,7 +49,7 @@ def main(args: Optional[list[str]] = None) -> int:
|
|
|
48
49
|
written.
|
|
49
50
|
"""
|
|
50
51
|
parsed = parsed_args(build_parser(), args)
|
|
51
|
-
return run_change_command(parsed, lambda data: _adjust(parsed, data))
|
|
52
|
+
return run_change_command(parsed, lambda _, data: _adjust(parsed, data))
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
if __name__ == '__main__': # pragma: no cover
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Version reporter for the backlogops_cli package."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2026 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
from typing import override
|
|
8
|
+
from backlogops.blo_version_reporter import BloVersionReporter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BloCliVersionReporter(BloVersionReporter):
|
|
12
|
+
"""Version reporter for the backlogops_cli package."""
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
def package_names(self) -> list[str]:
|
|
16
|
+
"""Return the package names that this package reports."""
|
|
17
|
+
ret = ['backlogops-cli']
|
|
18
|
+
ret += super().package_names()
|
|
19
|
+
return ret
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_main_package_name(cls) -> str:
|
|
24
|
+
"""Return the name of the main package."""
|
|
25
|
+
return 'backlogops-cli'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Run the backlog-ops configuration wizard and store the result."""
|
|
3
|
+
|
|
4
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
5
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
6
|
+
# MIT License
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from backlogops import backlog_ops_wizard
|
|
12
|
+
from backlogops_cli._command_io import parsed_args
|
|
13
|
+
from backlogops_cli._wizard_io import build_wizard_parser, run_wizard_to_file
|
|
14
|
+
|
|
15
|
+
DESCRIPTION = 'Create a backlog-ops configuration file via a wizard'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
19
|
+
"""Build the command line parser for the config wizard command."""
|
|
20
|
+
return build_wizard_parser(DESCRIPTION)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
24
|
+
"""Run the interactive wizard and write the backlog-ops configuration.
|
|
25
|
+
|
|
26
|
+
The output filename receives the ``.cfg`` extension when it is not
|
|
27
|
+
already present.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
``0`` on success, ``1`` when the entered configuration is rejected
|
|
34
|
+
or cannot be written.
|
|
35
|
+
"""
|
|
36
|
+
parsed = parsed_args(build_parser(), args)
|
|
37
|
+
return run_wizard_to_file(parsed, backlog_ops_wizard,
|
|
38
|
+
'Backlog-ops configuration')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == '__main__': # pragma: no cover
|
|
42
|
+
sys.exit(main())
|