backlogops-cli 0.2__py3-none-any.whl → 0.3__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 +43 -9
- backlogops_cli/add_releases_to_jira.py +144 -0
- backlogops_cli/add_to_jira.py +151 -0
- backlogops_cli/jira_fields.py +91 -0
- backlogops_cli/order_releases_in_jira.py +128 -0
- backlogops_cli/rank_in_jira.py +120 -0
- backlogops_cli/read_jira.py +92 -0
- backlogops_cli/rename_releases_in_jira.py +120 -0
- backlogops_cli/update_backlog_in_jira.py +177 -0
- backlogops_cli/update_releases_in_jira.py +135 -0
- {backlogops_cli-0.2.dist-info → backlogops_cli-0.3.dist-info}/METADATA +69 -17
- {backlogops_cli-0.2.dist-info → backlogops_cli-0.3.dist-info}/RECORD +15 -6
- {backlogops_cli-0.2.dist-info → backlogops_cli-0.3.dist-info}/WHEEL +1 -1
- {backlogops_cli-0.2.dist-info → backlogops_cli-0.3.dist-info}/licenses/LICENSE.txt +0 -0
- {backlogops_cli-0.2.dist-info → backlogops_cli-0.3.dist-info}/top_level.txt +0 -0
backlogops_cli/_command_io.py
CHANGED
|
@@ -20,11 +20,39 @@ from backlogops_cli._migrate_warn import (
|
|
|
20
20
|
CliMigrateWarnHook, CliPresetMigrateWarnHook)
|
|
21
21
|
from backlogops_cli.bloc_version_reporter import BloCliVersionReporter
|
|
22
22
|
from backlogops import (
|
|
23
|
-
BacklogOpsConfig, BacklogReleases, FileExistsCb, FormatRules,
|
|
24
|
-
ReleaseChanges, ReleaseDateChanges,
|
|
25
|
-
format_content_changes, format_date_changes,
|
|
26
|
-
read_backlog_releases, resolve_input_config,
|
|
27
|
-
write_backlog_releases, write_content_changes,
|
|
23
|
+
BacklogOpsConfig, BacklogReleases, FileExistsCb, FormatRules,
|
|
24
|
+
JiraRankAnchor, Levels, ReleaseChanges, ReleaseDateChanges,
|
|
25
|
+
allow_overwrite, format_content_changes, format_date_changes,
|
|
26
|
+
get_backlog_ops_config, read_backlog_releases, resolve_input_config,
|
|
27
|
+
resolve_output_config, write_backlog_releases, write_content_changes,
|
|
28
|
+
write_date_changes)
|
|
29
|
+
|
|
30
|
+
RANK_ANCHOR_CHOICES = {
|
|
31
|
+
'backlog-top': JiraRankAnchor.BACKLOG_TOP,
|
|
32
|
+
'backlog-bottom': JiraRankAnchor.BACKLOG_BOTTOM,
|
|
33
|
+
'first-key': JiraRankAnchor.FIRST_KEY,
|
|
34
|
+
'last-key': JiraRankAnchor.LAST_KEY}
|
|
35
|
+
"""Command-line anchor names mapped to :class:`JiraRankAnchor` members."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def rank_anchor(value: Optional[str]) -> Optional[JiraRankAnchor]:
|
|
39
|
+
"""Return the anchor for a CLI anchor name, or None for a None value."""
|
|
40
|
+
return None if value is None else RANK_ANCHOR_CHOICES[value]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def add_rank_arg(parser: argparse.ArgumentParser) -> None:
|
|
44
|
+
"""Add the optional ``--rank`` flag choosing a Jira rank anchor.
|
|
45
|
+
|
|
46
|
+
Shared by the add and update commands, where ranking is opt-in: without
|
|
47
|
+
``--rank`` the Jira rank order is left unchanged.
|
|
48
|
+
"""
|
|
49
|
+
parser.add_argument('--rank', dest='rank', default=None,
|
|
50
|
+
choices=sorted(RANK_ANCHOR_CHOICES),
|
|
51
|
+
help='Also set the Jira rank order to match the '
|
|
52
|
+
'supplied backlog, placing the items at this anchor: '
|
|
53
|
+
'backlog-top, backlog-bottom, first-key (keep the '
|
|
54
|
+
'first item fixed) or last-key (keep the last item '
|
|
55
|
+
'fixed). Omit to leave the Jira rank order unchanged.')
|
|
28
56
|
|
|
29
57
|
|
|
30
58
|
def overwrite_callback(force: bool, in_stream: Optional[TextIO] = None,
|
|
@@ -255,11 +283,14 @@ def _write_output(parsed: argparse.Namespace,
|
|
|
255
283
|
|
|
256
284
|
def run_write(parsed: argparse.Namespace,
|
|
257
285
|
data_source: Callable[[Optional[BacklogOpsConfig]],
|
|
258
|
-
BacklogReleases]
|
|
286
|
+
BacklogReleases],
|
|
287
|
+
require_config: bool = False) -> int:
|
|
259
288
|
"""Build the data, write it to the output file, and report the result.
|
|
260
289
|
|
|
261
|
-
The configuration is resolved once from ``-c`` or by discovery
|
|
262
|
-
back to the built-in defaults when none is found
|
|
290
|
+
The configuration is resolved once from ``-c`` or by discovery. It
|
|
291
|
+
falls back to the built-in defaults when none is found, unless
|
|
292
|
+
``require_config`` is True, in which case a missing configuration is
|
|
293
|
+
reported as an error.
|
|
263
294
|
|
|
264
295
|
Args:
|
|
265
296
|
parsed: Parsed command line arguments holding the output options
|
|
@@ -269,12 +300,15 @@ def run_write(parsed: argparse.Namespace,
|
|
|
269
300
|
None) and returns the backlog and releases to write. It is
|
|
270
301
|
called inside the error handling so that reading failures are
|
|
271
302
|
reported like writing failures.
|
|
303
|
+
require_config: When True a missing configuration is reported as an
|
|
304
|
+
error instead of falling back to the built-in defaults.
|
|
272
305
|
|
|
273
306
|
Returns:
|
|
274
307
|
``0`` on success, ``1`` when the data cannot be built or written.
|
|
275
308
|
"""
|
|
276
309
|
try:
|
|
277
|
-
config =
|
|
310
|
+
config = (required_config(parsed) if require_config
|
|
311
|
+
else optional_config(parsed))
|
|
278
312
|
_write_output(parsed, config, data_source(config))
|
|
279
313
|
except (ValueError, TypeError, KeyError, OSError) as error:
|
|
280
314
|
print(f'Could not write {parsed.output}: {error}', file=sys.stderr)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Add releases to Jira from an input file, creating one version per name.
|
|
3
|
+
|
|
4
|
+
The command reads a backlog and its releases from the input file, then
|
|
5
|
+
adds the releases to Jira using a named preset of the backlog-ops
|
|
6
|
+
configuration. By default it stops with an error when a release name
|
|
7
|
+
already exists in Jira; ``--skip-existing`` skips those releases instead.
|
|
8
|
+
|
|
9
|
+
The added releases and the releases already in Jira are printed to stdout
|
|
10
|
+
as two labelled lists, unless ``-q``/``--quiet`` is given. Each list is
|
|
11
|
+
also written, together with the unchanged input backlog, to a file when
|
|
12
|
+
``--added-file`` or ``--existing-file`` names one; without a file name the
|
|
13
|
+
list is not written. An encrypted Jira token is unlocked by a pass phrase
|
|
14
|
+
asked on the terminal only when it is needed.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
18
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
19
|
+
# MIT License
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import sys
|
|
23
|
+
from getpass import getpass
|
|
24
|
+
from typing import Optional
|
|
25
|
+
from backlogops import (
|
|
26
|
+
AddedReleasesToJira, BacklogOpsConfig, BacklogReleases, FormatRules,
|
|
27
|
+
JiraConnections, OnExistingKey, ReleaseExistsError, add_releases_to_jira,
|
|
28
|
+
format_release_result, resolve_output_config, write_backlog_releases)
|
|
29
|
+
from backlogops_cli._command_io import (
|
|
30
|
+
add_config_arg, add_input_args, overwrite_callback, parsed_args,
|
|
31
|
+
read_input, required_config)
|
|
32
|
+
from backlogops_cli._migrate_warn import CliPresetMigrateWarnHook
|
|
33
|
+
|
|
34
|
+
DESCRIPTION = 'Add releases to Jira, creating a new version per release'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
38
|
+
"""Build the command line parser for the add-releases command."""
|
|
39
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
40
|
+
add_input_args(parser)
|
|
41
|
+
add_config_arg(parser)
|
|
42
|
+
parser.add_argument('-p', '--preset', dest='preset', required=True,
|
|
43
|
+
help='Name of the Jira preset in the configuration.')
|
|
44
|
+
parser.add_argument('--skip-existing', dest='skip_existing',
|
|
45
|
+
action='store_true',
|
|
46
|
+
help='Skip releases whose name already exists in Jira '
|
|
47
|
+
'(the default stops with an error instead).')
|
|
48
|
+
parser.add_argument('--added-file', dest='added_file', metavar='FILE',
|
|
49
|
+
help='Write the added releases, with the input '
|
|
50
|
+
'backlog, to this file. Omit to not write it.')
|
|
51
|
+
parser.add_argument('--existing-file', dest='existing_file',
|
|
52
|
+
metavar='FILE',
|
|
53
|
+
help='Write the releases already in Jira, with the '
|
|
54
|
+
'input backlog, to this file. Omit to not write it.')
|
|
55
|
+
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
|
|
56
|
+
help='Do not print the added and already-present '
|
|
57
|
+
'lists to stdout.')
|
|
58
|
+
parser.add_argument('-f', '--force', dest='force', action='store_true',
|
|
59
|
+
help='Overwrite existing output files without '
|
|
60
|
+
'asking.')
|
|
61
|
+
return parser
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _passphrase() -> str:
|
|
65
|
+
"""Ask for the Jira token pass phrase on the terminal."""
|
|
66
|
+
return getpass('Jira API token pass phrase: ')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _add(parsed: argparse.Namespace, config: BacklogOpsConfig,
|
|
70
|
+
data: BacklogReleases) -> AddedReleasesToJira:
|
|
71
|
+
"""Add the input releases to Jira using the named preset."""
|
|
72
|
+
print(f"Adding releases to Jira using preset '{parsed.preset}'...",
|
|
73
|
+
file=sys.stderr)
|
|
74
|
+
connections = JiraConnections(config.get_jira_config(), _passphrase)
|
|
75
|
+
mode = (OnExistingKey.SKIP if parsed.skip_existing
|
|
76
|
+
else OnExistingKey.RAISE)
|
|
77
|
+
result = add_releases_to_jira(connections, parsed.preset, data.releases,
|
|
78
|
+
on_existing_key=mode)
|
|
79
|
+
print(f'Added {len(result.stored)} releases to Jira; '
|
|
80
|
+
f'{len(result.already_present)} already present; '
|
|
81
|
+
f'{len(result.failed)} failed.', file=sys.stderr)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _write_file(config: BacklogOpsConfig, path: str, data: BacklogReleases,
|
|
86
|
+
force: bool) -> None:
|
|
87
|
+
"""Write one backlog-and-releases result to a file."""
|
|
88
|
+
out_config = resolve_output_config(None, data_file=path,
|
|
89
|
+
presets=config.output_configs,
|
|
90
|
+
auto_ch_hook=CliPresetMigrateWarnHook())
|
|
91
|
+
write_backlog_releases(data, path, out_config, FormatRules(),
|
|
92
|
+
levels=config.get_levels(),
|
|
93
|
+
file_exists_callback=overwrite_callback(force))
|
|
94
|
+
print(f'Wrote {path}')
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _write_result_files(parsed: argparse.Namespace, config: BacklogOpsConfig,
|
|
98
|
+
data: BacklogReleases,
|
|
99
|
+
result: AddedReleasesToJira) -> None:
|
|
100
|
+
"""Write the added and already-present releases to any named files."""
|
|
101
|
+
backlog = list(data.backlog)
|
|
102
|
+
if parsed.added_file is not None:
|
|
103
|
+
added = BacklogReleases(backlog=backlog, releases=result.stored)
|
|
104
|
+
_write_file(config, parsed.added_file, added, parsed.force)
|
|
105
|
+
if parsed.existing_file is not None:
|
|
106
|
+
present = BacklogReleases(backlog=backlog,
|
|
107
|
+
releases=result.already_present)
|
|
108
|
+
_write_file(config, parsed.existing_file, present, parsed.force)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _run(parsed: argparse.Namespace) -> int:
|
|
112
|
+
"""Read the input, add the releases, write files and print the lists."""
|
|
113
|
+
try:
|
|
114
|
+
config = required_config(parsed)
|
|
115
|
+
data = read_input(parsed, config)
|
|
116
|
+
result = _add(parsed, config, data)
|
|
117
|
+
_write_result_files(parsed, config, data, result)
|
|
118
|
+
except ReleaseExistsError:
|
|
119
|
+
print('Nothing added to Jira.', file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
except (ValueError, TypeError, KeyError, OSError) as error:
|
|
122
|
+
print(f'Could not add releases to Jira: {error}', file=sys.stderr)
|
|
123
|
+
return 1
|
|
124
|
+
if not parsed.quiet:
|
|
125
|
+
print(format_release_result(result))
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
130
|
+
"""Add releases to Jira and report the added and present releases.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
``0`` on success, ``1`` when the releases cannot be added or a name
|
|
137
|
+
already exists in Jira without ``--skip-existing``.
|
|
138
|
+
"""
|
|
139
|
+
parsed = parsed_args(build_parser(), args)
|
|
140
|
+
return _run(parsed)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == '__main__': # pragma: no cover
|
|
144
|
+
sys.exit(main())
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Add a backlog to Jira from an input file, creating one issue per item.
|
|
3
|
+
|
|
4
|
+
The command reads a backlog (or a backlog and its releases) from the input
|
|
5
|
+
file, then adds the backlog items to Jira using a named preset of the
|
|
6
|
+
backlog-ops configuration. By default it stops with an error when an
|
|
7
|
+
item's key already exists in Jira; ``--skip-existing`` skips those items
|
|
8
|
+
instead. With ``--rank`` the items are also ranked in Jira to match the
|
|
9
|
+
supplied backlog order, at the chosen anchor.
|
|
10
|
+
|
|
11
|
+
The added items (carrying their new Jira keys) and the items already in
|
|
12
|
+
Jira are printed to stdout as two labelled lists, unless ``-q``/``--quiet``
|
|
13
|
+
is given. Each list is also written to a file when ``--added-file`` or
|
|
14
|
+
``--existing-file`` names one; without a file name the list is not written.
|
|
15
|
+
An encrypted Jira token is unlocked by a pass phrase asked on the terminal
|
|
16
|
+
only when it is needed.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
20
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
21
|
+
# MIT License
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import sys
|
|
25
|
+
from getpass import getpass
|
|
26
|
+
from typing import Optional
|
|
27
|
+
from backlogops import (
|
|
28
|
+
AddedToJira, BacklogOpsConfig, BacklogReleases, ExistsInJiraError,
|
|
29
|
+
FormatRules, JiraConnections, OnExistingKey, add_backlog_to_jira,
|
|
30
|
+
format_add_result, resolve_output_config, write_backlog_releases)
|
|
31
|
+
from backlogops_cli._command_io import (
|
|
32
|
+
add_config_arg, add_input_args, add_rank_arg, overwrite_callback,
|
|
33
|
+
parsed_args, rank_anchor, read_input, required_config)
|
|
34
|
+
from backlogops_cli._migrate_warn import CliPresetMigrateWarnHook
|
|
35
|
+
|
|
36
|
+
DESCRIPTION = 'Add a backlog to Jira, creating a new issue per item'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
40
|
+
"""Build the command line parser for the add-to-Jira command."""
|
|
41
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
42
|
+
add_input_args(parser)
|
|
43
|
+
add_config_arg(parser)
|
|
44
|
+
parser.add_argument('-p', '--preset', dest='preset', required=True,
|
|
45
|
+
help='Name of the Jira preset in the configuration.')
|
|
46
|
+
parser.add_argument('--skip-existing', dest='skip_existing',
|
|
47
|
+
action='store_true',
|
|
48
|
+
help='Skip items whose key already exists in Jira '
|
|
49
|
+
'(the default stops with an error instead).')
|
|
50
|
+
add_rank_arg(parser)
|
|
51
|
+
parser.add_argument('--added-file', dest='added_file', metavar='FILE',
|
|
52
|
+
help='Write the added items, with their new Jira '
|
|
53
|
+
'keys, to this file. Omit to not write it.')
|
|
54
|
+
parser.add_argument('--existing-file', dest='existing_file',
|
|
55
|
+
metavar='FILE',
|
|
56
|
+
help='Write the items already in Jira to this file. '
|
|
57
|
+
'Omit to not write it.')
|
|
58
|
+
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
|
|
59
|
+
help='Do not print the added and already-present '
|
|
60
|
+
'lists to stdout.')
|
|
61
|
+
parser.add_argument('-f', '--force', dest='force', action='store_true',
|
|
62
|
+
help='Overwrite existing output files without '
|
|
63
|
+
'asking.')
|
|
64
|
+
return parser
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _passphrase() -> str:
|
|
68
|
+
"""Ask for the Jira token pass phrase on the terminal."""
|
|
69
|
+
return getpass('Jira API token pass phrase: ')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _add(parsed: argparse.Namespace, config: BacklogOpsConfig,
|
|
73
|
+
data: BacklogReleases) -> AddedToJira:
|
|
74
|
+
"""Add the input backlog to Jira using the named write preset."""
|
|
75
|
+
print(f"Adding backlog to Jira using preset '{parsed.preset}'...",
|
|
76
|
+
file=sys.stderr)
|
|
77
|
+
connections = JiraConnections(config.get_jira_config(), _passphrase)
|
|
78
|
+
mode = (OnExistingKey.SKIP if parsed.skip_existing
|
|
79
|
+
else OnExistingKey.RAISE)
|
|
80
|
+
result = add_backlog_to_jira(connections, parsed.preset, data.backlog,
|
|
81
|
+
on_existing_key=mode,
|
|
82
|
+
rank_anchor=rank_anchor(parsed.rank),
|
|
83
|
+
levels=config.get_levels(),
|
|
84
|
+
status_map=config.get_status_input_map())
|
|
85
|
+
print(f'Added {len(result.stored)} items to Jira; '
|
|
86
|
+
f'{len(result.already_present)} already present; '
|
|
87
|
+
f'{len(result.failed)} failed; '
|
|
88
|
+
f'{len(result.failed_links)} links not written.', file=sys.stderr)
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _write_backlog_file(config: BacklogOpsConfig, path: str,
|
|
93
|
+
data: BacklogReleases, force: bool) -> None:
|
|
94
|
+
"""Write one returned backlog and the input releases to a file."""
|
|
95
|
+
out_config = resolve_output_config(None, data_file=path,
|
|
96
|
+
presets=config.output_configs,
|
|
97
|
+
auto_ch_hook=CliPresetMigrateWarnHook())
|
|
98
|
+
write_backlog_releases(data, path, out_config, FormatRules(),
|
|
99
|
+
levels=config.get_levels(),
|
|
100
|
+
file_exists_callback=overwrite_callback(force))
|
|
101
|
+
print(f'Wrote {path}')
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _write_result_files(parsed: argparse.Namespace, config: BacklogOpsConfig,
|
|
105
|
+
data: BacklogReleases, result: AddedToJira) -> None:
|
|
106
|
+
"""Write the added and already-present backlogs to any named files."""
|
|
107
|
+
releases = list(data.releases)
|
|
108
|
+
if parsed.added_file is not None:
|
|
109
|
+
added = BacklogReleases(backlog=result.stored, releases=releases)
|
|
110
|
+
_write_backlog_file(config, parsed.added_file, added, parsed.force)
|
|
111
|
+
if parsed.existing_file is not None:
|
|
112
|
+
present = BacklogReleases(backlog=result.already_present,
|
|
113
|
+
releases=releases)
|
|
114
|
+
_write_backlog_file(config, parsed.existing_file, present,
|
|
115
|
+
parsed.force)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _run(parsed: argparse.Namespace) -> int:
|
|
119
|
+
"""Read the input, add it to Jira, write files and print the lists."""
|
|
120
|
+
try:
|
|
121
|
+
config = required_config(parsed)
|
|
122
|
+
data = read_input(parsed, config)
|
|
123
|
+
result = _add(parsed, config, data)
|
|
124
|
+
_write_result_files(parsed, config, data, result)
|
|
125
|
+
except ExistsInJiraError:
|
|
126
|
+
print('Nothing added to Jira.', file=sys.stderr)
|
|
127
|
+
return 1
|
|
128
|
+
except (ValueError, TypeError, KeyError, OSError) as error:
|
|
129
|
+
print(f'Could not add to Jira: {error}', file=sys.stderr)
|
|
130
|
+
return 1
|
|
131
|
+
if not parsed.quiet:
|
|
132
|
+
print(format_add_result(result))
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
137
|
+
"""Add a backlog to Jira and report the added and present items.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
``0`` on success, ``1`` when the backlog cannot be added or a key
|
|
144
|
+
already exists in Jira without ``--skip-existing``.
|
|
145
|
+
"""
|
|
146
|
+
parsed = parsed_args(build_parser(), args)
|
|
147
|
+
return _run(parsed)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == '__main__': # pragma: no cover
|
|
151
|
+
sys.exit(main())
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Print Jira field information for a preset, to diagnose write mappings.
|
|
3
|
+
|
|
4
|
+
The command prints the custom field id to display name map that the reader
|
|
5
|
+
fetches from Jira, so a column-map name such as 'Story point estimate' can
|
|
6
|
+
be matched to its field id. With ``--issue`` it also prints the fields the
|
|
7
|
+
given issue's edit screen offers, which explains why a mapped field cannot
|
|
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.
|
|
10
|
+
|
|
11
|
+
An encrypted Jira token is unlocked by a pass phrase asked on the terminal
|
|
12
|
+
only when it is needed.
|
|
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 getpass import getpass
|
|
22
|
+
from typing import Optional
|
|
23
|
+
from jira import JIRAError
|
|
24
|
+
from backlogops import (
|
|
25
|
+
JiraConnections, jira_custom_fields, jira_editable_fields)
|
|
26
|
+
from backlogops_cli._command_io import (
|
|
27
|
+
add_config_arg, parsed_args, required_config)
|
|
28
|
+
|
|
29
|
+
DESCRIPTION = "Print Jira custom fields and an issue's editable fields"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
33
|
+
"""Build the command line parser for the field diagnostic command."""
|
|
34
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
35
|
+
add_config_arg(parser)
|
|
36
|
+
parser.add_argument('-p', '--preset', dest='preset', required=True,
|
|
37
|
+
help='Name of the Jira preset in the configuration.')
|
|
38
|
+
parser.add_argument('--issue', dest='issue', metavar='KEY',
|
|
39
|
+
help="Also print the fields this issue's edit "
|
|
40
|
+
'screen offers (for example SCRUM-15).')
|
|
41
|
+
return parser
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _passphrase() -> str:
|
|
45
|
+
"""Ask for the Jira token pass phrase on the terminal."""
|
|
46
|
+
return getpass('Jira API token pass phrase: ')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _print_pairs(heading: str, pairs: list[tuple[str, str]]) -> None:
|
|
50
|
+
"""Print a heading and each field id and display name pair."""
|
|
51
|
+
print(heading)
|
|
52
|
+
if not pairs:
|
|
53
|
+
print(' (none)')
|
|
54
|
+
for field_id, name in pairs:
|
|
55
|
+
print(f' {field_id} {name}')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _run(parsed: argparse.Namespace) -> int:
|
|
59
|
+
"""Print the custom field map and, optionally, editable fields."""
|
|
60
|
+
try:
|
|
61
|
+
config = required_config(parsed)
|
|
62
|
+
connections = JiraConnections(config.get_jira_config(), _passphrase)
|
|
63
|
+
customs = jira_custom_fields(connections, parsed.preset)
|
|
64
|
+
editable = (jira_editable_fields(connections, parsed.preset,
|
|
65
|
+
parsed.issue)
|
|
66
|
+
if parsed.issue is not None else None)
|
|
67
|
+
except (ValueError, TypeError, KeyError, OSError, JIRAError) as error:
|
|
68
|
+
print(f'Could not read Jira fields: {error}', file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
_print_pairs(f"Custom fields for preset '{parsed.preset}':", customs)
|
|
71
|
+
if editable is not None:
|
|
72
|
+
print()
|
|
73
|
+
_print_pairs(f'Fields settable on the edit screen of '
|
|
74
|
+
f'{parsed.issue}:', editable)
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
79
|
+
"""Print Jira field information for a preset.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
``0`` on success, ``1`` when the fields cannot be read.
|
|
86
|
+
"""
|
|
87
|
+
return _run(parsed_args(build_parser(), args))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == '__main__': # pragma: no cover
|
|
91
|
+
sys.exit(main())
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Order releases in Jira, changing the order of a project's versions.
|
|
3
|
+
|
|
4
|
+
The command reorders the Jira versions of a named preset of the backlog-ops
|
|
5
|
+
configuration. Exactly one order source must be given, and the three are
|
|
6
|
+
mutually exclusive: ``--by-date`` orders the versions by their own release
|
|
7
|
+
date, earliest first, with undated versions at the end; ``--name-list`` names
|
|
8
|
+
a file whose lines give the wanted order, one release name per line; and
|
|
9
|
+
``-i``/``--input`` names a backlog-and-releases input file whose release order
|
|
10
|
+
is used. argparse enforces that exactly one of the three is given.
|
|
11
|
+
|
|
12
|
+
With ``--by-date`` every version is ordered. With a name source, the named
|
|
13
|
+
versions are moved to the front in the listed order and every other version
|
|
14
|
+
keeps its existing relative order and trails them; a name that is not a
|
|
15
|
+
version is reported. The ordered names and the names not in Jira are printed
|
|
16
|
+
to stdout unless ``-q``/``--quiet`` is given. An encrypted Jira token is
|
|
17
|
+
unlocked by a pass phrase asked on the terminal only when it is needed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
21
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
22
|
+
# MIT License
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import sys
|
|
26
|
+
from getpass import getpass
|
|
27
|
+
from typing import Optional
|
|
28
|
+
from backlogops import (
|
|
29
|
+
BacklogOpsConfig, JiraConnections, OrderedReleasesInJira,
|
|
30
|
+
format_order_result, order_jira_rel_by_date, order_releases_in_jira,
|
|
31
|
+
read_name_list)
|
|
32
|
+
from backlogops_cli._command_io import (
|
|
33
|
+
add_config_arg, parsed_args, read_input, required_config)
|
|
34
|
+
|
|
35
|
+
DESCRIPTION = 'Order releases in Jira by date, a name list or the input order'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
39
|
+
"""Build the command line parser for the order-releases command.
|
|
40
|
+
|
|
41
|
+
The three order sources form a required, mutually exclusive group, so
|
|
42
|
+
argparse checks that exactly one of them is given. Giving the input file
|
|
43
|
+
with ``-i``/``--input`` is itself the third source; ``-I`` only names its
|
|
44
|
+
format.
|
|
45
|
+
"""
|
|
46
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
47
|
+
add_config_arg(parser)
|
|
48
|
+
parser.add_argument('-p', '--preset', dest='preset', required=True,
|
|
49
|
+
help='Name of the Jira preset in the configuration.')
|
|
50
|
+
source = parser.add_mutually_exclusive_group(required=True)
|
|
51
|
+
source.add_argument('--by-date', dest='by_date', action='store_true',
|
|
52
|
+
help='Order the versions by their release date, '
|
|
53
|
+
'earliest first, undated versions last.')
|
|
54
|
+
source.add_argument('--name-list', dest='name_list', metavar='FILE',
|
|
55
|
+
help='File whose lines give the wanted order, one '
|
|
56
|
+
'release name per line (a name may contain spaces); '
|
|
57
|
+
'a table file uses one name per row.')
|
|
58
|
+
source.add_argument('-i', '--input', dest='input', metavar='FILE',
|
|
59
|
+
help='Backlog-and-releases input file; ordering '
|
|
60
|
+
'follows the release order in this file.')
|
|
61
|
+
parser.add_argument('-I', '--input-config', dest='input_config',
|
|
62
|
+
help='Format of the -i/--input file: a config file '
|
|
63
|
+
'or a preset name.')
|
|
64
|
+
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
|
|
65
|
+
help='Do not print the result lists to stdout.')
|
|
66
|
+
return parser
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _passphrase() -> str:
|
|
70
|
+
"""Ask for the Jira token pass phrase on the terminal."""
|
|
71
|
+
return getpass('Jira API token pass phrase: ')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _names(parsed: argparse.Namespace, config: BacklogOpsConfig) -> list[str]:
|
|
75
|
+
"""Return the wanted order of names from the chosen name source.
|
|
76
|
+
|
|
77
|
+
Used only when ``--by-date`` is not given, so the source is either the
|
|
78
|
+
``--name-list`` file or, otherwise, the ``-i``/``--input`` file.
|
|
79
|
+
"""
|
|
80
|
+
if parsed.name_list is not None:
|
|
81
|
+
return read_name_list(parsed.name_list)
|
|
82
|
+
return [release.name for release in read_input(parsed, config).releases]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _order(parsed: argparse.Namespace,
|
|
86
|
+
config: BacklogOpsConfig) -> OrderedReleasesInJira:
|
|
87
|
+
"""Order the releases in Jira using the chosen order source."""
|
|
88
|
+
print(f"Ordering releases in Jira using preset '{parsed.preset}'...",
|
|
89
|
+
file=sys.stderr)
|
|
90
|
+
connections = JiraConnections(config.get_jira_config(), _passphrase)
|
|
91
|
+
if parsed.by_date:
|
|
92
|
+
result = order_jira_rel_by_date(connections, parsed.preset)
|
|
93
|
+
else:
|
|
94
|
+
result = order_releases_in_jira(connections, parsed.preset,
|
|
95
|
+
_names(parsed, config))
|
|
96
|
+
print(f'Ordered {len(result.ordered)} releases in Jira; '
|
|
97
|
+
f'{len(result.not_in_jira)} not in Jira.', file=sys.stderr)
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run(parsed: argparse.Namespace) -> int:
|
|
102
|
+
"""Resolve the order source, order the releases and print the result."""
|
|
103
|
+
try:
|
|
104
|
+
config = required_config(parsed)
|
|
105
|
+
result = _order(parsed, config)
|
|
106
|
+
except (ValueError, TypeError, KeyError, OSError) as error:
|
|
107
|
+
print(f'Could not order releases in Jira: {error}', file=sys.stderr)
|
|
108
|
+
return 1
|
|
109
|
+
if not parsed.quiet:
|
|
110
|
+
print(format_order_result(result))
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(args: Optional[list[str]] = None) -> int:
|
|
115
|
+
"""Order releases in Jira and report the ordered and skipped names.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
args: Optional replacement for ``sys.argv[1:]``, mainly for tests.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
``0`` on success, ``1`` when the order cannot be resolved or applied.
|
|
122
|
+
"""
|
|
123
|
+
parsed = parsed_args(build_parser(), args)
|
|
124
|
+
return _run(parsed)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == '__main__': # pragma: no cover
|
|
128
|
+
sys.exit(main())
|