backlogops 0.8__tar.gz → 1.0__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 (68) hide show
  1. {backlogops-0.8 → backlogops-1.0}/PKG-INFO +18 -5
  2. {backlogops-0.8 → backlogops-1.0}/README_pypi.md +15 -3
  3. {backlogops-0.8 → backlogops-1.0}/backlogops.egg-info/PKG-INFO +18 -5
  4. {backlogops-0.8 → backlogops-1.0}/backlogops.egg-info/SOURCES.txt +2 -0
  5. {backlogops-0.8 → backlogops-1.0}/backlogops.egg-info/requires.txt +2 -1
  6. {backlogops-0.8 → backlogops-1.0}/setup.py +4 -3
  7. {backlogops-0.8 → backlogops-1.0}/src/backlogops/__init__.py +21 -8
  8. backlogops-1.0/src/backlogops/config_descriptions.py +376 -0
  9. backlogops-1.0/src/backlogops/config_editing.py +90 -0
  10. {backlogops-0.8 → backlogops-1.0}/src/backlogops/config_file_io.py +42 -15
  11. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_io_config.py +14 -9
  12. {backlogops-0.8 → backlogops-1.0}/src/backlogops/levels.py +6 -3
  13. {backlogops-0.8 → backlogops-1.0}/LICENSE.txt +0 -0
  14. {backlogops-0.8 → backlogops-1.0}/backlogops.egg-info/dependency_links.txt +0 -0
  15. {backlogops-0.8 → backlogops-1.0}/backlogops.egg-info/top_level.txt +0 -0
  16. {backlogops-0.8 → backlogops-1.0}/pyproject.toml +0 -0
  17. {backlogops-0.8 → backlogops-1.0}/setup.cfg +0 -0
  18. {backlogops-0.8 → backlogops-1.0}/src/backlogops/apply_format_rules.py +0 -0
  19. {backlogops-0.8 → backlogops-1.0}/src/backlogops/available_teams.py +0 -0
  20. {backlogops-0.8 → backlogops-1.0}/src/backlogops/available_teams_config.py +0 -0
  21. {backlogops-0.8 → backlogops-1.0}/src/backlogops/backlog.py +0 -0
  22. {backlogops-0.8 → backlogops-1.0}/src/backlogops/backlog_helpers.py +0 -0
  23. {backlogops-0.8 → backlogops-1.0}/src/backlogops/backlog_in_release_order.py +0 -0
  24. {backlogops-0.8 → backlogops-1.0}/src/backlogops/backlog_ops_config.py +0 -0
  25. {backlogops-0.8 → backlogops-1.0}/src/backlogops/backlog_ops_wizard.py +0 -0
  26. {backlogops-0.8 → backlogops-1.0}/src/backlogops/backlog_releases.py +0 -0
  27. {backlogops-0.8 → backlogops-1.0}/src/backlogops/backlog_releases_io.py +0 -0
  28. {backlogops-0.8 → backlogops-1.0}/src/backlogops/blo_version_reporter.py +0 -0
  29. {backlogops-0.8 → backlogops-1.0}/src/backlogops/date_ranges.py +0 -0
  30. {backlogops-0.8 → backlogops-1.0}/src/backlogops/demo_backlog.py +0 -0
  31. {backlogops-0.8 → backlogops-1.0}/src/backlogops/estimate_ready_date.py +0 -0
  32. {backlogops-0.8 → backlogops-1.0}/src/backlogops/format_rules.py +0 -0
  33. {backlogops-0.8 → backlogops-1.0}/src/backlogops/io_config.py +0 -0
  34. {backlogops-0.8 → backlogops-1.0}/src/backlogops/io_preset_wizard.py +0 -0
  35. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_connect.py +0 -0
  36. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_order_releases.py +0 -0
  37. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_rank_backlog.py +0 -0
  38. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_rank_by_keys.py +0 -0
  39. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_rank_move_keys.py +0 -0
  40. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_read.py +0 -0
  41. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_rename_releases.py +0 -0
  42. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_search.py +0 -0
  43. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_token.py +0 -0
  44. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_update_backlog.py +0 -0
  45. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_update_releases.py +0 -0
  46. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_wizard.py +0 -0
  47. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_write.py +0 -0
  48. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_write_fields.py +0 -0
  49. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_write_format.py +0 -0
  50. {backlogops-0.8 → backlogops-1.0}/src/backlogops/jira_write_releases.py +0 -0
  51. {backlogops-0.8 → backlogops-1.0}/src/backlogops/key_list_io.py +0 -0
  52. {backlogops-0.8 → backlogops-1.0}/src/backlogops/move_keys_first.py +0 -0
  53. {backlogops-0.8 → backlogops-1.0}/src/backlogops/name_list_io.py +0 -0
  54. {backlogops-0.8 → backlogops-1.0}/src/backlogops/no_text_io.py +0 -0
  55. {backlogops-0.8 → backlogops-1.0}/src/backlogops/order_by_dependencies.py +0 -0
  56. {backlogops-0.8 → backlogops-1.0}/src/backlogops/person.py +0 -0
  57. {backlogops-0.8 → backlogops-1.0}/src/backlogops/py.typed +0 -0
  58. {backlogops-0.8 → backlogops-1.0}/src/backlogops/release_backlog_updates.py +0 -0
  59. {backlogops-0.8 → backlogops-1.0}/src/backlogops/release_change_io.py +0 -0
  60. {backlogops-0.8 → backlogops-1.0}/src/backlogops/releases.py +0 -0
  61. {backlogops-0.8 → backlogops-1.0}/src/backlogops/rename_list_io.py +0 -0
  62. {backlogops-0.8 → backlogops-1.0}/src/backlogops/table_create.py +0 -0
  63. {backlogops-0.8 → backlogops-1.0}/src/backlogops/table_rows.py +0 -0
  64. {backlogops-0.8 → backlogops-1.0}/src/backlogops/team.py +0 -0
  65. {backlogops-0.8 → backlogops-1.0}/src/backlogops/wizard_forms.py +0 -0
  66. {backlogops-0.8 → backlogops-1.0}/src/backlogops/wizard_helpers.py +0 -0
  67. {backlogops-0.8 → backlogops-1.0}/src/backlogops/wizard_navigator.py +0 -0
  68. {backlogops-0.8 → backlogops-1.0}/src/backlogops/work_hours.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops
3
- Version: 0.8
3
+ Version: 1.0
4
4
  Summary: Library with backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -25,9 +25,10 @@ Requires-Dist: config-as-json>=1.5
25
25
  Requires-Dist: cryptography>=50.0.0
26
26
  Requires-Dist: jira[cli,opt]>=3.10.5
