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