issues-fs 0.3.0__py3-none-any.whl → 0.4.0__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.
- issues_fs/issues/Issue__Path__Config.py +25 -0
- issues_fs/issues/__init__.py +3 -0
- issues_fs/issues/graph_services/Comments__Service.py +235 -0
- issues_fs/issues/graph_services/Graph__Repository.py +307 -0
- issues_fs/issues/graph_services/Graph__Repository__Factory.py +106 -0
- issues_fs/issues/graph_services/Link__Service.py +227 -0
- issues_fs/issues/graph_services/Node__Service.py +392 -0
- issues_fs/issues/graph_services/Type__Service.py +209 -0
- issues_fs/issues/graph_services/__init__.py +3 -0
- issues_fs/issues/phase_1/Issue__Children__Service.py +340 -0
- issues_fs/issues/phase_1/Root__Issue__Service.py +137 -0
- issues_fs/issues/phase_1/Root__Selection__Service.py +310 -0
- issues_fs/issues/phase_1/__init__.py +3 -0
- issues_fs/issues/status/Git__Status__Service.py +115 -0
- issues_fs/issues/status/Index__Status__Service.py +131 -0
- issues_fs/issues/status/Server__Status__Service.py +113 -0
- issues_fs/issues/status/Storage__Status__Service.py +75 -0
- issues_fs/issues/status/Types__Status__Service.py +75 -0
- issues_fs/issues/status/__init__.py +3 -0
- issues_fs/issues/storage/Path__Handler__Graph_Node.py +185 -0
- issues_fs/issues/storage/Path__Handler__Issues.py +57 -0
- issues_fs/issues/storage/__init__.py +3 -0
- issues_fs/schemas/__init__.py +0 -0
- issues_fs/schemas/enums/Enum__Comment__Author.py +10 -0
- issues_fs/schemas/enums/Enum__Graph__Storage__Backend.py +8 -0
- issues_fs/schemas/enums/Enum__Issue__Status.py +13 -0
- issues_fs/schemas/enums/__init__.py +3 -0
- issues_fs/schemas/graph/Safe_Str__Graph_Types.py +63 -0
- issues_fs/schemas/graph/Schema__Global__Index.py +16 -0
- issues_fs/schemas/graph/Schema__Graph__Link.py +8 -0
- issues_fs/schemas/graph/Schema__Graph__Node.py +10 -0
- issues_fs/schemas/graph/Schema__Graph__Response.py +21 -0
- issues_fs/schemas/graph/Schema__Link__Create__Request.py +12 -0
- issues_fs/schemas/graph/Schema__Link__Create__Response.py +15 -0
- issues_fs/schemas/graph/Schema__Link__Delete__Response.py +16 -0
- issues_fs/schemas/graph/Schema__Link__List__Response.py +15 -0
- issues_fs/schemas/graph/Schema__Link__Type.py +20 -0
- issues_fs/schemas/graph/Schema__Node.py +43 -0
- issues_fs/schemas/graph/Schema__Node__Create__Request.py +19 -0
- issues_fs/schemas/graph/Schema__Node__Create__Response.py +14 -0
- issues_fs/schemas/graph/Schema__Node__Delete__Response.py +15 -0
- issues_fs/schemas/graph/Schema__Node__Link.py +17 -0
- issues_fs/schemas/graph/Schema__Node__List__Response.py +16 -0
- issues_fs/schemas/graph/Schema__Node__Summary.py +15 -0
- issues_fs/schemas/graph/Schema__Node__Type.py +28 -0
- issues_fs/schemas/graph/Schema__Node__Update__Request.py +18 -0
- issues_fs/schemas/graph/Schema__Node__Update__Response.py +14 -0
- issues_fs/schemas/graph/Schema__Property__Definition.py +27 -0
- issues_fs/schemas/graph/Schema__Type__Index.py +16 -0
- issues_fs/schemas/graph/Schema__Type__Summary.py +13 -0
- issues_fs/schemas/graph/__init__.py +0 -0
- issues_fs/schemas/identifiers/Comment_Id.py +5 -0
- issues_fs/schemas/identifiers/Issue_Id.py +13 -0
- issues_fs/schemas/identifiers/__init__.py +0 -0
- issues_fs/schemas/issues/Schema__Comment.py +61 -0
- issues_fs/schemas/issues/__init__.py +0 -0
- issues_fs/schemas/issues/phase_1/Schema__Issue__Children.py +85 -0
- issues_fs/schemas/issues/phase_1/Schema__Root.py +60 -0
- issues_fs/schemas/issues/phase_1/__init__.py +0 -0
- issues_fs/schemas/safe_str/Safe_Str__Hex_Color.py +10 -0
- issues_fs/schemas/safe_str/Safe_Str__Issue_Id.py +14 -0
- issues_fs/schemas/safe_str/Safe_Str__Issue__Node__Description.py +15 -0
- issues_fs/schemas/safe_str/Safe_Str__Label_Name.py +9 -0
- issues_fs/schemas/safe_str/__init__.py +0 -0
- issues_fs/schemas/status/Schema__API__Info.py +15 -0
- issues_fs/schemas/status/Schema__Git__Status.py +21 -0
- issues_fs/schemas/status/Schema__Index__Status.py +22 -0
- issues_fs/schemas/status/Schema__Server__Status.py +28 -0
- issues_fs/schemas/status/Schema__Storage__Status.py +23 -0
- issues_fs/schemas/status/Schema__Types__Status.py +30 -0
- issues_fs/schemas/status/__init__.py +0 -0
- issues_fs/version +1 -1
- {issues_fs-0.3.0.dist-info → issues_fs-0.4.0.dist-info}/METADATA +2 -1
- issues_fs-0.4.0.dist-info/RECORD +79 -0
- issues_fs-0.3.0.dist-info/RECORD +0 -8
- {issues_fs-0.3.0.dist-info → issues_fs-0.4.0.dist-info}/LICENSE +0 -0
- {issues_fs-0.3.0.dist-info → issues_fs-0.4.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# Issue__Children__Service - Service for managing child issues in issues/ folders
|
|
3
|
+
# Phase 1: Enables adding children to issues and converting to hierarchical structure
|
|
4
|
+
#
|
|
5
|
+
# Key Operations:
|
|
6
|
+
# - add_child_issue: Create a child issue in parent's issues/ folder
|
|
7
|
+
# - convert_to_new_structure: Create issues/ folder for an existing issue
|
|
8
|
+
# - list_children: List all children in an issue's issues/ folder
|
|
9
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
from typing import List
|
|
12
|
+
from osbot_utils.type_safe.Type_Safe import Type_Safe
|
|
13
|
+
from osbot_utils.type_safe.primitives.core.Safe_UInt import Safe_UInt
|
|
14
|
+
from osbot_utils.type_safe.primitives.domains.files.safe_str.Safe_Str__File__Path import Safe_Str__File__Path
|
|
15
|
+
from osbot_utils.type_safe.primitives.domains.identifiers.Node_Id import Node_Id
|
|
16
|
+
from osbot_utils.type_safe.primitives.domains.identifiers.Obj_Id import Obj_Id
|
|
17
|
+
from osbot_utils.type_safe.primitives.domains.identifiers.safe_int.Timestamp_Now import Timestamp_Now
|
|
18
|
+
from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
|
|
19
|
+
from osbot_utils.utils.Json import json_dumps, json_loads
|
|
20
|
+
from issues_fs.schemas.graph.Safe_Str__Graph_Types import Safe_Str__Node_Type, Safe_Str__Node_Label, Safe_Str__Status
|
|
21
|
+
from issues_fs.schemas.graph.Schema__Node import Schema__Node
|
|
22
|
+
from issues_fs.schemas.issues.phase_1.Schema__Issue__Children import Schema__Issue__Child__Create, Schema__Issue__Child__Response, Schema__Issue__Convert__Response, Schema__Issue__Children__List__Response
|
|
23
|
+
from issues_fs.issues.graph_services.Graph__Repository import Graph__Repository
|
|
24
|
+
from issues_fs.issues.storage.Path__Handler__Graph_Node import Path__Handler__Graph_Node, FILE_NAME__ISSUE_JSON
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# todo: fix casting to types (like str(..) ) , which is not needed since Type_Safe handles that well (as long are we go into a Type_Safe class, primitive of decorator)
|
|
28
|
+
# create vulns for path transversal issues (i.e. all places where are are doing a path combine using strings
|
|
29
|
+
# see how we can make this work with the Safe_Str__File__Path , for example using the same trick with "/" that Path uses
|
|
30
|
+
# we could use this to create a variation of Safe_Str__File__Path that did not supported path transversal attacks
|
|
31
|
+
|
|
32
|
+
class Issue__Children__Service(Type_Safe): # Service for child issue management
|
|
33
|
+
repository : Graph__Repository # Data access layer
|
|
34
|
+
path_handler : Path__Handler__Graph_Node # Path generation
|
|
35
|
+
|
|
36
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
37
|
+
# Add Child Issue
|
|
38
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
@type_safe
|
|
41
|
+
def add_child_issue(self , # Add child issue to parent
|
|
42
|
+
parent_path : Safe_Str__File__Path , # Path to parent (relative or full)
|
|
43
|
+
child_data : Schema__Issue__Child__Create
|
|
44
|
+
) -> Schema__Issue__Child__Response:
|
|
45
|
+
full_parent_path = self.resolve_full_path(str(parent_path)) # Resolve parent path
|
|
46
|
+
|
|
47
|
+
if self.parent_exists(full_parent_path) is False: # Validate parent exists
|
|
48
|
+
return Schema__Issue__Child__Response(success = False ,
|
|
49
|
+
message = f'Parent not found: {parent_path}' )
|
|
50
|
+
|
|
51
|
+
issues_folder = f"{full_parent_path}/issues" # Ensure issues/ folder exists
|
|
52
|
+
self.ensure_folder_exists(issues_folder)
|
|
53
|
+
|
|
54
|
+
child_type = str(child_data.issue_type) # Generate label for child
|
|
55
|
+
child_label = self.generate_child_label(issues_folder, child_type)
|
|
56
|
+
|
|
57
|
+
child_folder = f"{issues_folder}/{child_label}" # Create child folder
|
|
58
|
+
self.ensure_folder_exists(child_folder)
|
|
59
|
+
|
|
60
|
+
now = Timestamp_Now() # Create child issue
|
|
61
|
+
child_issue = Schema__Node(node_id = Node_Id() ,
|
|
62
|
+
node_type = Safe_Str__Node_Type(child_type) ,
|
|
63
|
+
node_index = self.extract_index_from_label(child_label) ,
|
|
64
|
+
label = Safe_Str__Node_Label(child_label) ,
|
|
65
|
+
title = str(child_data.title) ,
|
|
66
|
+
description = str(child_data.description) if child_data.description else '',
|
|
67
|
+
status = Safe_Str__Status(str(child_data.status)) if child_data.status else Safe_Str__Status('backlog'),
|
|
68
|
+
created_at = now ,
|
|
69
|
+
updated_at = now ,
|
|
70
|
+
created_by = Obj_Id() ,
|
|
71
|
+
tags = [] ,
|
|
72
|
+
links = [] ,
|
|
73
|
+
properties = {} )
|
|
74
|
+
|
|
75
|
+
child_path = f"{child_folder}/{FILE_NAME__ISSUE_JSON}" # Save child issue
|
|
76
|
+
data = child_issue.json()
|
|
77
|
+
content = json_dumps(data, indent=2)
|
|
78
|
+
saved = self.repository.storage_fs.file__save(child_path, content.encode('utf-8'))
|
|
79
|
+
|
|
80
|
+
if saved is False:
|
|
81
|
+
return Schema__Issue__Child__Response(success = False ,
|
|
82
|
+
message = 'Failed to save child issue' )
|
|
83
|
+
|
|
84
|
+
relative_child_path = self.make_relative_path(child_folder) # Calculate relative path for response
|
|
85
|
+
|
|
86
|
+
return Schema__Issue__Child__Response(success = True ,
|
|
87
|
+
path = relative_child_path ,
|
|
88
|
+
label = child_label ,
|
|
89
|
+
issue_type = child_type ,
|
|
90
|
+
title = str(child_data.title))
|
|
91
|
+
|
|
92
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
93
|
+
# Convert to New Structure
|
|
94
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
95
|
+
|
|
96
|
+
@type_safe
|
|
97
|
+
def convert_to_new_structure(self , # Create issues/ folder for issue
|
|
98
|
+
issue_path : Safe_Str__File__Path
|
|
99
|
+
) -> Schema__Issue__Convert__Response:
|
|
100
|
+
full_path = self.resolve_full_path(str(issue_path))
|
|
101
|
+
|
|
102
|
+
if self.parent_exists(full_path) is False: # Validate issue exists
|
|
103
|
+
return Schema__Issue__Convert__Response(success = False ,
|
|
104
|
+
message = f'Issue not found: {issue_path}' )
|
|
105
|
+
|
|
106
|
+
issues_folder = f"{full_path}/issues"
|
|
107
|
+
|
|
108
|
+
if self.folder_exists(issues_folder): # Check if already converted
|
|
109
|
+
return Schema__Issue__Convert__Response(success = True ,
|
|
110
|
+
converted = False ,
|
|
111
|
+
issues_path = self.make_relative_path(issues_folder),
|
|
112
|
+
message = 'Already has issues/ folder' )
|
|
113
|
+
|
|
114
|
+
self.ensure_folder_exists(issues_folder) # Create the issues/ folder
|
|
115
|
+
|
|
116
|
+
placeholder_path = f"{issues_folder}/.gitkeep" # Create placeholder to ensure folder persists
|
|
117
|
+
self.repository.storage_fs.file__save(placeholder_path, b'')
|
|
118
|
+
|
|
119
|
+
return Schema__Issue__Convert__Response(success = True ,
|
|
120
|
+
converted = True ,
|
|
121
|
+
issues_path = self.make_relative_path(issues_folder) ,
|
|
122
|
+
message = 'Created issues/ folder' )
|
|
123
|
+
|
|
124
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
125
|
+
# List Children
|
|
126
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
127
|
+
|
|
128
|
+
@type_safe
|
|
129
|
+
def list_children(self , # List children in issues/ folder
|
|
130
|
+
parent_path : Safe_Str__File__Path
|
|
131
|
+
) -> Schema__Issue__Children__List__Response:
|
|
132
|
+
full_path = self.resolve_full_path(str(parent_path))
|
|
133
|
+
issues_folder = f"{full_path}/issues"
|
|
134
|
+
|
|
135
|
+
if self.folder_exists(issues_folder) is False: # Check if issues/ folder exists
|
|
136
|
+
return Schema__Issue__Children__List__Response(success = True ,
|
|
137
|
+
children = [] ,
|
|
138
|
+
total = Safe_UInt(0))
|
|
139
|
+
|
|
140
|
+
children = []
|
|
141
|
+
child_folders = self.scan_child_folders(issues_folder) # Scan for child issue folders
|
|
142
|
+
|
|
143
|
+
for child_folder in sorted(child_folders):
|
|
144
|
+
child_data = self.load_child_summary(child_folder)
|
|
145
|
+
if child_data:
|
|
146
|
+
children.append(child_data)
|
|
147
|
+
|
|
148
|
+
return Schema__Issue__Children__List__Response(success = True ,
|
|
149
|
+
children = children ,
|
|
150
|
+
total = Safe_UInt(len(children)))
|
|
151
|
+
|
|
152
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
153
|
+
# Path Resolution Helpers
|
|
154
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
155
|
+
|
|
156
|
+
def resolve_full_path(self, path: str) -> str: # Convert relative path to full path
|
|
157
|
+
if not path:
|
|
158
|
+
base_path = str(self.path_handler.base_path)
|
|
159
|
+
if base_path and base_path != '.':
|
|
160
|
+
return base_path
|
|
161
|
+
return '' # Empty base_path = root is ''
|
|
162
|
+
|
|
163
|
+
base_path = str(self.path_handler.base_path)
|
|
164
|
+
|
|
165
|
+
if not base_path or base_path == '.': # No base_path, path is already relative to storage root
|
|
166
|
+
return path
|
|
167
|
+
|
|
168
|
+
if path.startswith(base_path):
|
|
169
|
+
return path
|
|
170
|
+
|
|
171
|
+
return f"{base_path}/{path}"
|
|
172
|
+
|
|
173
|
+
def make_relative_path(self, full_path: str) -> str: # Convert full path to relative
|
|
174
|
+
base_path = str(self.path_handler.base_path)
|
|
175
|
+
|
|
176
|
+
if not base_path or base_path == '.': # No base_path, path is already relative
|
|
177
|
+
return full_path
|
|
178
|
+
|
|
179
|
+
prefix = f"{base_path}/"
|
|
180
|
+
if full_path.startswith(prefix):
|
|
181
|
+
return full_path[len(prefix):]
|
|
182
|
+
|
|
183
|
+
return full_path
|
|
184
|
+
|
|
185
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
186
|
+
# Folder Operations
|
|
187
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
188
|
+
|
|
189
|
+
def parent_exists(self, folder_path: str) -> bool: # Check if parent issue exists
|
|
190
|
+
issue_json = f"{folder_path}/{FILE_NAME__ISSUE_JSON}" if folder_path else FILE_NAME__ISSUE_JSON
|
|
191
|
+
node_json = f"{folder_path}/node.json" if folder_path else "node.json"
|
|
192
|
+
|
|
193
|
+
if self.repository.storage_fs.file__exists(issue_json):
|
|
194
|
+
return True
|
|
195
|
+
if self.repository.storage_fs.file__exists(node_json):
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
base_path = str(self.path_handler.base_path) # Root is always valid parent
|
|
199
|
+
if not folder_path or folder_path == base_path or folder_path == '.' or folder_path == '':
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
def folder_exists(self, folder_path: str) -> bool: # Check if folder exists (has any files)
|
|
205
|
+
all_paths = self.repository.storage_fs.files__paths()
|
|
206
|
+
prefix = f"{folder_path}/"
|
|
207
|
+
|
|
208
|
+
for path in all_paths:
|
|
209
|
+
if path.startswith(prefix):
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def ensure_folder_exists(self, folder_path: str) -> None: # Ensure folder exists in storage
|
|
215
|
+
# todo: see if we need this method
|
|
216
|
+
# if we do add the logic to create the folder when not in memory
|
|
217
|
+
# if we don't need it delete this method
|
|
218
|
+
pass # Memory-FS creates folders implicitly
|
|
219
|
+
|
|
220
|
+
@type_safe
|
|
221
|
+
def scan_child_folders(self, # Find all child folders in issues/
|
|
222
|
+
issues_folder: Safe_Str__File__Path
|
|
223
|
+
) -> List[Safe_Str__File__Path]:
|
|
224
|
+
folders = set()
|
|
225
|
+
all_paths = self.repository.storage_fs.files__paths()
|
|
226
|
+
prefix = f"{issues_folder}/"
|
|
227
|
+
|
|
228
|
+
for path in all_paths:
|
|
229
|
+
if path.startswith(prefix) is False:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
relative = path[len(prefix):] # Remove prefix
|
|
233
|
+
parts = relative.split('/')
|
|
234
|
+
|
|
235
|
+
if len(parts) >= 2:
|
|
236
|
+
child_folder = parts[0] # First segment is child folder
|
|
237
|
+
filename = parts[1]
|
|
238
|
+
|
|
239
|
+
if filename in (FILE_NAME__ISSUE_JSON, 'node.json'):
|
|
240
|
+
folders.add(f"{issues_folder}/{child_folder}")
|
|
241
|
+
|
|
242
|
+
return folders
|
|
243
|
+
|
|
244
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
245
|
+
# Label Generation
|
|
246
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
247
|
+
|
|
248
|
+
def generate_child_label(self , # Generate label for new child
|
|
249
|
+
issues_folder : Safe_Str__File__Path ,
|
|
250
|
+
child_type : str
|
|
251
|
+
) -> str:
|
|
252
|
+
existing_indices = self.get_existing_indices(issues_folder, child_type) # Find highest existing index
|
|
253
|
+
|
|
254
|
+
next_index = 1
|
|
255
|
+
if existing_indices:
|
|
256
|
+
next_index = max(existing_indices) + 1
|
|
257
|
+
|
|
258
|
+
display_type = child_type.capitalize() # Generate label: "Task" + "-" + "1"
|
|
259
|
+
|
|
260
|
+
if '-' in child_type: # Handle types like "git-repo"
|
|
261
|
+
parts = child_type.split('-')
|
|
262
|
+
display_type = ''.join(p.capitalize() for p in parts)
|
|
263
|
+
|
|
264
|
+
return f"{display_type}-{next_index}"
|
|
265
|
+
|
|
266
|
+
# todo: these str should be type_safe primitives
|
|
267
|
+
def get_existing_indices(self , # Get all existing indices for type
|
|
268
|
+
issues_folder : Safe_Str__File__Path ,
|
|
269
|
+
child_type : str
|
|
270
|
+
) -> List[int]:
|
|
271
|
+
indices = []
|
|
272
|
+
all_paths = self.repository.storage_fs.files__paths()
|
|
273
|
+
prefix = f"{issues_folder}/"
|
|
274
|
+
|
|
275
|
+
display_type = child_type.capitalize()
|
|
276
|
+
if '-' in child_type:
|
|
277
|
+
parts = child_type.split('-')
|
|
278
|
+
display_type = ''.join(p.capitalize() for p in parts)
|
|
279
|
+
|
|
280
|
+
label_prefix = f"{display_type}-"
|
|
281
|
+
|
|
282
|
+
for path in all_paths:
|
|
283
|
+
if path.startswith(prefix) is False:
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
relative = path[len(prefix):]
|
|
287
|
+
parts = relative.split('/')
|
|
288
|
+
|
|
289
|
+
if len(parts) >= 1:
|
|
290
|
+
folder_name = parts[0]
|
|
291
|
+
|
|
292
|
+
if folder_name.startswith(label_prefix):
|
|
293
|
+
try:
|
|
294
|
+
index_str = folder_name[len(label_prefix):]
|
|
295
|
+
index = int(index_str)
|
|
296
|
+
indices.append(index)
|
|
297
|
+
except ValueError:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
return indices
|
|
301
|
+
|
|
302
|
+
def extract_index_from_label(self, label: str) -> Safe_UInt: # Extract index number from label
|
|
303
|
+
if '-' in label:
|
|
304
|
+
try:
|
|
305
|
+
index_str = label.rsplit('-', 1)[1]
|
|
306
|
+
return Safe_UInt(int(index_str))
|
|
307
|
+
except (ValueError, IndexError):
|
|
308
|
+
pass
|
|
309
|
+
return Safe_UInt(1)
|
|
310
|
+
|
|
311
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
312
|
+
# Child Data Loading
|
|
313
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
314
|
+
|
|
315
|
+
# todo: this should not be a raw dict
|
|
316
|
+
def load_child_summary(self, child_folder: Safe_Str__File__Path) -> dict: # Load summary data for child
|
|
317
|
+
issue_path = f"{child_folder}/{FILE_NAME__ISSUE_JSON}" # Try issue.json first
|
|
318
|
+
data = self.load_issue_from_path(issue_path)
|
|
319
|
+
if data:
|
|
320
|
+
data['path'] = self.make_relative_path(child_folder)
|
|
321
|
+
return data
|
|
322
|
+
|
|
323
|
+
node_path = f"{child_folder}/node.json" # Fall back to node.json
|
|
324
|
+
data = self.load_issue_from_path(node_path)
|
|
325
|
+
if data:
|
|
326
|
+
data['path'] = self.make_relative_path(child_folder)
|
|
327
|
+
return data
|
|
328
|
+
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
# todo: this should not be a raw dict
|
|
332
|
+
def load_issue_from_path(self, file_path: Safe_Str__File__Path) -> dict: # Load issue JSON from path
|
|
333
|
+
if self.repository.storage_fs.file__exists(file_path) is False:
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
content = self.repository.storage_fs.file__str(file_path)
|
|
337
|
+
if not content:
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
return json_loads(content)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# Root__Issue__Service - Service for creating and managing the root issue
|
|
3
|
+
# Phase 1: Creates GitRepo-1 issue in .issues/issue.json
|
|
4
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
from osbot_utils.type_safe.Type_Safe import Type_Safe
|
|
7
|
+
from osbot_utils.type_safe.primitives.core.Safe_UInt import Safe_UInt
|
|
8
|
+
from osbot_utils.type_safe.primitives.domains.common.safe_str.Safe_Str__Text import Safe_Str__Text
|
|
9
|
+
from osbot_utils.type_safe.primitives.domains.identifiers.Node_Id import Node_Id
|
|
10
|
+
from osbot_utils.type_safe.primitives.domains.identifiers.Obj_Id import Obj_Id
|
|
11
|
+
from osbot_utils.type_safe.primitives.domains.identifiers.safe_int.Timestamp_Now import Timestamp_Now
|
|
12
|
+
from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
|
|
13
|
+
from osbot_utils.utils.Json import json_dumps, json_loads
|
|
14
|
+
from issues_fs.schemas.graph.Safe_Str__Graph_Types import Safe_Str__Node_Type, Safe_Str__Node_Label, Safe_Str__Status
|
|
15
|
+
from issues_fs.schemas.graph.Schema__Node import Schema__Node
|
|
16
|
+
from issues_fs.issues.graph_services.Graph__Repository import Graph__Repository
|
|
17
|
+
from issues_fs.issues.storage.Path__Handler__Graph_Node import Path__Handler__Graph_Node
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
# Constants for Root Issue
|
|
22
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
|
|
24
|
+
ROOT_ISSUE_TYPE = 'git-repo'
|
|
25
|
+
ROOT_ISSUE_LABEL = 'Gitrepo-1'
|
|
26
|
+
ROOT_ISSUE_DEFAULT_TITLE = 'Project Issues'
|
|
27
|
+
ROOT_ISSUE_STATUS = 'active'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Root__Issue__Service(Type_Safe): # Service for root issue management
|
|
31
|
+
repository : Graph__Repository # Data access layer
|
|
32
|
+
path_handler : Path__Handler__Graph_Node # Path generation
|
|
33
|
+
|
|
34
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
35
|
+
# Root Issue Initialization
|
|
36
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
37
|
+
|
|
38
|
+
def ensure_root_issue_exists(self , # Create root issue if not exists
|
|
39
|
+
title : str = ROOT_ISSUE_DEFAULT_TITLE
|
|
40
|
+
) -> bool:
|
|
41
|
+
if self.root_issue_exists():
|
|
42
|
+
return True # Already exists
|
|
43
|
+
|
|
44
|
+
return self.create_root_issue(title=title)
|
|
45
|
+
|
|
46
|
+
@type_safe
|
|
47
|
+
def create_root_issue(self , # Create the root GitRepo-1 issue
|
|
48
|
+
title : str = ROOT_ISSUE_DEFAULT_TITLE
|
|
49
|
+
) -> bool:
|
|
50
|
+
if self.root_issue_exists(): # Don't overwrite existing
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
now = Timestamp_Now()
|
|
54
|
+
|
|
55
|
+
root_issue = Schema__Node(node_id = Node_Id(Obj_Id()) ,
|
|
56
|
+
node_type = Safe_Str__Node_Type(ROOT_ISSUE_TYPE) ,
|
|
57
|
+
node_index = Safe_UInt(1) ,
|
|
58
|
+
label = Safe_Str__Node_Label(ROOT_ISSUE_LABEL) ,
|
|
59
|
+
title = title ,
|
|
60
|
+
description = 'Root issue for all project issues' ,
|
|
61
|
+
status = Safe_Str__Status(ROOT_ISSUE_STATUS) ,
|
|
62
|
+
created_at = now ,
|
|
63
|
+
updated_at = now ,
|
|
64
|
+
created_by = Obj_Id() ,
|
|
65
|
+
tags = [] ,
|
|
66
|
+
links = [] ,
|
|
67
|
+
properties = {} )
|
|
68
|
+
|
|
69
|
+
return self.save_root_issue(root_issue)
|
|
70
|
+
|
|
71
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
# Root Issue Persistence
|
|
73
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
|
|
75
|
+
def save_root_issue(self, root_issue: Schema__Node) -> bool: # Save root issue to .issues/issue.json
|
|
76
|
+
path = self.path_handler.path_for_root_issue()
|
|
77
|
+
data = root_issue.json()
|
|
78
|
+
content = json_dumps(data, indent=2)
|
|
79
|
+
return self.repository.storage_fs.file__save(path, content.encode('utf-8'))
|
|
80
|
+
|
|
81
|
+
def root_issue_exists(self) -> bool: # Check if root issue.json exists
|
|
82
|
+
path = self.path_handler.path_for_root_issue()
|
|
83
|
+
return self.repository.storage_fs.file__exists(path)
|
|
84
|
+
|
|
85
|
+
def load_root_issue(self) -> Schema__Node: # Load the root issue
|
|
86
|
+
path = self.path_handler.path_for_root_issue()
|
|
87
|
+
|
|
88
|
+
if self.repository.storage_fs.file__exists(path) is False:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
content = self.repository.storage_fs.file__str(path)
|
|
92
|
+
if not content:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
data = json_loads(content)
|
|
96
|
+
if data is None:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
return Schema__Node.from_json(data)
|
|
100
|
+
|
|
101
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
102
|
+
# Root Issue Updates
|
|
103
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
104
|
+
|
|
105
|
+
@type_safe
|
|
106
|
+
def update_root_issue_title(self, title: Safe_Str__Text) -> bool: # Update root issue title
|
|
107
|
+
root_issue = self.load_root_issue()
|
|
108
|
+
if root_issue is None:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
root_issue.title = str(title)
|
|
112
|
+
root_issue.updated_at = Timestamp_Now()
|
|
113
|
+
|
|
114
|
+
return self.save_root_issue(root_issue)
|
|
115
|
+
|
|
116
|
+
@type_safe
|
|
117
|
+
def update_root_issue_description(self , # Update root issue description
|
|
118
|
+
description : Safe_Str__Text
|
|
119
|
+
) -> bool:
|
|
120
|
+
root_issue = self.load_root_issue()
|
|
121
|
+
if root_issue is None:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
root_issue.description = str(description)
|
|
125
|
+
root_issue.updated_at = Timestamp_Now()
|
|
126
|
+
|
|
127
|
+
return self.save_root_issue(root_issue)
|
|
128
|
+
|
|
129
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
130
|
+
# Root Issue Deletion (for testing)
|
|
131
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
132
|
+
|
|
133
|
+
def delete_root_issue(self) -> bool: # Delete root issue (for tests)
|
|
134
|
+
path = self.path_handler.path_for_root_issue()
|
|
135
|
+
if self.repository.storage_fs.file__exists(path):
|
|
136
|
+
return self.repository.storage_fs.file__delete(path)
|
|
137
|
+
return False
|