27
27
  Requires-Dist: tableio>=1.1
28
- Requires-Dist: tableio-cfg-json>=1.2
28
+ Requires-Dist: tableio-cfg-json>=1.3
29
29
  Requires-Dist: wizard-ui-bridge[textual]>=1.3
30
30
  Requires-Dist: versionreporter>=0.4
31
+ Requires-Dist: edit-cfg-json==0.0.4
31
32
  Dynamic: author
32
33
  Dynamic: license-file
33
34
  Dynamic: requires-dist
@@ -268,6 +269,18 @@ from backlogops import (
268
269
 
269
270
  - `get_demo_backlog`: create a demonstration backlog and releases.
270
271
 
272
+ ### Editing a configuration
273
+
274
+ - `CONFIG_DESCRIPTIONS`, `INPUT_DESCRIPTIONS`, `OUTPUT_DESCRIPTIONS`:
275
+ what each configuration member is for, shown below it in the editor.
276
+ A member has no docstring at runtime, so this is what the editor is
277
+ told; `descriptions_for` picks the mapping of one configuration class.
278
+ The nested TableIO endpoint is described by `tableio-cfg-json`, which
279
+ is asked for that text rather than having it repeated here.
280
+
281
+ - `EDIT_SETTINGS`: what the editor may do to a configuration file of this
282
+ application, including keeping what it writes over as a `.bak` file.
283
+
271
284
  ### Operations
272
285
 
273
286
  - `order_by_dependencies`: reorder the backlog so that dependencies are
@@ -312,10 +325,10 @@ For the full set of public names see the API documentation linked above.
312
325
 
313
326
  ## Test summary
314
327
 
315
- - Test result: 2173 passed, 1 deselected in 35s
328
+ - Test result: 2234 passed, 1 deselected in 45s
316
329
  - No flake8 warnings.
317
330
  - No mypy errors found.
318
331
  - No pylint warnings.
319
332
  - No python layout warnings.
320
- - Built version(s): 0.8
321
- - Build and test using Python 3.14.6
333
+ - Built version(s): 1.0
334
+ - Build and test using Python 3.14.7
@@ -233,6 +233,18 @@ from backlogops import (
233
233
 
234
234
  - `get_demo_backlog`: create a demonstration backlog and releases.
235
235
 
236
+ ### Editing a configuration
237
+
238
+ - `CONFIG_DESCRIPTIONS`, `INPUT_DESCRIPTIONS`, `OUTPUT_DESCRIPTIONS`:
239
+ what each configuration member is for, shown below it in the editor.
240
+ A member has no docstring at runtime, so this is what the editor is
241
+ told; `descriptions_for` picks the mapping of one configuration class.
242
+ The nested TableIO endpoint is described by `tableio-cfg-json`, which
243
+ is asked for that text rather than having it repeated here.
244
+
245
+ - `EDIT_SETTINGS`: what the editor may do to a configuration file of this
246
+ application, including keeping what it writes over as a `.bak` file.
247
+
236
248
  ### Operations
237
249
 
238
250
  - `order_by_dependencies`: reorder the backlog so that dependencies are
@@ -277,10 +289,10 @@ For the full set of public names see the API documentation linked above.
277
289
 
278
290
  ## Test summary
279
291
 
280
- - Test result: 2173 passed, 1 deselected in 35s
292
+ - Test result: 2234 passed, 1 deselected in 45s
281
293
  - No flake8 warnings.
282
294
  - No mypy errors found.
283
295
  - No pylint warnings.
284
296
  - No python layout warnings.
285
- - Built version(s): 0.8
286
- - Build and test using Python 3.14.6
297
+ - Built version(s): 1.0
298
+ - Build and test using Python 3.14.7
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops
3
- Version: 0.8
3
+ Version: 1.0
4
4
  Summary: Library with backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -25,9 +25,10 @@ Requires-Dist: config-as-json>=1.5
25
25
  Requires-Dist: cryptography>=50.0.0
26
26
  Requires-Dist: jira[cli,opt]>=3.10.5
27
27
  Requires-Dist: tableio>=1.1
28
- Requires-Dist: tableio-cfg-json>=1.2
28
+ Requires-Dist: tableio-cfg-json>=1.3
29
29
  Requires-Dist: wizard-ui-bridge[textual]>=1.3
30
30
  Requires-Dist: versionreporter>=0.4
31
+ Requires-Dist: edit-cfg-json==0.0.4
31
32
  Dynamic: author
32
33
  Dynamic: license-file
33
34
  Dynamic: requires-dist
@@ -268,6 +269,18 @@ from backlogops import (
268
269
 
269
270
  - `get_demo_backlog`: create a demonstration backlog and releases.
270
271
 
272
+ ### Editing a configuration
273
+
274
+ - `CONFIG_DESCRIPTIONS`, `INPUT_DESCRIPTIONS`, `OUTPUT_DESCRIPTIONS`:
275
+ what each configuration member is for, shown below it in the editor.
276
+ A member has no docstring at runtime, so this is what the editor is
277
+ told; `descriptions_for` picks the mapping of one configuration class.
278
+ The nested TableIO endpoint is described by `tableio-cfg-json`, which
279
+ is asked for that text rather than having it repeated here.
280
+
281
+ - `EDIT_SETTINGS`: what the editor may do to a configuration file of this
282
+ application, including keeping what it writes over as a `.bak` file.
283
+
271
284
  ### Operations
272
285
 
273
286
  - `order_by_dependencies`: reorder the backlog so that dependencies are
@@ -312,10 +325,10 @@ For the full set of public names see the API documentation linked above.
312
325
 
313
326
  ## Test summary
314
327
 
315
- - Test result: 2173 passed, 1 deselected in 35s
328
+ - Test result: 2234 passed, 1 deselected in 45s
316
329
  - No flake8 warnings.
317
330
  - No mypy errors found.
318
331
  - No pylint warnings.
319
332
  - No python layout warnings.
320
- - Built version(s): 0.8
321
- - Build and test using Python 3.14.6
333
+ - Built version(s): 1.0
334
+ - Build and test using Python 3.14.7
@@ -19,6 +19,8 @@ src/backlogops/backlog_ops_wizard.py
19
19
  src/backlogops/backlog_releases.py
20
20
  src/backlogops/backlog_releases_io.py
21
21
  src/backlogops/blo_version_reporter.py
22
+ src/backlogops/config_descriptions.py
23
+ src/backlogops/config_editing.py
22
24
  src/backlogops/config_file_io.py
23
25
  src/backlogops/date_ranges.py
24
26
  src/backlogops/demo_backlog.py
@@ -2,6 +2,7 @@ config-as-json>=1.5
2
2
  cryptography>=50.0.0
3
3
  jira[cli,opt]>=3.10.5
4
4
  tableio>=1.1
5
- tableio-cfg-json>=1.2
5
+ tableio-cfg-json>=1.3
6
6
  wizard-ui-bridge[textual]>=1.3
7
7
  versionreporter>=0.4
8
+ edit-cfg-json==0.0.4
@@ -5,7 +5,7 @@ from setuptools import setup
5
5
 
6
6
  setup(
7
7
  name='backlogops',
8
- version='0.8',
8
+ version='1.0',
9
9
  description='Library with backlog operations.',
10
10
  author='Tom Björkholm',
11
11
  author_email='klausuler_linnet0q@icloud.com',
@@ -18,8 +18,9 @@ setup(
18
18
  'cryptography >= 50.0.0',
19
19
  'jira[cli,opt] >= 3.10.5',
20
20
  'tableio >= 1.1',
21
- 'tableio-cfg-json >= 1.2',
21
+ 'tableio-cfg-json >= 1.3',
22
22
  'wizard-ui-bridge[textual] >= 1.3',
23
- 'versionreporter >= 0.4'
23
+ 'versionreporter >= 0.4',
24
+ 'edit-cfg-json == 0.0.4'
24
25
  ]
25
26
  )
@@ -29,7 +29,12 @@ from backlogops.backlog_ops_wizard import (
29
29
  from backlogops.backlog_releases import BacklogReleases
30
30
  from backlogops.backlog_releases_io import (
31
31
  read_backlog_releases, write_backlog_releases)
32
- from backlogops.config_file_io import read_io_preset, safe_write_config
32
+ from backlogops.config_descriptions import (
33
+ CONFIG_DESCRIPTIONS, GUI_DESCRIPTIONS, INPUT_DESCRIPTIONS,
34
+ JIRA_DESCRIPTIONS, OUTPUT_DESCRIPTIONS, WORKFORCE_DESCRIPTIONS)
35
+ from backlogops.config_editing import EDIT_SETTINGS, descriptions_for
36
+ from backlogops.config_file_io import (
37
+ CONFIG_EXTENSION, io_preset_class, read_io_preset, safe_write_config)
33
38
  from backlogops.date_ranges import check_date_range, check_no_overlap
34
39
  from backlogops.demo_backlog import get_demo_backlog
35
40
  from backlogops.estimate_ready_date import (
@@ -108,30 +113,37 @@ __all__ = [
108
113
  'AddedReleasesToJira', 'AddedToJira', 'AvailableTeams',
109
114
  'AvailableTeamsConfig', 'Backlog', 'BacklogItem', 'BacklogOpsConfig',
110
115
  'BacklogReleaseChange', 'BacklogReleases', 'BadJiraRankFilter',
111
- 'CLEAR_TOKEN_WARNING', 'CompanyWorkHours', 'DEFAULT_LEVELS',
116
+ 'CLEAR_TOKEN_WARNING', 'CONFIG_DESCRIPTIONS', 'CONFIG_EXTENSION',
117
+ 'CompanyWorkHours', 'DEFAULT_LEVELS',
112
118
  'DEFAULT_WORK_WEEK', 'DEF_BACKLOG_COLUMN_MAP', 'DEF_RELEASE_COLUMN_MAP',
113
- 'DEF_STATUS_INPUT_MAP', 'DependencyMode', 'ExceptionWorkHours',
119
+ 'DEF_STATUS_INPUT_MAP', 'DependencyMode', 'EDIT_SETTINGS',
120
+ 'ExceptionWorkHours',
114
121
  'ExistsInJiraError', 'FailedItem', 'FailedLink', 'FailedRelease',
115
122
  'FailedRename', 'FileExistsCb', 'FormatRules', 'FteException',
116
- 'GuiDisplayConfig', 'InputFormatConfig', 'ItemNotInJiraError',
123
+ 'GUI_DESCRIPTIONS', 'GuiDisplayConfig', 'INPUT_DESCRIPTIONS',
124
+ 'InputFormatConfig', 'ItemNotInJiraError', 'JIRA_DESCRIPTIONS',
117
125
  'JiraAttrPath', 'JiraAttrType', 'JiraColumnMap', 'JiraConnectConfig',
118
126
  'JiraConnections', 'JiraIOConfig', 'JiraIssueTypeMap', 'JiraKeyError',
119
127
  'JiraPreset', 'JiraRankAnchor', 'JiraTooManyLoops', 'JiraType',
120
128
  'LEVEL_COLUMN', 'LEVEL_NAME_COLUMN', 'Level', 'LevelDisplay', 'Levels',
121
- 'LinkUpdate', 'Membership', 'NoTextIO', 'OnExistingKey', 'OnMissingKey',
129
+ 'LinkUpdate', 'Membership', 'NoTextIO', 'OUTPUT_DESCRIPTIONS',
130
+ 'OnExistingKey', 'OnMissingKey',
122
131
  'OrderedReleasesInJira', 'OutputFormatConfig', 'Person', 'RankedInJira',
123
132
  'Release', 'ReleaseChange', 'ReleaseChanges', 'ReleaseDateChange',
124
133
  'ReleaseDateChanges', 'ReleaseExistsError', 'ReleaseRename', 'Releases',
125
134
  'ReleasesAndDateChanges', 'RenamedReleasesInJira', 'ScheduleWorkHours',
126
135
  'Status', 'StatusMismatch', 'Team', 'TokenStorage',
127
136
  'UnknownIssueTypeError', 'UpdatedBacklogInJira', 'UpdatedReleasesInJira',
137
+ 'WORKFORCE_DESCRIPTIONS',
128
138
  'WeekDay', 'add_backlog_to_jira', 'add_releases_to_jira',
129
139
  'adjust_release_content', 'allow_overwrite', 'apply_column_map',
130
140
  'apply_jira_keys', 'available_teams_wizard', 'backlog_in_release_order',
131
141
  'backlog_ops_wizard', 'build_backlog_releases', 'build_dependency_graph',
132
142
  'check_backlog_consistency', 'check_date_range',
133
- 'check_levels_consistency', 'check_no_overlap', 'default_jira_filter',
134
- 'display_level_order', 'display_level_rows', 'encrypt_token_file',
143
+ 'check_levels_consistency', 'check_no_overlap',
144
+ 'default_jira_filter', 'descriptions_for',
145
+ 'display_level_order', 'display_level_rows',
146
+ 'encrypt_token_file',
135
147
  'encrypt_token_to_file', 'estimate_ready_date', 'estimate_release_dates',
136
148
  'event_finish', 'event_start', 'find_cycle', 'fold_level_name',
137
149
  'format_add_result', 'format_backlog', 'format_backlog_updates',
@@ -140,7 +152,8 @@ __all__ = [
140
152
  'format_releases', 'format_rename_result', 'get_backlog',
141
153
  'get_backlog_item', 'get_backlog_ops_config', 'get_demo_backlog',
142
154
  'get_keys_in_order', 'get_release', 'get_releases',
143
- 'item_dependency_edges', 'item_to_row', 'jira_custom_fields',
155
+ 'io_preset_class', 'item_dependency_edges', 'item_to_row',
156
+ 'jira_custom_fields',
144
157
  'jira_editable_fields', 'jira_rank_backlog', 'jira_rank_by_keys_raw',
145
158
  'jira_rank_move_keys', 'level_name', 'level_number_from_name',
146
159
  'levels_from_list', 'make_input_config', 'make_output_config',
@@ -0,0 +1,376 @@
1
+ #! /usr/local/bin/python3
2
+ """What each configuration member is for, for the configuration editor.
3
+
4
+ A configuration class carries its own docstring, so the editor of
5
+ ``edit_cfg_json`` can label a configuration object without being told
6
+ anything. A *member* carries nothing at runtime: a string literal written
7
+ after an assignment is discarded and an annotation on an instance attribute
8
+ is recorded nowhere. This module is therefore where backlog-ops says what
9
+ its members are for, as the ``edit_cfg_json.Descriptions`` mappings the
10
+ editor takes.
11
+
12
+ A member is named by the absolute ``config_as_json.ConfigPath`` that
13
+ addresses it, and the :data:`EVERY` step stands for every element of a list
14
+ and every value of a dict at that point. A selector may cross the boundary
15
+ into a nested configuration object, so one mapping describes a whole tree.
16
+
17
+ The same classes appear in more than one place: an ``InputFormatConfig`` is
18
+ both a value of ``input_configs`` and the whole of a stand-alone input
19
+ preset file, and a work-hours exception belongs both to a person and to the
20
+ company. Each class is therefore described once, relative to itself, and
21
+ :func:`prefixed` puts one of those mappings under the path where the class
22
+ is used. So every mapping here says the same thing about one member
23
+ wherever that member appears, and
24
+ :data:`CONFIG_DESCRIPTIONS` is built from the others rather than beside
25
+ them.
26
+
27
+ Nothing the editor works out for itself is repeated here. It reads the
28
+ class of a nested configuration object and shows its docstring, the enum
29
+ class of a member and shows its summary line and the names it accepts, the
30
+ kind of value a member holds, and whether its class may leave it out of the
31
+ file. So a text here neither lists the names of an enum, nor spells out
32
+ ``true`` and ``false``, nor calls a number a number, nor says that a member
33
+ may be left out.
34
+
35
+ What a name means is another matter, and it is the one thing a list of
36
+ names does not say. It is written where it is read once for every member
37
+ that holds it: in the summary line of the enum class, which is the line the
38
+ editor shows and the only line of that docstring it shows. A choice with
39
+ more names than fit in one such line has the meaning of them said about the
40
+ member instead, which is why ``token_storage`` explains its modes here and
41
+ ``level_display`` does not.
42
+
43
+ The nested TableIO endpoint is described by ``tableio_cfg_json``, which
44
+ owns those members: :func:`tio_json_descriptions` is asked for their text
45
+ under the path of the member holding the endpoint, so the TableIO
46
+ documentation is neither repeated here nor able to drift from it. What is
47
+ written here about that endpoint is the one line about the member itself.
48
+
49
+ One more thing is deliberately left out: a limit that lives inside a
50
+ validator is not read by the editor and is stated in words where it
51
+ matters.
52
+ """
53
+
54
+ # Copyright (c) 2026, Tom Björkholm
55
+ # MIT License
56
+
57
+ from config_as_json import ConfigPath
58
+ from edit_cfg_json import Descriptions
59
+ from tableio_cfg_json import tio_json_descriptions
60
+
61
+ EVERY = '['
62
+ """The path step meaning every element of a list or every value of a dict.
63
+
64
+ It is the ``config_as_json`` selector step, spelled once here so that a
65
+ description path reads as the tree it walks.
66
+ """
67
+
68
+
69
+ def prefixed(prefix: ConfigPath, members: Descriptions) -> Descriptions:
70
+ """Return the descriptions of one class, put where the class is used.
71
+
72
+ Args:
73
+ prefix: The path of the member holding that configuration, ending
74
+ with :data:`EVERY` where it holds several of them.
75
+ members: The descriptions of that class, relative to itself.
76
+
77
+ Returns:
78
+ The same descriptions, each under the absolute path of its member.
79
+ """
80
+ return {prefix + path: text for path, text in members.items()}
81
+
82
+
83
+ _HOURS_EXCEPTION: Descriptions = {
84
+ ('start_date',): 'First day of the exception, as YYYY-MM-DD.',
85
+ ('end_date',): 'Last day of the exception, as YYYY-MM-DD. Not before '
86
+ 'the first day.',
87
+ ('hours_per_day',): 'Work hours per day during the exception. Zero for '
88
+ 'a holiday or a closed period. Not negative.',
89
+ ('new_work_days',): 'Whether these hours also apply to days the '
90
+ 'schedule gives no work hours, which is what an '
91
+ 'exception for work on a closed day needs. '
92
+ 'Otherwise the exception only changes the days '
93
+ 'that already have work hours.'}
94
+ """Every member of one work-hours exception, of a person or the company."""
95
+
96
+ _FTE_EXCEPTION: Descriptions = {
97
+ ('start_date',): 'First day of the exception, as YYYY-MM-DD.',
98
+ ('end_date',): 'Last day of the exception, as YYYY-MM-DD. Not before '
99
+ 'the first day.',
100
+ ('fte',): 'Full-time equivalent this person gives the team during the '
101
+ 'exception, such as 0.5 for half time. Not negative.'}
102
+ """Every member of one full-time-equivalent exception of a membership."""
103
+
104
+ _MEMBERSHIP: Descriptions = {
105
+ ('person_name',): 'Name of the person, which must be one of the names '
106
+ 'under persons. Matched without regard to case.',
107
+ ('fte',): 'Full-time equivalent this person gives the team outside the '
108
+ 'exceptions below. 1.0 is full time. Not negative.',
109
+ ('start_date',): 'First day of the membership, as YYYY-MM-DD, or empty '
110
+ 'for a membership that was always there.',
111
+ ('end_date',): 'Last day of the membership, as YYYY-MM-DD, or empty '
112
+ 'for a membership that does not end.',
113
+ ('fte_exceptions',): 'Periods where this person gives the team another '
114
+ 'share of their time, such as a learning period. '
115
+ 'The periods must not overlap.'}
116
+ """Every member of one team membership, plus its exception list."""
117
+
118
+ _TEAM: Descriptions = {
119
+ ('name',): 'Name of the team, as a backlog item names the team that '
120
+ 'does it. Unique across the teams, and not empty.',
121
+ ('velocity',): 'Story points the team completes per sprint, as it was '
122
+ 'measured. Not negative.',
123
+ ('sum_fte_at_velocity',): 'Sum of the full-time equivalents of the '
124
+ 'members when that velocity was measured. It '
125
+ 'is what rescales the velocity when the team '
126
+ 'grows or shrinks, so it must be positive.',
127
+ ('sprint_length',): 'Length of one sprint in working days, not calendar '
128
+ 'days. Must be positive.',
129
+ ('aliases',): 'Other names a backlog may use for this team. Each is '
130
+ 'unique across the teams and matched without case.',
131
+ ('members',): 'Who is in the team, and for how much of their time. One '
132
+ 'person may be in several teams, and over several '
133
+ 'periods of time.'}
134
+ """Every member of one team, plus its alias and membership lists."""
135
+
136
+ _PERSON: Descriptions = {
137
+ ('name',): 'Name of the person. The key of this entry must be this '
138
+ 'name in lower case.',
139
+ ('exceptions',): 'Periods where this person works other hours than the '
140
+ 'company schedule: vacation, part time, or ordered '
141
+ 'over-time. The company periods apply as well and are '
142
+ 'not repeated here. The periods must not overlap.'}
143
+ """Every member of one person, plus their work-hours exception list."""
144
+
145
+ _WORKFORCE: Descriptions = {
146
+ ('persons',): 'Everybody who works on this backlog, keyed by their '
147
+ 'name in lower case. A person is here once, however many '
148
+ 'teams they are in.',
149
+ ('teams',): 'The teams that do the work, in no particular order. A '
150
+ 'backlog item names its team by the team name or an alias.',
151
+ ('company_work_hours',): 'The working week and the days the whole '
152
+ 'company is closed, which is what every '
153
+ "person's own hours are counted against.",
154
+ ('company_work_hours', 'work_hours'): 'Work hours of each week day, '
155
+ 'keyed by the day name in upper '
156
+ 'case. Every day of the week has '
157
+ 'an entry, and a day nobody works '
158
+ 'has zero.',
159
+ ('company_work_hours', 'exceptions'): 'National holidays, company-wide '
160
+ 'vacations, and every other '
161
+ 'period the company works other '
162
+ 'hours. The periods must not '
163
+ 'overlap.'}
164
+ """Every member of the workforce, without the lists it holds."""
165
+
166
+ WORKFORCE_DESCRIPTIONS: Descriptions = {
167
+ **_WORKFORCE,
168
+ **prefixed(('persons', EVERY), _PERSON),
169
+ **prefixed(('persons', EVERY, 'exceptions', EVERY), _HOURS_EXCEPTION),
170
+ **prefixed(('company_work_hours', 'exceptions', EVERY), _HOURS_EXCEPTION),
171
+ **prefixed(('teams', EVERY), _TEAM),
172
+ **prefixed(('teams', EVERY, 'members', EVERY), _MEMBERSHIP),
173
+ **prefixed(('teams', EVERY, 'members', EVERY, 'fte_exceptions', EVERY),
174
+ _FTE_EXCEPTION)}
175
+ """What every member of an ``AvailableTeamsConfig`` is for."""
176
+
177
+ _TABLEIO: Descriptions = {
178
+ ('tableio',): 'How the file itself is read or written: its format, and '
179
+ 'the settings of that format. A setting that is not in '
180
+ 'the file keeps its default and has no line here.',
181
+ **tio_json_descriptions(('tableio',))}
182
+ """What the nested TableIO endpoint of one preset and its settings are for.
183
+
184
+ Only the line about the member itself is written here. Everything below it
185
+ belongs to ``tableio_cfg_json``, which is asked for it, so the formats,
186
+ implementations and values named are the ones registered now.
187
+ """
188
+
189
+ _MAPPED_COLUMN = ('Empty drops the column altogether. A column that is not '
190
+ 'named here keeps its own name.')
191
+ """What one entry of a column-name map means, beyond being a rename."""
192
+
193
+ INPUT_DESCRIPTIONS: Descriptions = {
194
+ ('backlog_to_internal',): 'What the backlog columns of the file are '
195
+ 'called, keyed by the file column name and '
196
+ 'naming the internal field it is read into. '
197
+ 'Several file columns may be read into one '
198
+ 'internal field.',
199
+ ('backlog_to_internal', EVERY): 'Internal field this file column is '
200
+ f'read into. {_MAPPED_COLUMN}',
201
+ ('release_to_internal',): 'The same for the releases table of the file.',
202
+ ('release_to_internal', EVERY): 'Internal field this file column is '
203
+ f'read into. {_MAPPED_COLUMN}',
204
+ ('status_input_map',): 'Extra status names this file uses, keyed by the '
205
+ 'name in the file. They are matched without '
206
+ 'regard to case, and they override the status '
207
+ 'map of the whole configuration for this preset '
208
+ 'alone.',
209
+ ('status_input_map', EVERY): 'Internal status this name is read as: one '
210
+ 'of TODO, IN_PROGRESS, DONE or REJECTED.',
211
+ **_TABLEIO}
212
+ """What every member of an ``InputFormatConfig`` is for."""
213
+
214
+
215
+ def _display_members(action: str) -> Descriptions:
216
+ """Return what the column maps and the level display say about a part.
217
+
218
+ An output preset writes these columns to a file and the display shows
219
+ them on a screen, so the one word that differs between the two is a
220
+ parameter and everything else is said once.
221
+
222
+ Args:
223
+ action: What becomes of a column here, as a past participle:
224
+ ``'written'`` for an output preset, ``'shown'`` for a display.
225
+
226
+ Returns:
227
+ The descriptions of those members, relative to the class.
228
+ """
229
+ column = f'Column name this internal field is {action} under. ' \
230
+ f'{_MAPPED_COLUMN}'
231
+ return {
232
+ ('backlog_to_external',): 'What the backlog columns are called when '
233
+ f'they are {action}, keyed by the internal '
234
+ 'field name and naming the column name to '
235
+ 'use.',
236
+ ('backlog_to_external', EVERY): column,
237
+ ('release_to_external',): 'The same for the releases table.',
238
+ ('release_to_external', EVERY): column,
239
+ ('level_display',): 'Which columns the level of a backlog item is '
240
+ f'{action} in.'}
241
+
242
+
243
+ OUTPUT_DESCRIPTIONS: Descriptions = {**_display_members('written'),
244
+ **_TABLEIO}
245
+ """What every member of an ``OutputFormatConfig`` is for."""
246
+
247
+ GUI_DESCRIPTIONS: Descriptions = _display_members('shown')
248
+ """What every member of a ``GuiDisplayConfig`` is for.
249
+
250
+ The same members as an output preset, without the TableIO endpoint, and
251
+ said of showing a column rather than of writing one, because the graphical
252
+ interface shows the tables rather than writing them.
253
+ """
254
+
255
+ _CONNECTION: Descriptions = {
256
+ ('base_url',): 'Address of the Jira server, such as '
257
+ 'https://example.atlassian.net',
258
+ ('login_email',): 'Email address the API token belongs to.',
259
+ ('token_storage',): 'A clear mode keeps the token unprotected and is '
260
+ 'meant for demonstration data only, while an '
261
+ 'encrypted mode asks for a pass phrase whenever the '
262
+ 'token is stored and whenever it is used. A file '
263
+ 'mode keeps the token in the file named below, an '
264
+ 'internal mode in this configuration itself.',
265
+ ('token_file_path',): 'File holding the token, for a file storage mode. '
266
+ 'Empty for an internal storage mode.',
267
+ ('stored_token',): 'The token itself, for an internal storage mode: '
268
+ 'encrypted text for an encrypted mode and the token '
269
+ 'as it is for a clear mode. Empty for a file storage '
270
+ 'mode. It is written by the wizard and by the token '
271
+ 'encryption command rather than typed here.'}
272
+ """Every member of one Jira connection."""
273
+
274
+ _JIRA_PRESET: Descriptions = {
275
+ ('connection_name',): 'Name of the connection this preset uses, from '
276
+ 'the connections above.',
277
+ ('backlog_column_map_name',): 'Name of the backlog column map used to '
278
+ 'read, from the backlog column maps '
279
+ 'above.',
280
+ ('release_column_map_name',): 'Name of the release column map, from the '
281
+ 'release column maps above.',
282
+ ('backlog_write_map_name',): 'Name of the backlog column map used to '
283
+ 'write, when writing needs another one. '
284
+ 'Empty writes through the map used to read.',
285
+ ('issue_type_map_name',): 'Name of the level-to-issue-type map used to '
286
+ 'write, from the issue type maps above. Empty '
287
+ 'writes each level under its own name.',
288
+ ('def_project',): 'Jira project key this preset works in, such as ABC. '
289
+ 'The releases are read from this project even when '
290
+ 'the filter below is overridden.',
291
+ ('def_filter',): 'Jira Query Language filter selecting the issues to '
292
+ 'read, such as project = "ABC" ORDER BY rank ASC.'}
293
+ """Every member of one Jira preset."""
294
+
295
+ JIRA_DESCRIPTIONS: Descriptions = {
296
+ ('connections',): 'The Jira servers this configuration can reach, by a '
297
+ 'name of your own. Several presets may share one.',
298
+ ('backlog_column_maps',): 'Where the value of each backlog field is '
299
+ 'found on a Jira issue, by a name of your '
300
+ 'own. A preset names the map it uses.',
301
+ ('backlog_column_maps', EVERY): 'One map, keyed by the internal backlog '
302
+ 'field name. An internal field that is '
303
+ 'not here is not read.',
304
+ ('backlog_column_maps', EVERY, EVERY): 'Where that field is found: the '
305
+ 'kind (ATTRIBUTE, FIELD, '
306
+ 'CUSTOM_FIELD or FILTERED_FIELD) '
307
+ 'and then the path steps.',
308
+ ('release_column_maps',): 'The same for the fields of a release, which '
309
+ 'is a version in Jira.',
310
+ ('release_column_maps', EVERY): 'One map, keyed by the internal release '
311
+ 'field name.',
312
+ ('release_column_maps', EVERY, EVERY): 'Where that field is found: the '
313
+ 'kind and then the path steps.',
314
+ ('issue_type_maps',): 'Which Jira issue type to create for a level, by '
315
+ 'a name of your own. Used only when writing.',
316
+ ('issue_type_maps', EVERY): 'One map, keyed by the level number as '
317
+ 'text. A level that is not here is written '
318
+ 'under its own name.',
319
+ ('issue_type_maps', EVERY, EVERY): 'Jira issue type created for that '
320
+ 'level, such as Deluppgift.',
321
+ ('presets',): 'The named presets, each tying the sections above '
322
+ 'together. A preset name is what a command asks for '
323
+ 'with -p.',
324
+ **prefixed(('connections', EVERY), _CONNECTION),
325
+ **prefixed(('presets', EVERY), _JIRA_PRESET)}
326
+ """What every member of a ``JiraIOConfig`` is for."""
327
+
328
+ _LEVEL: Descriptions = {
329
+ ('level',): 'A higher number is a bigger item, so a story is above a '
330
+ 'sub-task. Used once across the levels.',
331
+ ('name',): 'Name of the level, such as Story. Unique across the levels '
332
+ 'and their aliases, and not empty.',
333
+ ('aliases',): 'Other names for this level, as another tool may call it. '
334
+ 'Each is unique across the levels and their aliases.'}
335
+ """Every member of one backlog item level."""
336
+
337
+ _TOP_LEVEL: Descriptions = {
338
+ ('available_teams',): 'Who does the work: the persons, the teams, and '
339
+ 'the working week of the company. This is what an '
340
+ 'estimate is calculated from.',
341
+ ('input_configs',): 'Named input presets, each saying how to read a '
342
+ 'backlog file. A preset name is what a command asks '
343
+ 'for with -I.',
344
+ ('output_configs',): 'Named output presets, each saying how to write a '
345
+ 'backlog file. A preset name is what a command '
346
+ 'asks for with -O.',
347
+ ('gui_display',): 'What the graphical interface shows. A written file '
348
+ 'follows its output preset above instead.',
349
+ ('status_input_map',): 'Status names as files and Jira use them, keyed '
350
+ 'by that name and matched without regard to '
351
+ 'case. An input preset may override an entry for '
352
+ 'itself.',
353
+ ('status_input_map', EVERY): 'Internal status this name is read as: one '
354
+ 'of TODO, IN_PROGRESS, DONE or REJECTED.',
355
+ ('jira',): 'Everything about Jira. A backlog kept in files alone needs '
356
+ 'none of it.',
357
+ ('levels',): 'The levels of a backlog item, from the smallest upwards. '
358
+ 'Without them the built-in levels are used: Sub-Task, '
359
+ 'Story, Epic and Initiative.'}
360
+ """What every member of the top-level configuration is for."""
361
+
362
+ CONFIG_DESCRIPTIONS: Descriptions = {
363
+ **_TOP_LEVEL,
364
+ **prefixed(('available_teams',), WORKFORCE_DESCRIPTIONS),
365
+ **prefixed(('input_configs', EVERY), INPUT_DESCRIPTIONS),
366
+ **prefixed(('output_configs', EVERY), OUTPUT_DESCRIPTIONS),
367
+ **prefixed(('gui_display',), GUI_DESCRIPTIONS),
368
+ **prefixed(('jira',), JIRA_DESCRIPTIONS),
369
+ **prefixed(('levels', EVERY), _LEVEL)}
370
+ """What every member of a ``BacklogOpsConfig`` is for.
371
+
372
+ One mapping for the whole tree, because a description selector crosses the
373
+ boundary into a nested configuration object. It is built from the mapping
374
+ of each class, so a member says the same thing here as it does in the
375
+ stand-alone preset file that holds the same class.
376
+ """
@@ -0,0 +1,90 @@
1
+ #! /usr/local/bin/python3
2
+ """What a configuration editor of a UI is told about this application.
3
+
4
+ The wizard asks one question after another, which is what building a
5
+ configuration from nothing wants. Editing an existing one wants the other
6
+ shape: the whole configuration on the screen at once, folded where it is
7
+ deep, so a single value can be changed without walking past everything
8
+ else. ``edit_cfg_json`` is that editor, and this module is the two answers
9
+ it asks the application for, so that the command line and the graphical
10
+ interface show one configuration the same way.
11
+
12
+ The first answer is :func:`descriptions_for`, which is what each editable
13
+ class says about its own members. Which class is edited is the class of the
14
+ configuration object the editor is given, so the caller decides whether a
15
+ complete backlog-ops configuration or a stand-alone preset is being edited,
16
+ and the descriptions of that class follow from it. A caller that has a
17
+ class rather than an object, such as a command line naming the kind of file
18
+ it edits, gets the object from ``edit_cfg_json.default_config``.
19
+
20
+ The second answer is :data:`EDIT_SETTINGS`, which is what the editor may do
21
+ to a file. Saving is the editor's own: it validates the whole configuration
22
+ through the configuration class and only then writes, keeping what it wrote
23
+ over as a ``.bak`` file, because an editor overwrites the file it read.
24
+
25
+ Nothing here opens an editor. A user interface that already runs its own
26
+ toolkit mounts the editor itself — ``edit_cfg_json_tk.TkEditorPanel`` in a
27
+ window of the application, ``edit_cfg_json_textual.edit`` in a terminal of
28
+ its own — and hands those two answers to it.
29
+
30
+ Three things the editor cannot do are worth knowing before it is offered
31
+ instead of the wizard, and all three are of ``edit_cfg_json`` itself rather
32
+ than of this configuration:
33
+
34
+ * A dict whose keys the application validates for itself cannot gain or
35
+ lose a key. That is the status map, the column-name maps of a preset and
36
+ of the display, and the Jira column and issue-type maps: their values are
37
+ editable, and a new entry is the wizard's to create.
38
+ * A member left out of the file has no row, so the levels can be edited
39
+ only in a configuration that already states them.
40
+ * A person is keyed by their own name in lower case, so renaming one means
41
+ adding an entry under the new key and removing the old one; editing the
42
+ name alone leaves a configuration the class refuses.
43
+ """
44
+
45
+ # Copyright (c) 2026, Tom Björkholm
46
+ # MIT License
47
+
48
+ from typing import Optional
49
+ from config_as_json import Config
50
+ from edit_cfg_json import Descriptions, Settings
51
+ from backlogops.backlog_ops_config import BacklogOpsConfig
52
+ from backlogops.config_descriptions import CONFIG_DESCRIPTIONS, \
53
+ INPUT_DESCRIPTIONS, OUTPUT_DESCRIPTIONS
54
+ from backlogops.config_file_io import CONFIG_EXTENSION
55
+ from backlogops.io_config import InputFormatConfig, OutputFormatConfig
56
+
57
+ EDIT_SETTINGS = Settings(file_extension=CONFIG_EXTENSION,
58
+ extension_enforced=False, backup_suffix='.bak',
59
+ backup_count=1, confirm_overwrite=True)
60
+ """What the editor may do to a configuration file of this application.
61
+
62
+ The extension is added to a destination that has none, and a name with
63
+ another extension is accepted, which is how the wizard commands complete a
64
+ file name too. Overwriting a file this session has not written is confirmed
65
+ first and the previous content is then kept as that name plus ``.bak``,
66
+ because the editor writes over the file it read and that file may hold a
67
+ configuration somebody else wrote.
68
+
69
+ The default key combinations apply: none of them is taken by the
70
+ application around the editor.
71
+ """
72
+
73
+ CLASS_DESCRIPTIONS: dict[type[Config], Descriptions] = {
74
+ BacklogOpsConfig: CONFIG_DESCRIPTIONS,
75
+ InputFormatConfig: INPUT_DESCRIPTIONS,
76
+ OutputFormatConfig: OUTPUT_DESCRIPTIONS}
77
+ """What each editable configuration class says about its own members."""
78
+
79
+
80
+ def descriptions_for(config: Config) -> Optional[Descriptions]:
81
+ """Return what the class of one configuration says about its members.
82
+
83
+ Args:
84
+ config: The configuration object that is to be edited.
85
+
86
+ Returns:
87
+ The descriptions of that class, or None for a class this library
88
+ says nothing about, which the editor shows without descriptions.
89
+ """
90
+ return CLASS_DESCRIPTIONS.get(type(config))
@@ -1,14 +1,17 @@
1
1
  #! /usr/local/bin/python3
2
2
  """Read a stand-alone preset file and write a configuration crash-safely.
3
3
 
4
- Two helpers shared by the command line and the graphical interface. Both
4
+ Helpers shared by the command line and the graphical interface. Both
5
5
  interfaces let the user build a configuration or a stand-alone preset file
6
6
  through a wizard, optionally pre-filled from an existing file, and then
7
7
  write the result. :func:`read_io_preset` reads a stand-alone preset file
8
8
  and detects whether it is an input or an output preset from its own
9
- contents. :func:`safe_write_config` writes any configuration so that a
10
- crash or a kill at any moment leaves the whole configuration in either the
11
- old file or a sibling ``.in_progress`` file, never lost between the two.
9
+ contents; :func:`io_preset_class` answers that question alone, for a caller
10
+ that hands the file to something else that reads it, such as the
11
+ configuration editor of :mod:`backlogops.config_editing`.
12
+ :func:`safe_write_config` writes any configuration so that a crash or a
13
+ kill at any moment leaves the whole configuration in either the old file or
14
+ a sibling ``.in_progress`` file, never lost between the two.
12
15
 
13
16
  The direction is chosen from the top-level keys of the file. A common
14
17
  mistake is to pick a complete backlog-ops configuration file where a
@@ -33,6 +36,9 @@ from backlogops.io_config import InputFormatConfig, OutputFormatConfig
33
36
  IN_PROGRESS_SUFFIX = '.in_progress'
34
37
  """Extra extension of the sibling file written before the atomic move."""
35
38
 
39
+ CONFIG_EXTENSION = '.cfg'
40
+ """File name extension of a backlog-ops configuration or preset file."""
41
+
36
42
  _INPUT_KEYS = ('backlog_to_internal', 'release_to_internal',
37
43
  'status_input_map', 'to_internal')
38
44
  """Top-level keys that mark a stand-alone input preset file (new or old)."""
@@ -87,17 +93,40 @@ def _preset_direction(data: dict[str, object], filename: str) -> str:
87
93
  'preset, or backlog-ops configuration file.')
88
94
 
89
95
 
96
+ def io_preset_class(filename: str
97
+ ) -> type[InputFormatConfig] | type[OutputFormatConfig]:
98
+ """Return the preset class a stand-alone preset file is written in.
99
+
100
+ The direction is chosen by inspecting the top-level keys of the file:
101
+ the file-column-to-internal maps or a status map mark an input preset,
102
+ while the internal-to-file maps or a level display mark an output
103
+ preset. Only the JSON of the file is read, so a caller that wants the
104
+ class before it reads the preset itself, such as one handing the file
105
+ to a configuration editor, needs nothing more than this.
106
+
107
+ Args:
108
+ filename: The stand-alone preset file to look at.
109
+
110
+ Returns:
111
+ :class:`InputFormatConfig` or :class:`OutputFormatConfig`.
112
+
113
+ Raises:
114
+ ValueError: The file is missing, is not valid JSON, is a complete
115
+ backlog-ops configuration, or matches neither direction.
116
+ """
117
+ direction = _preset_direction(_load_preset_json(filename), filename)
118
+ return InputFormatConfig if direction == 'input' else OutputFormatConfig
119
+
120
+
90
121
  def read_io_preset(filename: str, auto_ch_hook: ConfigAutoChangeHook,
91
122
  stderr_file: TextIO = sys.stderr
92
123
  ) -> InputFormatConfig | OutputFormatConfig:
93
124
  """Read a stand-alone preset file, auto-detecting its direction.
94
125
 
95
- The direction is chosen by inspecting the top-level keys of the file:
96
- the file-column-to-internal maps or a status map mark an input preset,
97
- while the internal-to-file maps or a level display mark an output
98
- preset. A complete backlog-ops configuration file carries its own
99
- identifying keys and is rejected, as is a file that matches no
100
- direction, so the caller can report the mistake.
126
+ The direction is detected by :func:`io_preset_class`. A complete
127
+ backlog-ops configuration file carries its own identifying keys and is
128
+ rejected, as is a file that matches no direction, so the caller can
129
+ report the mistake.
101
130
 
102
131
  Args:
103
132
  filename: The stand-alone preset file to read.
@@ -112,11 +141,9 @@ def read_io_preset(filename: str, auto_ch_hook: ConfigAutoChangeHook,
112
141
  ValueError: The file is missing, is not valid JSON, is a complete
113
142
  backlog-ops configuration, or matches neither direction.
114
143
  """
115
- direction = _preset_direction(_load_preset_json(filename), filename)
116
- config_class = (InputFormatConfig if direction == 'input'
117
- else OutputFormatConfig)
118
- return config_class(from_json_filename=filename, auto_ch_hook=auto_ch_hook,
119
- stderr_file=stderr_file)
144
+ return io_preset_class(filename)(from_json_filename=filename,
145
+ auto_ch_hook=auto_ch_hook,
146
+ stderr_file=stderr_file)
120
147
 
121
148
 
122
149
  def safe_write_config(config: Config, output: str,
@@ -50,21 +50,26 @@ from backlogops.jira_token import decrypt_token, encrypt_token
50
50
 
51
51
 
52
52
  class JiraType(Enum):
53
- """Jira deployment type, cloud or server."""
53
+ """Jira deployment type: CLOUD is hosted by Atlassian, SERVER is not."""
54
54
 
55
55
  CLOUD = auto()
56
56
  SERVER = auto()
57
57
 
58
58
 
59
59
  class TokenStorage(Enum):
60
- """How the Jira API token is stored.
61
-
62
- A ``CLEAR`` mode stores the token unprotected and is meant for demo
63
- data only. An ``ENCRYPTED`` mode stores the token encrypted with a
64
- pass phrase the user supplies when the token is stored and when it is
65
- used. A ``FILE`` mode keeps the token in a separate file named by
66
- ``token_file_path``; an ``INTERNAL`` mode keeps it in the
67
- configuration itself.
60
+ """CLEAR or ENCRYPTED is the protection, FILE or INTERNAL the place.
61
+
62
+ Each member names both, which is what the summary line above says,
63
+ because that line is the one the configuration editor shows under
64
+ every member holding one of these. What each half of a name then
65
+ means is said about the member itself in
66
+ :mod:`backlogops.config_descriptions`, since four modes do not fit in
67
+ a summary line. A ``CLEAR`` mode stores the token unprotected and is
68
+ meant for demo data only. An ``ENCRYPTED`` mode stores the token
69
+ encrypted with a pass phrase the user supplies when the token is
70
+ stored and when it is used. A ``FILE`` mode keeps the token in a
71
+ separate file named by ``token_file_path``; an ``INTERNAL`` mode
72
+ keeps it in the configuration itself.
68
73
  """
69
74
 
70
75
  CLEAR_FILE = auto()
@@ -13,10 +13,13 @@ from backlogops.backlog_helpers import report_wrong_type
13
13
 
14
14
 
15
15
  class LevelDisplay(Enum):
16
- """How a backlog item level is represented in a table or display.
16
+ """NUMERIC is the level number, NAME its name, BOTH a column of each.
17
17
 
18
- NUMERIC writes only the level number, NAME writes only the level
19
- name, and BOTH writes the number and the name in separate columns.
18
+ That is how a backlog item level is represented in a table or a
19
+ display. What each member means is said in the summary line above
20
+ rather than here, because the summary is the line the configuration
21
+ editor shows under every member holding one of these, and the names
22
+ on their own are what it can find without being told.
20
23
  """
21
24
 
22
25
  NUMERIC = auto()
File without changes
File without changes
File without changes