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.
@@ -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, Levels,
24
- ReleaseChanges, ReleaseDateChanges, allow_overwrite,
25
- format_content_changes, format_date_changes, get_backlog_ops_config,
26
- read_backlog_releases, resolve_input_config, resolve_output_config,
27
- write_backlog_releases, write_content_changes, write_date_changes)
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]) -> int:
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, falling
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 = optional_config(parsed)
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())