backlogops 0.4__tar.gz → 0.6__tar.gz

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.
Files changed (66) hide show
  1. {backlogops-0.4 → backlogops-0.6}/PKG-INFO +9 -12
  2. {backlogops-0.4 → backlogops-0.6}/README_pypi.md +5 -8
  3. {backlogops-0.4 → backlogops-0.6}/backlogops.egg-info/PKG-INFO +9 -12
  4. {backlogops-0.4 → backlogops-0.6}/backlogops.egg-info/requires.txt +3 -2
  5. {backlogops-0.4 → backlogops-0.6}/pyproject.toml +0 -1
  6. {backlogops-0.4 → backlogops-0.6}/setup.py +4 -3
  7. {backlogops-0.4 → backlogops-0.6}/src/backlogops/backlog_ops_wizard.py +144 -22
  8. {backlogops-0.4 → backlogops-0.6}/src/backlogops/blo_version_reporter.py +2 -2
  9. {backlogops-0.4 → backlogops-0.6}/src/backlogops/io_preset_wizard.py +1 -1
  10. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_wizard.py +39 -3
  11. {backlogops-0.4 → backlogops-0.6}/src/backlogops/wizard_forms.py +107 -117
  12. {backlogops-0.4 → backlogops-0.6}/src/backlogops/wizard_helpers.py +10 -178
  13. {backlogops-0.4 → backlogops-0.6}/src/backlogops/wizard_navigator.py +13 -30
  14. {backlogops-0.4 → backlogops-0.6}/LICENSE.txt +0 -0
  15. {backlogops-0.4 → backlogops-0.6}/backlogops.egg-info/SOURCES.txt +0 -0
  16. {backlogops-0.4 → backlogops-0.6}/backlogops.egg-info/dependency_links.txt +0 -0
  17. {backlogops-0.4 → backlogops-0.6}/backlogops.egg-info/top_level.txt +0 -0
  18. {backlogops-0.4 → backlogops-0.6}/setup.cfg +0 -0
  19. {backlogops-0.4 → backlogops-0.6}/src/backlogops/__init__.py +0 -0
  20. {backlogops-0.4 → backlogops-0.6}/src/backlogops/apply_format_rules.py +0 -0
  21. {backlogops-0.4 → backlogops-0.6}/src/backlogops/available_teams.py +0 -0
  22. {backlogops-0.4 → backlogops-0.6}/src/backlogops/available_teams_config.py +0 -0
  23. {backlogops-0.4 → backlogops-0.6}/src/backlogops/backlog.py +0 -0
  24. {backlogops-0.4 → backlogops-0.6}/src/backlogops/backlog_helpers.py +0 -0
  25. {backlogops-0.4 → backlogops-0.6}/src/backlogops/backlog_in_release_order.py +0 -0
  26. {backlogops-0.4 → backlogops-0.6}/src/backlogops/backlog_ops_config.py +0 -0
  27. {backlogops-0.4 → backlogops-0.6}/src/backlogops/backlog_releases.py +0 -0
  28. {backlogops-0.4 → backlogops-0.6}/src/backlogops/backlog_releases_io.py +0 -0
  29. {backlogops-0.4 → backlogops-0.6}/src/backlogops/config_file_io.py +0 -0
  30. {backlogops-0.4 → backlogops-0.6}/src/backlogops/date_ranges.py +0 -0
  31. {backlogops-0.4 → backlogops-0.6}/src/backlogops/demo_backlog.py +0 -0
  32. {backlogops-0.4 → backlogops-0.6}/src/backlogops/estimate_ready_date.py +0 -0
  33. {backlogops-0.4 → backlogops-0.6}/src/backlogops/format_rules.py +0 -0
  34. {backlogops-0.4 → backlogops-0.6}/src/backlogops/io_config.py +0 -0
  35. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_connect.py +0 -0
  36. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_io_config.py +0 -0
  37. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_order_releases.py +0 -0
  38. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_rank_backlog.py +0 -0
  39. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_rank_by_keys.py +0 -0
  40. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_rank_move_keys.py +0 -0
  41. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_read.py +0 -0
  42. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_rename_releases.py +0 -0
  43. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_search.py +0 -0
  44. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_token.py +0 -0
  45. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_update_backlog.py +0 -0
  46. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_update_releases.py +0 -0
  47. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_write.py +0 -0
  48. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_write_fields.py +0 -0
  49. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_write_format.py +0 -0
  50. {backlogops-0.4 → backlogops-0.6}/src/backlogops/jira_write_releases.py +0 -0
  51. {backlogops-0.4 → backlogops-0.6}/src/backlogops/key_list_io.py +0 -0
  52. {backlogops-0.4 → backlogops-0.6}/src/backlogops/levels.py +0 -0
  53. {backlogops-0.4 → backlogops-0.6}/src/backlogops/move_keys_first.py +0 -0
  54. {backlogops-0.4 → backlogops-0.6}/src/backlogops/name_list_io.py +0 -0
  55. {backlogops-0.4 → backlogops-0.6}/src/backlogops/no_text_io.py +0 -0
  56. {backlogops-0.4 → backlogops-0.6}/src/backlogops/order_by_dependencies.py +0 -0
  57. {backlogops-0.4 → backlogops-0.6}/src/backlogops/person.py +0 -0
  58. {backlogops-0.4 → backlogops-0.6}/src/backlogops/py.typed +0 -0
  59. {backlogops-0.4 → backlogops-0.6}/src/backlogops/release_backlog_updates.py +0 -0
  60. {backlogops-0.4 → backlogops-0.6}/src/backlogops/release_change_io.py +0 -0
  61. {backlogops-0.4 → backlogops-0.6}/src/backlogops/releases.py +0 -0
  62. {backlogops-0.4 → backlogops-0.6}/src/backlogops/rename_list_io.py +0 -0
  63. {backlogops-0.4 → backlogops-0.6}/src/backlogops/table_create.py +0 -0
  64. {backlogops-0.4 → backlogops-0.6}/src/backlogops/table_rows.py +0 -0
  65. {backlogops-0.4 → backlogops-0.6}/src/backlogops/team.py +0 -0
  66. {backlogops-0.4 → backlogops-0.6}/src/backlogops/work_hours.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops
