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.
- {backlogops-0.7 → backlogops-0.9}/PKG-INFO +22 -6
- {backlogops-0.7 → backlogops-0.9}/README_pypi.md +18 -3
- {backlogops-0.7 → backlogops-0.9}/backlogops.egg-info/PKG-INFO +22 -6
- {backlogops-0.7 → backlogops-0.9}/backlogops.egg-info/SOURCES.txt +2 -0
- {backlogops-0.7 → backlogops-0.9}/backlogops.egg-info/requires.txt +3 -2
- {backlogops-0.7 → backlogops-0.9}/setup.py +4 -3
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/__init__.py +22 -8
- backlogops-0.9/src/backlogops/config_descriptions.py +366 -0
- backlogops-0.9/src/backlogops/config_editing.py +149 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/config_file_io.py +42 -15
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_io_config.py +14 -9
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/levels.py +6 -3
- {backlogops-0.7 → backlogops-0.9}/LICENSE.txt +0 -0
- {backlogops-0.7 → backlogops-0.9}/backlogops.egg-info/dependency_links.txt +0 -0
- {backlogops-0.7 → backlogops-0.9}/backlogops.egg-info/top_level.txt +0 -0
- {backlogops-0.7 → backlogops-0.9}/pyproject.toml +0 -0
- {backlogops-0.7 → backlogops-0.9}/setup.cfg +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/apply_format_rules.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/available_teams.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/available_teams_config.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/backlog.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/backlog_helpers.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/backlog_in_release_order.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/backlog_ops_config.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/backlog_ops_wizard.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/backlog_releases.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/backlog_releases_io.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/blo_version_reporter.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/date_ranges.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/demo_backlog.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/estimate_ready_date.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/format_rules.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/io_config.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/io_preset_wizard.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_connect.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_order_releases.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_rank_backlog.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_rank_by_keys.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_rank_move_keys.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_read.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_rename_releases.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_search.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_token.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_update_backlog.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_update_releases.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_wizard.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_write.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_write_fields.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_write_format.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/jira_write_releases.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/key_list_io.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/move_keys_first.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/name_list_io.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/no_text_io.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/order_by_dependencies.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/person.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/py.typed +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/release_backlog_updates.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/release_change_io.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/releases.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/rename_list_io.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/table_create.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/table_rows.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/team.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/wizard_forms.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/wizard_helpers.py +0 -0
- {backlogops-0.7 → backlogops-0.9}/src/backlogops/wizard_navigator.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
321
|
-
- Build and test using Python 3.14.
|
|
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:
|
|
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.
|
|
286
|
-
- Build and test using Python 3.14.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
321
|
-
- Build and test using Python 3.14.
|
|
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
|
|
@@ -5,7 +5,7 @@ from setuptools import setup
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name='backlogops',
|
|
8
|
-
version='0.
|
|
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.
|
|
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.
|
|
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.
|
|
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', '
|
|
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', '
|
|
120
|
+
'DEF_STATUS_INPUT_MAP', 'DependencyMode', 'EDIT_SETTINGS',
|
|
121
|
+
'ExceptionWorkHours',
|
|
114
122
|
'ExistsInJiraError', 'FailedItem', 'FailedLink', 'FailedRelease',
|
|
115
123
|
'FailedRename', 'FileExistsCb', 'FormatRules', 'FteException',
|
|
116
|
-
'
|
|
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', '
|
|
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', '
|
|
134
|
-
'
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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,
|
|
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
|
-
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
"""
|
|
16
|
+
"""NUMERIC is the level number, NAME its name, BOTH a column of each.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|