elspais 0.11.1__py3-none-any.whl → 0.11.2__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.
- elspais/__init__.py +1 -1
- elspais/cli.py +29 -10
- elspais/commands/analyze.py +5 -6
- elspais/commands/changed.py +2 -6
- elspais/commands/config_cmd.py +4 -4
- elspais/commands/edit.py +32 -36
- elspais/commands/hash_cmd.py +24 -18
- elspais/commands/index.py +8 -7
- elspais/commands/init.py +4 -4
- elspais/commands/reformat_cmd.py +32 -43
- elspais/commands/rules_cmd.py +6 -2
- elspais/commands/trace.py +23 -19
- elspais/commands/validate.py +8 -10
- elspais/config/defaults.py +7 -1
- elspais/core/content_rules.py +0 -1
- elspais/core/git.py +4 -10
- elspais/core/parser.py +55 -56
- elspais/core/patterns.py +2 -6
- elspais/core/rules.py +10 -15
- elspais/mcp/__init__.py +2 -0
- elspais/mcp/context.py +1 -0
- elspais/mcp/serializers.py +1 -1
- elspais/mcp/server.py +54 -39
- elspais/reformat/__init__.py +13 -13
- elspais/reformat/detector.py +9 -16
- elspais/reformat/hierarchy.py +8 -7
- elspais/reformat/line_breaks.py +36 -38
- elspais/reformat/prompts.py +22 -12
- elspais/reformat/transformer.py +43 -41
- elspais/sponsors/__init__.py +0 -2
- elspais/testing/__init__.py +1 -1
- elspais/testing/result_parser.py +25 -21
- elspais/trace_view/__init__.py +4 -3
- elspais/trace_view/coverage.py +5 -5
- elspais/trace_view/generators/__init__.py +1 -1
- elspais/trace_view/generators/base.py +17 -12
- elspais/trace_view/generators/csv.py +2 -6
- elspais/trace_view/generators/markdown.py +3 -8
- elspais/trace_view/html/__init__.py +4 -2
- elspais/trace_view/html/generator.py +423 -289
- elspais/trace_view/models.py +25 -0
- elspais/trace_view/review/__init__.py +21 -18
- elspais/trace_view/review/branches.py +114 -121
- elspais/trace_view/review/models.py +232 -237
- elspais/trace_view/review/position.py +53 -71
- elspais/trace_view/review/server.py +264 -288
- elspais/trace_view/review/status.py +43 -58
- elspais/trace_view/review/storage.py +48 -72
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/METADATA +1 -1
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/RECORD +53 -53
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/WHEEL +0 -0
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/entry_points.txt +0 -0
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -21,19 +21,20 @@ IMPLEMENTS REQUIREMENTS:
|
|
|
21
21
|
|
|
22
22
|
import re
|
|
23
23
|
import uuid
|
|
24
|
-
from dataclasses import dataclass, field
|
|
24
|
+
from dataclasses import asdict, dataclass, field
|
|
25
25
|
from datetime import datetime, timezone
|
|
26
26
|
from enum import Enum
|
|
27
27
|
from typing import Any, Dict, List, Optional, Tuple
|
|
28
28
|
|
|
29
|
-
|
|
30
29
|
# =============================================================================
|
|
31
30
|
# Enums and Constants
|
|
32
31
|
# REQ-tv-d00010-B: String enums for JSON compatibility
|
|
33
32
|
# =============================================================================
|
|
34
33
|
|
|
34
|
+
|
|
35
35
|
class PositionType(str, Enum):
|
|
36
36
|
"""Type of comment position anchor"""
|
|
37
|
+
|
|
37
38
|
LINE = "line"
|
|
38
39
|
BLOCK = "block"
|
|
39
40
|
WORD = "word"
|
|
@@ -42,6 +43,7 @@ class PositionType(str, Enum):
|
|
|
42
43
|
|
|
43
44
|
class RequestState(str, Enum):
|
|
44
45
|
"""State of a status change request"""
|
|
46
|
+
|
|
45
47
|
PENDING = "pending"
|
|
46
48
|
APPROVED = "approved"
|
|
47
49
|
REJECTED = "rejected"
|
|
@@ -50,6 +52,7 @@ class RequestState(str, Enum):
|
|
|
50
52
|
|
|
51
53
|
class ApprovalDecision(str, Enum):
|
|
52
54
|
"""Approval decision type"""
|
|
55
|
+
|
|
53
56
|
APPROVE = "approve"
|
|
54
57
|
REJECT = "reject"
|
|
55
58
|
|
|
@@ -61,7 +64,7 @@ VALID_REQ_STATUSES = {"Draft", "Active", "Deprecated"}
|
|
|
61
64
|
DEFAULT_APPROVAL_RULES: Dict[str, List[str]] = {
|
|
62
65
|
"Draft->Active": ["product_owner", "tech_lead"],
|
|
63
66
|
"Active->Deprecated": ["product_owner"],
|
|
64
|
-
"Draft->Deprecated": ["product_owner"]
|
|
67
|
+
"Draft->Deprecated": ["product_owner"],
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
|
|
@@ -69,6 +72,7 @@ DEFAULT_APPROVAL_RULES: Dict[str, List[str]] = {
|
|
|
69
72
|
# Utility Functions
|
|
70
73
|
# =============================================================================
|
|
71
74
|
|
|
75
|
+
|
|
72
76
|
def generate_uuid() -> str:
|
|
73
77
|
"""Generate a new UUID string"""
|
|
74
78
|
return str(uuid.uuid4())
|
|
@@ -86,8 +90,8 @@ def now_iso() -> str:
|
|
|
86
90
|
def parse_iso_datetime(iso_str: str) -> datetime:
|
|
87
91
|
"""Parse ISO 8601 datetime string to datetime object"""
|
|
88
92
|
# Handle both with and without timezone, and Z suffix
|
|
89
|
-
if iso_str.endswith(
|
|
90
|
-
iso_str = iso_str[:-1] +
|
|
93
|
+
if iso_str.endswith("Z"):
|
|
94
|
+
iso_str = iso_str[:-1] + "+00:00"
|
|
91
95
|
return datetime.fromisoformat(iso_str)
|
|
92
96
|
|
|
93
97
|
|
|
@@ -104,7 +108,7 @@ def validate_req_id(req_id: str) -> bool:
|
|
|
104
108
|
if not req_id:
|
|
105
109
|
return False
|
|
106
110
|
# Negative lookahead to reject REQ- prefix; only sponsor prefixes allowed
|
|
107
|
-
pattern = r
|
|
111
|
+
pattern = r"^(?!REQ-)(?:[A-Z]{2,4}-)?[pod]\d{5}$"
|
|
108
112
|
return bool(re.match(pattern, req_id))
|
|
109
113
|
|
|
110
114
|
|
|
@@ -112,7 +116,7 @@ def validate_hash(hash_value: str) -> bool:
|
|
|
112
116
|
"""Validate 8-character hex hash format"""
|
|
113
117
|
if not hash_value:
|
|
114
118
|
return False
|
|
115
|
-
return bool(re.match(r
|
|
119
|
+
return bool(re.match(r"^[a-fA-F0-9]{8}$", hash_value))
|
|
116
120
|
|
|
117
121
|
|
|
118
122
|
# =============================================================================
|
|
@@ -123,6 +127,7 @@ def validate_hash(hash_value: str) -> bool:
|
|
|
123
127
|
# REQ-tv-d00010-E: Each dataclass implements validate()
|
|
124
128
|
# =============================================================================
|
|
125
129
|
|
|
130
|
+
|
|
126
131
|
@dataclass
|
|
127
132
|
class CommentPosition:
|
|
128
133
|
"""
|
|
@@ -138,6 +143,7 @@ class CommentPosition:
|
|
|
138
143
|
|
|
139
144
|
The hashWhenCreated allows detection of content drift.
|
|
140
145
|
"""
|
|
146
|
+
|
|
141
147
|
type: str # PositionType value as string for JSON compatibility
|
|
142
148
|
hashWhenCreated: str # 8-char REQ hash when comment was created
|
|
143
149
|
lineNumber: Optional[int] = None
|
|
@@ -152,46 +158,46 @@ class CommentPosition:
|
|
|
152
158
|
self.type = self.type.value
|
|
153
159
|
|
|
154
160
|
@classmethod
|
|
155
|
-
def create_line(
|
|
156
|
-
|
|
161
|
+
def create_line(
|
|
162
|
+
cls, hash_value: str, line_number: int, context: Optional[str] = None
|
|
163
|
+
) -> "CommentPosition":
|
|
157
164
|
"""Factory for line-anchored position"""
|
|
158
165
|
return cls(
|
|
159
166
|
type=PositionType.LINE.value,
|
|
160
167
|
hashWhenCreated=hash_value,
|
|
161
168
|
lineNumber=line_number,
|
|
162
|
-
fallbackContext=context
|
|
169
|
+
fallbackContext=context,
|
|
163
170
|
)
|
|
164
171
|
|
|
165
172
|
@classmethod
|
|
166
|
-
def create_block(
|
|
167
|
-
|
|
173
|
+
def create_block(
|
|
174
|
+
cls, hash_value: str, start_line: int, end_line: int, context: Optional[str] = None
|
|
175
|
+
) -> "CommentPosition":
|
|
168
176
|
"""Factory for block-anchored position"""
|
|
169
177
|
return cls(
|
|
170
178
|
type=PositionType.BLOCK.value,
|
|
171
179
|
hashWhenCreated=hash_value,
|
|
172
180
|
lineRange=(start_line, end_line),
|
|
173
|
-
fallbackContext=context
|
|
181
|
+
fallbackContext=context,
|
|
174
182
|
)
|
|
175
183
|
|
|
176
184
|
@classmethod
|
|
177
|
-
def create_word(
|
|
178
|
-
|
|
185
|
+
def create_word(
|
|
186
|
+
cls, hash_value: str, keyword: str, occurrence: int = 1, context: Optional[str] = None
|
|
187
|
+
) -> "CommentPosition":
|
|
179
188
|
"""Factory for word-anchored position"""
|
|
180
189
|
return cls(
|
|
181
190
|
type=PositionType.WORD.value,
|
|
182
191
|
hashWhenCreated=hash_value,
|
|
183
192
|
keyword=keyword,
|
|
184
193
|
keywordOccurrence=occurrence,
|
|
185
|
-
fallbackContext=context
|
|
194
|
+
fallbackContext=context,
|
|
186
195
|
)
|
|
187
196
|
|
|
188
197
|
@classmethod
|
|
189
|
-
def create_general(cls, hash_value: str) ->
|
|
198
|
+
def create_general(cls, hash_value: str) -> "CommentPosition":
|
|
190
199
|
"""Factory for general (whole REQ) position"""
|
|
191
|
-
return cls(
|
|
192
|
-
type=PositionType.GENERAL.value,
|
|
193
|
-
hashWhenCreated=hash_value
|
|
194
|
-
)
|
|
200
|
+
return cls(type=PositionType.GENERAL.value, hashWhenCreated=hash_value)
|
|
195
201
|
|
|
196
202
|
def validate(self) -> Tuple[bool, List[str]]:
|
|
197
203
|
"""
|
|
@@ -236,40 +242,37 @@ class CommentPosition:
|
|
|
236
242
|
|
|
237
243
|
REQ-tv-d00010-C: Returns JSON-serializable dict.
|
|
238
244
|
"""
|
|
239
|
-
result: Dict[str, Any] = {
|
|
240
|
-
'type': self.type,
|
|
241
|
-
'hashWhenCreated': self.hashWhenCreated
|
|
242
|
-
}
|
|
245
|
+
result: Dict[str, Any] = {"type": self.type, "hashWhenCreated": self.hashWhenCreated}
|
|
243
246
|
if self.lineNumber is not None:
|
|
244
|
-
result[
|
|
247
|
+
result["lineNumber"] = self.lineNumber
|
|
245
248
|
if self.lineRange is not None:
|
|
246
|
-
result[
|
|
249
|
+
result["lineRange"] = list(self.lineRange) # Tuple to list for JSON
|
|
247
250
|
if self.keyword is not None:
|
|
248
|
-
result[
|
|
251
|
+
result["keyword"] = self.keyword
|
|
249
252
|
if self.keywordOccurrence is not None:
|
|
250
|
-
result[
|
|
253
|
+
result["keywordOccurrence"] = self.keywordOccurrence
|
|
251
254
|
if self.fallbackContext is not None:
|
|
252
|
-
result[
|
|
255
|
+
result["fallbackContext"] = self.fallbackContext
|
|
253
256
|
return result
|
|
254
257
|
|
|
255
258
|
@classmethod
|
|
256
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
259
|
+
def from_dict(cls, data: Dict[str, Any]) -> "CommentPosition":
|
|
257
260
|
"""
|
|
258
261
|
Create from dictionary (JSON deserialization).
|
|
259
262
|
|
|
260
263
|
REQ-tv-d00010-D: Deserializes from dictionaries.
|
|
261
264
|
"""
|
|
262
|
-
line_range = data.get(
|
|
265
|
+
line_range = data.get("lineRange")
|
|
263
266
|
if line_range is not None:
|
|
264
267
|
line_range = tuple(line_range) # List to tuple
|
|
265
268
|
return cls(
|
|
266
|
-
type=data[
|
|
267
|
-
hashWhenCreated=data[
|
|
268
|
-
lineNumber=data.get(
|
|
269
|
+
type=data["type"],
|
|
270
|
+
hashWhenCreated=data["hashWhenCreated"],
|
|
271
|
+
lineNumber=data.get("lineNumber"),
|
|
269
272
|
lineRange=line_range,
|
|
270
|
-
keyword=data.get(
|
|
271
|
-
keywordOccurrence=data.get(
|
|
272
|
-
fallbackContext=data.get(
|
|
273
|
+
keyword=data.get("keyword"),
|
|
274
|
+
keywordOccurrence=data.get("keywordOccurrence"),
|
|
275
|
+
fallbackContext=data.get("fallbackContext"),
|
|
273
276
|
)
|
|
274
277
|
|
|
275
278
|
|
|
@@ -281,25 +284,21 @@ class Comment:
|
|
|
281
284
|
Comments are immutable once created - edits create new comments
|
|
282
285
|
with references to the original.
|
|
283
286
|
"""
|
|
287
|
+
|
|
284
288
|
id: str # UUID
|
|
285
289
|
author: str # Username
|
|
286
290
|
timestamp: str # ISO 8601 datetime
|
|
287
291
|
body: str # Markdown content
|
|
288
292
|
|
|
289
293
|
@classmethod
|
|
290
|
-
def create(cls, author: str, body: str) ->
|
|
294
|
+
def create(cls, author: str, body: str) -> "Comment":
|
|
291
295
|
"""
|
|
292
296
|
Factory for creating new comment with auto-generated fields.
|
|
293
297
|
|
|
294
298
|
REQ-tv-d00010-F: Auto-generates IDs and timestamps.
|
|
295
299
|
REQ-tv-d00010-J: Uses UTC timestamps.
|
|
296
300
|
"""
|
|
297
|
-
return cls(
|
|
298
|
-
id=generate_uuid(),
|
|
299
|
-
author=author,
|
|
300
|
-
timestamp=now_iso(),
|
|
301
|
-
body=body
|
|
302
|
-
)
|
|
301
|
+
return cls(id=generate_uuid(), author=author, timestamp=now_iso(), body=body)
|
|
303
302
|
|
|
304
303
|
def validate(self) -> Tuple[bool, List[str]]:
|
|
305
304
|
"""Validate comment fields"""
|
|
@@ -328,13 +327,10 @@ class Comment:
|
|
|
328
327
|
return asdict(self)
|
|
329
328
|
|
|
330
329
|
@classmethod
|
|
331
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
330
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Comment":
|
|
332
331
|
"""Create from dictionary"""
|
|
333
332
|
return cls(
|
|
334
|
-
id=data[
|
|
335
|
-
author=data['author'],
|
|
336
|
-
timestamp=data['timestamp'],
|
|
337
|
-
body=data['body']
|
|
333
|
+
id=data["id"], author=data["author"], timestamp=data["timestamp"], body=data["body"]
|
|
338
334
|
)
|
|
339
335
|
|
|
340
336
|
|
|
@@ -348,6 +344,7 @@ class Thread:
|
|
|
348
344
|
|
|
349
345
|
REQ-d00094-A: Thread model with packageId for package ownership.
|
|
350
346
|
"""
|
|
347
|
+
|
|
351
348
|
threadId: str # UUID
|
|
352
349
|
reqId: str # Requirement ID (e.g., "d00027")
|
|
353
350
|
createdBy: str # Username who started thread
|
|
@@ -361,9 +358,14 @@ class Thread:
|
|
|
361
358
|
packageId: Optional[str] = None
|
|
362
359
|
|
|
363
360
|
@classmethod
|
|
364
|
-
def create(
|
|
365
|
-
|
|
366
|
-
|
|
361
|
+
def create(
|
|
362
|
+
cls,
|
|
363
|
+
req_id: str,
|
|
364
|
+
creator: str,
|
|
365
|
+
position: CommentPosition,
|
|
366
|
+
initial_comment: Optional[str] = None,
|
|
367
|
+
package_id: Optional[str] = None,
|
|
368
|
+
) -> "Thread":
|
|
367
369
|
"""
|
|
368
370
|
Factory for creating new thread.
|
|
369
371
|
|
|
@@ -383,7 +385,7 @@ class Thread:
|
|
|
383
385
|
createdBy=creator,
|
|
384
386
|
createdAt=now_iso(),
|
|
385
387
|
position=position,
|
|
386
|
-
packageId=package_id
|
|
388
|
+
packageId=package_id,
|
|
387
389
|
)
|
|
388
390
|
if initial_comment:
|
|
389
391
|
thread.add_comment(creator, initial_comment)
|
|
@@ -439,36 +441,36 @@ class Thread:
|
|
|
439
441
|
def to_dict(self) -> Dict[str, Any]:
|
|
440
442
|
"""Convert to JSON-serializable dictionary"""
|
|
441
443
|
result = {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
444
|
+
"threadId": self.threadId,
|
|
445
|
+
"reqId": self.reqId,
|
|
446
|
+
"createdBy": self.createdBy,
|
|
447
|
+
"createdAt": self.createdAt,
|
|
448
|
+
"position": self.position.to_dict(),
|
|
449
|
+
"resolved": self.resolved,
|
|
450
|
+
"resolvedBy": self.resolvedBy,
|
|
451
|
+
"resolvedAt": self.resolvedAt,
|
|
452
|
+
"comments": [c.to_dict() for c in self.comments],
|
|
451
453
|
}
|
|
452
454
|
# REQ-d00094-A: Include packageId if set
|
|
453
455
|
if self.packageId is not None:
|
|
454
|
-
result[
|
|
456
|
+
result["packageId"] = self.packageId
|
|
455
457
|
return result
|
|
456
458
|
|
|
457
459
|
@classmethod
|
|
458
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
460
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Thread":
|
|
459
461
|
"""Create from dictionary"""
|
|
460
462
|
return cls(
|
|
461
|
-
threadId=data[
|
|
462
|
-
reqId=data[
|
|
463
|
-
createdBy=data[
|
|
464
|
-
createdAt=data[
|
|
465
|
-
position=CommentPosition.from_dict(data[
|
|
466
|
-
resolved=data.get(
|
|
467
|
-
resolvedBy=data.get(
|
|
468
|
-
resolvedAt=data.get(
|
|
469
|
-
comments=[Comment.from_dict(c) for c in data.get(
|
|
463
|
+
threadId=data["threadId"],
|
|
464
|
+
reqId=data["reqId"],
|
|
465
|
+
createdBy=data["createdBy"],
|
|
466
|
+
createdAt=data["createdAt"],
|
|
467
|
+
position=CommentPosition.from_dict(data["position"]),
|
|
468
|
+
resolved=data.get("resolved", False),
|
|
469
|
+
resolvedBy=data.get("resolvedBy"),
|
|
470
|
+
resolvedAt=data.get("resolvedAt"),
|
|
471
|
+
comments=[Comment.from_dict(c) for c in data.get("comments", [])],
|
|
470
472
|
# REQ-d00094-A: Package ownership (optional for backward compatibility)
|
|
471
|
-
packageId=data.get(
|
|
473
|
+
packageId=data.get("packageId"),
|
|
472
474
|
)
|
|
473
475
|
|
|
474
476
|
|
|
@@ -480,6 +482,7 @@ class ReviewFlag:
|
|
|
480
482
|
When a requirement is flagged, it signals that reviewers in the
|
|
481
483
|
specified scope should examine it.
|
|
482
484
|
"""
|
|
485
|
+
|
|
483
486
|
flaggedForReview: bool
|
|
484
487
|
flaggedBy: str # Username
|
|
485
488
|
flaggedAt: str # ISO 8601 datetime
|
|
@@ -487,33 +490,23 @@ class ReviewFlag:
|
|
|
487
490
|
scope: List[str] # List of roles/users who should review
|
|
488
491
|
|
|
489
492
|
@classmethod
|
|
490
|
-
def create(cls, user: str, reason: str, scope: List[str]) ->
|
|
493
|
+
def create(cls, user: str, reason: str, scope: List[str]) -> "ReviewFlag":
|
|
491
494
|
"""Factory for creating new review flag"""
|
|
492
495
|
return cls(
|
|
493
|
-
flaggedForReview=True,
|
|
494
|
-
flaggedBy=user,
|
|
495
|
-
flaggedAt=now_iso(),
|
|
496
|
-
reason=reason,
|
|
497
|
-
scope=scope
|
|
496
|
+
flaggedForReview=True, flaggedBy=user, flaggedAt=now_iso(), reason=reason, scope=scope
|
|
498
497
|
)
|
|
499
498
|
|
|
500
499
|
@classmethod
|
|
501
|
-
def cleared(cls) ->
|
|
500
|
+
def cleared(cls) -> "ReviewFlag":
|
|
502
501
|
"""Factory for an unflagged state"""
|
|
503
|
-
return cls(
|
|
504
|
-
flaggedForReview=False,
|
|
505
|
-
flaggedBy='',
|
|
506
|
-
flaggedAt='',
|
|
507
|
-
reason='',
|
|
508
|
-
scope=[]
|
|
509
|
-
)
|
|
502
|
+
return cls(flaggedForReview=False, flaggedBy="", flaggedAt="", reason="", scope=[])
|
|
510
503
|
|
|
511
504
|
def clear(self) -> None:
|
|
512
505
|
"""Clear the review flag"""
|
|
513
506
|
self.flaggedForReview = False
|
|
514
|
-
self.flaggedBy =
|
|
515
|
-
self.flaggedAt =
|
|
516
|
-
self.reason =
|
|
507
|
+
self.flaggedBy = ""
|
|
508
|
+
self.flaggedAt = ""
|
|
509
|
+
self.reason = ""
|
|
517
510
|
self.scope = []
|
|
518
511
|
|
|
519
512
|
def validate(self) -> Tuple[bool, List[str]]:
|
|
@@ -537,14 +530,14 @@ class ReviewFlag:
|
|
|
537
530
|
return asdict(self)
|
|
538
531
|
|
|
539
532
|
@classmethod
|
|
540
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
533
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ReviewFlag":
|
|
541
534
|
"""Create from dictionary"""
|
|
542
535
|
return cls(
|
|
543
|
-
flaggedForReview=data[
|
|
544
|
-
flaggedBy=data.get(
|
|
545
|
-
flaggedAt=data.get(
|
|
546
|
-
reason=data.get(
|
|
547
|
-
scope=data.get(
|
|
536
|
+
flaggedForReview=data["flaggedForReview"],
|
|
537
|
+
flaggedBy=data.get("flaggedBy", ""),
|
|
538
|
+
flaggedAt=data.get("flaggedAt", ""),
|
|
539
|
+
reason=data.get("reason", ""),
|
|
540
|
+
scope=data.get("scope", []),
|
|
548
541
|
)
|
|
549
542
|
|
|
550
543
|
|
|
@@ -553,21 +546,16 @@ class Approval:
|
|
|
553
546
|
"""
|
|
554
547
|
Single approval on a status change request.
|
|
555
548
|
"""
|
|
549
|
+
|
|
556
550
|
user: str # Username
|
|
557
551
|
decision: str # ApprovalDecision value
|
|
558
552
|
at: str # ISO 8601 datetime
|
|
559
553
|
comment: Optional[str] = None
|
|
560
554
|
|
|
561
555
|
@classmethod
|
|
562
|
-
def create(cls, user: str, decision: str,
|
|
563
|
-
comment: Optional[str] = None) -> 'Approval':
|
|
556
|
+
def create(cls, user: str, decision: str, comment: Optional[str] = None) -> "Approval":
|
|
564
557
|
"""Factory for creating new approval"""
|
|
565
|
-
return cls(
|
|
566
|
-
user=user,
|
|
567
|
-
decision=decision,
|
|
568
|
-
at=now_iso(),
|
|
569
|
-
comment=comment
|
|
570
|
-
)
|
|
558
|
+
return cls(user=user, decision=decision, at=now_iso(), comment=comment)
|
|
571
559
|
|
|
572
560
|
def validate(self) -> Tuple[bool, List[str]]:
|
|
573
561
|
"""Validate approval"""
|
|
@@ -584,23 +572,16 @@ class Approval:
|
|
|
584
572
|
|
|
585
573
|
def to_dict(self) -> Dict[str, Any]:
|
|
586
574
|
"""Convert to JSON-serializable dictionary"""
|
|
587
|
-
result: Dict[str, Any] = {
|
|
588
|
-
'user': self.user,
|
|
589
|
-
'decision': self.decision,
|
|
590
|
-
'at': self.at
|
|
591
|
-
}
|
|
575
|
+
result: Dict[str, Any] = {"user": self.user, "decision": self.decision, "at": self.at}
|
|
592
576
|
if self.comment is not None:
|
|
593
|
-
result[
|
|
577
|
+
result["comment"] = self.comment
|
|
594
578
|
return result
|
|
595
579
|
|
|
596
580
|
@classmethod
|
|
597
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
581
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Approval":
|
|
598
582
|
"""Create from dictionary"""
|
|
599
583
|
return cls(
|
|
600
|
-
user=data[
|
|
601
|
-
decision=data['decision'],
|
|
602
|
-
at=data['at'],
|
|
603
|
-
comment=data.get('comment')
|
|
584
|
+
user=data["user"], decision=data["decision"], at=data["at"], comment=data.get("comment")
|
|
604
585
|
)
|
|
605
586
|
|
|
606
587
|
|
|
@@ -617,6 +598,7 @@ class StatusRequest:
|
|
|
617
598
|
- Active -> Deprecated (requires product_owner)
|
|
618
599
|
- Draft -> Deprecated (requires product_owner)
|
|
619
600
|
"""
|
|
601
|
+
|
|
620
602
|
requestId: str # UUID
|
|
621
603
|
reqId: str # Requirement ID
|
|
622
604
|
type: str # Always "status_change"
|
|
@@ -630,9 +612,15 @@ class StatusRequest:
|
|
|
630
612
|
state: str # RequestState value
|
|
631
613
|
|
|
632
614
|
@classmethod
|
|
633
|
-
def create(
|
|
634
|
-
|
|
635
|
-
|
|
615
|
+
def create(
|
|
616
|
+
cls,
|
|
617
|
+
req_id: str,
|
|
618
|
+
from_status: str,
|
|
619
|
+
to_status: str,
|
|
620
|
+
requested_by: str,
|
|
621
|
+
justification: str,
|
|
622
|
+
required_approvers: Optional[List[str]] = None,
|
|
623
|
+
) -> "StatusRequest":
|
|
636
624
|
"""
|
|
637
625
|
Factory for creating new status change request.
|
|
638
626
|
|
|
@@ -649,9 +637,7 @@ class StatusRequest:
|
|
|
649
637
|
# Determine required approvers from defaults if not provided
|
|
650
638
|
if required_approvers is None:
|
|
651
639
|
transition_key = f"{from_status}->{to_status}"
|
|
652
|
-
required_approvers = DEFAULT_APPROVAL_RULES.get(
|
|
653
|
-
transition_key, ["product_owner"]
|
|
654
|
-
)
|
|
640
|
+
required_approvers = DEFAULT_APPROVAL_RULES.get(transition_key, ["product_owner"])
|
|
655
641
|
|
|
656
642
|
return cls(
|
|
657
643
|
requestId=generate_uuid(),
|
|
@@ -664,11 +650,10 @@ class StatusRequest:
|
|
|
664
650
|
justification=justification,
|
|
665
651
|
approvals=[],
|
|
666
652
|
requiredApprovers=required_approvers,
|
|
667
|
-
state=RequestState.PENDING.value
|
|
653
|
+
state=RequestState.PENDING.value,
|
|
668
654
|
)
|
|
669
655
|
|
|
670
|
-
def add_approval(self, user: str, decision: str,
|
|
671
|
-
comment: Optional[str] = None) -> Approval:
|
|
656
|
+
def add_approval(self, user: str, decision: str, comment: Optional[str] = None) -> Approval:
|
|
672
657
|
"""Add an approval to the request"""
|
|
673
658
|
approval = Approval.create(user, decision, comment)
|
|
674
659
|
self.approvals.append(approval)
|
|
@@ -692,15 +677,11 @@ class StatusRequest:
|
|
|
692
677
|
|
|
693
678
|
# Check if all required approvers have approved
|
|
694
679
|
approved_users = {
|
|
695
|
-
a.user for a in self.approvals
|
|
696
|
-
if a.decision == ApprovalDecision.APPROVE.value
|
|
680
|
+
a.user for a in self.approvals if a.decision == ApprovalDecision.APPROVE.value
|
|
697
681
|
}
|
|
698
682
|
|
|
699
683
|
# Check if required approvers are satisfied
|
|
700
|
-
all_approved = all(
|
|
701
|
-
approver in approved_users
|
|
702
|
-
for approver in self.requiredApprovers
|
|
703
|
-
)
|
|
684
|
+
all_approved = all(approver in approved_users for approver in self.requiredApprovers)
|
|
704
685
|
|
|
705
686
|
if all_approved:
|
|
706
687
|
self.state = RequestState.APPROVED.value
|
|
@@ -746,34 +727,34 @@ class StatusRequest:
|
|
|
746
727
|
def to_dict(self) -> Dict[str, Any]:
|
|
747
728
|
"""Convert to JSON-serializable dictionary"""
|
|
748
729
|
return {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
730
|
+
"requestId": self.requestId,
|
|
731
|
+
"reqId": self.reqId,
|
|
732
|
+
"type": self.type,
|
|
733
|
+
"fromStatus": self.fromStatus,
|
|
734
|
+
"toStatus": self.toStatus,
|
|
735
|
+
"requestedBy": self.requestedBy,
|
|
736
|
+
"requestedAt": self.requestedAt,
|
|
737
|
+
"justification": self.justification,
|
|
738
|
+
"approvals": [a.to_dict() for a in self.approvals],
|
|
739
|
+
"requiredApprovers": self.requiredApprovers,
|
|
740
|
+
"state": self.state,
|
|
760
741
|
}
|
|
761
742
|
|
|
762
743
|
@classmethod
|
|
763
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
744
|
+
def from_dict(cls, data: Dict[str, Any]) -> "StatusRequest":
|
|
764
745
|
"""Create from dictionary"""
|
|
765
746
|
return cls(
|
|
766
|
-
requestId=data[
|
|
767
|
-
reqId=data[
|
|
768
|
-
type=data[
|
|
769
|
-
fromStatus=data[
|
|
770
|
-
toStatus=data[
|
|
771
|
-
requestedBy=data[
|
|
772
|
-
requestedAt=data[
|
|
773
|
-
justification=data[
|
|
774
|
-
approvals=[Approval.from_dict(a) for a in data.get(
|
|
775
|
-
requiredApprovers=data.get(
|
|
776
|
-
state=data[
|
|
747
|
+
requestId=data["requestId"],
|
|
748
|
+
reqId=data["reqId"],
|
|
749
|
+
type=data["type"],
|
|
750
|
+
fromStatus=data["fromStatus"],
|
|
751
|
+
toStatus=data["toStatus"],
|
|
752
|
+
requestedBy=data["requestedBy"],
|
|
753
|
+
requestedAt=data["requestedAt"],
|
|
754
|
+
justification=data["justification"],
|
|
755
|
+
approvals=[Approval.from_dict(a) for a in data.get("approvals", [])],
|
|
756
|
+
requiredApprovers=data.get("requiredApprovers", []),
|
|
757
|
+
state=data["state"],
|
|
777
758
|
)
|
|
778
759
|
|
|
779
760
|
|
|
@@ -784,6 +765,7 @@ class ReviewSession:
|
|
|
784
765
|
|
|
785
766
|
Sessions help organize reviews and track progress over time.
|
|
786
767
|
"""
|
|
768
|
+
|
|
787
769
|
sessionId: str # UUID
|
|
788
770
|
user: str # Username
|
|
789
771
|
name: str # Session name (e.g., "Sprint 23 Review")
|
|
@@ -791,15 +773,14 @@ class ReviewSession:
|
|
|
791
773
|
description: Optional[str] = None
|
|
792
774
|
|
|
793
775
|
@classmethod
|
|
794
|
-
def create(cls, user: str, name: str,
|
|
795
|
-
description: Optional[str] = None) -> 'ReviewSession':
|
|
776
|
+
def create(cls, user: str, name: str, description: Optional[str] = None) -> "ReviewSession":
|
|
796
777
|
"""Factory for creating new session"""
|
|
797
778
|
return cls(
|
|
798
779
|
sessionId=generate_uuid(),
|
|
799
780
|
user=user,
|
|
800
781
|
name=name,
|
|
801
782
|
createdAt=now_iso(),
|
|
802
|
-
description=description
|
|
783
|
+
description=description,
|
|
803
784
|
)
|
|
804
785
|
|
|
805
786
|
def validate(self) -> Tuple[bool, List[str]]:
|
|
@@ -820,24 +801,24 @@ class ReviewSession:
|
|
|
820
801
|
def to_dict(self) -> Dict[str, Any]:
|
|
821
802
|
"""Convert to JSON-serializable dictionary"""
|
|
822
803
|
result: Dict[str, Any] = {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
804
|
+
"sessionId": self.sessionId,
|
|
805
|
+
"user": self.user,
|
|
806
|
+
"name": self.name,
|
|
807
|
+
"createdAt": self.createdAt,
|
|
827
808
|
}
|
|
828
809
|
if self.description is not None:
|
|
829
|
-
result[
|
|
810
|
+
result["description"] = self.description
|
|
830
811
|
return result
|
|
831
812
|
|
|
832
813
|
@classmethod
|
|
833
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
814
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ReviewSession":
|
|
834
815
|
"""Create from dictionary"""
|
|
835
816
|
return cls(
|
|
836
|
-
sessionId=data[
|
|
837
|
-
user=data[
|
|
838
|
-
name=data[
|
|
839
|
-
createdAt=data[
|
|
840
|
-
description=data.get(
|
|
817
|
+
sessionId=data["sessionId"],
|
|
818
|
+
user=data["user"],
|
|
819
|
+
name=data["name"],
|
|
820
|
+
createdAt=data["createdAt"],
|
|
821
|
+
description=data.get("description"),
|
|
841
822
|
)
|
|
842
823
|
|
|
843
824
|
|
|
@@ -846,17 +827,16 @@ class ReviewConfig:
|
|
|
846
827
|
"""
|
|
847
828
|
System configuration for the review system.
|
|
848
829
|
"""
|
|
830
|
+
|
|
849
831
|
approvalRules: Dict[str, List[str]] # Status transition -> required approvers
|
|
850
832
|
pushOnComment: bool = True # Auto git push when adding comments
|
|
851
833
|
autoFetchOnOpen: bool = True # Auto git fetch when opening reviews
|
|
852
834
|
|
|
853
835
|
@classmethod
|
|
854
|
-
def default(cls) ->
|
|
836
|
+
def default(cls) -> "ReviewConfig":
|
|
855
837
|
"""Factory for default configuration"""
|
|
856
838
|
return cls(
|
|
857
|
-
approvalRules=DEFAULT_APPROVAL_RULES.copy(),
|
|
858
|
-
pushOnComment=True,
|
|
859
|
-
autoFetchOnOpen=True
|
|
839
|
+
approvalRules=DEFAULT_APPROVAL_RULES.copy(), pushOnComment=True, autoFetchOnOpen=True
|
|
860
840
|
)
|
|
861
841
|
|
|
862
842
|
def get_required_approvers(self, from_status: str, to_status: str) -> List[str]:
|
|
@@ -870,7 +850,7 @@ class ReviewConfig:
|
|
|
870
850
|
|
|
871
851
|
# Validate approval rules
|
|
872
852
|
for transition, approvers in self.approvalRules.items():
|
|
873
|
-
parts = transition.split(
|
|
853
|
+
parts = transition.split("->")
|
|
874
854
|
if len(parts) != 2:
|
|
875
855
|
errors.append(f"Invalid transition format: {transition}")
|
|
876
856
|
continue
|
|
@@ -887,18 +867,18 @@ class ReviewConfig:
|
|
|
887
867
|
def to_dict(self) -> Dict[str, Any]:
|
|
888
868
|
"""Convert to JSON-serializable dictionary"""
|
|
889
869
|
return {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
870
|
+
"approvalRules": self.approvalRules,
|
|
871
|
+
"pushOnComment": self.pushOnComment,
|
|
872
|
+
"autoFetchOnOpen": self.autoFetchOnOpen,
|
|
893
873
|
}
|
|
894
874
|
|
|
895
875
|
@classmethod
|
|
896
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
876
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ReviewConfig":
|
|
897
877
|
"""Create from dictionary"""
|
|
898
878
|
return cls(
|
|
899
|
-
approvalRules=data.get(
|
|
900
|
-
pushOnComment=data.get(
|
|
901
|
-
autoFetchOnOpen=data.get(
|
|
879
|
+
approvalRules=data.get("approvalRules", DEFAULT_APPROVAL_RULES.copy()),
|
|
880
|
+
pushOnComment=data.get("pushOnComment", True),
|
|
881
|
+
autoFetchOnOpen=data.get("autoFetchOnOpen", True),
|
|
902
882
|
)
|
|
903
883
|
|
|
904
884
|
|
|
@@ -920,6 +900,7 @@ class ReviewPackage:
|
|
|
920
900
|
REQ-d00097: Review Package Archival
|
|
921
901
|
REQ-d00098: Review Git Audit Trail
|
|
922
902
|
"""
|
|
903
|
+
|
|
923
904
|
packageId: str # UUID
|
|
924
905
|
name: str
|
|
925
906
|
description: str
|
|
@@ -941,9 +922,14 @@ class ReviewPackage:
|
|
|
941
922
|
isDefault: bool = False
|
|
942
923
|
|
|
943
924
|
@classmethod
|
|
944
|
-
def create(
|
|
945
|
-
|
|
946
|
-
|
|
925
|
+
def create(
|
|
926
|
+
cls,
|
|
927
|
+
name: str,
|
|
928
|
+
description: str,
|
|
929
|
+
created_by: str,
|
|
930
|
+
branch_name: Optional[str] = None,
|
|
931
|
+
commit_hash: Optional[str] = None,
|
|
932
|
+
) -> "ReviewPackage":
|
|
947
933
|
"""
|
|
948
934
|
Factory for creating new package.
|
|
949
935
|
|
|
@@ -967,11 +953,11 @@ class ReviewPackage:
|
|
|
967
953
|
branchName=branch_name,
|
|
968
954
|
creationCommitHash=commit_hash,
|
|
969
955
|
lastReviewedCommitHash=commit_hash,
|
|
970
|
-
isDefault=False
|
|
956
|
+
isDefault=False,
|
|
971
957
|
)
|
|
972
958
|
|
|
973
959
|
@classmethod
|
|
974
|
-
def create_default(cls) ->
|
|
960
|
+
def create_default(cls) -> "ReviewPackage":
|
|
975
961
|
"""
|
|
976
962
|
Create the default package.
|
|
977
963
|
|
|
@@ -979,10 +965,11 @@ class ReviewPackage:
|
|
|
979
965
|
This method is kept for backward compatibility during migration.
|
|
980
966
|
"""
|
|
981
967
|
import warnings
|
|
968
|
+
|
|
982
969
|
warnings.warn(
|
|
983
970
|
"create_default() is deprecated. Packages should be explicitly created.",
|
|
984
971
|
DeprecationWarning,
|
|
985
|
-
stacklevel=2
|
|
972
|
+
stacklevel=2,
|
|
986
973
|
)
|
|
987
974
|
return cls(
|
|
988
975
|
packageId="default",
|
|
@@ -991,7 +978,7 @@ class ReviewPackage:
|
|
|
991
978
|
reqIds=[],
|
|
992
979
|
createdBy="system",
|
|
993
980
|
createdAt=now_iso(),
|
|
994
|
-
isDefault=True
|
|
981
|
+
isDefault=True,
|
|
995
982
|
)
|
|
996
983
|
|
|
997
984
|
def update_last_reviewed_commit(self, commit_hash: str) -> None:
|
|
@@ -1042,7 +1029,11 @@ class ReviewPackage:
|
|
|
1042
1029
|
errors.append("archivedBy is required when archivedAt is set")
|
|
1043
1030
|
if not self.archiveReason:
|
|
1044
1031
|
errors.append("archiveReason is required when archivedAt is set")
|
|
1045
|
-
elif self.archiveReason not in (
|
|
1032
|
+
elif self.archiveReason not in (
|
|
1033
|
+
ARCHIVE_REASON_RESOLVED,
|
|
1034
|
+
ARCHIVE_REASON_DELETED,
|
|
1035
|
+
ARCHIVE_REASON_MANUAL,
|
|
1036
|
+
):
|
|
1046
1037
|
errors.append(f"Invalid archiveReason: {self.archiveReason}")
|
|
1047
1038
|
|
|
1048
1039
|
return len(errors) == 0, errors
|
|
@@ -1050,54 +1041,54 @@ class ReviewPackage:
|
|
|
1050
1041
|
def to_dict(self) -> Dict[str, Any]:
|
|
1051
1042
|
"""Convert to JSON-serializable dictionary"""
|
|
1052
1043
|
result: Dict[str, Any] = {
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1044
|
+
"packageId": self.packageId,
|
|
1045
|
+
"name": self.name,
|
|
1046
|
+
"description": self.description,
|
|
1047
|
+
"reqIds": self.reqIds.copy(),
|
|
1048
|
+
"createdBy": self.createdBy,
|
|
1049
|
+
"createdAt": self.createdAt,
|
|
1059
1050
|
}
|
|
1060
1051
|
|
|
1061
1052
|
# REQ-d00098: Git audit trail (only include if set)
|
|
1062
1053
|
if self.branchName is not None:
|
|
1063
|
-
result[
|
|
1054
|
+
result["branchName"] = self.branchName
|
|
1064
1055
|
if self.creationCommitHash is not None:
|
|
1065
|
-
result[
|
|
1056
|
+
result["creationCommitHash"] = self.creationCommitHash
|
|
1066
1057
|
if self.lastReviewedCommitHash is not None:
|
|
1067
|
-
result[
|
|
1058
|
+
result["lastReviewedCommitHash"] = self.lastReviewedCommitHash
|
|
1068
1059
|
|
|
1069
1060
|
# REQ-d00097: Archive metadata (only include if archived)
|
|
1070
1061
|
if self.archivedAt is not None:
|
|
1071
|
-
result[
|
|
1072
|
-
result[
|
|
1073
|
-
result[
|
|
1062
|
+
result["archivedAt"] = self.archivedAt
|
|
1063
|
+
result["archivedBy"] = self.archivedBy
|
|
1064
|
+
result["archiveReason"] = self.archiveReason
|
|
1074
1065
|
|
|
1075
1066
|
# Deprecated field - still included for backward compatibility
|
|
1076
1067
|
if self.isDefault:
|
|
1077
|
-
result[
|
|
1068
|
+
result["isDefault"] = self.isDefault
|
|
1078
1069
|
|
|
1079
1070
|
return result
|
|
1080
1071
|
|
|
1081
1072
|
@classmethod
|
|
1082
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
1073
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ReviewPackage":
|
|
1083
1074
|
"""Create from dictionary"""
|
|
1084
1075
|
return cls(
|
|
1085
|
-
packageId=data.get(
|
|
1086
|
-
name=data.get(
|
|
1087
|
-
description=data.get(
|
|
1088
|
-
reqIds=data.get(
|
|
1089
|
-
createdBy=data.get(
|
|
1090
|
-
createdAt=data.get(
|
|
1076
|
+
packageId=data.get("packageId", ""),
|
|
1077
|
+
name=data.get("name", ""),
|
|
1078
|
+
description=data.get("description", ""),
|
|
1079
|
+
reqIds=data.get("reqIds", []).copy(),
|
|
1080
|
+
createdBy=data.get("createdBy", ""),
|
|
1081
|
+
createdAt=data.get("createdAt", ""),
|
|
1091
1082
|
# REQ-d00098: Git audit trail
|
|
1092
|
-
branchName=data.get(
|
|
1093
|
-
creationCommitHash=data.get(
|
|
1094
|
-
lastReviewedCommitHash=data.get(
|
|
1083
|
+
branchName=data.get("branchName"),
|
|
1084
|
+
creationCommitHash=data.get("creationCommitHash"),
|
|
1085
|
+
lastReviewedCommitHash=data.get("lastReviewedCommitHash"),
|
|
1095
1086
|
# REQ-d00097: Archive metadata
|
|
1096
|
-
archivedAt=data.get(
|
|
1097
|
-
archivedBy=data.get(
|
|
1098
|
-
archiveReason=data.get(
|
|
1087
|
+
archivedAt=data.get("archivedAt"),
|
|
1088
|
+
archivedBy=data.get("archivedBy"),
|
|
1089
|
+
archiveReason=data.get("archiveReason"),
|
|
1099
1090
|
# Deprecated field
|
|
1100
|
-
isDefault=data.get(
|
|
1091
|
+
isDefault=data.get("isDefault", False),
|
|
1101
1092
|
)
|
|
1102
1093
|
|
|
1103
1094
|
|
|
@@ -1106,9 +1097,11 @@ class ReviewPackage:
|
|
|
1106
1097
|
# REQ-tv-d00010-G: Container classes with version tracking
|
|
1107
1098
|
# =============================================================================
|
|
1108
1099
|
|
|
1100
|
+
|
|
1109
1101
|
@dataclass
|
|
1110
1102
|
class ThreadsFile:
|
|
1111
1103
|
"""Container for threads.json file contents"""
|
|
1104
|
+
|
|
1112
1105
|
reqId: str
|
|
1113
1106
|
threads: List[Thread]
|
|
1114
1107
|
version: str = "1.0"
|
|
@@ -1116,24 +1109,25 @@ class ThreadsFile:
|
|
|
1116
1109
|
def to_dict(self) -> Dict[str, Any]:
|
|
1117
1110
|
"""Convert to JSON-serializable dictionary"""
|
|
1118
1111
|
return {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1112
|
+
"version": self.version,
|
|
1113
|
+
"reqId": self.reqId,
|
|
1114
|
+
"threads": [t.to_dict() for t in self.threads],
|
|
1122
1115
|
}
|
|
1123
1116
|
|
|
1124
1117
|
@classmethod
|
|
1125
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
1118
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ThreadsFile":
|
|
1126
1119
|
"""Create from dictionary"""
|
|
1127
1120
|
return cls(
|
|
1128
|
-
version=data.get(
|
|
1129
|
-
reqId=data[
|
|
1130
|
-
threads=[Thread.from_dict(t) for t in data.get(
|
|
1121
|
+
version=data.get("version", "1.0"),
|
|
1122
|
+
reqId=data["reqId"],
|
|
1123
|
+
threads=[Thread.from_dict(t) for t in data.get("threads", [])],
|
|
1131
1124
|
)
|
|
1132
1125
|
|
|
1133
1126
|
|
|
1134
1127
|
@dataclass
|
|
1135
1128
|
class StatusFile:
|
|
1136
1129
|
"""Container for status.json file contents"""
|
|
1130
|
+
|
|
1137
1131
|
reqId: str
|
|
1138
1132
|
requests: List[StatusRequest]
|
|
1139
1133
|
version: str = "1.0"
|
|
@@ -1141,24 +1135,25 @@ class StatusFile:
|
|
|
1141
1135
|
def to_dict(self) -> Dict[str, Any]:
|
|
1142
1136
|
"""Convert to JSON-serializable dictionary"""
|
|
1143
1137
|
return {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1138
|
+
"version": self.version,
|
|
1139
|
+
"reqId": self.reqId,
|
|
1140
|
+
"requests": [r.to_dict() for r in self.requests],
|
|
1147
1141
|
}
|
|
1148
1142
|
|
|
1149
1143
|
@classmethod
|
|
1150
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
1144
|
+
def from_dict(cls, data: Dict[str, Any]) -> "StatusFile":
|
|
1151
1145
|
"""Create from dictionary"""
|
|
1152
1146
|
return cls(
|
|
1153
|
-
version=data.get(
|
|
1154
|
-
reqId=data[
|
|
1155
|
-
requests=[StatusRequest.from_dict(r) for r in data.get(
|
|
1147
|
+
version=data.get("version", "1.0"),
|
|
1148
|
+
reqId=data["reqId"],
|
|
1149
|
+
requests=[StatusRequest.from_dict(r) for r in data.get("requests", [])],
|
|
1156
1150
|
)
|
|
1157
1151
|
|
|
1158
1152
|
|
|
1159
1153
|
@dataclass
|
|
1160
1154
|
class PackagesFile:
|
|
1161
1155
|
"""Container for packages.json file contents"""
|
|
1156
|
+
|
|
1162
1157
|
packages: List[ReviewPackage]
|
|
1163
1158
|
activePackageId: Optional[str] = None
|
|
1164
1159
|
version: str = "1.0"
|
|
@@ -1166,19 +1161,19 @@ class PackagesFile:
|
|
|
1166
1161
|
def to_dict(self) -> Dict[str, Any]:
|
|
1167
1162
|
"""Convert to JSON-serializable dictionary"""
|
|
1168
1163
|
return {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1164
|
+
"version": self.version,
|
|
1165
|
+
"packages": [p.to_dict() for p in self.packages],
|
|
1166
|
+
"activePackageId": self.activePackageId,
|
|
1172
1167
|
}
|
|
1173
1168
|
|
|
1174
1169
|
@classmethod
|
|
1175
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
1170
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PackagesFile":
|
|
1176
1171
|
"""Create from dictionary"""
|
|
1177
|
-
packages = [ReviewPackage.from_dict(p) for p in data.get(
|
|
1172
|
+
packages = [ReviewPackage.from_dict(p) for p in data.get("packages", [])]
|
|
1178
1173
|
return cls(
|
|
1179
|
-
version=data.get(
|
|
1174
|
+
version=data.get("version", "1.0"),
|
|
1180
1175
|
packages=packages,
|
|
1181
|
-
activePackageId=data.get(
|
|
1176
|
+
activePackageId=data.get("activePackageId"),
|
|
1182
1177
|
)
|
|
1183
1178
|
|
|
1184
1179
|
def get_default(self) -> Optional[ReviewPackage]:
|