3
- Version: 0.4
3
+ Version: 0.6
4
4
  Summary: Library with backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Development Status :: 4 - Beta
18
17
  Classifier: Intended Audience :: Developers
19
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
19
  Classifier: Topic :: Office/Business :: Scheduling
@@ -23,10 +22,11 @@ Requires-Python: >=3.12
23
22
  Description-Content-Type: text/markdown
24
23
  License-File: LICENSE.txt
25
24
  Requires-Dist: config-as-json>=1.4
26
- Requires-Dist: cryptography>=49.0.0
25
+ Requires-Dist: cryptography>=50.0.0
27
26
  Requires-Dist: jira[cli,opt]>=3.10.5
28
27
  Requires-Dist: tableio>=1.1
29
- Requires-Dist: tableio-cfg-json>=0.9
28
+ Requires-Dist: tableio-cfg-json>=1.1
29
+ Requires-Dist: wizard-ui-bridge[textual]>=1.1
30
30
  Requires-Dist: versionreporter>=0.4
31
31
  Dynamic: author
32
32
  Dynamic: license-file
@@ -51,15 +51,11 @@ There are 3 related packages for backlog operations:
51
51
 
52
52
  ## Project status
53
53
 
54
- backlogops is in beta. The algorithms intended for its initial scope are in
55
- place, so the main work from here is on the user experience rather than on new
56
- core functionality. The three packages share a version number and are released
57
- together.
54
+ The three packages share a version number and are released together.
58
55
 
59
56
  The library is not meant to ever be "finished": using it for real planning keeps
60
57
  surfacing new ideas. Some of those will be added to backlogops itself, and
61
- others will fit better in separate packages built on top of backlogops. Early
62
- adopters are very welcome, and their feedback shapes what comes next.
58
+ others will fit better in separate packages built on top of backlogops.
63
59
 
64
60
  ## Available functionality
65
61
 
@@ -316,9 +312,10 @@ For the full set of public names see the API documentation linked above.
316
312
 
317
313
  ## Test summary
318
314
 
319
- - Test result: 2638 passed, 7 deselected in 51s
315
+ - Test result: 2450 passed, 9 deselected in 39s
320
316
  - No flake8 warnings.
321
317
  - No mypy errors found.
318
+ - No pylint warnings.
322
319
  - No python layout warnings.
323
- - Built version(s): 0.4
320
+ - Built version(s): 0.6
324
321
  - Build and test using Python 3.14.6
