backlogops-cli 1.2__py3-none-any.whl → 1.4__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 +27 -6
- backlogops_cli/add_to_jira.py +4 -1
- backlogops_cli/encrypt_token_file.py +1 -1
- backlogops_cli/estimate_ready_date.py +39 -11
- backlogops_cli/jira_fields.py +64 -12
- backlogops_cli/preset_wizard.py +3 -2
- backlogops_cli/update_backlog_in_jira.py +43 -15
- {backlogops_cli-1.2.dist-info → backlogops_cli-1.4.dist-info}/METADATA +48 -13
- {backlogops_cli-1.2.dist-info → backlogops_cli-1.4.dist-info}/RECORD +12 -12
- {backlogops_cli-1.2.dist-info → backlogops_cli-1.4.dist-info}/WHEEL +0 -0
- {backlogops_cli-1.2.dist-info → backlogops_cli-1.4.dist-info}/licenses/LICENSE.txt +0 -0
- {backlogops_cli-1.2.dist-info → backlogops_cli-1.4.dist-info}/top_level.txt +0 -0
backlogops_cli/_command_io.py
CHANGED
|
@@ -145,12 +145,16 @@ def add_config_arg(parser: argparse.ArgumentParser) -> None:
|
|
|
145
145
|
"""Add the ``-c``/``--config`` backlog-ops configuration argument.
|
|
146
146
|
|
|
147
147
|
The configuration file holds the workforce, the named input and output
|
|
148
|
-
presets, the levels
|
|
149
|
-
|
|
148
|
+
presets, the levels, the global status map, the guess for unestimated
|
|
149
|
+
items, the remaining time settings, the GUI display settings and the
|
|
150
|
+
Jira configuration. Without
|
|
151
|
+
``-c`` the file is discovered the same way as the GUI.
|
|
150
152
|
"""
|
|
151
153
|
parser.add_argument('-c', '--config', dest='config',
|
|
152
154
|
help='Backlog-ops configuration file (workforce, '
|
|
153
|
-
'named presets, levels, status map
|
|
155
|
+
'named presets, levels, status map, default story '
|
|
156
|
+
'points, remaining time settings, Jira '
|
|
157
|
+
'configuration). Without -c the '
|
|
154
158
|
'file is found from $BACKLOGOPS_CFG, else '
|
|
155
159
|
'backlogops.cfg in $BACKLOGOPS_DIR, else '
|
|
156
160
|
'$HOME/.backlogops.cfg.')
|
|
@@ -231,6 +235,20 @@ def io_levels(config: Optional[BacklogOpsConfig]) -> Optional[Levels]:
|
|
|
231
235
|
return config.get_levels() if config is not None else None
|
|
232
236
|
|
|
233
237
|
|
|
238
|
+
def io_remaining_time(config: Optional[BacklogOpsConfig]) -> bool:
|
|
239
|
+
"""Return whether ``config`` enables remaining time estimates.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
config: The resolved backlog-ops configuration, or None for the
|
|
243
|
+
defaults, which do not use remaining time estimates.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
True when remaining time estimates are enabled in ``config``.
|
|
247
|
+
"""
|
|
248
|
+
return config is not None and \
|
|
249
|
+
config.remaining_time.enable_remaining_time
|
|
250
|
+
|
|
251
|
+
|
|
234
252
|
def read_input(parsed: argparse.Namespace,
|
|
235
253
|
config: Optional[BacklogOpsConfig]) -> BacklogReleases:
|
|
236
254
|
"""Read and validate the backlog and releases from the input file.
|
|
@@ -343,7 +361,8 @@ def _write_output(parsed: argparse.Namespace,
|
|
|
343
361
|
write_backlog_releases(data, parsed.output, out_config, rules,
|
|
344
362
|
levels=io_levels(config),
|
|
345
363
|
file_exists_callback=overwrite_callback(
|
|
346
|
-
parsed.force)
|
|
364
|
+
parsed.force),
|
|
365
|
+
use_remaining_time=io_remaining_time(config))
|
|
347
366
|
|
|
348
367
|
|
|
349
368
|
def run_write(parsed: argparse.Namespace,
|
|
@@ -405,7 +424,8 @@ def write_result_file(config: BacklogOpsConfig, path: str,
|
|
|
405
424
|
auto_ch_hook=CliPresetMigrateWarnHook())
|
|
406
425
|
write_backlog_releases(data, path, out_config, FormatRules(),
|
|
407
426
|
levels=config.get_levels(),
|
|
408
|
-
file_exists_callback=overwrite_callback(force)
|
|
427
|
+
file_exists_callback=overwrite_callback(force),
|
|
428
|
+
use_remaining_time=io_remaining_time(config))
|
|
409
429
|
print(f'Wrote {path}')
|
|
410
430
|
|
|
411
431
|
|
|
@@ -418,7 +438,8 @@ def run_added_to_jira(
|
|
|
418
438
|
"""Read the input, add it to Jira, and print the added lists.
|
|
419
439
|
|
|
420
440
|
``add`` receives the parsed args, the config and the input data, adds
|
|
421
|
-
to Jira, writes any ``--added``/``--existing`` files, and
|
|
441
|
+
to Jira, writes any ``--added-file``/``--existing-file`` files, and
|
|
442
|
+
returns the
|
|
422
443
|
result. ``report`` formats that result for stdout, printed unless
|
|
423
444
|
``-q``/``--quiet`` was given. ``exists_error`` is the key-exists error
|
|
424
445
|
meaning nothing was added; other value or OS errors are reported with
|
backlogops_cli/add_to_jira.py
CHANGED
|
@@ -66,10 +66,13 @@ def _add(parsed: argparse.Namespace, config: BacklogOpsConfig,
|
|
|
66
66
|
on_existing_key=mode,
|
|
67
67
|
rank_anchor=rank_anchor(parsed.rank),
|
|
68
68
|
levels=config.get_levels(),
|
|
69
|
-
status_map=config.get_status_input_map()
|
|
69
|
+
status_map=config.get_status_input_map(),
|
|
70
|
+
use_remaining_time=config.remaining_time
|
|
71
|
+
.enable_remaining_time)
|
|
70
72
|
print(f'Added {len(result.stored)} items to Jira; '
|
|
71
73
|
f'{len(result.already_present)} already present; '
|
|
72
74
|
f'{len(result.failed)} failed; '
|
|
75
|
+
f'{len(result.failed_fields)} fields not set; '
|
|
73
76
|
f'{len(result.failed_links)} links not written.', file=sys.stderr)
|
|
74
77
|
return result
|
|
75
78
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Read a clear text API token from a file or from standard input, encrypt it
|
|
5
5
|
with a pass phrase and write the encrypted token to a file. The pass phrase
|
|
6
6
|
is requested on the terminal and is not echoed. The clear text token file
|
|
7
|
-
is not modified, unless the
|
|
7
|
+
is not modified, unless the encrypted token file is the same as the
|
|
8
8
|
clear text token file, in which case the clear text token file is overwritten
|
|
9
9
|
with the encrypted token. The encrypted token file is written atomically,
|
|
10
10
|
so that it is either the old version or the new version, and never a
|
|
@@ -6,10 +6,17 @@ estimates the ready date of each backlog item from the available teams,
|
|
|
6
6
|
as documented for :func:`backlogops.estimate_ready_date`. The teams
|
|
7
7
|
configuration (velocity, work hours, vacations and so on) is taken from
|
|
8
8
|
the file given by ``--config`` or, when that is absent, from the
|
|
9
|
-
configured backlog-ops file
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
configured backlog-ops file, and so is what a backlog item that nobody
|
|
10
|
+
has estimated is worked with. With ``--remaining-time`` the ready dates
|
|
11
|
+
are instead estimated from the remaining time of the items and the focus
|
|
12
|
+
factor of the teams (or the configured default focus factor), as
|
|
13
|
+
documented for
|
|
14
|
+
:func:`backlogops.estimate_rt_ready_date`; that needs remaining time
|
|
15
|
+
estimates to be enabled in the configuration, and the command fails
|
|
16
|
+
without writing anything when they are not. The backlog with the
|
|
17
|
+
estimated dates and the releases are written to the output file. The
|
|
18
|
+
input and output formats are inferred from the file name extensions, but
|
|
19
|
+
can be overridden by a configuration file or by a named preset.
|
|
13
20
|
"""
|
|
14
21
|
|
|
15
22
|
# PYTHON_ARGCOMPLETE_OK
|
|
@@ -35,6 +42,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
35
42
|
parser.add_argument('-d', '--start-date', dest='start_date',
|
|
36
43
|
metavar='ISO_DATE',
|
|
37
44
|
help='Day the teams start working (default today).')
|
|
45
|
+
parser.add_argument('--remaining-time', dest='remaining_time',
|
|
46
|
+
action='store_true',
|
|
47
|
+
help='Estimate from the remaining time of the items '
|
|
48
|
+
'and the focus factor of the teams (or the default '
|
|
49
|
+
'focus factor) instead of from '
|
|
50
|
+
'story points and velocity. Requires remaining time '
|
|
51
|
+
'estimates to be enabled in the configuration '
|
|
52
|
+
'("enable_remaining_time": true in its '
|
|
53
|
+
'"remaining_time" section); otherwise the command '
|
|
54
|
+
'fails and writes nothing.')
|
|
38
55
|
parser.add_argument('--set-plan', dest='set_plan', action='store_true',
|
|
39
56
|
help='Also copy each estimated date to the planned '
|
|
40
57
|
'date.')
|
|
@@ -54,12 +71,20 @@ def _estimate(parsed: argparse.Namespace, config: Optional[BacklogOpsConfig],
|
|
|
54
71
|
) -> tuple[str, Optional[Callable[[str], None]]]:
|
|
55
72
|
"""Estimate the dates and return the release date change report.
|
|
56
73
|
|
|
57
|
-
The
|
|
58
|
-
|
|
74
|
+
The dates are estimated from remaining time when ``--remaining-time``
|
|
75
|
+
is given, and from story points otherwise. The configuration is
|
|
76
|
+
required for this command, so ``config`` is never None here; the
|
|
77
|
+
assertion makes that explicit for the type checker.
|
|
59
78
|
"""
|
|
60
79
|
assert isinstance(config, BacklogOpsConfig)
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
start = _start_date(parsed)
|
|
81
|
+
if parsed.remaining_time:
|
|
82
|
+
changes = data.estimate_rt_ready_date(config.available_teams,
|
|
83
|
+
config.remaining_time, start)
|
|
84
|
+
else:
|
|
85
|
+
changes = data.estimate_ready_date(
|
|
86
|
+
config.available_teams, start,
|
|
87
|
+
default_story_points=config.default_story_points)
|
|
63
88
|
if parsed.set_plan:
|
|
64
89
|
data.set_plan_from_estimate()
|
|
65
90
|
return date_report(changes, overwrite_callback(parsed.force))
|
|
@@ -69,9 +94,12 @@ def main(args: Optional[list[str]] = None) -> int:
|
|
|
69
94
|
"""Estimate the ready dates and write the output file.
|
|
70
95
|
|
|
71
96
|
The backlog with the estimated dates and the releases are written to
|
|
72
|
-
the output file. The
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
the output file. The ready dates are estimated from story points, or
|
|
98
|
+
from remaining time with ``--remaining-time``, which fails when the
|
|
99
|
+
configuration does not enable remaining time estimates. The estimated
|
|
100
|
+
release dates are updated as well, and the list of release date
|
|
101
|
+
changes is printed to stdout, or also saved to a file when
|
|
102
|
+
``--changes-file`` is given.
|
|
75
103
|
|
|
76
104
|
Args:
|
|
77
105
|
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
backlogops_cli/jira_fields.py
CHANGED
|
@@ -6,7 +6,11 @@ fetches from Jira, so a column-map name such as 'Story point estimate' can
|
|
|
6
6
|
be matched to its field id. With ``--issue`` it also prints the fields the
|
|
7
7
|
given issue's edit screen offers, which explains why a mapped field cannot
|
|
8
8
|
be set on that issue's type: a field missing from the edit screen cannot be
|
|
9
|
-
set through the issue edit REST endpoint.
|
|
9
|
+
set through the issue edit REST endpoint. Each ``--field`` (which needs
|
|
10
|
+
``--issue``) prints the raw Jira JSON at a field id or dotted path of that
|
|
11
|
+
issue, such as ``timetracking.remainingEstimateSeconds``, and the field's
|
|
12
|
+
edit screen entry, so a column map path can be checked against what Jira
|
|
13
|
+
really holds and how it may be written.
|
|
10
14
|
|
|
11
15
|
An encrypted Jira token is unlocked by a pass phrase asked on the terminal
|
|
12
16
|
only when it is needed.
|
|
@@ -17,15 +21,17 @@ only when it is needed.
|
|
|
17
21
|
# MIT License
|
|
18
22
|
|
|
19
23
|
import argparse
|
|
24
|
+
import json
|
|
20
25
|
import sys
|
|
21
26
|
from typing import Optional
|
|
22
27
|
from jira import JIRAError
|
|
23
28
|
from backlogops import (
|
|
24
|
-
JiraConnections, jira_custom_fields, jira_editable_fields
|
|
29
|
+
JiraConnections, JiraFieldJson, jira_custom_fields, jira_editable_fields,
|
|
30
|
+
jira_field_json)
|
|
25
31
|
from backlogops_cli._command_io import (
|
|
26
32
|
build_jira_parser, jira_passphrase, parsed_args, required_config)
|
|
27
33
|
|
|
28
|
-
DESCRIPTION = "Print Jira custom fields
|
|
34
|
+
DESCRIPTION = "Print Jira custom fields, an issue's editable fields and JSON"
|
|
29
35
|
|
|
30
36
|
|
|
31
37
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -34,6 +40,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
34
40
|
parser.add_argument('--issue', dest='issue', metavar='KEY',
|
|
35
41
|
help="Also print the fields this issue's edit "
|
|
36
42
|
'screen offers (for example SCRUM-15).')
|
|
43
|
+
parser.add_argument('--field', dest='fields', action='append',
|
|
44
|
+
metavar='NAME', default=[],
|
|
45
|
+
help='Also print the raw Jira JSON of this field of '
|
|
46
|
+
'the --issue: a field id, a custom field name, or a '
|
|
47
|
+
'dotted path such as '
|
|
48
|
+
'timetracking.remainingEstimateSeconds. Repeatable.')
|
|
37
49
|
return parser
|
|
38
50
|
|
|
39
51
|
|
|
@@ -46,16 +58,46 @@ def _print_pairs(heading: str, pairs: list[tuple[str, str]]) -> None:
|
|
|
46
58
|
print(f' {field_id} {name}')
|
|
47
59
|
|
|
48
60
|
|
|
61
|
+
def _print_json(heading: str, value: object) -> None:
|
|
62
|
+
"""Print a heading and the value as indented JSON below it."""
|
|
63
|
+
print(f' {heading}:')
|
|
64
|
+
text = json.dumps(value, indent=2, ensure_ascii=False, default=str)
|
|
65
|
+
for line in text.splitlines():
|
|
66
|
+
print(f' {line}')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _print_field(issue: str, found: JiraFieldJson) -> None:
|
|
70
|
+
"""Print one field path's JSON value and its edit screen entry."""
|
|
71
|
+
print(f"\nField '{found.name}' of {issue} (Jira field {found.field_id}):")
|
|
72
|
+
if found.present:
|
|
73
|
+
_print_json('value', found.value)
|
|
74
|
+
else:
|
|
75
|
+
print(' value: (no such path in the issue)')
|
|
76
|
+
if found.edit_entry is None:
|
|
77
|
+
print(' edit screen: (not offered, so not writable by an edit)')
|
|
78
|
+
else:
|
|
79
|
+
_print_json('edit screen', found.edit_entry)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _read_all(parsed: argparse.Namespace) -> tuple[
|
|
83
|
+
list[tuple[str, str]], Optional[list[tuple[str, str]]],
|
|
84
|
+
list[JiraFieldJson]]:
|
|
85
|
+
"""Read the custom fields, edit screen fields and field JSON asked for."""
|
|
86
|
+
config = required_config(parsed)
|
|
87
|
+
connections = JiraConnections(config.get_jira_config(), jira_passphrase)
|
|
88
|
+
customs = jira_custom_fields(connections, parsed.preset)
|
|
89
|
+
if parsed.issue is None:
|
|
90
|
+
return customs, None, []
|
|
91
|
+
editable = jira_editable_fields(connections, parsed.preset, parsed.issue)
|
|
92
|
+
found = jira_field_json(connections, parsed.preset, parsed.issue,
|
|
93
|
+
parsed.fields)
|
|
94
|
+
return customs, editable, found
|
|
95
|
+
|
|
96
|
+
|
|
49
97
|
def _run(parsed: argparse.Namespace) -> int:
|
|
50
|
-
"""Print the custom field map and, optionally,
|
|
98
|
+
"""Print the custom field map and, optionally, the issue's fields."""
|
|
51
99
|
try:
|
|
52
|
-
|
|
53
|
-
connections = JiraConnections(config.get_jira_config(),
|
|
54
|
-
jira_passphrase)
|
|
55
|
-
customs = jira_custom_fields(connections, parsed.preset)
|
|
56
|
-
editable = (jira_editable_fields(connections, parsed.preset,
|
|
57
|
-
parsed.issue)
|
|
58
|
-
if parsed.issue is not None else None)
|
|
100
|
+
customs, editable, found = _read_all(parsed)
|
|
59
101
|
except (ValueError, TypeError, KeyError, OSError, JIRAError) as error:
|
|
60
102
|
print(f'Could not read Jira fields: {error}', file=sys.stderr)
|
|
61
103
|
return 1
|
|
@@ -64,6 +106,8 @@ def _run(parsed: argparse.Namespace) -> int:
|
|
|
64
106
|
print()
|
|
65
107
|
_print_pairs(f'Fields settable on the edit screen of '
|
|
66
108
|
f'{parsed.issue}:', editable)
|
|
109
|
+
for field in found:
|
|
110
|
+
_print_field(parsed.issue, field)
|
|
67
111
|
return 0
|
|
68
112
|
|
|
69
113
|
|
|
@@ -75,8 +119,16 @@ def main(args: Optional[list[str]] = None) -> int:
|
|
|
75
119
|
|
|
76
120
|
Returns:
|
|
77
121
|
``0`` on success, ``1`` when the fields cannot be read.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
SystemExit: With status ``2`` when ``--field`` is given without
|
|
125
|
+
``--issue``, as for any other command line usage error.
|
|
78
126
|
"""
|
|
79
|
-
|
|
127
|
+
parser = build_parser()
|
|
128
|
+
parsed = parsed_args(parser, args)
|
|
129
|
+
if parsed.fields and parsed.issue is None:
|
|
130
|
+
parser.error('--field needs --issue')
|
|
131
|
+
return _run(parsed)
|
|
80
132
|
|
|
81
133
|
|
|
82
134
|
if __name__ == '__main__': # pragma: no cover
|
backlogops_cli/preset_wizard.py
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
"""Run the IO preset wizard and store the created preset file.
|
|
3
3
|
|
|
4
4
|
The created file holds a single input or output TableIO preset (a format
|
|
5
|
-
configuration with its column-name maps, and a level display
|
|
6
|
-
preset). Such a stand-alone file
|
|
5
|
+
configuration with its column-name maps, and a level display and whether
|
|
6
|
+
to leave out empty columns for an output preset). Such a stand-alone file
|
|
7
|
+
is used wherever an input or output
|
|
7
8
|
configuration is taken, by giving its file name.
|
|
8
9
|
"""
|
|
9
10
|
|
|
@@ -7,11 +7,13 @@ backlog-ops configuration, changing only a chosen subset of the mapped
|
|
|
7
7
|
fields. The subset is chosen with exactly one of two flags: ``-s``/``--store``
|
|
8
8
|
lists the columns to update (or the single word ``all`` for every mapped
|
|
9
9
|
writable column), while ``-e``/``--exclude`` updates every mapped writable
|
|
10
|
-
column except the listed ones.
|
|
10
|
+
column except the listed ones. The remaining time is a writable column only
|
|
11
|
+
while remaining time estimates are enabled.
|
|
11
12
|
|
|
12
13
|
``--on-missing`` chooses what to do with an item whose key is not present in
|
|
13
14
|
Jira: ``raise`` (the default) stops with an error, ``ignore`` leaves it
|
|
14
|
-
alone, and ``add`` creates it with all of its fields
|
|
15
|
+
alone, and ``add`` creates it with all of its mapped fields (the remaining
|
|
16
|
+
time only while remaining time estimates are enabled). ``--links`` chooses how
|
|
15
17
|
the parent and dependency links are updated: ``reconcile`` (the default) makes
|
|
16
18
|
the Jira links match the backlog exactly, removing a Jira link the backlog no
|
|
17
19
|
longer has and clearing a dropped parent, while ``add`` only adds the missing
|
|
@@ -20,8 +22,9 @@ selected fields are updated the same way under either value. With ``--rank``
|
|
|
20
22
|
the items are also ranked in Jira to match the backlog order, at the chosen
|
|
21
23
|
anchor.
|
|
22
24
|
|
|
23
|
-
The updated, already-correct, ignored
|
|
24
|
-
to
|
|
25
|
+
The updated, already-correct, ignored and added items, and what Jira
|
|
26
|
+
refused to add, to set or to link, are printed to stdout as labelled
|
|
27
|
+
lists, unless ``-q``/``--quiet`` is given. An
|
|
25
28
|
encrypted Jira token is unlocked by a pass phrase asked on the terminal
|
|
26
29
|
only when it is needed.
|
|
27
30
|
"""
|
|
@@ -48,6 +51,9 @@ _MISSING_MODES = {'raise': OnMissingKey.RAISE, 'ignore': OnMissingKey.IGNORE,
|
|
|
48
51
|
_LINK_MODES = {'reconcile': LinkUpdate.RECONCILE,
|
|
49
52
|
'add': LinkUpdate.ADD_MISSING}
|
|
50
53
|
_STORE_ALL = 'all'
|
|
54
|
+
_RT_WRITABLE = ('remaining_time is writable only while '
|
|
55
|
+
'"enable_remaining_time" is true in the configuration.')
|
|
56
|
+
"""Help and message note on when the remaining time can be updated."""
|
|
51
57
|
|
|
52
58
|
|
|
53
59
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -75,40 +81,57 @@ def _add_column_flags(parser: argparse.ArgumentParser) -> None:
|
|
|
75
81
|
group.add_argument('-s', '--store', dest='store', nargs='+',
|
|
76
82
|
metavar='COLUMN',
|
|
77
83
|
help='Columns to update, or the single word "all" for '
|
|
78
|
-
'every mapped writable column.')
|
|
84
|
+
'every mapped writable column. ' + _RT_WRITABLE)
|
|
79
85
|
group.add_argument('-e', '--exclude', dest='exclude', nargs='+',
|
|
80
86
|
metavar='COLUMN',
|
|
81
|
-
help='Update every mapped writable column but these.'
|
|
87
|
+
help='Update every mapped writable column but these. '
|
|
88
|
+
+ _RT_WRITABLE)
|
|
82
89
|
|
|
83
90
|
|
|
84
|
-
def _resolve_fields(parsed: argparse.Namespace,
|
|
85
|
-
|
|
91
|
+
def _resolve_fields(parsed: argparse.Namespace, connections: JiraConnections,
|
|
92
|
+
use_remaining_time: bool) -> list[str]:
|
|
86
93
|
"""Return the internal field names to update from the -s/-e flags.
|
|
87
94
|
|
|
88
95
|
``-s all`` and ``-e`` are resolved against the preset's updatable
|
|
89
96
|
columns. A ``-s`` name that is not an updatable column is reported and
|
|
90
|
-
dropped, so a typo does not silently update nothing.
|
|
97
|
+
dropped, so a typo does not silently update nothing. The remaining
|
|
98
|
+
time is an updatable column only while remaining time estimates are
|
|
99
|
+
used.
|
|
91
100
|
"""
|
|
92
|
-
updatable = updatable_backlog_fields(connections, parsed.preset
|
|
101
|
+
updatable = updatable_backlog_fields(connections, parsed.preset,
|
|
102
|
+
use_remaining_time=use_remaining_time)
|
|
93
103
|
if parsed.store is not None:
|
|
94
104
|
if parsed.store == [_STORE_ALL]:
|
|
95
105
|
return updatable
|
|
96
106
|
unknown = [name for name in parsed.store if name not in updatable]
|
|
97
107
|
if unknown:
|
|
98
|
-
|
|
99
|
-
+ ', '.join(unknown), file=sys.stderr)
|
|
108
|
+
_report_ignored(unknown, use_remaining_time)
|
|
100
109
|
return [name for name in parsed.store if name in updatable]
|
|
101
110
|
excluded = set(parsed.exclude)
|
|
102
111
|
return [name for name in updatable if name not in excluded]
|
|
103
112
|
|
|
104
113
|
|
|
114
|
+
def _report_ignored(unknown: list[str], use_remaining_time: bool) -> None:
|
|
115
|
+
"""Report the ``-s`` columns dropped as not updatable to stderr.
|
|
116
|
+
|
|
117
|
+
A dropped ``remaining_time`` while remaining time estimates are not
|
|
118
|
+
used gets a note saying why, because the preset may well map it.
|
|
119
|
+
"""
|
|
120
|
+
note = ''
|
|
121
|
+
if 'remaining_time' in unknown and not use_remaining_time:
|
|
122
|
+
note = ' (' + _RT_WRITABLE + ')'
|
|
123
|
+
print('Ignoring columns not updatable in this preset: '
|
|
124
|
+
+ ', '.join(unknown) + note, file=sys.stderr)
|
|
125
|
+
|
|
126
|
+
|
|
105
127
|
def _update(parsed: argparse.Namespace, config: BacklogOpsConfig,
|
|
106
128
|
data_backlog: object) -> UpdatedBacklogInJira:
|
|
107
129
|
"""Update the input backlog in Jira using the named preset."""
|
|
108
130
|
print(f"Updating backlog in Jira using preset '{parsed.preset}'...",
|
|
109
131
|
file=sys.stderr)
|
|
110
132
|
connections = JiraConnections(config.get_jira_config(), jira_passphrase)
|
|
111
|
-
|
|
133
|
+
use_rt = config.remaining_time.enable_remaining_time
|
|
134
|
+
fields = _resolve_fields(parsed, connections, use_rt)
|
|
112
135
|
mode = _MISSING_MODES[parsed.on_missing]
|
|
113
136
|
link_update = _LINK_MODES[parsed.links]
|
|
114
137
|
assert isinstance(data_backlog, list)
|
|
@@ -118,7 +141,8 @@ def _update(parsed: argparse.Namespace, config: BacklogOpsConfig,
|
|
|
118
141
|
link_update=link_update,
|
|
119
142
|
rank_anchor=rank_anchor(parsed.rank),
|
|
120
143
|
levels=config.get_levels(),
|
|
121
|
-
status_map=config.get_status_input_map()
|
|
144
|
+
status_map=config.get_status_input_map(),
|
|
145
|
+
use_remaining_time=use_rt)
|
|
122
146
|
_report_summary(result)
|
|
123
147
|
return result
|
|
124
148
|
|
|
@@ -128,7 +152,11 @@ def _report_summary(result: UpdatedBacklogInJira) -> None:
|
|
|
128
152
|
print(f'Updated {len(result.updated)} items in Jira; '
|
|
129
153
|
f'{len(result.already_correct)} already correct; '
|
|
130
154
|
f'{len(result.ignored)} ignored; {len(result.added.stored)} added; '
|
|
131
|
-
f'{len(result.failed)}
|
|
155
|
+
f'{len(result.added.failed)} not added; '
|
|
156
|
+
f'{len(result.failed_fields) + len(result.added.failed_fields)} '
|
|
157
|
+
'fields not set; '
|
|
158
|
+
f'{len(result.failed_links) + len(result.added.failed_links)} '
|
|
159
|
+
'links not written.', file=sys.stderr)
|
|
132
160
|
|
|
133
161
|
|
|
134
162
|
def _run(parsed: argparse.Namespace) -> int:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backlogops-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.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>
|
|
@@ -24,9 +24,9 @@ Requires-Python: >=3.12
|
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE.txt
|
|
26
26
|
Requires-Dist: argcomplete>=3.7.2
|
|
27
|
-
Requires-Dist: backlogops>=1.
|
|
28
|
-
Requires-Dist: wizard-ui-bridge[textual]>=1.
|
|
29
|
-
Requires-Dist: edit-cfg-json-textual>=0.
|
|
27
|
+
Requires-Dist: backlogops>=1.4
|
|
28
|
+
Requires-Dist: wizard-ui-bridge[textual]>=1.4
|
|
29
|
+
Requires-Dist: edit-cfg-json-textual>=0.4.0
|
|
30
30
|
Requires-Dist: versionreporter>=0.4
|
|
31
31
|
Dynamic: author
|
|
32
32
|
Dynamic: license-file
|
|
@@ -76,6 +76,14 @@ The following functionality is available in all 3 packages:
|
|
|
76
76
|
- Estimate ready date for the backlog items based on available teams, team
|
|
77
77
|
velocity, vacation dates, periods with half time work, etc.
|
|
78
78
|
|
|
79
|
+
- Alternatively, where remaining time estimates are required, estimate ready
|
|
80
|
+
dates from the remaining time of the backlog items and the focus factor of the
|
|
81
|
+
teams (story points are recommended).
|
|
82
|
+
|
|
83
|
+
- Configure what a backlog item that nobody has estimated counts as, per backlog
|
|
84
|
+
item level, so that an unestimated item does not make the estimated dates too
|
|
85
|
+
optimistic.
|
|
86
|
+
|
|
79
87
|
- Extract backlog keys at given backlog item levels.
|
|
80
88
|
|
|
81
89
|
- Reorder the backlog so that the dependencies are satisfied.
|
|
@@ -100,6 +108,9 @@ The following functionality is available in all 3 packages:
|
|
|
100
108
|
|
|
101
109
|
- Rename columns when reading a file and when writing a file.
|
|
102
110
|
|
|
111
|
+
- Leave out columns that are empty on every row, when writing a file and when
|
|
112
|
+
showing the backlog in the GUI.
|
|
113
|
+
|
|
103
114
|
- Map custom status names in input files to backlog item statuses.
|
|
104
115
|
|
|
105
116
|
- Choose how backlog item levels are written: by number, by name, or both, and
|
|
@@ -110,7 +121,16 @@ The following functionality is available in all 3 packages:
|
|
|
110
121
|
- Migrate an older configuration or preset file to the current file format.
|
|
111
122
|
|
|
112
123
|
- A wizard to create a backlog-ops configuration file with the workforce, named
|
|
113
|
-
input and output presets, level names,
|
|
124
|
+
input and output presets, level names, status name mapping, the guess for
|
|
125
|
+
unestimated items, whether remaining time estimates are also used, how the
|
|
126
|
+
graphical user interface shows the data, and the Jira integration.
|
|
127
|
+
|
|
128
|
+
- An editor showing a whole configuration file or a stand-alone preset file at
|
|
129
|
+
once, folded where it is deep, to change a single value without stepping
|
|
130
|
+
through the wizard.
|
|
131
|
+
|
|
132
|
+
- Encrypt the Jira API token to a pass-phrase-protected file, so that no
|
|
133
|
+
configuration file holds the token in clear text.
|
|
114
134
|
|
|
115
135
|
- Read a backlog and releases from Jira into a backlog and release table.
|
|
116
136
|
|
|
@@ -129,6 +149,14 @@ The following functionality is available in all 3 packages:
|
|
|
129
149
|
- Move backlog items to a chosen anchor in the Jira rank order, following a key
|
|
130
150
|
list.
|
|
131
151
|
|
|
152
|
+
- Report what a Jira operation did not do. Every Jira result opens with what
|
|
153
|
+
Jira refused or what was skipped, so a value that was not written is visible
|
|
154
|
+
without reading the whole listing.
|
|
155
|
+
|
|
156
|
+
- Show the custom fields of Jira, the fields an issue's edit screen accepts, and
|
|
157
|
+
the raw JSON Jira holds for a field of an issue, to find the correct Jira
|
|
158
|
+
field mapping.
|
|
159
|
+
|
|
132
160
|
## The operating model
|
|
133
161
|
|
|
134
162
|
The operating model that most of the functionality is designed for is that the
|
|
@@ -137,8 +165,8 @@ are ordered by priority and dependencies to allow the teams to work in the
|
|
|
137
165
|
backlog order. Each backlog item and each release may have a planned ready date,
|
|
138
166
|
that records what has been communicated to the customer. Each backlog item and
|
|
139
167
|
each release may have an estimated ready date, that is calculated from the
|
|
140
|
-
current backlog state, the team velocity,
|
|
141
|
-
availability of the team members.
|
|
168
|
+
current backlog state, the team velocity (or, for remaining time estimates, the
|
|
169
|
+
focus factor), and what we know about the availability of the team members.
|
|
142
170
|
|
|
143
171
|
## The backlog item fields
|
|
144
172
|
|
|
@@ -153,7 +181,10 @@ the library:
|
|
|
153
181
|
|
|
154
182
|
- `title` The title of the backlog item. Required.
|
|
155
183
|
|
|
156
|
-
- `story_points` The story points of the backlog item.
|
|
184
|
+
- `story_points` The story points of the backlog item. Optional. An item with no
|
|
185
|
+
story points is one that nobody has estimated yet, and what such an item is
|
|
186
|
+
worked with when a completion date is estimated is configured. May be a
|
|
187
|
+
fraction, such as 0.5.
|
|
157
188
|
|
|
158
189
|
- `status` The status of the backlog item. Required.
|
|
159
190
|
|
|
@@ -170,7 +201,11 @@ the library:
|
|
|
170
201
|
|
|
171
202
|
- `team` The team responsible for the backlog item. Optional. Must not be empty
|
|
172
203
|
string. Must be a valid team name. If None the item can be done by any team.
|
|
173
|
-
If not None
|
|
204
|
+
If not None, the item can only be done by the specified team.
|
|
205
|
+
|
|
206
|
+
- `remaining_time` The remaining time of the backlog item, as ideal focused work
|
|
207
|
+
time of one person. Optional. Only used by the alternative remaining time
|
|
208
|
+
estimates; story points are recommended. Must not be negative.
|
|
174
209
|
|
|
175
210
|
- `depends_on_f2s` The list of keys of the backlog items that must have been
|
|
176
211
|
finished before the current item can start. May be empty.
|
|
@@ -210,7 +245,7 @@ pip install --upgrade backlogops-cli
|
|
|
210
245
|
|
|
211
246
|
For a better understanding of how to use the library, CLI or GUI, see the user
|
|
212
247
|
documentation:
|
|
213
|
-
[Using backlogops](https://github.com/tom-bjorkholm/backlog-ops/blob/master/doc
|
|
248
|
+
[Using backlogops](https://github.com/tom-bjorkholm/backlog-ops/blob/master/doc/using_backlogops/README.md)
|
|
214
249
|
|
|
215
250
|
## API documentation
|
|
216
251
|
|
|
@@ -271,7 +306,7 @@ backlogops_cli serves 2 purposes:
|
|
|
271
306
|
Extract backlog keys at the given levels to a key list
|
|
272
307
|
|
|
273
308
|
python3 -m backlogops_cli.jira_fields
|
|
274
|
-
Print Jira custom fields
|
|
309
|
+
Print Jira custom fields, an issue's editable fields and JSON
|
|
275
310
|
|
|
276
311
|
python3 -m backlogops_cli.list
|
|
277
312
|
List all commands available in backlogops_cli
|
|
@@ -322,10 +357,10 @@ backlogops_cli serves 2 purposes:
|
|
|
322
357
|
|
|
323
358
|
## Test summary
|
|
324
359
|
|
|
325
|
-
- Test result:
|
|
360
|
+
- Test result: 2902 passed, 1 deselected in 68s (0:01:08)
|
|
326
361
|
- No flake8 warnings.
|
|
327
362
|
- No mypy errors found.
|
|
328
363
|
- No pylint warnings.
|
|
329
364
|
- No python layout warnings.
|
|
330
|
-
- Built version(s): 1.
|
|
365
|
+
- Built version(s): 1.4
|
|
331
366
|
- Build and test using Python 3.14.7
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
backlogops_cli/__init__.py,sha256=_fQCpIvVjUpxy90m9QHLW13b0KTJXttFRH_P6UrfVD8,168
|
|
2
2
|
backlogops_cli/__main__.py,sha256=3VGp1eTHo0ihcDtJe_702Aub_NeoIEUpUDkMUsAXCJs,268
|
|
3
|
-
backlogops_cli/_command_io.py,sha256=
|
|
3
|
+
backlogops_cli/_command_io.py,sha256=Du39zElm50Cch5y0xc-6nd1ftBI7DfAsDmWwFuWPmr8,24521
|
|
4
4
|
backlogops_cli/_migrate_warn.py,sha256=XhUATa5qG7ZT4YTIEHZaJTjEhi5H_K7mEgCcNTW3HBM,2269
|
|
5
5
|
backlogops_cli/_wizard_io.py,sha256=IrVHsMqyKHi6pTgTgfashqLd0VzSUc18XucNGVLXuaE,5628
|
|
6
6
|
backlogops_cli/add_releases_to_jira.py,sha256=_X--t5UsNf9pomdmFjdTEAHAOWzfvZxVh-1Mh0reYcI,4968
|
|
7
|
-
backlogops_cli/add_to_jira.py,sha256=
|
|
7
|
+
backlogops_cli/add_to_jira.py,sha256=dqqsG7hOhz2W6MOJH0PDbMgj-MtR1CousGsAp7dQoRc,5433
|
|
8
8
|
backlogops_cli/adjust_release_content.py,sha256=bowohyiURAqroZBsL7uOh2qE3aQZbZPh_JkxykSbQaA,1975
|
|
9
9
|
backlogops_cli/bloc_version_reporter.py,sha256=HBzCBQBKXjxVjaPKIrpliaXbKIPK3KBo_gehb6SHbpg,706
|
|
10
10
|
backlogops_cli/config_edit.py,sha256=rj6kwR9qGnCPdgtIICMbV7hFQ3gbiurenKfxHqWgEEo,5729
|
|
11
11
|
backlogops_cli/config_wizard.py,sha256=QAqdIjS13FjLAODs9o2OAEa_fJUFxgJFXru09XP6FZQ,1917
|
|
12
12
|
backlogops_cli/convert.py,sha256=I66pUgtSwKrIkS_08skRyJusYFHYdeGz2GzVcxgXD2s,1420
|
|
13
13
|
backlogops_cli/demo_backlog.py,sha256=jR8G8P4MDe0OqHUsuUUdOOW3XlptaeIgz73BrcWCQDg,1309
|
|
14
|
-
backlogops_cli/encrypt_token_file.py,sha256=
|
|
15
|
-
backlogops_cli/estimate_ready_date.py,sha256=
|
|
14
|
+
backlogops_cli/encrypt_token_file.py,sha256=FbjNgKEJsBggGE7HMZMKSzXfn1O8G3qjA3Zk32T4htQ,4553
|
|
15
|
+
backlogops_cli/estimate_ready_date.py,sha256=syJtL7MIeyWGaDOsX8QKjSsdTtRBu3SRnbWvtoIsZiY,5160
|
|
16
16
|
backlogops_cli/extract_keys.py,sha256=Y_YdWuytwDokA3m4j9y1hLOr_Z84gSBvSLEKM4Xks1c,2909
|
|
17
|
-
backlogops_cli/jira_fields.py,sha256=
|
|
17
|
+
backlogops_cli/jira_fields.py,sha256=uRz9HqxkuEAN1Z8EKtZw2lL-S5xdTlKpZvAzvSwy9_g,5305
|
|
18
18
|
backlogops_cli/list.py,sha256=lvvdb_UjNKLnGb1c2pilRWfSQrOptTrAiMGRcEVizhQ,2168
|
|
19
19
|
backlogops_cli/migrate_cfg.py,sha256=6nnihDLZ8IxY0GFTrMzKeboHTUwA2UuFXE0Isr7_vT8,2528
|
|
20
20
|
backlogops_cli/order_by_deps.py,sha256=N0vCRAnfV1xuC10T0ll1UUuOgWghus7eiXTut-QLaSA,2696
|
|
@@ -23,16 +23,16 @@ backlogops_cli/order_by_release.py,sha256=VUm6yywNEV9zMs0n0LqQvy3BZzjFrDLQKRZOqz
|
|
|
23
23
|
backlogops_cli/order_releases.py,sha256=LbK0irFrXs_ib581oxV8JzZ8yl5bYf8g_MUhZB91FGU,2102
|
|
24
24
|
backlogops_cli/order_releases_in_jira.py,sha256=kd2bV6tQexNBgRoCU0txM9aaxRGBAqUz7cX4-fCV3XA,5131
|
|
25
25
|
backlogops_cli/plan_release_dates.py,sha256=d_8idlpQRpZFfHgo-fWmvm9Gc3g23eNh9mFNrSemt_w,1977
|
|
26
|
-
backlogops_cli/preset_wizard.py,sha256=
|
|
26
|
+
backlogops_cli/preset_wizard.py,sha256=NltOoDiHLDZujvJAnlo6pLblaaXcDpSRLyRq-RNXIZQ,2435
|
|
27
27
|
backlogops_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
28
|
backlogops_cli/rank_in_jira.py,sha256=-uGFXpne40dY_4ZtYqJTz6JIUelzsaBp8B2G_4EbMC4,4923
|
|
29
29
|
backlogops_cli/read_jira.py,sha256=2-HQKIH7avENw7XMEIcFqt1mzPpsmoZ0Oy1SfBiFXlU,3423
|
|
30
30
|
backlogops_cli/rename_releases_in_jira.py,sha256=yq2Xwst5tEOay0wAIWJso09GB1XP4z58zJlYo4Pga5I,4656
|
|
31
|
-
backlogops_cli/update_backlog_in_jira.py,sha256=
|
|
31
|
+
backlogops_cli/update_backlog_in_jira.py,sha256=Gg4nOldRRR8l6MfV7qgJeszOAlvL9zlmD8v4sKG038M,8888
|
|
32
32
|
backlogops_cli/update_releases_in_jira.py,sha256=U96CVl6lJHOpEFRukQofleK8EGBR6BKSXMNk-OE7BNY,5300
|
|
33
33
|
backlogops_cli/version.py,sha256=rhU5rq0tdxwJk0rJZKYZXc0rvH7aKd5zykDo2bPjmyY,528
|
|
34
|
-
backlogops_cli-1.
|
|
35
|
-
backlogops_cli-1.
|
|
36
|
-
backlogops_cli-1.
|
|
37
|
-
backlogops_cli-1.
|
|
38
|
-
backlogops_cli-1.
|
|
34
|
+
backlogops_cli-1.4.dist-info/licenses/LICENSE.txt,sha256=YbzYf1byKHV7rKnb_zN_ouS8n9ttzGcYHpfTdTf-dPk,1072
|
|
35
|
+
backlogops_cli-1.4.dist-info/METADATA,sha256=m88PDAQJU_JM249OWiga0ISDrdYw2GTLAPDJHQWDBHc,14093
|
|
36
|
+
backlogops_cli-1.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
37
|
+
backlogops_cli-1.4.dist-info/top_level.txt,sha256=6Vp2UR5vFqzujUt9VJ7sE9SkjBBICuS6zTe86Nzwd2w,15
|
|
38
|
+
backlogops_cli-1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|