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.
@@ -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 and the global status map. Without ``-c`` the file
149
- is discovered the same way as the GUI.
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). Without -c the '
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 returns the
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
@@ -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 the encrypted token file is the same as 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. 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.
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 configuration is required for this command, so ``config`` is never
58
- None here; the assertion makes that explicit for the type checker.
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
- changes = data.estimate_ready_date(config.available_teams,
62
- _start_date(parsed))
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 estimated release dates are updated as well, and
73
- the list of release date changes is printed to stdout, or also saved
74
- to a file when ``--changes-file`` is given.
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.
@@ -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 and an issue's editable 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, editable fields."""
98
+ """Print the custom field map and, optionally, the issue's fields."""
51
99
  try:
52
- config = required_config(parsed)
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
- return _run(parsed_args(build_parser(), args))
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
@@ -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 for an output
6
- preset). Such a stand-alone file is used wherever an input or output
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. ``--links`` chooses how
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, added and failed items are printed
24
- to stdout as labelled lists, unless ``-q``/``--quiet`` is given. An
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
- connections: JiraConnections) -> list[str]:
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
- print('Ignoring columns not updatable in this preset: '
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
- fields = _resolve_fields(parsed, connections)
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)} failed.', file=sys.stderr)
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.2
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.2
28
- Requires-Dist: wizard-ui-bridge[textual]>=1.3
29
- Requires-Dist: edit-cfg-json-textual>=0.2.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, and status name mapping.
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, and what we know about the
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. Required.
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. the item can only be done by the specified team.
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//using_backlogops/README.md)
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 and an issue's editable 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: 2309 passed, 1 deselected in 58s
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.2
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=UaqHRy8bLW4xWyJVDE1wcFRyQF2CDJwRDH1ZlKtuos4,23656
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=Ia5CDDpPgGESLEHDTMz7GGl2Iyy8L50xx9HrLdT5cZ0,5244
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=F27c0fJZFAiHjUp12r6InkjF3Z4Qi85Ftbh-fgImnLQ,4557
15
- backlogops_cli/estimate_ready_date.py,sha256=ArKQTOnxKessobXAY17rJdAUjC7lgX1bHv_f72ayzU8,3491
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=bAiQ_IV8cQt9HpGHvt2EmhHeCzmO8rkqs6BG8ehXulo,3072
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=9Bnbes_E-nbYHEB0w4_HlHPARN8q2qv3m8Gm6nW16U4,2396
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=3H50cogxyDUsx8NxL6MMKsokw1YPwcBrW1Uhf9avwao,7413
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.2.dist-info/licenses/LICENSE.txt,sha256=YbzYf1byKHV7rKnb_zN_ouS8n9ttzGcYHpfTdTf-dPk,1072
35
- backlogops_cli-1.2.dist-info/METADATA,sha256=pGp7rfKgHpVID2oGCaK6FXHrGyjOxXgMJGRLKUOBES4,12293
36
- backlogops_cli-1.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
37
- backlogops_cli-1.2.dist-info/top_level.txt,sha256=6Vp2UR5vFqzujUt9VJ7sE9SkjBBICuS6zTe86Nzwd2w,15
38
- backlogops_cli-1.2.dist-info/RECORD,,
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,,