@@ -16,15 +16,11 @@ There are 3 related packages for backlog operations:
16
16
 
17
17
  ## Project status
18
18
 
19
- backlogops is in beta. The algorithms intended for its initial scope are in
20
- place, so the main work from here is on the user experience rather than on new
21
- core functionality. The three packages share a version number and are released
22
- together.
19
+ The three packages share a version number and are released together.
23
20
 
24
21
  The library is not meant to ever be "finished": using it for real planning keeps
25
22
  surfacing new ideas. Some of those will be added to backlogops itself, and
26
- others will fit better in separate packages built on top of backlogops. Early
27
- adopters are very welcome, and their feedback shapes what comes next.
23
+ others will fit better in separate packages built on top of backlogops.
28
24
 
29
25
  ## Available functionality
30
26
 
@@ -281,9 +277,10 @@ For the full set of public names see the API documentation linked above.
281
277
 
282
278
  ## Test summary
283
279
 
284
- - Test result: 2638 passed, 7 deselected in 51s
280
+ - Test result: 2450 passed, 9 deselected in 39s
285
281
  - No flake8 warnings.
286
282
  - No mypy errors found.
283
+ - No pylint warnings.
287
284
  - No python layout warnings.
288
- - Built version(s): 0.4
285
+ - Built version(s): 0.6
289
286
  - Build and test using Python 3.14.6
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops
3
- Version: 0.4
3
+ Version: 0.6
4
4
  Summary: Library with backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Development Status :: 4 - Beta
18
17
  Classifier: Intended Audience :: Developers
19
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
19
  Classifier: Topic :: Office/Business :: Scheduling
@@ -23,10 +22,11 @@ Requires-Python: >=3.12
23
22
  Description-Content-Type: text/markdown
24
23
  License-File: LICENSE.txt
25
24
  Requires-Dist: config-as-json>=1.4
26
- Requires-Dist: cryptography>=49.0.0
25
+ Requires-Dist: cryptography>=50.0.0
27
26
  Requires-Dist: jira[cli,opt]>=3.10.5
28
27
  Requires-Dist: tableio>=1.1
29
- Requires-Dist: tableio-cfg-json>=0.9
28
+ Requires-Dist: tableio-cfg-json>=1.1
29
+ Requires-Dist: wizard-ui-bridge[textual]>=1.1
30
30
  Requires-Dist: versionreporter>=0.4
31
31
  Dynamic: author
32
32
  Dynamic: license-file
@@ -51,15 +51,11 @@ There are 3 related packages for backlog operations:
51
51
 
52
52
  ## Project status
53
53
 
54
- backlogops is in beta. The algorithms intended for its initial scope are in
55
- place, so the main work from here is on the user experience rather than on new
56
- core functionality. The three packages share a version number and are released
57
- together.
54
+ The three packages share a version number and are released together.
58
55
 
59
56
  The library is not meant to ever be "finished": using it for real planning keeps
60
57
  surfacing new ideas. Some of those will be added to backlogops itself, and
61
- others will fit better in separate packages built on top of backlogops. Early
62
- adopters are very welcome, and their feedback shapes what comes next.
58
+ others will fit better in separate packages built on top of backlogops.
63
59
 
64
60
  ## Available functionality
65
61
 
@@ -316,9 +312,10 @@ For the full set of public names see the API documentation linked above.
316
312
 
317
313
  ## Test summary
318
314
 
319
- - Test result: 2638 passed, 7 deselected in 51s
315
+ - Test result: 2450 passed, 9 deselected in 39s
320
316
  - No flake8 warnings.
321
317
  - No mypy errors found.
318
+ - No pylint warnings.
322
319
  - No python layout warnings.
323
- - Built version(s): 0.4
320
+ - Built version(s): 0.6
324
321
  - Build and test using Python 3.14.6
@@ -1,6 +1,7 @@
1
1
  config-as-json>=1.4
2
- cryptography>=49.0.0
2
+ cryptography>=50.0.0
3
3
  jira[cli,opt]>=3.10.5
4
4
  tableio>=1.1
5
- tableio-cfg-json>=0.9
5
+ tableio-cfg-json>=1.1
6
+ wizard-ui-bridge[textual]>=1.1
6
7
  versionreporter>=0.4
