stint 0.1.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stint/__init__.py +110 -0
- stint/autogen/__init__.py +25 -0
- stint/autogen/desired.py +351 -0
- stint/autogen/diff.py +1001 -0
- stint/autogen/emit.py +578 -0
- stint/autogen/loader.py +48 -0
- stint/autogen/stamp.py +272 -0
- stint/cli/__init__.py +5 -0
- stint/cli/app.py +25 -0
- stint/cli/cmd_reflect.py +123 -0
- stint/cli/cmd_revision.py +235 -0
- stint/cli/cmd_stamp.py +89 -0
- stint/cli/cmd_upgrade.py +165 -0
- stint/cli/cmd_validate.py +48 -0
- stint/cli/env_config.py +137 -0
- stint/cli/main.py +28 -0
- stint/client/__init__.py +6 -0
- stint/client/auth.py +67 -0
- stint/client/http.py +131 -0
- stint/dialects/__init__.py +8 -0
- stint/dialects/base.py +33 -0
- stint/dialects/jira/__init__.py +5 -0
- stint/dialects/jira/_base.py +883 -0
- stint/dialects/jira/cloud.py +158 -0
- stint/dialects/jira/common.py +245 -0
- stint/engine.py +89 -0
- stint/exceptions.py +90 -0
- stint/fields.py +83 -0
- stint/migrations/__init__.py +40 -0
- stint/migrations/base.py +148 -0
- stint/migrations/context.py +52 -0
- stint/migrations/exceptions.py +24 -0
- stint/migrations/loader.py +62 -0
- stint/migrations/op.py +889 -0
- stint/migrations/runner.py +167 -0
- stint/py.typed +0 -0
- stint/query/__init__.py +27 -0
- stint/query/adf.py +28 -0
- stint/query/columns.py +47 -0
- stint/query/expr.py +198 -0
- stint/query/hydrate.py +146 -0
- stint/query/payload.py +206 -0
- stint/query/select.py +64 -0
- stint/query/session.py +282 -0
- stint/query/sync.py +103 -0
- stint/registry.py +81 -0
- stint/schema/__init__.py +18 -0
- stint/schema/_meta.py +49 -0
- stint/schema/field_config.py +29 -0
- stint/schema/issuetype.py +177 -0
- stint/schema/project.py +91 -0
- stint/schema/screen.py +53 -0
- stint/state/__init__.py +6 -0
- stint/state/file.py +187 -0
- stint/state/lock.py +82 -0
- stint/state/snapshot.py +160 -0
- stint/validate.py +73 -0
- stint-0.1.0a0.dist-info/METADATA +268 -0
- stint-0.1.0a0.dist-info/RECORD +63 -0
- stint-0.1.0a0.dist-info/WHEEL +5 -0
- stint-0.1.0a0.dist-info/entry_points.txt +2 -0
- stint-0.1.0a0.dist-info/licenses/LICENSE +21 -0
- stint-0.1.0a0.dist-info/top_level.txt +1 -0
stint/__init__.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""stint - declarative schema-as-code and ORM for work-management systems.
|
|
2
|
+
|
|
3
|
+
Define projects, issuetypes, and custom fields as Pydantic models. Apply that
|
|
4
|
+
definition to a target Jira instance, mirror it across environments, then
|
|
5
|
+
insert and query issues through the same models. Jira is the first dialect.
|
|
6
|
+
The dialect protocol is the extension point for additional backends.
|
|
7
|
+
|
|
8
|
+
M0: schema-plane declarative classes (IssueType, Project, CustomField, Screen,
|
|
9
|
+
ScreenScheme, FieldConfiguration). Validation only, no I/O.
|
|
10
|
+
|
|
11
|
+
M1 first slice: dialect protocol, engine factory, HTTP client with PAT auth,
|
|
12
|
+
Jira DC dialect with /serverInfo detection and /field reflection, YAML state
|
|
13
|
+
file load/save.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from stint import migrations
|
|
17
|
+
from stint.client.auth import APITokenAuth, BasicAuth, PATAuth
|
|
18
|
+
from stint.engine import Engine, create_engine
|
|
19
|
+
from stint.exceptions import (
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
NotFoundError,
|
|
23
|
+
PartialCommitError,
|
|
24
|
+
PermissionError,
|
|
25
|
+
ReflectionError,
|
|
26
|
+
RegistryError,
|
|
27
|
+
StateFileCorruptError,
|
|
28
|
+
StateFileError,
|
|
29
|
+
StintError,
|
|
30
|
+
TransportError,
|
|
31
|
+
UnsupportedTMPOpError,
|
|
32
|
+
)
|
|
33
|
+
from stint.fields import (
|
|
34
|
+
CustomField,
|
|
35
|
+
DateField,
|
|
36
|
+
DateTimeField,
|
|
37
|
+
MultiSelectField,
|
|
38
|
+
NumberField,
|
|
39
|
+
SelectField,
|
|
40
|
+
TextAreaField,
|
|
41
|
+
TextField,
|
|
42
|
+
UserField,
|
|
43
|
+
)
|
|
44
|
+
from stint.migrations import Migration, MigrationContext, get_context, load_migrations, op
|
|
45
|
+
from stint.query import AsyncSession, CommitResult, Session, and_, not_, or_, select
|
|
46
|
+
from stint.registry import registry
|
|
47
|
+
from stint.schema import FieldConfiguration, IssueType, Project, Screen, ScreenScheme
|
|
48
|
+
from stint.state import (
|
|
49
|
+
CustomFieldSnapshot,
|
|
50
|
+
ServerInfoSnapshot,
|
|
51
|
+
Snapshot,
|
|
52
|
+
StateFile,
|
|
53
|
+
)
|
|
54
|
+
from stint.validate import validate, validate_or_raise
|
|
55
|
+
|
|
56
|
+
__version__ = "0.1.0a0"
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"APITokenAuth",
|
|
60
|
+
"AsyncSession",
|
|
61
|
+
"AuthenticationError",
|
|
62
|
+
"BasicAuth",
|
|
63
|
+
"CommitResult",
|
|
64
|
+
"ConfigurationError",
|
|
65
|
+
"CustomField",
|
|
66
|
+
"CustomFieldSnapshot",
|
|
67
|
+
"DateField",
|
|
68
|
+
"DateTimeField",
|
|
69
|
+
"Engine",
|
|
70
|
+
"FieldConfiguration",
|
|
71
|
+
"IssueType",
|
|
72
|
+
"Migration",
|
|
73
|
+
"MigrationContext",
|
|
74
|
+
"MultiSelectField",
|
|
75
|
+
"NotFoundError",
|
|
76
|
+
"NumberField",
|
|
77
|
+
"PATAuth",
|
|
78
|
+
"PartialCommitError",
|
|
79
|
+
"StintError",
|
|
80
|
+
"PermissionError",
|
|
81
|
+
"Project",
|
|
82
|
+
"ReflectionError",
|
|
83
|
+
"RegistryError",
|
|
84
|
+
"Screen",
|
|
85
|
+
"ScreenScheme",
|
|
86
|
+
"SelectField",
|
|
87
|
+
"Session",
|
|
88
|
+
"ServerInfoSnapshot",
|
|
89
|
+
"Snapshot",
|
|
90
|
+
"StateFile",
|
|
91
|
+
"StateFileCorruptError",
|
|
92
|
+
"StateFileError",
|
|
93
|
+
"TextAreaField",
|
|
94
|
+
"TextField",
|
|
95
|
+
"TransportError",
|
|
96
|
+
"UnsupportedTMPOpError",
|
|
97
|
+
"UserField",
|
|
98
|
+
"and_",
|
|
99
|
+
"create_engine",
|
|
100
|
+
"get_context",
|
|
101
|
+
"load_migrations",
|
|
102
|
+
"migrations",
|
|
103
|
+
"not_",
|
|
104
|
+
"op",
|
|
105
|
+
"or_",
|
|
106
|
+
"registry",
|
|
107
|
+
"select",
|
|
108
|
+
"validate",
|
|
109
|
+
"validate_or_raise",
|
|
110
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Migration autogeneration.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
- autogenerate(schema_module, state, snapshot, *, message, allow_delete=False)
|
|
5
|
+
-> str (Python source of the new migration file)
|
|
6
|
+
- render_empty(message, *, parents) -> str (skeleton with no ops)
|
|
7
|
+
- render_merge(message, *, parents) -> str (merge-only skeleton)
|
|
8
|
+
- new_revision_id() -> str
|
|
9
|
+
- new_filename(message, when=None) -> str
|
|
10
|
+
- stamp(schema_module, state, snapshot) -> StampReport
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from stint.autogen.emit import (
|
|
14
|
+
new_filename,
|
|
15
|
+
new_revision_id,
|
|
16
|
+
render_empty,
|
|
17
|
+
render_merge,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"new_filename",
|
|
22
|
+
"new_revision_id",
|
|
23
|
+
"render_empty",
|
|
24
|
+
"render_merge",
|
|
25
|
+
]
|
stint/autogen/desired.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Build a DesiredSnapshot from the schema registry.
|
|
2
|
+
|
|
3
|
+
The desired snapshot has the same shape as the reflected Snapshot but is built
|
|
4
|
+
from Pydantic class declarations rather than HTTP reflection. The diff
|
|
5
|
+
algorithm compares the two.
|
|
6
|
+
|
|
7
|
+
ITSS and FCS are derived per-project: each Project gets one synthesized ITSS
|
|
8
|
+
(alias `{project_key}_itss`) and one synthesized FCS (alias `{project_key}_fcs`),
|
|
9
|
+
with `default` mapped to the first issuetype's screen scheme / field config.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from stint.fields import CustomField
|
|
18
|
+
from stint.registry import registry as default_registry
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from stint.registry import Registry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class DesiredCustomField:
|
|
26
|
+
alias: str
|
|
27
|
+
name: str
|
|
28
|
+
type_id: str
|
|
29
|
+
description: str
|
|
30
|
+
options: tuple[str, ...] # ordered
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class DesiredIssueType:
|
|
35
|
+
alias: str
|
|
36
|
+
name: str
|
|
37
|
+
description: str
|
|
38
|
+
subtask: bool = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class DesiredScreenTab:
|
|
43
|
+
"""One tab per screen for now: 'Fields'. Field order preserved."""
|
|
44
|
+
|
|
45
|
+
name: str
|
|
46
|
+
field_refs: tuple[str, ...] # system field names OR custom-field aliases
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class DesiredScreen:
|
|
51
|
+
alias: str
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
tabs: tuple[DesiredScreenTab, ...]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class DesiredScreenScheme:
|
|
59
|
+
alias: str
|
|
60
|
+
name: str
|
|
61
|
+
description: str
|
|
62
|
+
screens: dict[str, str] # op ("default","create","edit","view") -> screen alias
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class DesiredFieldConfigurationItem:
|
|
67
|
+
field_alias: str
|
|
68
|
+
required: bool = False
|
|
69
|
+
hidden: bool = False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class DesiredFieldConfiguration:
|
|
74
|
+
alias: str
|
|
75
|
+
name: str
|
|
76
|
+
description: str
|
|
77
|
+
items: tuple[DesiredFieldConfigurationItem, ...]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class DesiredIssueTypeScreenScheme:
|
|
82
|
+
alias: str
|
|
83
|
+
name: str
|
|
84
|
+
description: str
|
|
85
|
+
# issuetype alias (or "default") -> screen-scheme alias
|
|
86
|
+
mappings: dict[str, str]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class DesiredIssueTypeScheme:
|
|
91
|
+
"""An IssueTypeScheme: the set of issuetypes available in a project. Stint
|
|
92
|
+
synthesizes one per Project from its ``__issuetypes__`` list."""
|
|
93
|
+
|
|
94
|
+
alias: str
|
|
95
|
+
name: str
|
|
96
|
+
description: str
|
|
97
|
+
issuetypes: tuple[str, ...] # ordered issuetype aliases
|
|
98
|
+
default_issuetype: str # one of the entries in ``issuetypes``
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class DesiredFieldConfigurationScheme:
|
|
103
|
+
alias: str
|
|
104
|
+
name: str
|
|
105
|
+
description: str
|
|
106
|
+
# issuetype alias (or "default") -> field-configuration alias
|
|
107
|
+
mappings: dict[str, str]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class DesiredProject:
|
|
112
|
+
alias: str # uses project key
|
|
113
|
+
key: str
|
|
114
|
+
name: str
|
|
115
|
+
project_type_key: str
|
|
116
|
+
lead: str
|
|
117
|
+
description: str
|
|
118
|
+
style: str # "company-managed" or "team-managed"
|
|
119
|
+
issuetype_scheme: str | None # alias
|
|
120
|
+
issuetype_screen_scheme: str | None # alias
|
|
121
|
+
field_configuration_scheme: str | None # alias
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class DesiredSnapshot:
|
|
126
|
+
custom_fields: dict[str, DesiredCustomField] = field(default_factory=dict)
|
|
127
|
+
issuetypes: dict[str, DesiredIssueType] = field(default_factory=dict)
|
|
128
|
+
screens: dict[str, DesiredScreen] = field(default_factory=dict)
|
|
129
|
+
screen_schemes: dict[str, DesiredScreenScheme] = field(default_factory=dict)
|
|
130
|
+
issuetype_schemes: dict[str, DesiredIssueTypeScheme] = field(default_factory=dict)
|
|
131
|
+
issuetype_screen_schemes: dict[str, DesiredIssueTypeScreenScheme] = field(
|
|
132
|
+
default_factory=dict,
|
|
133
|
+
)
|
|
134
|
+
field_configurations: dict[str, DesiredFieldConfiguration] = field(
|
|
135
|
+
default_factory=dict,
|
|
136
|
+
)
|
|
137
|
+
field_configuration_schemes: dict[str, DesiredFieldConfigurationScheme] = field(
|
|
138
|
+
default_factory=dict,
|
|
139
|
+
)
|
|
140
|
+
projects: dict[str, DesiredProject] = field(default_factory=dict)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
SYSTEM_FIELD_NAMES = {
|
|
144
|
+
"Summary",
|
|
145
|
+
"Description",
|
|
146
|
+
"Reporter",
|
|
147
|
+
"Assignee",
|
|
148
|
+
"Priority",
|
|
149
|
+
"Created",
|
|
150
|
+
"Updated",
|
|
151
|
+
"Status",
|
|
152
|
+
"Resolution",
|
|
153
|
+
"Labels",
|
|
154
|
+
"Components",
|
|
155
|
+
"Fix Version/s",
|
|
156
|
+
"Affects Version/s",
|
|
157
|
+
"Due Date",
|
|
158
|
+
"Environment",
|
|
159
|
+
"Issue Type",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def build_desired_snapshot(registry: Registry | None = None) -> DesiredSnapshot:
|
|
164
|
+
"""Walk the registry and assemble a complete desired snapshot.
|
|
165
|
+
|
|
166
|
+
The default registry is the process-wide one populated by class definition.
|
|
167
|
+
Pass an explicit registry for isolation in tests.
|
|
168
|
+
"""
|
|
169
|
+
reg = registry or default_registry
|
|
170
|
+
desired = DesiredSnapshot()
|
|
171
|
+
|
|
172
|
+
# ── Custom fields ────────────────────────────────────────────────
|
|
173
|
+
for alias, cf in reg.custom_fields.items():
|
|
174
|
+
desired.custom_fields[alias] = DesiredCustomField(
|
|
175
|
+
alias=alias,
|
|
176
|
+
name=cf.name,
|
|
177
|
+
type_id=cf.type.jira_type_id,
|
|
178
|
+
description=cf.description,
|
|
179
|
+
options=tuple(cf.options),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# ── Issue types ──────────────────────────────────────────────────
|
|
183
|
+
for alias, it_cls in reg.issuetypes.items():
|
|
184
|
+
desired.issuetypes[alias] = DesiredIssueType(
|
|
185
|
+
alias=alias,
|
|
186
|
+
name=getattr(it_cls, "__title__", alias),
|
|
187
|
+
description=getattr(it_cls, "__description__", "") or "",
|
|
188
|
+
subtask=getattr(it_cls, "__subtask__", False),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# ── Screens (one tab per screen: "Fields") ──────────────────────
|
|
192
|
+
for alias, scr in reg.screens.items():
|
|
193
|
+
field_refs: list[str] = []
|
|
194
|
+
for ref in scr.fields:
|
|
195
|
+
if isinstance(ref, CustomField):
|
|
196
|
+
field_refs.append(ref.alias) # marker: resolve via state at apply time
|
|
197
|
+
else:
|
|
198
|
+
field_refs.append(str(ref)) # system field
|
|
199
|
+
desired.screens[alias] = DesiredScreen(
|
|
200
|
+
alias=alias,
|
|
201
|
+
name=scr.name,
|
|
202
|
+
description=scr.description,
|
|
203
|
+
tabs=(DesiredScreenTab(name="Fields", field_refs=tuple(field_refs)),),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# ── Screen schemes ───────────────────────────────────────────────
|
|
207
|
+
for alias, ss in reg.screen_schemes.items():
|
|
208
|
+
# Map the user's create/edit/view to Jira's default/create/edit/view.
|
|
209
|
+
# Default falls back to view (matches Jira's "no override" behavior).
|
|
210
|
+
screens = {
|
|
211
|
+
"default": ss.view.alias,
|
|
212
|
+
"create": ss.create.alias,
|
|
213
|
+
"edit": ss.edit.alias,
|
|
214
|
+
"view": ss.view.alias,
|
|
215
|
+
}
|
|
216
|
+
desired.screen_schemes[alias] = DesiredScreenScheme(
|
|
217
|
+
alias=alias,
|
|
218
|
+
name=ss.name,
|
|
219
|
+
description=ss.description,
|
|
220
|
+
screens=screens,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# ── Field configurations ─────────────────────────────────────────
|
|
224
|
+
for alias, fc in reg.field_configurations.items():
|
|
225
|
+
items: list[DesiredFieldConfigurationItem] = []
|
|
226
|
+
for ref in fc.required:
|
|
227
|
+
field_alias = ref.alias if isinstance(ref, CustomField) else str(ref)
|
|
228
|
+
items.append(
|
|
229
|
+
DesiredFieldConfigurationItem(
|
|
230
|
+
field_alias=field_alias,
|
|
231
|
+
required=True,
|
|
232
|
+
hidden=False,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
for ref in fc.hidden:
|
|
236
|
+
field_alias = ref.alias if isinstance(ref, CustomField) else str(ref)
|
|
237
|
+
items.append(
|
|
238
|
+
DesiredFieldConfigurationItem(
|
|
239
|
+
field_alias=field_alias,
|
|
240
|
+
required=False,
|
|
241
|
+
hidden=True,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
desired.field_configurations[alias] = DesiredFieldConfiguration(
|
|
245
|
+
alias=alias,
|
|
246
|
+
name=fc.name,
|
|
247
|
+
description=fc.description,
|
|
248
|
+
items=tuple(items),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# ── Projects + derived IssueTypeScheme / ITSS / FCS ──────────────
|
|
252
|
+
for project_key, proj_cls in reg.projects.items():
|
|
253
|
+
issuetypes = list(getattr(proj_cls, "__issuetypes__", []))
|
|
254
|
+
its_alias = _derive_its(desired, project_key, issuetypes)
|
|
255
|
+
itss_alias = _derive_itss(desired, project_key, issuetypes)
|
|
256
|
+
fcs_alias = _derive_fcs(desired, project_key, issuetypes)
|
|
257
|
+
desired.projects[project_key] = DesiredProject(
|
|
258
|
+
alias=project_key,
|
|
259
|
+
key=project_key,
|
|
260
|
+
name=getattr(proj_cls, "__title__", project_key),
|
|
261
|
+
project_type_key=getattr(proj_cls, "__project_type__", "software"),
|
|
262
|
+
lead=getattr(proj_cls, "__lead__", "") or "",
|
|
263
|
+
description=getattr(proj_cls, "__description__", "") or "",
|
|
264
|
+
style=getattr(proj_cls, "__style__", "company-managed"),
|
|
265
|
+
issuetype_scheme=its_alias,
|
|
266
|
+
issuetype_screen_scheme=itss_alias,
|
|
267
|
+
field_configuration_scheme=fcs_alias,
|
|
268
|
+
)
|
|
269
|
+
return desired
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _derive_its(
|
|
273
|
+
desired: DesiredSnapshot,
|
|
274
|
+
project_key: str,
|
|
275
|
+
issuetypes: list,
|
|
276
|
+
) -> str | None:
|
|
277
|
+
"""Synthesize an IssueTypeScheme for the project from its ``__issuetypes__``
|
|
278
|
+
list. None if the project has no issuetypes (the project will fall through
|
|
279
|
+
to Jira's default scheme). Default issuetype is the first non-subtask
|
|
280
|
+
entry (Atlassian requires at least one standard issuetype in a scheme)."""
|
|
281
|
+
if not issuetypes:
|
|
282
|
+
return None
|
|
283
|
+
aliases = [it_cls.__alias__ for it_cls in issuetypes]
|
|
284
|
+
standard = [it_cls.__alias__ for it_cls in issuetypes if not getattr(it_cls, "__subtask__", False)]
|
|
285
|
+
if not standard:
|
|
286
|
+
# Jira rejects schemes with only subtasks; skip derivation and let
|
|
287
|
+
# the project use Jira's default scheme.
|
|
288
|
+
return None
|
|
289
|
+
alias = f"{project_key}_its"
|
|
290
|
+
desired.issuetype_schemes[alias] = DesiredIssueTypeScheme(
|
|
291
|
+
alias=alias,
|
|
292
|
+
name=f"{project_key} Issue Type Scheme",
|
|
293
|
+
description=f"Auto-derived for project {project_key}",
|
|
294
|
+
issuetypes=tuple(aliases),
|
|
295
|
+
default_issuetype=standard[0],
|
|
296
|
+
)
|
|
297
|
+
return alias
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _derive_itss(
|
|
301
|
+
desired: DesiredSnapshot,
|
|
302
|
+
project_key: str,
|
|
303
|
+
issuetypes: list,
|
|
304
|
+
) -> str | None:
|
|
305
|
+
"""Synthesize an ITSS for the project. None if no issuetype carries a
|
|
306
|
+
screen scheme. Mappings: each issuetype's __screen_scheme__ is its mapping;
|
|
307
|
+
'default' = the first issuetype that carries one."""
|
|
308
|
+
typed: list[tuple[str, str]] = [] # (issuetype_alias, ss_alias)
|
|
309
|
+
for it_cls in issuetypes:
|
|
310
|
+
ss = getattr(it_cls, "__screen_scheme__", None)
|
|
311
|
+
if ss is not None:
|
|
312
|
+
typed.append((it_cls.__alias__, ss.alias))
|
|
313
|
+
if not typed:
|
|
314
|
+
return None
|
|
315
|
+
alias = f"{project_key}_itss"
|
|
316
|
+
mappings: dict[str, str] = {"default": typed[0][1]}
|
|
317
|
+
for it_alias, ss_alias in typed:
|
|
318
|
+
mappings[it_alias] = ss_alias
|
|
319
|
+
desired.issuetype_screen_schemes[alias] = DesiredIssueTypeScreenScheme(
|
|
320
|
+
alias=alias,
|
|
321
|
+
name=f"{project_key} Issue Type Screen Scheme",
|
|
322
|
+
description=f"Auto-derived for project {project_key}",
|
|
323
|
+
mappings=mappings,
|
|
324
|
+
)
|
|
325
|
+
return alias
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _derive_fcs(
|
|
329
|
+
desired: DesiredSnapshot,
|
|
330
|
+
project_key: str,
|
|
331
|
+
issuetypes: list,
|
|
332
|
+
) -> str | None:
|
|
333
|
+
"""Same shape as _derive_itss but for FieldConfiguration."""
|
|
334
|
+
typed: list[tuple[str, str]] = []
|
|
335
|
+
for it_cls in issuetypes:
|
|
336
|
+
fc = getattr(it_cls, "__field_configuration__", None)
|
|
337
|
+
if fc is not None:
|
|
338
|
+
typed.append((it_cls.__alias__, fc.alias))
|
|
339
|
+
if not typed:
|
|
340
|
+
return None
|
|
341
|
+
alias = f"{project_key}_fcs"
|
|
342
|
+
mappings: dict[str, str] = {"default": typed[0][1]}
|
|
343
|
+
for it_alias, fc_alias in typed:
|
|
344
|
+
mappings[it_alias] = fc_alias
|
|
345
|
+
desired.field_configuration_schemes[alias] = DesiredFieldConfigurationScheme(
|
|
346
|
+
alias=alias,
|
|
347
|
+
name=f"{project_key} Field Configuration Scheme",
|
|
348
|
+
description=f"Auto-derived for project {project_key}",
|
|
349
|
+
mappings=mappings,
|
|
350
|
+
)
|
|
351
|
+
return alias
|