backlogops-cli 0.1__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/__init__.py +6 -0
- backlogops_cli/__main__.py +12 -0
- backlogops_cli/_command_io.py +220 -0
- backlogops_cli/adjust_release_content.py +55 -0
- backlogops_cli/convert.py +47 -0
- backlogops_cli/demo_backlog.py +44 -0
- backlogops_cli/estimate_ready_date.py +99 -0
- backlogops_cli/extract_keys.py +79 -0
- backlogops_cli/list.py +63 -0
- backlogops_cli/order_by_deps.py +70 -0
- backlogops_cli/order_by_keys.py +59 -0
- backlogops_cli/plan_release_dates.py +55 -0
- backlogops_cli/py.typed +0 -0
- backlogops_cli/teams_wizard.py +55 -0
- backlogops_cli-0.1.dist-info/METADATA +216 -0
- backlogops_cli-0.1.dist-info/RECORD +19 -0
- backlogops_cli-0.1.dist-info/WHEEL +5 -0
- backlogops_cli-0.1.dist-info/licenses/LICENSE.txt +22 -0
- backlogops_cli-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Run backlogops_cli without a command by listing the commands."""
|
|
3
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
4
|
+
|
|
5
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
6
|
+
# MIT License
|
|
7
|
+
|
|
8
|
+
from backlogops_cli.list import main
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == '__main__': # pragma: no cover
|
|
12
|
+
main()
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Shared command helpers for resolving output configs and writing.
|
|
3
|
+
|
|
4
|
+
The helpers here are used by more than one command (for example by the
|
|
5
|
+
``convert`` command and the ``demo_backlog`` command). The leading
|
|
6
|
+
underscore in the module name keeps it out of the command listing.
|
|
7
|
+
"""
|
|
8
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
9
|
+
|
|
10
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
11
|
+
# MIT License
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from typing import Optional
|
|
17
|
+
import argcomplete
|
|
18
|
+
from backlogops import (
|
|
19
|
+
BacklogReleases, FormatRules, InputFormatConfig, OutputFormatConfig,
|
|
20
|
+
ReleaseChanges, ReleaseDateChanges, format_content_changes,
|
|
21
|
+
format_date_changes, read_available_teams, read_backlog_releases,
|
|
22
|
+
resolve_input_config, resolve_output_config, write_backlog_releases,
|
|
23
|
+
write_content_changes, write_date_changes)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parsed_args(parser: argparse.ArgumentParser,
|
|
27
|
+
args: Optional[list[str]]) -> argparse.Namespace:
|
|
28
|
+
"""Enable shell completion and parse the command line arguments."""
|
|
29
|
+
argcomplete.autocomplete(parser)
|
|
30
|
+
return parser.parse_args(args)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def add_input_args(parser: argparse.ArgumentParser) -> None:
|
|
34
|
+
"""Add the input-file and input-config arguments."""
|
|
35
|
+
parser.add_argument('-i', '--input', dest='input', required=True,
|
|
36
|
+
help='Input data file to read.')
|
|
37
|
+
parser.add_argument('-I', '--input-config', dest='input_config',
|
|
38
|
+
help='Input format: a config file or a preset name.')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _input_presets(io_config: Optional[str]
|
|
42
|
+
) -> Optional[dict[str, InputFormatConfig]]:
|
|
43
|
+
"""Return the named input presets from a presets file, if given."""
|
|
44
|
+
if io_config is None:
|
|
45
|
+
return None
|
|
46
|
+
return read_available_teams(io_config, sys.stderr).input_configs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def read_input(parsed: argparse.Namespace) -> BacklogReleases:
|
|
50
|
+
"""Read and validate the backlog and releases from the input file.
|
|
51
|
+
|
|
52
|
+
The input format is resolved from the ``--input-config`` value, which
|
|
53
|
+
may be empty (inferred from the file name), a preset name looked up in
|
|
54
|
+
the presets file given by ``--io-config``, or a config file path.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
parsed: Parsed command line arguments holding the input options
|
|
58
|
+
added by :func:`add_input_args` and, optionally, the
|
|
59
|
+
``--io-config`` option added by :func:`add_output_args`.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The validated backlog and releases read from the input file.
|
|
63
|
+
"""
|
|
64
|
+
io_config = getattr(parsed, 'io_config', None)
|
|
65
|
+
presets = _input_presets(io_config)
|
|
66
|
+
config = resolve_input_config(parsed.input_config, data_file=parsed.input,
|
|
67
|
+
presets=presets)
|
|
68
|
+
data = read_backlog_releases(parsed.input, config)
|
|
69
|
+
data.check_consistency(sys.stderr)
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def add_output_args(parser: argparse.ArgumentParser) -> None:
|
|
74
|
+
"""Add the output-file, output-config and ordering arguments."""
|
|
75
|
+
parser.add_argument('-o', '--output', dest='output', required=True,
|
|
76
|
+
help='Output data file to create.')
|
|
77
|
+
parser.add_argument('-O', '--output-config', dest='output_config',
|
|
78
|
+
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
|
+
parser.add_argument('--releases-first', dest='releases_first',
|
|
83
|
+
action='store_true',
|
|
84
|
+
help='Write the releases before the backlog.')
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _output_presets(io_config: Optional[str]
|
|
88
|
+
) -> Optional[dict[str, OutputFormatConfig]]:
|
|
89
|
+
"""Return the named output presets from a presets file, if given."""
|
|
90
|
+
if io_config is None:
|
|
91
|
+
return None
|
|
92
|
+
return read_available_teams(io_config, sys.stderr).output_configs
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _write_output(parsed: argparse.Namespace, data: BacklogReleases) -> None:
|
|
96
|
+
"""Write the backlog and releases to the configured output file."""
|
|
97
|
+
presets = _output_presets(parsed.io_config)
|
|
98
|
+
config = resolve_output_config(parsed.output_config,
|
|
99
|
+
data_file=parsed.output, presets=presets)
|
|
100
|
+
rules = FormatRules(backlog_first=not parsed.releases_first)
|
|
101
|
+
write_backlog_releases(data, parsed.output, config, rules)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_write(parsed: argparse.Namespace,
|
|
105
|
+
data_source: Callable[[], BacklogReleases]) -> int:
|
|
106
|
+
"""Build the data, write it to the output file, and report the result.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
parsed: Parsed command line arguments holding the output options
|
|
110
|
+
added by :func:`add_output_args`.
|
|
111
|
+
data_source: Callable that returns the backlog and releases to
|
|
112
|
+
write. It is called inside the error handling so that reading
|
|
113
|
+
failures are reported like writing failures.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
``0`` on success, ``1`` when the data cannot be built or written.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
_write_output(parsed, data_source())
|
|
120
|
+
except (ValueError, TypeError, KeyError, OSError) as error:
|
|
121
|
+
print(f'Could not write {parsed.output}: {error}', file=sys.stderr)
|
|
122
|
+
return 1
|
|
123
|
+
print(f'Wrote {parsed.output}')
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
DEFAULT_BUFFER_DAYS = 5
|
|
128
|
+
"""Default slack in calendar days added when fitting dates."""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def add_buffer_arg(parser: argparse.ArgumentParser) -> None:
|
|
132
|
+
"""Add the buffer-days argument with the default slack."""
|
|
133
|
+
parser.add_argument('--buffer-days', dest='buffer_days', type=int,
|
|
134
|
+
default=DEFAULT_BUFFER_DAYS, metavar='DAYS',
|
|
135
|
+
help='Slack in calendar days kept against the planned '
|
|
136
|
+
f'dates (default {DEFAULT_BUFFER_DAYS}). Must not be '
|
|
137
|
+
'negative.')
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def add_changes_arg(parser: argparse.ArgumentParser) -> None:
|
|
141
|
+
"""Add the optional file to also save the list of changes to."""
|
|
142
|
+
parser.add_argument('--changes-file', dest='changes_file', metavar='FILE',
|
|
143
|
+
help='Also save the list of changes to a TableIO '
|
|
144
|
+
'file. Without it the changes are only printed to '
|
|
145
|
+
'stdout.')
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_change_parser(description: str) -> argparse.ArgumentParser:
|
|
149
|
+
"""Build a parser with input, buffer, output and changes arguments."""
|
|
150
|
+
parser = argparse.ArgumentParser(description=description)
|
|
151
|
+
add_input_args(parser)
|
|
152
|
+
add_buffer_arg(parser)
|
|
153
|
+
add_output_args(parser)
|
|
154
|
+
add_changes_arg(parser)
|
|
155
|
+
return parser
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def date_report(changes: ReleaseDateChanges
|
|
159
|
+
) -> tuple[str, Optional[Callable[[str], None]]]:
|
|
160
|
+
"""Return the date change listing and a writer, None when empty."""
|
|
161
|
+
writer = None if not changes else \
|
|
162
|
+
(lambda path: write_date_changes(changes, path))
|
|
163
|
+
return format_date_changes(changes), writer
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def content_report(changes: ReleaseChanges
|
|
167
|
+
) -> 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
|
+
|
|
173
|
+
|
|
174
|
+
def run_change_command(parsed: argparse.Namespace,
|
|
175
|
+
produce: Callable[[BacklogReleases], tuple[
|
|
176
|
+
str, Optional[Callable[[str], None]]]]) -> int:
|
|
177
|
+
"""Read, change, write the data, and emit the list of changes.
|
|
178
|
+
|
|
179
|
+
The input is read and validated, ``produce`` changes it in place and
|
|
180
|
+
returns the change listing as text together with a callback that
|
|
181
|
+
writes the same changes to a file. The changed data is written to the
|
|
182
|
+
output file, the listing is printed to stdout, and, when
|
|
183
|
+
``--changes-file`` is given, the changes are also written to that file.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
parsed: Parsed command line arguments holding the input, output
|
|
187
|
+
and ``--changes-file`` options.
|
|
188
|
+
produce: Callable that changes the data and returns the change
|
|
189
|
+
listing text and a writer for the change file.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
``0`` on success, ``1`` when any step fails.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
data = read_input(parsed)
|
|
196
|
+
listing, write_changes = produce(data)
|
|
197
|
+
_write_output(parsed, data)
|
|
198
|
+
print(f'Wrote {parsed.output}')
|
|
199
|
+
print(listing)
|
|
200
|
+
_save_changes(parsed, write_changes)
|
|
201
|
+
except (ValueError, TypeError, KeyError, OSError) as error:
|
|
202
|
+
print(f'Could not complete: {error}', file=sys.stderr)
|
|
203
|
+
return 1
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _save_changes(parsed: argparse.Namespace,
|
|
208
|
+
write_changes: Optional[Callable[[str], None]]) -> None:
|
|
209
|
+
"""Save the changes to ``--changes-file`` when one is requested.
|
|
210
|
+
|
|
211
|
+
A ``write_changes`` of None means there were no changes, so nothing is
|
|
212
|
+
written and a short note is printed instead.
|
|
213
|
+
"""
|
|
214
|
+
if parsed.changes_file is None:
|
|
215
|
+
return
|
|
216
|
+
if write_changes is None:
|
|
217
|
+
print('No changes to write.')
|
|
218
|
+
return
|
|
219
|
+
write_changes(parsed.changes_file)
|
|
220
|
+
print(f'Wrote {parsed.changes_file}')
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Adjust release content to fit the planned release dates.
|
|
3
|
+
|
|
4
|
+
The command reads an already estimated backlog and its releases, then
|
|
5
|
+
moves each backlog item to the earliest release whose planned date is on
|
|
6
|
+
or after the item's estimated ready date plus a slack buffer, as
|
|
7
|
+
documented for :func:`backlogops.adjust_release_content`. The adjusted
|
|
8
|
+
backlog and the releases are written to the output file, and the list of
|
|
9
|
+
content changes is printed to stdout, or also saved to a file when
|
|
10
|
+
``--changes-file`` is given.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
14
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
15
|
+
# MIT License
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import timedelta
|
|
20
|
+
from typing import Callable, Optional
|
|
21
|
+
from backlogops import BacklogReleases
|
|
22
|
+
from backlogops_cli._command_io import (
|
|
23
|
+
build_change_parser, content_report, parsed_args, run_change_command)
|
|
24
|
+
|
|
25
|
+
DESCRIPTION = 'Adjust release content to fit the planned release dates'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
29
|
+
"""Build the command line parser for the adjust-content command."""
|
|
30
|
+
return build_change_parser(DESCRIPTION)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _adjust(parsed: argparse.Namespace, data: BacklogReleases
|
|
34
|
+
) -> tuple[str, Optional[Callable[[str], None]]]:
|
|
35
|
+
"""Adjust the release content and return the change report."""
|
|
36
|
+
changes = data.adjust_release_content(timedelta(days=parsed.buffer_days))
|
|
37
|
+
return content_report(changes)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
41
|
+
"""Adjust the release content and write the output file.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
``0`` on success, ``1`` when the data cannot be read, adjusted or
|
|
48
|
+
written.
|
|
49
|
+
"""
|
|
50
|
+
parsed = parsed_args(build_parser(), args)
|
|
51
|
+
return run_change_command(parsed, lambda data: _adjust(parsed, data))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == '__main__': # pragma: no cover
|
|
55
|
+
sys.exit(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Read a backlog and releases from one file and write them to another.
|
|
3
|
+
|
|
4
|
+
The command reads a backlog, releases, or both from an input file and
|
|
5
|
+
writes them to an output file, possibly in another format and with other
|
|
6
|
+
column names. The input and output formats are inferred from the file
|
|
7
|
+
name extensions, but can be overridden by a configuration file or by a
|
|
8
|
+
named preset stored in the teams configuration file.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
12
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
13
|
+
# MIT License
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional
|
|
18
|
+
from backlogops_cli._command_io import (
|
|
19
|
+
add_input_args, add_output_args, parsed_args, read_input, run_write)
|
|
20
|
+
|
|
21
|
+
DESCRIPTION = 'Convert a backlog and releases between table file formats'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
25
|
+
"""Build the command line parser for the convert command."""
|
|
26
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
27
|
+
add_input_args(parser)
|
|
28
|
+
add_output_args(parser)
|
|
29
|
+
return parser
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
33
|
+
"""Convert a backlog and releases from the input to the output file.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
``0`` on success, ``1`` when the data cannot be read, validated
|
|
40
|
+
or written.
|
|
41
|
+
"""
|
|
42
|
+
parsed = parsed_args(build_parser(), args)
|
|
43
|
+
return run_write(parsed, lambda: read_input(parsed))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == '__main__': # pragma: no cover
|
|
47
|
+
sys.exit(main())
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Write a demonstration backlog and releases to a file.
|
|
3
|
+
|
|
4
|
+
The data comes from :func:`backlogops.get_demo_backlog`. The output
|
|
5
|
+
format is inferred from the output file name extension, but can be
|
|
6
|
+
overridden by a configuration file or by a named preset stored in the
|
|
7
|
+
teams configuration file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
11
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
12
|
+
# MIT License
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Optional
|
|
17
|
+
from backlogops import get_demo_backlog
|
|
18
|
+
from backlogops_cli._command_io import add_output_args, parsed_args, run_write
|
|
19
|
+
|
|
20
|
+
DESCRIPTION = 'Write a demonstration backlog and releases to a file'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
"""Build the command line parser for the demo backlog command."""
|
|
25
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
26
|
+
add_output_args(parser)
|
|
27
|
+
return parser
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
31
|
+
"""Write the demonstration backlog and releases to the output file.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
``0`` on success, ``1`` when the data cannot be written.
|
|
38
|
+
"""
|
|
39
|
+
parsed = parsed_args(build_parser(), args)
|
|
40
|
+
return run_write(parsed, get_demo_backlog)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == '__main__': # pragma: no cover
|
|
44
|
+
sys.exit(main())
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Estimate ready dates for a backlog and write the result.
|
|
3
|
+
|
|
4
|
+
The command reads a backlog and its releases from an input file and
|
|
5
|
+
estimates the ready date of each backlog item from the available teams,
|
|
6
|
+
as documented for :func:`backlogops.estimate_ready_date`. The teams
|
|
7
|
+
configuration (velocity, work hours, vacations and so on) is taken from
|
|
8
|
+
the file given by ``--config`` or, when that is absent, from the
|
|
9
|
+
configured teams file. The backlog with the estimated dates and the
|
|
10
|
+
releases are written to the output file. The input and output formats are
|
|
11
|
+
inferred from the file name extensions, but can be overridden by a
|
|
12
|
+
configuration file or by a named preset.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
16
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
17
|
+
# MIT License
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import date
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Callable, Optional
|
|
24
|
+
from backlogops import AvailableTeams, BacklogReleases, get_available_teams
|
|
25
|
+
from backlogops_cli._command_io import (
|
|
26
|
+
add_changes_arg, add_input_args, add_output_args, date_report,
|
|
27
|
+
parsed_args, run_change_command)
|
|
28
|
+
|
|
29
|
+
DESCRIPTION = 'Estimate ready dates for the backlog items'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
33
|
+
"""Build the command line parser for the estimate command."""
|
|
34
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
35
|
+
add_input_args(parser)
|
|
36
|
+
parser.add_argument('-c', '--config', dest='config',
|
|
37
|
+
help='Teams configuration file (velocity, work '
|
|
38
|
+
'hours, vacations). Without -c the file is found '
|
|
39
|
+
'from $BACKLOGOPS_CFG, else backlogops.cfg in '
|
|
40
|
+
'$BACKLOGOPS_DIR, else $HOME/.backlogops.cfg.')
|
|
41
|
+
parser.add_argument('-d', '--start-date', dest='start_date',
|
|
42
|
+
metavar='ISO_DATE',
|
|
43
|
+
help='Day the teams start working (default today).')
|
|
44
|
+
parser.add_argument('--set-plan', dest='set_plan', action='store_true',
|
|
45
|
+
help='Also copy each estimated date to the planned '
|
|
46
|
+
'date.')
|
|
47
|
+
add_output_args(parser)
|
|
48
|
+
add_changes_arg(parser)
|
|
49
|
+
return parser
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _start_date(parsed: argparse.Namespace) -> Optional[date]:
|
|
53
|
+
"""Return the start date from the command line, or None for today."""
|
|
54
|
+
if parsed.start_date is None:
|
|
55
|
+
return None
|
|
56
|
+
return date.fromisoformat(parsed.start_date)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _load_teams(config: Optional[str]) -> AvailableTeams:
|
|
60
|
+
"""Return the available teams, mapping a missing file to ValueError."""
|
|
61
|
+
if config is not None and not Path(config).is_file():
|
|
62
|
+
raise ValueError(f'Teams configuration file not found: {config}')
|
|
63
|
+
try:
|
|
64
|
+
return get_available_teams(config)
|
|
65
|
+
except RuntimeError as error:
|
|
66
|
+
raise ValueError(str(error)) from error
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _estimate(parsed: argparse.Namespace, data: BacklogReleases
|
|
70
|
+
) -> tuple[str, Optional[Callable[[str], None]]]:
|
|
71
|
+
"""Estimate the dates and return the release date change report."""
|
|
72
|
+
teams = _load_teams(parsed.config)
|
|
73
|
+
changes = data.estimate_ready_date(teams, _start_date(parsed))
|
|
74
|
+
if parsed.set_plan:
|
|
75
|
+
data.set_plan_from_estimate()
|
|
76
|
+
return date_report(changes)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
80
|
+
"""Estimate the ready dates and write the output file.
|
|
81
|
+
|
|
82
|
+
The backlog with the estimated dates and the releases are written to
|
|
83
|
+
the output file. The estimated release dates are updated as well, and
|
|
84
|
+
the list of release date changes is printed to stdout, or also saved
|
|
85
|
+
to a file when ``--changes-file`` is given.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
``0`` on success, ``1`` when the data cannot be read, estimated
|
|
92
|
+
or written.
|
|
93
|
+
"""
|
|
94
|
+
parsed = parsed_args(build_parser(), args)
|
|
95
|
+
return run_change_command(parsed, lambda data: _estimate(parsed, data))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == '__main__': # pragma: no cover
|
|
99
|
+
sys.exit(main())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Extract the keys of a backlog at the given levels.
|
|
3
|
+
|
|
4
|
+
The command reads a backlog from an input file and extracts the keys of
|
|
5
|
+
the items at the levels named on the command line, in backlog order, as
|
|
6
|
+
documented for :func:`backlogops.get_keys_in_order`. A level is given by
|
|
7
|
+
name, alias or number. The keys are written to the key list file given by
|
|
8
|
+
``-o``, or to standard output when ``-o`` is omitted.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
12
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
13
|
+
# MIT License
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional
|
|
18
|
+
from backlogops import get_keys_in_order, write_key_list
|
|
19
|
+
from backlogops_cli._command_io import add_input_args, parsed_args, read_input
|
|
20
|
+
|
|
21
|
+
DESCRIPTION = 'Extract backlog keys at the given levels to a key list'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
25
|
+
"""Build the command line parser for the extract-keys command."""
|
|
26
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
27
|
+
add_input_args(parser)
|
|
28
|
+
parser.add_argument('-l', '--levels', dest='levels', nargs='+',
|
|
29
|
+
required=True, metavar='LEVEL',
|
|
30
|
+
help='Levels to extract keys at, by name or number.')
|
|
31
|
+
parser.add_argument('-o', '--output', dest='output',
|
|
32
|
+
help='Key list file to create; stdout if omitted.')
|
|
33
|
+
parser.add_argument('--io-config', dest='io_config',
|
|
34
|
+
help='Configuration file holding the named presets.')
|
|
35
|
+
return parser
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _level_value(text: str) -> int | str:
|
|
39
|
+
"""Return a level token as an int when numeric, else as a name."""
|
|
40
|
+
try:
|
|
41
|
+
return int(text)
|
|
42
|
+
except ValueError:
|
|
43
|
+
return text
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _emit(keys: list[str], output: Optional[str]) -> None:
|
|
47
|
+
"""Write the keys to the output file, or to stdout when none is given."""
|
|
48
|
+
if output is None:
|
|
49
|
+
for key in keys:
|
|
50
|
+
print(key)
|
|
51
|
+
else:
|
|
52
|
+
write_key_list(keys, output)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
56
|
+
"""Extract the backlog keys at the given levels and emit them.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
``0`` on success, ``1`` when the backlog cannot be read or the
|
|
63
|
+
keys cannot be written.
|
|
64
|
+
"""
|
|
65
|
+
parsed = parsed_args(build_parser(), args)
|
|
66
|
+
try:
|
|
67
|
+
data = read_input(parsed)
|
|
68
|
+
levels = [_level_value(text) for text in parsed.levels]
|
|
69
|
+
_emit(get_keys_in_order(data.backlog, levels), parsed.output)
|
|
70
|
+
except (ValueError, TypeError, KeyError, OSError) as error:
|
|
71
|
+
print(f'Could not extract keys: {error}', file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
if parsed.output is not None:
|
|
74
|
+
print(f'Wrote {parsed.output}')
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == '__main__': # pragma: no cover
|
|
79
|
+
sys.exit(main())
|
backlogops_cli/list.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""List the commands available in backlogops_cli."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import pkgutil
|
|
9
|
+
import importlib
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
import backlogops_cli
|
|
12
|
+
|
|
13
|
+
DESCRIPTION = 'List all commands available in backlogops_cli'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _python_prefix() -> str:
|
|
17
|
+
"""Return the 'python -m' prefix matching the running OS."""
|
|
18
|
+
runner = 'python' if os.name == 'nt' else 'python3'
|
|
19
|
+
return f'{runner} -m'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_command(module: ModuleType) -> bool:
|
|
23
|
+
"""Tell whether a module is a usable backlogops_cli command."""
|
|
24
|
+
has_main = callable(getattr(module, 'main', None))
|
|
25
|
+
description = getattr(module, 'DESCRIPTION', None)
|
|
26
|
+
return has_main and isinstance(description, str)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def command_modules() -> list[tuple[str, ModuleType]]:
|
|
30
|
+
"""Return sorted (name, module) pairs for all command modules."""
|
|
31
|
+
found: list[tuple[str, ModuleType]] = []
|
|
32
|
+
for info in pkgutil.iter_modules(backlogops_cli.__path__):
|
|
33
|
+
if info.ispkg or info.name.startswith('_'):
|
|
34
|
+
continue
|
|
35
|
+
module = importlib.import_module(f'backlogops_cli.{info.name}')
|
|
36
|
+
if _is_command(module):
|
|
37
|
+
found.append((info.name, module))
|
|
38
|
+
found.sort(key=lambda item: item[0])
|
|
39
|
+
return found
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _description_lines(description: str) -> list[str]:
|
|
43
|
+
"""Return the description as indented lines, one per text line."""
|
|
44
|
+
return [f' {line}' for line in description.splitlines()]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def format_listing(commands: list[tuple[str, ModuleType]]) -> str:
|
|
48
|
+
"""Format the command listing for printing to the user."""
|
|
49
|
+
prefix = _python_prefix()
|
|
50
|
+
lines: list[str] = []
|
|
51
|
+
for name, module in commands:
|
|
52
|
+
lines.append(f' {prefix} backlogops_cli.{name}')
|
|
53
|
+
lines.extend(_description_lines(module.DESCRIPTION))
|
|
54
|
+
return '\n'.join(lines)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> None:
|
|
58
|
+
"""Print the list of available backlogops_cli commands."""
|
|
59
|
+
print(format_listing(command_modules()))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == '__main__': # pragma: no cover
|
|
63
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Reorder a backlog by its dependencies and write the result.
|
|
3
|
+
|
|
4
|
+
The command reads a backlog and its releases from an input file and
|
|
5
|
+
reorders the backlog so that a team can start the items in backlog order
|
|
6
|
+
without starting an item before the items it depends on, as documented
|
|
7
|
+
for :func:`backlogops.order_by_dependencies`. The reordered backlog and
|
|
8
|
+
the releases are written to the output file. The input and output formats
|
|
9
|
+
are inferred from the file name extensions, but can be overridden by a
|
|
10
|
+
configuration file or by a named preset.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
14
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
15
|
+
# MIT License
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from typing import Optional
|
|
20
|
+
from backlogops import BacklogReleases, DependencyMode
|
|
21
|
+
from backlogops_cli._command_io import (
|
|
22
|
+
add_input_args, add_output_args, parsed_args, read_input, run_write)
|
|
23
|
+
|
|
24
|
+
DESCRIPTION = 'Reorder a backlog so that dependencies are fulfilled'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
28
|
+
"""Build the command line parser for the order_by_deps command."""
|
|
29
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
30
|
+
add_input_args(parser)
|
|
31
|
+
parser.add_argument('-s', '--space-around', dest='space_around',
|
|
32
|
+
action='append', metavar='KEY',
|
|
33
|
+
help='Key to keep far from its dependencies. May '
|
|
34
|
+
'be given more than once.')
|
|
35
|
+
parser.add_argument('-L', '--later', dest='later', action='store_true',
|
|
36
|
+
help='Push dependent items later instead of pulling '
|
|
37
|
+
'prerequisites earlier.')
|
|
38
|
+
parser.add_argument('-m', '--mode', dest='mode',
|
|
39
|
+
choices=[mode.name for mode in DependencyMode],
|
|
40
|
+
default=DependencyMode.KEEP.name,
|
|
41
|
+
help='Placement of dependency items (default KEEP).')
|
|
42
|
+
add_output_args(parser)
|
|
43
|
+
return parser
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _ordered(parsed: argparse.Namespace) -> BacklogReleases:
|
|
47
|
+
"""Read the backlog and return it reordered by dependencies."""
|
|
48
|
+
data = read_input(parsed)
|
|
49
|
+
data.order_by_dependencies(later=parsed.later,
|
|
50
|
+
mode=DependencyMode[parsed.mode],
|
|
51
|
+
space_around=parsed.space_around)
|
|
52
|
+
return data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
56
|
+
"""Reorder the backlog by dependencies and write the output file.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
``0`` on success, ``1`` when the data cannot be read, reordered
|
|
63
|
+
or written.
|
|
64
|
+
"""
|
|
65
|
+
parsed = parsed_args(build_parser(), args)
|
|
66
|
+
return run_write(parsed, lambda: _ordered(parsed))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == '__main__': # pragma: no cover
|
|
70
|
+
sys.exit(main())
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Reorder a backlog from a key list and write the result.
|
|
3
|
+
|
|
4
|
+
The command reads a backlog and its releases from an input file, reads a
|
|
5
|
+
key list from another file, and reorders the backlog so that the items
|
|
6
|
+
named by the key list come first, as documented for
|
|
7
|
+
:func:`backlogops.move_keys_first`. The reordered backlog and the
|
|
8
|
+
releases are written to the output file. The input and output formats are
|
|
9
|
+
inferred from the file name extensions, but can be overridden by a
|
|
10
|
+
configuration file or by a named preset.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
14
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
15
|
+
# MIT License
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from typing import Optional
|
|
20
|
+
from backlogops import BacklogReleases, read_key_list
|
|
21
|
+
from backlogops_cli._command_io import (
|
|
22
|
+
add_input_args, add_output_args, parsed_args, read_input, run_write)
|
|
23
|
+
|
|
24
|
+
DESCRIPTION = 'Reorder a backlog so that key-list items come first'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
28
|
+
"""Build the command line parser for the order_by_keys command."""
|
|
29
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
30
|
+
add_input_args(parser)
|
|
31
|
+
parser.add_argument('-k', '--key-list', dest='key_list', required=True,
|
|
32
|
+
help='Key list file giving the new leading order.')
|
|
33
|
+
add_output_args(parser)
|
|
34
|
+
return parser
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _reordered(parsed: argparse.Namespace) -> BacklogReleases:
|
|
38
|
+
"""Read the backlog and key list and return the reordered data."""
|
|
39
|
+
data = read_input(parsed)
|
|
40
|
+
data.move_keys_first(read_key_list(parsed.key_list))
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
45
|
+
"""Reorder the backlog from the key list and write the output file.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
``0`` on success, ``1`` when the data cannot be read, reordered
|
|
52
|
+
or written.
|
|
53
|
+
"""
|
|
54
|
+
parsed = parsed_args(build_parser(), args)
|
|
55
|
+
return run_write(parsed, lambda: _reordered(parsed))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == '__main__': # pragma: no cover
|
|
59
|
+
sys.exit(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Set planned release dates from the estimated release dates.
|
|
3
|
+
|
|
4
|
+
The command reads a backlog and its releases whose estimated release
|
|
5
|
+
dates are already filled in, then sets each planned release date to the
|
|
6
|
+
estimated release date plus a slack buffer, as documented for
|
|
7
|
+
:func:`backlogops.release_plan_on_estimate`. The backlog and the releases
|
|
8
|
+
with the new planned dates are written to the output file, and the list of
|
|
9
|
+
planned date changes is printed to stdout, or also saved to a file when
|
|
10
|
+
``--changes-file`` is given.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
14
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
15
|
+
# MIT License
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import timedelta
|
|
20
|
+
from typing import Callable, Optional
|
|
21
|
+
from backlogops import BacklogReleases
|
|
22
|
+
from backlogops_cli._command_io import (
|
|
23
|
+
build_change_parser, date_report, parsed_args, run_change_command)
|
|
24
|
+
|
|
25
|
+
DESCRIPTION = 'Set planned release dates from the estimated release dates'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
29
|
+
"""Build the command line parser for the plan-dates command."""
|
|
30
|
+
return build_change_parser(DESCRIPTION)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _plan(parsed: argparse.Namespace, data: BacklogReleases
|
|
34
|
+
) -> tuple[str, Optional[Callable[[str], None]]]:
|
|
35
|
+
"""Set the planned release dates and return the change report."""
|
|
36
|
+
changes = data.release_plan_on_estimate(timedelta(days=parsed.buffer_days))
|
|
37
|
+
return date_report(changes)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
41
|
+
"""Set the planned release dates and write the output file.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
``0`` on success, ``1`` when the data cannot be read, planned or
|
|
48
|
+
written.
|
|
49
|
+
"""
|
|
50
|
+
parsed = parsed_args(build_parser(), args)
|
|
51
|
+
return run_change_command(parsed, lambda data: _plan(parsed, data))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == '__main__': # pragma: no cover
|
|
55
|
+
sys.exit(main())
|
backlogops_cli/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Run the available-teams wizard and store the result to a file."""
|
|
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 config_as_json.file_extension import fix_file_extension
|
|
12
|
+
from backlogops import ConsoleYesNoUiBridge, teams_config_wizard
|
|
13
|
+
from backlogops_cli._command_io import parsed_args
|
|
14
|
+
|
|
15
|
+
DESCRIPTION = 'Create an AvailableTeams configuration file via a wizard'
|
|
16
|
+
CONFIG_EXTENSION = '.cfg'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
20
|
+
"""Build the command line parser for the teams wizard command."""
|
|
21
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
22
|
+
parser.add_argument('-o', '--output', dest='output', required=True,
|
|
23
|
+
help='Configuration file to write; the '
|
|
24
|
+
f'{CONFIG_EXTENSION} extension is added if missing.')
|
|
25
|
+
return parser
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
29
|
+
"""Run the interactive wizard and write the workforce configuration.
|
|
30
|
+
|
|
31
|
+
The output filename receives the ``.cfg`` extension when it is not
|
|
32
|
+
already present.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
``0`` on success, ``1`` when the entered workforce is rejected or
|
|
39
|
+
cannot be written.
|
|
40
|
+
"""
|
|
41
|
+
parsed = parsed_args(build_parser(), args)
|
|
42
|
+
output = fix_file_extension(parsed.output, CONFIG_EXTENSION)
|
|
43
|
+
bridge = ConsoleYesNoUiBridge(sys.stdout, sys.stdin, sys.stderr)
|
|
44
|
+
try:
|
|
45
|
+
config = teams_config_wizard(bridge)
|
|
46
|
+
config.write(to_json_filename=output, stderr_file=sys.stderr)
|
|
47
|
+
except (ValueError, TypeError, KeyError, EOFError, OSError) as error:
|
|
48
|
+
print(f'Could not create the configuration: {error}', file=sys.stderr)
|
|
49
|
+
return 1
|
|
50
|
+
print(f'Workforce configuration written to {output}')
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == '__main__': # pragma: no cover
|
|
55
|
+
sys.exit(main())
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: backlogops-cli
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Command line interface for backlog operations.
|
|
5
|
+
Author: Tom Björkholm
|
|
6
|
+
Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://bitbucket.org/tom-bjorkholm/backlog-ops
|
|
9
|
+
Project-URL: Source code, https://bitbucket.org/tom-bjorkholm/backlog-ops
|
|
10
|
+
Project-URL: Documentation, https://bitbucket.org/tom-bjorkholm/backlog-ops/src/master/doc/
|
|
11
|
+
Keywords: backlog,release planning,agile,scrum,scheduling,roadmap,project management,product owner,product management
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Development Status :: 3 - Alpha
|
|
18
|
+
Classifier: Environment :: Console
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
21
|
+
Classifier: Intended Audience :: Information Technology
|
|
22
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE.txt
|
|
26
|
+
Requires-Dist: argcomplete>=3.6.3
|
|
27
|
+
Requires-Dist: backlogops>=0.1
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: requires-dist
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
|
|
33
|
+
# backlogops-cli
|
|
34
|
+
|
|
35
|
+
There are 3 related packages for backlog operations:
|
|
36
|
+
|
|
37
|
+
- backlogops: a collection of library functions to manipulate backlogs
|
|
38
|
+
|
|
39
|
+
- backlogops-cli: command line interface to use the functions in the library.
|
|
40
|
+
This is just a thin wrapper around the library functions. It serves a dual
|
|
41
|
+
purpose as both an example of how to use the library and as a tool for the
|
|
42
|
+
user to use the library.
|
|
43
|
+
|
|
44
|
+
- backlogops-gui: graphical user interface to use the functions in the library.
|
|
45
|
+
It is based on TkInter. The ambition is to keep it as a thin wrapper around
|
|
46
|
+
the library.
|
|
47
|
+
|
|
48
|
+
## Available functionality
|
|
49
|
+
|
|
50
|
+
The following functionality is available in all 3 packages:
|
|
51
|
+
|
|
52
|
+
- Reading backlog and releases from file types that TableIO supports reading
|
|
53
|
+
from (Currently CSV, Excel, and ODS).
|
|
54
|
+
|
|
55
|
+
- Writing backlog and releases to file types that TableIO supports writing to
|
|
56
|
+
(Currently CSV, Excel, ODS and 9 other file formats).
|
|
57
|
+
|
|
58
|
+
- File format is detected from the file extension, but may be overridden.
|
|
59
|
+
|
|
60
|
+
- Adjust release content to fit the planned release dates.
|
|
61
|
+
|
|
62
|
+
- Create a demonstration backlog and releases (for exploring the features).
|
|
63
|
+
|
|
64
|
+
- Estimate ready date for the backlog items based on available teams, team
|
|
65
|
+
velocity, vacation dates, periods with half time work, etc.
|
|
66
|
+
|
|
67
|
+
- Extract backlog keys at given backlog item levels.
|
|
68
|
+
|
|
69
|
+
- Reorder the backlog so that the dependencies are satisfied.
|
|
70
|
+
|
|
71
|
+
- Reorder the backlog so that items identified by keys in a list come first. If
|
|
72
|
+
the key is at a higher level it will bring all items it is a parent of in
|
|
73
|
+
front of it (recursively).
|
|
74
|
+
|
|
75
|
+
- Set planned release dates from the estimated release dates.
|
|
76
|
+
|
|
77
|
+
- Calculate the release dates from the backlog items estimated ready dates, with
|
|
78
|
+
a configurable buffer time.
|
|
79
|
+
|
|
80
|
+
- Validate the backlog and releases for consistency.
|
|
81
|
+
|
|
82
|
+
- A wizard to create an available teams configuration.
|
|
83
|
+
|
|
84
|
+
## The operating model
|
|
85
|
+
|
|
86
|
+
The operating model that most of the functionality is designed for is that the
|
|
87
|
+
teams work off a single backlog in the order of the backlog. The backlog items
|
|
88
|
+
are ordered by priority and dependencies to allow the teams to work in the
|
|
89
|
+
backlog order. Each backlog item and each release may have a planned ready date,
|
|
90
|
+
that records what has been communicated to the customer. Each backlog item and
|
|
91
|
+
each release may have an estimated ready date, that is calculated from the
|
|
92
|
+
current backlog state, the team velocity, and what we know about the
|
|
93
|
+
availability of the team members.
|
|
94
|
+
|
|
95
|
+
## The backlog item fields
|
|
96
|
+
|
|
97
|
+
Each backlog item has the following fields that are used by the algorithms in
|
|
98
|
+
the library:
|
|
99
|
+
|
|
100
|
+
- key: The key of the backlog item. Required. Must be unique. Must not be empty,
|
|
101
|
+
must not contain whitespace and must not contain any of the characters , . ; :
|
|
102
|
+
( ) \[ \] \{ \}.
|
|
103
|
+
|
|
104
|
+
- level: The level of the backlog item. Required. Must be an integer.
|
|
105
|
+
|
|
106
|
+
- title: The title of the backlog item. Required.
|
|
107
|
+
|
|
108
|
+
- story_points: The story points of the backlog item. Required.
|
|
109
|
+
|
|
110
|
+
- status: The status of the backlog item. Required.
|
|
111
|
+
|
|
112
|
+
- parent_key: The key of the parent backlog item. Optional. Must exist as a key
|
|
113
|
+
in the backlog. Parent keys are used to build the hierarchy of the backlog.
|
|
114
|
+
The parent key must be at a higher level than the current item. Parent keys
|
|
115
|
+
introduce implicit dependencies between items: the current item cannot start
|
|
116
|
+
before the parent item starts, and the parent item cannot finish before all
|
|
117
|
+
its children have finished.
|
|
118
|
+
|
|
119
|
+
- release: The release of the backlog item. Optional. Follows the same character
|
|
120
|
+
rules as the key. Must not be empty string.
|
|
121
|
+
|
|
122
|
+
- team: The team responsible for the backlog item. Optional. Must not be empty
|
|
123
|
+
string. Must be a valid team name. If None the item can be done by any team.
|
|
124
|
+
If not None. the item can only be done by the specified team.
|
|
125
|
+
|
|
126
|
+
- depends_on_f2s: The list of keys of the backlog items that must have been
|
|
127
|
+
finished before the current item can start. May be empty.
|
|
128
|
+
|
|
129
|
+
- depends_on_f2f: The list of keys of the backlog items that must have been
|
|
130
|
+
finished before the current item can finish. May be empty.
|
|
131
|
+
|
|
132
|
+
- depends_on_s2s: The list of keys of the backlog items that must have been
|
|
133
|
+
started before the current item can start. May be empty.
|
|
134
|
+
|
|
135
|
+
- planned_ready_date: The planned ready date of the backlog item. The date that
|
|
136
|
+
is communicated to the customer. Optional.
|
|
137
|
+
|
|
138
|
+
- estimated_ready_date: The estimated ready date of the backlog item. Optional.
|
|
139
|
+
|
|
140
|
+
Additionally each backlog item can have any number of other fields.
|
|
141
|
+
|
|
142
|
+
## Installing backlogops-cli
|
|
143
|
+
|
|
144
|
+
### On macOS and Linux
|
|
145
|
+
|
|
146
|
+
To install backlogops-cli on macOS and Linux, run the following command:
|
|
147
|
+
|
|
148
|
+
````sh
|
|
149
|
+
pip3 install --upgrade backlogops-cli
|
|
150
|
+
````
|
|
151
|
+
|
|
152
|
+
### On Microsoft Windows
|
|
153
|
+
|
|
154
|
+
To install backlogops-cli on Microsoft Windows, run the following command:
|
|
155
|
+
|
|
156
|
+
````sh
|
|
157
|
+
pip install --upgrade backlogops-cli
|
|
158
|
+
````
|
|
159
|
+
|
|
160
|
+
## API documentation
|
|
161
|
+
|
|
162
|
+
For more detailed code documentation, see the API documentation:
|
|
163
|
+
|
|
164
|
+
- [Library public API](https://bitbucket.org/tom-bjorkholm/backlog-ops/src/master/doc/backlogops_api.md)
|
|
165
|
+
|
|
166
|
+
- [Library protected API](https://bitbucket.org/tom-bjorkholm/backlog-ops/src/master/doc/backlogops_protected_api.md)
|
|
167
|
+
|
|
168
|
+
- [Library public CLI](https://bitbucket.org/tom-bjorkholm/backlog-ops/src/master/doc/backlogops_cli.md)
|
|
169
|
+
|
|
170
|
+
- [Library protected CLI](https://bitbucket.org/tom-bjorkholm/backlog-ops/src/master/doc/backlogops_protected_cli.md)
|
|
171
|
+
|
|
172
|
+
- [Library public GUI](https://bitbucket.org/tom-bjorkholm/backlog-ops/src/master/doc/backlogops_gui.md)
|
|
173
|
+
|
|
174
|
+
- [Library protected GUI](https://bitbucket.org/tom-bjorkholm/backlog-ops/src/master/doc/backlogops_protected_gui.md)
|
|
175
|
+
|
|
176
|
+
## Command line backlog operations
|
|
177
|
+
|
|
178
|
+
backlogops_cli serves 2 purposes:
|
|
179
|
+
|
|
180
|
+
- as a command line utility collection for manipulating backlogs
|
|
181
|
+
|
|
182
|
+
- as an example of how to write your own programs that use the backlogops library
|
|
183
|
+
|
|
184
|
+
### Currently available commands
|
|
185
|
+
|
|
186
|
+
````text
|
|
187
|
+
python3 -m backlogops_cli.adjust_release_content
|
|
188
|
+
Adjust release content to fit the planned release dates
|
|
189
|
+
python3 -m backlogops_cli.convert
|
|
190
|
+
Convert a backlog and releases between table file formats
|
|
191
|
+
python3 -m backlogops_cli.demo_backlog
|
|
192
|
+
Write a demonstration backlog and releases to a file
|
|
193
|
+
python3 -m backlogops_cli.estimate_ready_date
|
|
194
|
+
Estimate ready dates for the backlog items
|
|
195
|
+
python3 -m backlogops_cli.extract_keys
|
|
196
|
+
Extract backlog keys at the given levels to a key list
|
|
197
|
+
python3 -m backlogops_cli.list
|
|
198
|
+
List all commands available in backlogops_cli
|
|
199
|
+
python3 -m backlogops_cli.order_by_deps
|
|
200
|
+
Reorder a backlog so that dependencies are fulfilled
|
|
201
|
+
python3 -m backlogops_cli.order_by_keys
|
|
202
|
+
Reorder a backlog so that key-list items come first
|
|
203
|
+
python3 -m backlogops_cli.plan_release_dates
|
|
204
|
+
Set planned release dates from the estimated release dates
|
|
205
|
+
python3 -m backlogops_cli.teams_wizard
|
|
206
|
+
Create an AvailableTeams configuration file via a wizard
|
|
207
|
+
````
|
|
208
|
+
|
|
209
|
+
## Test summary
|
|
210
|
+
|
|
211
|
+
- Test result: 1066 passed in 16s
|
|
212
|
+
- No flake8 warnings.
|
|
213
|
+
- No mypy errors found.
|
|
214
|
+
- No python layout warnings.
|
|
215
|
+
- Built version(s): 0.1
|
|
216
|
+
- Build and test using Python 3.14.6
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
backlogops_cli/__init__.py,sha256=_fQCpIvVjUpxy90m9QHLW13b0KTJXttFRH_P6UrfVD8,168
|
|
2
|
+
backlogops_cli/__main__.py,sha256=3VGp1eTHo0ihcDtJe_702Aub_NeoIEUpUDkMUsAXCJs,268
|
|
3
|
+
backlogops_cli/_command_io.py,sha256=WSfOc7hzcuk79sSTVp3wqr7NApgD_N3WgQj71hd-E6A,9064
|
|
4
|
+
backlogops_cli/adjust_release_content.py,sha256=OO3nTg2puxodGi7-_Yr_-XFMe0v11NN3Zle2p4OUS0s,1914
|
|
5
|
+
backlogops_cli/convert.py,sha256=hqlyXlHV8QPCRBqS6BSJxAEpij1m5aNghcwCMvPMQQM,1510
|
|
6
|
+
backlogops_cli/demo_backlog.py,sha256=nUx4zr49nviQS0cJENCAIe0t0pEUg6Y8UF8sK1Vw86I,1329
|
|
7
|
+
backlogops_cli/estimate_ready_date.py,sha256=2_BA4GqJYR-boXybadbAJVFb0WGdXlM-j7Qmdd4DOR0,3979
|
|
8
|
+
backlogops_cli/extract_keys.py,sha256=pImQ7edlgLap9pg_DS-wjx6ioY62At4FCHKJUr6ACG8,2775
|
|
9
|
+
backlogops_cli/list.py,sha256=N7SDE0asqCKf0B6UStsQuZqN-51YrIez_8pQ7Rf4j8c,2012
|
|
10
|
+
backlogops_cli/order_by_deps.py,sha256=38WyIFI8HtOsamkVotHjTinMcLEquZZ6IEByRr4PkAE,2760
|
|
11
|
+
backlogops_cli/order_by_keys.py,sha256=0J4FnnPCVgOE5XCYfbEyHHwRZJqYdf9Uxv36vTFFXJg,2048
|
|
12
|
+
backlogops_cli/plan_release_dates.py,sha256=SWdV9Jmyn5flOM6hTJf_PWuL1gcIP9UuYuzMs0pzjtQ,1916
|
|
13
|
+
backlogops_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
backlogops_cli/teams_wizard.py,sha256=W5FkAS_hb-CX_FynhaSe6l62dnrKSkntsYLNvNfrMeQ,1962
|
|
15
|
+
backlogops_cli-0.1.dist-info/licenses/LICENSE.txt,sha256=YbzYf1byKHV7rKnb_zN_ouS8n9ttzGcYHpfTdTf-dPk,1072
|
|
16
|
+
backlogops_cli-0.1.dist-info/METADATA,sha256=5-sS5fEbdhForLSCeyooPGN0Xyl_YEUojFrQ0dSwDxY,8358
|
|
17
|
+
backlogops_cli-0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
backlogops_cli-0.1.dist-info/top_level.txt,sha256=6Vp2UR5vFqzujUt9VJ7sE9SkjBBICuS6zTe86Nzwd2w,15
|
|
19
|
+
backlogops_cli-0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Björkholm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
backlogops_cli
|