@@ -21,7 +21,6 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.13",
22
22
  "Programming Language :: Python :: 3.14",
23
23
  "Operating System :: OS Independent",
24
- "Development Status :: 4 - Beta",
25
24
  "Intended Audience :: Developers",
26
25
  "Topic :: Software Development :: Libraries :: Python Modules",
27
26
  "Topic :: Office/Business :: Scheduling",
@@ -5,7 +5,7 @@ from setuptools import setup
5
5
 
6
6
  setup(
7
7
  name='backlogops',
8
- version='0.4',
8
+ version='0.6',
9
9
  description='Library with backlog operations.',
10
10
  author='Tom Björkholm',
11
11
  author_email='klausuler_linnet0q@icloud.com',
@@ -15,10 +15,11 @@ setup(
15
15
  package_data={'backlogops': ['src/py.typed']},
16
16
  install_requires=[
17
17
  'config-as-json >= 1.4',
18
- 'cryptography >= 49.0.0',
18
+ 'cryptography >= 50.0.0',
19
19
  'jira[cli,opt] >= 3.10.5',
20
20
  'tableio >= 1.1',
21
- 'tableio-cfg-json >= 0.9',
21
+ 'tableio-cfg-json >= 1.1',
22
+ 'wizard-ui-bridge[textual] >= 1.1',
22
23
  'versionreporter >= 0.4'
23
24
  ]
24
25
  )
@@ -6,9 +6,9 @@ The public helpers :func:`available_teams_wizard` and
6
6
  persons and their personal work-hour exceptions, the teams with their
7
7
  members, and, for the full configuration, the named TableIO presets, the
8
8
  backlog item levels, the status-name map, the GUI display and the Jira
9
- integration. They drive any ``WizardUiBridge``
10
- of ``tableio_cfg_json``, so the same wizard logic runs on a console text
11
- interface, a Textual full-screen interface or a graphical user interface.
9
+ integration. They drive any ``WizardUiBridge`` of ``wizard_ui_bridge``,
10
+ so the same wizard logic runs on a console text interface, a Textual
11
+ full-screen interface or a graphical user interface.
12
12
 
13
13
  Each repeated part is asked by first requesting a count and then collecting
14
14
  exactly that many items, so there are no open-ended "add another?" prompts.
@@ -22,7 +22,7 @@ in :mod:`backlogops.io_preset_wizard`.
22
22
 
23
23
  from functools import partial
24
24
  from typing import Optional
25
- from tableio_cfg_json import WizardAbort, WizardUiBridge
25
+ from wizard_ui_bridge import WizardAbort, WizardUiBridge
26
26
  from backlogops.available_teams import AvailableTeams
27
27
  from backlogops.backlog_ops_config import BacklogOpsConfig, \
28
28
  DEF_STATUS_INPUT_MAP
@@ -31,7 +31,8 @@ from backlogops.levels import DEFAULT_LEVELS, Level
31
31
  from backlogops.person import Person
32
32
  from backlogops.table_rows import RELEASE_FIELDS
33
33
  from backlogops.team import FteException, Membership, Team
34
- from backlogops.work_hours import CompanyWorkHours, ExceptionWorkHours
34
+ from backlogops.work_hours import CompanyWorkHours, DEFAULT_WORK_WEEK, \
35
+ ExceptionWorkHours, ScheduleWorkHours, WeekDay
35
36
  from backlogops.wizard_helpers import _backlog_map_fields
36
37
  from backlogops.wizard_navigator import _Navigator, _ask_level_display
37
38
  from backlogops.wizard_forms import FormField, FormResult, choice_field, \
@@ -220,29 +221,145 @@ def _levels_or_none(levels: list[Level]) -> Optional[list[Level]]:
220
221
  return levels
221
222
 
222
223
 
224
+ _COMPANY_QUESTION = (
225
+ 'Configure the company work hours.\n'
226
+ 'Set the work hours for each week day, then, if there are any, the '
227
+ 'first holiday, closure or special-work period and how many more '
228
+ 'there are.')
229
+ """Instruction shown above the combined company work-hours form."""
230
+
231
+
232
+ _MAX_EXCEPTIONS = 60
233
+ """Upper bound on the number of extra company periods the wizard adds."""
234
+
235
+
223
236
  def _build_company(nav: _Navigator,
224
237
  default: Optional[AvailableTeams]) -> CompanyWorkHours:
225
238
  """Ask for the company weekly schedule and holiday periods.
226
239
 
227
- The weekly schedule is one table and the holiday, closure and
228
- special-work periods are a second table, so the whole company work
229
- schedule is two screens.
240
+ The weekly work hours and the first holiday, closure or special-work
241
+ period are asked together on one form; any further periods each get
242
+ their own form whose question lists the periods entered so far.
230
243
  """
231
244
  company = default.company_work_hours if default else None
232
- work_hours = nav.ask_schedule(seed=company.work_hours if company else None)
233
- exceptions = nav.ask_exceptions(
234
- 'Company holiday, closure and special-work periods.',
235
- seed=company.exceptions if company else None)
236
- return CompanyWorkHours(work_hours=work_hours, exceptions=exceptions)
245
+ values = nav.ask_form(_COMPANY_QUESTION, _company_fields(), _company_rule,
246
+ seed=_company_seed(company))
247
+ stored = company.exceptions if company else []
248
+ return CompanyWorkHours(work_hours=_schedule_from(values),
249
+ exceptions=_build_exceptions(nav, values, stored))
250
+
251
+
252
+ def _schedule_fields() -> list[FormField]:
253
+ """Return one work-hours field per week day, Monday first.
254
+
255
+ A day has between zero and twenty-four work hours, so the float field
256
+ enforces that inclusive range.
257
+ """
258
+ return [number_field(day.name.lower(), day.name.capitalize(),
259
+ default=DEFAULT_WORK_WEEK[day], minimum=0.0,
260
+ maximum=24.0)
261
+ for day in WeekDay]
262
+
263
+
264
+ def _company_fields() -> list[FormField]:
265
+ """Return the combined company schedule and first-period fields."""
266
+ periods_q = 'Any holiday, closure or special-work periods?'
267
+ more_q = 'Number of additional such periods'
268
+ return (_schedule_fields()
269
+ + [yes_no_field('has_periods', periods_q, False)]
270
+ + _exception_fields()
271
+ + [int_field('more', more_q, default=0, minimum=0,
272
+ maximum=_MAX_EXCEPTIONS)])
273
+
274
+
275
+ def _company_rule(values: FormResult) -> tuple[Optional[str], set[str]]:
276
+ """Disable the period fields with no periods, else order-check them."""
277
+ if not values.flag('has_periods'):
278
+ return None, {'start', 'end', 'hours', 'new_days', 'more'}
279
+ return _period_rule(values)
280
+
281
+
282
+ def _company_seed(company: Optional[CompanyWorkHours]) -> Optional[FormResult]:
283
+ """Return the combined company form values from stored work hours."""
284
+ if company is None:
285
+ return None
286
+ values: dict[str, object] = {
287
+ day.name.lower(): company.work_hours.get(day, DEFAULT_WORK_WEEK[day])
288
+ for day in WeekDay}
289
+ periods = company.exceptions
290
+ values['has_periods'] = bool(periods)
291
+ values['more'] = max(len(periods) - 1, 0)
292
+ if periods:
293
+ values.update(_period_values(periods[0]))
294
+ return FormResult(values)
295
+
296
+
297
+ def _schedule_from(values: FormResult) -> ScheduleWorkHours:
298
+ """Return the weekly schedule read from the combined company form."""
299
+ return {day: values.number(day.name.lower()) for day in WeekDay}
300
+
301
+
302
+ def _build_exceptions(nav: _Navigator, values: FormResult,
303
+ stored: list[ExceptionWorkHours]
304
+ ) -> list[ExceptionWorkHours]:
305
+ """Return the company periods, the first from the combined form."""
306
+ if not values.flag('has_periods'):
307
+ return []
308
+ first = _period_from(values)
309
+ rest = stored[1:] if len(stored) > 1 else []
310
+ return [first] + _more_exceptions(nav, values.whole('more'), first, rest)
311
+
312
+
313
+ def _more_exceptions(nav: _Navigator, count: int, first: ExceptionWorkHours,
314
+ seeds: list[ExceptionWorkHours]
315
+ ) -> list[ExceptionWorkHours]:
316
+ """Collect the additional company periods after the first one.
317
+
318
+ Each period is asked on its own form whose question lists the periods
319
+ entered so far. The running list is reset on the first item, so a
320
+ replayed sub-level after a back or cancel request does not double it.
321
+ """
322
+ shown = [first]
323
+
324
+ def build(item: Optional[tuple[int, Optional[ExceptionWorkHours]]]
325
+ ) -> ExceptionWorkHours:
326
+ """Ask one additional period, shown after the earlier ones."""
327
+ assert item is not None
328
+ index, seed = item
329
+ if index == 0:
330
+ del shown[1:]
331
+ values = nav.ask_form(_more_question(shown), _exception_fields(),
332
+ _period_rule, seed=_exc_seed(seed))
333
+ shown.append(_period_from(values))
334
+ return shown[-1]
335
+ indexed: list[tuple[int, Optional[ExceptionWorkHours]]] = [
336
+ (k, seeds[k] if k < len(seeds) else None) for k in range(count)]
337
+ return nav.counted(count, indexed, build)
338
+
339
+
340
+ def _more_question(shown: list[ExceptionWorkHours]) -> str:
341
+ """Return the next-period question listing the periods entered so far."""
342
+ lines = ['Configure the next holiday, closure or special-work period.',
343
+ 'Periods entered so far:']
344
+ return '\n'.join(lines + [_period_line(period) for period in shown])
345
+
346
+
347
+ def _period_line(period: ExceptionWorkHours) -> str:
348
+ """Return a one-line summary of one exception period."""
349
+ extra = ' (adds free-day work)' if period.new_work_days else ''
350
+ return (f' {period.start_date} to {period.end_date}: '
351
+ f'{period.hours_per_day:g} h/day{extra}')
352
+
353
+
354
+ def _period_values(period: ExceptionWorkHours) -> dict[str, object]:
355
+ """Return the form values for one work-hour exception period."""
356
+ return {'start': period.start_date, 'end': period.end_date,
357
+ 'hours': period.hours_per_day, 'new_days': period.new_work_days}
237
358
 
238
359
 
239
360
  def _exc_seed(exc: Optional[ExceptionWorkHours]) -> Optional[FormResult]:
240
361
  """Return the work-hour exception form values from an exception."""
241
- if exc is None:
242
- return None
243
- return FormResult({'start': exc.start_date, 'end': exc.end_date,
244
- 'hours': exc.hours_per_day,
245
- 'new_days': exc.new_work_days})
362
+ return None if exc is None else FormResult(_period_values(exc))
246
363
 
247
364
 
248
365
  def _period_rule(values: FormResult) -> tuple[Optional[str], set[str]]:
@@ -265,16 +382,21 @@ def _exception_fields() -> list[FormField]:
265
382
  'normally free?', False)]
266
383
 
267
384
 
385
+ def _period_from(values: FormResult) -> ExceptionWorkHours:
386
+ """Return one exception period from a form's period fields."""
387
+ return ExceptionWorkHours(start_date=values.day('start'),
388
+ end_date=values.day('end'),
389
+ hours_per_day=values.number('hours'),
390
+ new_work_days=values.flag('new_days'))
391
+
392
+
268
393
  def _ask_exception(nav: _Navigator, seed: Optional[ExceptionWorkHours] = None
269
394
  ) -> ExceptionWorkHours:
270
395
  """Ask for one work-hour exception period on a single form."""
271
396
  values = nav.ask_form('Configure the work-hour exception period.',
272
397
  _exception_fields(), _period_rule,
273
398
  seed=_exc_seed(seed))
274
- return ExceptionWorkHours(start_date=values.day('start'),
275
- end_date=values.day('end'),
276
- hours_per_day=values.number('hours'),
277
- new_work_days=values.flag('new_days'))
399
+ return _period_from(values)
278
400
 
279
401
 
280
402
  def _build_persons(nav: _Navigator,
@@ -16,8 +16,8 @@ class BloVersionReporter(VersionReporter):
16
16
  @override
17
17
  def package_names(self) -> list[str]:
18
18
  """Return the package names that this package reports."""
19
- ret = ['backlogops', 'tableio', 'tableio-cfg-json', 'config-as-json',
20
- 'mformat']
19
+ ret = ['backlogops', 'tableio', 'tableio-cfg-json', 'wizard-ui-bridge',
20
+ 'config-as-json', 'mformat']
21
21
  ret += super().package_names()
22
22
  return ret
23
23
 
@@ -19,7 +19,7 @@ wizard, where each preset additionally has a name.
19
19
  from functools import partial
20
20
  from typing import Optional
21
21
  from tableio import FileAccess
22
- from tableio_cfg_json import WizardAbort, WizardUiBridge
22
+ from wizard_ui_bridge import WizardAbort, WizardUiBridge
23
23
  from backlogops.io_config import InputFormatConfig, OutputFormatConfig, \
24
24
  _FormatConfig, make_input_config, make_output_config
25
25
  from backlogops.table_rows import RELEASE_FIELDS
@@ -25,7 +25,7 @@ token is visible while typed; the pass phrases are masked.
25
25
  from dataclasses import dataclass
26
26
  from functools import partial
27
27
  from typing import Callable, Optional, TypeVar
28
- from tableio_cfg_json import WizardPathKind
28
+ from wizard_ui_bridge import PrefillValueType, WizardPathKind
29
29
  from backlogops.jira_io_config import DEF_BACKLOG_COLUMN_MAP, \
30
30
  DEF_RELEASE_COLUMN_MAP, JiraColumnMap, JiraConnectConfig, JiraIOConfig, \
31
31
  JiraIssueTypeMap, JiraPreset, JiraType, TokenStorage, _FILE_MODES, \
@@ -364,8 +364,9 @@ _ISSUE_MAP_Q = ('Use a level-to-issue-type map when writing (otherwise the '
364
364
 
365
365
  _FILTER_EG = default_jira_filter('<project key>')
366
366
  """Illustrative blank-filter default shown in the filter help text."""
367
- _FILTER_HELP = ('Leave blank to select the default project in rank '
368
- f'order, i.e. {_FILTER_EG}.')
367
+ _FILTER_HELP = ('Leave blank to select the whole project in rank order; '
368
+ f'the default {_FILTER_EG} is offered once the project key '
369
+ 'is set.')
369
370
  """Help shown for the blank-defaulting issue filter field."""
370
371
 
371
372
 
@@ -417,6 +418,40 @@ def _preset_rule(has_issue_type: bool
417
418
  return rule
418
419
 
419
420
 
421
+ # pylint: disable-next=too-few-public-methods
422
+ class _FilterPrefill:
423
+ """Offer the default rank filter while the preset filter stays blank.
424
+
425
+ While the user has not written their own issue filter, every change to
426
+ the project key re-derives the default filter and offers it to the
427
+ filter field, so the filter tracks the project as it is typed. The
428
+ moment the filter holds text the user placed there, tracking stops and
429
+ the filter is left alone. The last value offered is remembered so the
430
+ bridge writing that value back does not look like the user typing it.
431
+ """
432
+
433
+ def __init__(self) -> None:
434
+ """Start with nothing offered and the filter not yet user-owned."""
435
+ self._taken_over = False
436
+ self._last_fill: Optional[str] = None
437
+
438
+ def __call__(self, values: FormResult,
439
+ changed: str) -> list[tuple[str, PrefillValueType]]:
440
+ """Return a filter prefill for a project change, or nothing."""
441
+ self._note_user_filter(values)
442
+ project = values.opt_text('def_project')
443
+ if self._taken_over or changed != 'def_project' or not project:
444
+ return []
445
+ self._last_fill = default_jira_filter(project)
446
+ return [('def_filter', self._last_fill)]
447
+
448
+ def _note_user_filter(self, values: FormResult) -> None:
449
+ """Mark the filter user-owned once it holds text we did not offer."""
450
+ current = values.opt_text('def_filter')
451
+ if current and current != self._last_fill:
452
+ self._taken_over = True
453
+
454
+
420
455
  def _preset_seed(name: Optional[str], preset: Optional[JiraPreset],
421
456
  choices: _PresetChoices) -> Optional[FormResult]:
422
457
  """Return the preset form values from a stored preset."""
@@ -444,6 +479,7 @@ def _ask_preset(nav: _Navigator, choices: _PresetChoices, used: set[str],
444
479
  values = nav.ask_form('Configure the Jira preset.',
445
480
  _preset_fields(used, choices),
446
481
  _preset_rule(bool(choices.issue_type_maps)),
482
+ prefill=_FilterPrefill(),
447
483
  seed=_preset_seed(seed_name, seed_preset, choices))
448
484
  return _preset_from(nav, values, choices)
449
485