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.
Files changed (77) hide show
  1. issues_fs/issues/Issue__Path__Config.py +25 -0
  2. issues_fs/issues/__init__.py +3 -0
  3. issues_fs/issues/graph_services/Comments__Service.py +235 -0
  4. issues_fs/issues/graph_services/Graph__Repository.py +307 -0
  5. issues_fs/issues/graph_services/Graph__Repository__Factory.py +106 -0
  6. issues_fs/issues/graph_services/Link__Service.py +227 -0
  7. issues_fs/issues/graph_services/Node__Service.py +392 -0
  8. issues_fs/issues/graph_services/Type__Service.py +209 -0
  9. issues_fs/issues/graph_services/__init__.py +3 -0
  10. issues_fs/issues/phase_1/Issue__Children__Service.py +340 -0
  11. issues_fs/issues/phase_1/Root__Issue__Service.py +137 -0
  12. issues_fs/issues/phase_1/Root__Selection__Service.py +310 -0
  13. issues_fs/issues/phase_1/__init__.py +3 -0
  14. issues_fs/issues/status/Git__Status__Service.py +115 -0
  15. issues_fs/issues/status/Index__Status__Service.py +131 -0
  16. issues_fs/issues/status/Server__Status__Service.py +113 -0
  17. issues_fs/issues/status/Storage__Status__Service.py +75 -0
  18. issues_fs/issues/status/Types__Status__Service.py +75 -0
  19. issues_fs/issues/status/__init__.py +3 -0
  20. issues_fs/issues/storage/Path__Handler__Graph_Node.py +185 -0
  21. issues_fs/issues/storage/Path__Handler__Issues.py +57 -0
  22. issues_fs/issues/storage/__init__.py +3 -0
  23. issues_fs/schemas/__init__.py +0 -0
  24. issues_fs/schemas/enums/Enum__Comment__Author.py +10 -0
  25. issues_fs/schemas/enums/Enum__Graph__Storage__Backend.py +8 -0
  26. issues_fs/schemas/enums/Enum__Issue__Status.py +13 -0
  27. issues_fs/schemas/enums/__init__.py +3 -0
  28. issues_fs/schemas/graph/Safe_Str__Graph_Types.py +63 -0
  29. issues_fs/schemas/graph/Schema__Global__Index.py +16 -0
  30. issues_fs/schemas/graph/Schema__Graph__Link.py +8 -0
  31. issues_fs/schemas/graph/Schema__Graph__Node.py +10 -0
  32. issues_fs/schemas/graph/Schema__Graph__Response.py +21 -0
  33. issues_fs/schemas/graph/Schema__Link__Create__Request.py +12 -0
  34. issues_fs/schemas/graph/Schema__Link__Create__Response.py +15 -0
  35. issues_fs/schemas/graph/Schema__Link__Delete__Response.py +16 -0
  36. issues_fs/schemas/graph/Schema__Link__List__Response.py +15 -0
  37. issues_fs/schemas/graph/Schema__Link__Type.py +20 -0
  38. issues_fs/schemas/graph/Schema__Node.py +43 -0
  39. issues_fs/schemas/graph/Schema__Node__Create__Request.py +19 -0
  40. issues_fs/schemas/graph/Schema__Node__Create__Response.py +14 -0
  41. issues_fs/schemas/graph/Schema__Node__Delete__Response.py +15 -0
  42. issues_fs/schemas/graph/Schema__Node__Link.py +17 -0
  43. issues_fs/schemas/graph/Schema__Node__List__Response.py +16 -0
  44. issues_fs/schemas/graph/Schema__Node__Summary.py +15 -0
  45. issues_fs/schemas/graph/Schema__Node__Type.py +28 -0
  46. issues_fs/schemas/graph/Schema__Node__Update__Request.py +18 -0
  47. issues_fs/schemas/graph/Schema__Node__Update__Response.py +14 -0
  48. issues_fs/schemas/graph/Schema__Property__Definition.py +27 -0
  49. issues_fs/schemas/graph/Schema__Type__Index.py +16 -0
  50. issues_fs/schemas/graph/Schema__Type__Summary.py +13 -0
  51. issues_fs/schemas/graph/__init__.py +0 -0
  52. issues_fs/schemas/identifiers/Comment_Id.py +5 -0
  53. issues_fs/schemas/identifiers/Issue_Id.py +13 -0
  54. issues_fs/schemas/identifiers/__init__.py +0 -0
  55. issues_fs/schemas/issues/Schema__Comment.py +61 -0
  56. issues_fs/schemas/issues/__init__.py +0 -0
  57. issues_fs/schemas/issues/phase_1/Schema__Issue__Children.py +85 -0
  58. issues_fs/schemas/issues/phase_1/Schema__Root.py +60 -0
  59. issues_fs/schemas/issues/phase_1/__init__.py +0 -0
  60. issues_fs/schemas/safe_str/Safe_Str__Hex_Color.py +10 -0
  61. issues_fs/schemas/safe_str/Safe_Str__Issue_Id.py +14 -0
  62. issues_fs/schemas/safe_str/Safe_Str__Issue__Node__Description.py +15 -0
  63. issues_fs/schemas/safe_str/Safe_Str__Label_Name.py +9 -0
  64. issues_fs/schemas/safe_str/__init__.py +0 -0
  65. issues_fs/schemas/status/Schema__API__Info.py +15 -0
  66. issues_fs/schemas/status/Schema__Git__Status.py +21 -0
  67. issues_fs/schemas/status/Schema__Index__Status.py +22 -0
  68. issues_fs/schemas/status/Schema__Server__Status.py +28 -0
  69. issues_fs/schemas/status/Schema__Storage__Status.py +23 -0
  70. issues_fs/schemas/status/Schema__Types__Status.py +30 -0
  71. issues_fs/schemas/status/__init__.py +0 -0
  72. issues_fs/version +1 -1
  73. {issues_fs-0.3.0.dist-info → issues_fs-0.4.0.dist-info}/METADATA +2 -1
  74. issues_fs-0.4.0.dist-info/RECORD +79 -0
  75. issues_fs-0.3.0.dist-info/RECORD +0 -8
  76. {issues_fs-0.3.0.dist-info → issues_fs-0.4.0.dist-info}/LICENSE +0 -0
  77. {issues_fs-0.3.0.dist-info → issues_fs-0.4.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,25 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Issue__Path__Config - Configuration for issue tracking paths
3
+ # Provides the default issues directory path based on project structure
4
+ # ═══════════════════════════════════════════════════════════════════════════════
5
+ import issues_fs
6
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
7
+ from osbot_utils.type_safe.primitives.domains.files.safe_str.Safe_Str__File__Path import Safe_Str__File__Path
8
+ from osbot_utils.utils.Files import path_combine, folder_exists, folder_create
9
+
10
+
11
+ class Issue__Path__Config(Type_Safe): # Issue path configuration
12
+
13
+ @classmethod
14
+ def default_issues_path(cls) -> Safe_Str__File__Path: # Get default issues directory
15
+ root_path = issues_fs.path
16
+ issues_path = path_combine(root_path, '../docs/dev-briefs/issues')
17
+ return issues_path
18
+
19
+ @classmethod
20
+ def ensure_issues_directory(cls, path: Safe_Str__File__Path = None) -> Safe_Str__File__Path: # Create issues directory if needed
21
+ if path is None:
22
+ path = cls.default_issues_path()
23
+ if folder_exists(path) is False:
24
+ folder_create(path)
25
+ return path
@@ -0,0 +1,3 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Issue tracking services package
3
+ # ═══════════════════════════════════════════════════════════════════════════════
@@ -0,0 +1,235 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Comments__Service - Business logic for comment CRUD operations
3
+ # ═══════════════════════════════════════════════════════════════════════════════
4
+
5
+ from typing import List
6
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
7
+ from osbot_utils.type_safe.primitives.domains.identifiers.Obj_Id import Obj_Id
8
+ from osbot_utils.type_safe.primitives.domains.identifiers.safe_int.Timestamp_Now import Timestamp_Now
9
+ from issues_fs.schemas.graph.Safe_Str__Graph_Types import Safe_Str__Node_Type, Safe_Str__Node_Label
10
+ from issues_fs.schemas.issues.Schema__Comment import Schema__Comment__List__Response, Schema__Comment__Create__Request, Schema__Comment__Response, Schema__Comment, Schema__Comment__Update__Request, Schema__Comment__Delete__Response
11
+ from issues_fs.issues.graph_services.Graph__Repository import Graph__Repository
12
+
13
+
14
+ class Comments__Service(Type_Safe): # Comment business logic service
15
+ repository : Graph__Repository = None # Graph__Repository instance
16
+
17
+ # ═══════════════════════════════════════════════════════════════════════════════
18
+ # List Comments
19
+ # ═══════════════════════════════════════════════════════════════════════════════
20
+
21
+ def list_comments(self , # List all comments on a node
22
+ node_type : Safe_Str__Node_Type ,
23
+ label : Safe_Str__Node_Label
24
+ ) -> Schema__Comment__List__Response:
25
+ node = self.repository.node_load(node_type = node_type ,
26
+ label = label )
27
+ if node is None:
28
+ return Schema__Comment__List__Response(success = False ,
29
+ comments = [] ,
30
+ total = 0 ,
31
+ message = f'Node not found: {label}')
32
+
33
+ raw_comments = node.properties.get('comments', []) if node.properties else []
34
+ comments = self._parse_comments(raw_comments)
35
+
36
+ return Schema__Comment__List__Response(success = True ,
37
+ comments = comments ,
38
+ total = len(comments) )
39
+
40
+ # ═══════════════════════════════════════════════════════════════════════════════
41
+ # Create Comment
42
+ # ═══════════════════════════════════════════════════════════════════════════════
43
+
44
+ def create_comment(self , # Add comment to node
45
+ node_type : Safe_Str__Node_Type ,
46
+ label : Safe_Str__Node_Label ,
47
+ request : Schema__Comment__Create__Request
48
+ ) -> Schema__Comment__Response:
49
+ # Validate request
50
+ if not request.text or str(request.text).strip() == '':
51
+ return Schema__Comment__Response(success = False ,
52
+ message = 'Comment text is required')
53
+
54
+ if not request.author or str(request.author).strip() == '':
55
+ return Schema__Comment__Response(success = False ,
56
+ message = 'Author is required' )
57
+
58
+ # Load node
59
+ node = self.repository.node_load(node_type = node_type ,
60
+ label = label )
61
+ if node is None:
62
+ return Schema__Comment__Response(success = False ,
63
+ message = f'Node not found: {label}')
64
+
65
+ # Create comment with server-generated ID and timestamp
66
+ now = Timestamp_Now()
67
+ comment = Schema__Comment(id = Obj_Id() ,
68
+ author = request.author ,
69
+ text = request.text ,
70
+ created_at = now ,
71
+ updated_at = now )
72
+
73
+ # Add to node properties
74
+ if node.properties is None:
75
+ node.properties = {}
76
+ if 'comments' not in node.properties:
77
+ node.properties['comments'] = []
78
+
79
+ node.properties['comments'].append(comment.json())
80
+ node.updated_at = now
81
+
82
+ # Save node
83
+ if self.repository.node_save(node) is False:
84
+ return Schema__Comment__Response(success = False ,
85
+ message = 'Failed to save node' )
86
+
87
+ return Schema__Comment__Response(success = True ,
88
+ comment = comment )
89
+
90
+ # ═══════════════════════════════════════════════════════════════════════════════
91
+ # Get Single Comment
92
+ # ═══════════════════════════════════════════════════════════════════════════════
93
+
94
+ def get_comment(self , # Get single comment by ID
95
+ node_type : Safe_Str__Node_Type ,
96
+ label : Safe_Str__Node_Label ,
97
+ comment_id : str
98
+ ) -> Schema__Comment__Response:
99
+ node = self.repository.node_load(node_type = node_type ,
100
+ label = label )
101
+ if node is None:
102
+ return Schema__Comment__Response(success = False ,
103
+ message = f'Node not found: {label}')
104
+
105
+ raw_comments = node.properties.get('comments', []) if node.properties else []
106
+
107
+ for raw in raw_comments:
108
+ if raw.get('id') == comment_id:
109
+ comment = self._parse_comment(raw)
110
+ return Schema__Comment__Response(success = True ,
111
+ comment = comment )
112
+
113
+ return Schema__Comment__Response(success = False ,
114
+ message = f'Comment not found: {comment_id}')
115
+
116
+ # ═══════════════════════════════════════════════════════════════════════════════
117
+ # Update Comment
118
+ # ═══════════════════════════════════════════════════════════════════════════════
119
+
120
+ def update_comment(self , # Edit comment text
121
+ node_type : Safe_Str__Node_Type ,
122
+ label : Safe_Str__Node_Label ,
123
+ comment_id : str ,
124
+ request : Schema__Comment__Update__Request
125
+ ) -> Schema__Comment__Response:
126
+ # Validate request
127
+ if not request.text or str(request.text).strip() == '':
128
+ return Schema__Comment__Response(success = False ,
129
+ message = 'Comment text is required')
130
+
131
+ # Load node
132
+ node = self.repository.node_load(node_type = node_type ,
133
+ label = label )
134
+ if node is None:
135
+ return Schema__Comment__Response(success = False ,
136
+ message = f'Node not found: {label}')
137
+
138
+ raw_comments = node.properties.get('comments', []) if node.properties else []
139
+
140
+ # Find and update comment
141
+ found = False
142
+ updated = None
143
+ now = Timestamp_Now()
144
+
145
+ for raw in raw_comments:
146
+ if raw.get('id') == comment_id:
147
+ raw['text'] = str(request.text)
148
+ raw['updated_at'] = int(now)
149
+ updated = self._parse_comment(raw)
150
+ found = True
151
+ break
152
+
153
+ if not found:
154
+ return Schema__Comment__Response(success = False ,
155
+ message = f'Comment not found: {comment_id}')
156
+
157
+ # Save node
158
+ node.properties['comments'] = raw_comments
159
+ node.updated_at = now
160
+
161
+ if self.repository.node_save(node) is False:
162
+ return Schema__Comment__Response(success = False ,
163
+ message = 'Failed to save node' )
164
+
165
+ return Schema__Comment__Response(success = True ,
166
+ comment = updated )
167
+
168
+ # ═══════════════════════════════════════════════════════════════════════════════
169
+ # Delete Comment
170
+ # ═══════════════════════════════════════════════════════════════════════════════
171
+
172
+ def delete_comment(self , # Remove comment from node
173
+ node_type : Safe_Str__Node_Type ,
174
+ label : Safe_Str__Node_Label ,
175
+ comment_id : str
176
+ ) -> Schema__Comment__Delete__Response:
177
+ # Load node
178
+ node = self.repository.node_load(node_type = node_type ,
179
+ label = label )
180
+ if node is None:
181
+ return Schema__Comment__Delete__Response(success = False ,
182
+ deleted = False ,
183
+ comment_id = comment_id ,
184
+ message = f'Node not found: {label}')
185
+
186
+ raw_comments = node.properties.get('comments', []) if node.properties else []
187
+ original_len = len(raw_comments)
188
+
189
+ # Filter out the comment to delete
190
+ raw_comments = [c for c in raw_comments if c.get('id') != comment_id]
191
+
192
+ if len(raw_comments) == original_len:
193
+ return Schema__Comment__Delete__Response(success = False ,
194
+ deleted = False ,
195
+ comment_id = comment_id ,
196
+ message = f'Comment not found: {comment_id}')
197
+
198
+ # Save node
199
+ node.properties['comments'] = raw_comments
200
+ node.updated_at = Timestamp_Now()
201
+
202
+ if self.repository.node_save(node) is False:
203
+ return Schema__Comment__Delete__Response(success = False ,
204
+ deleted = False ,
205
+ comment_id = comment_id ,
206
+ message = 'Failed to save node' )
207
+
208
+ return Schema__Comment__Delete__Response(success = True ,
209
+ deleted = True ,
210
+ comment_id = comment_id )
211
+
212
+ # ═══════════════════════════════════════════════════════════════════════════════
213
+ # Helper Methods
214
+ # ═══════════════════════════════════════════════════════════════════════════════
215
+
216
+ def _parse_comments(self, raw_comments: list) -> List[Schema__Comment]: # Parse raw dicts to Schema__Comment
217
+ comments = []
218
+ for raw in raw_comments:
219
+ comment = self._parse_comment(raw)
220
+ if comment:
221
+ comments.append(comment)
222
+ return comments
223
+
224
+ def _parse_comment(self, raw: dict) -> Schema__Comment: # Parse single raw dict
225
+ if not raw:
226
+ return None
227
+
228
+ try:
229
+ return Schema__Comment(id = Obj_Id(raw.get('id', str(Obj_Id()))) ,
230
+ author = raw.get('author', 'unknown') ,
231
+ text = raw.get('text', '') ,
232
+ created_at = Timestamp_Now(raw.get('created_at', raw.get('timestamp', 0))) ,
233
+ updated_at = Timestamp_Now(raw.get('updated_at', raw.get('created_at', 0))) )
234
+ except Exception:
235
+ return None
@@ -0,0 +1,307 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Graph__Repository - Memory-FS based data access for graph nodes
3
+ # Storage-agnostic: works with Memory, Local Disk, S3, SQLite, ZIP backends
4
+ #
5
+ # Phase 1 Changes:
6
+ # - node_load: Reads issue.json first, falls back to node.json
7
+ # - node_save: Always writes to issue.json (preserves node.json for now)
8
+ # - node_exists: Checks for either issue.json or node.json
9
+ # ═══════════════════════════════════════════════════════════════════════════════
10
+
11
+ from typing import List, Optional
12
+ from memory_fs.Memory_FS import Memory_FS
13
+ from memory_fs.storage_fs.Storage_FS import Storage_FS
14
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
15
+ from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
16
+ from osbot_utils.utils.Json import json_loads, json_dumps
17
+ from issues_fs.schemas.graph.Safe_Str__Graph_Types import Safe_Str__Node_Type, Safe_Str__Node_Label
18
+ from issues_fs.schemas.graph.Schema__Global__Index import Schema__Global__Index
19
+ from issues_fs.schemas.graph.Schema__Node import Schema__Node
20
+ from issues_fs.schemas.graph.Schema__Node__Type import Schema__Node__Type
21
+ from issues_fs.schemas.graph.Schema__Link__Type import Schema__Link__Type
22
+ from issues_fs.schemas.graph.Schema__Type__Index import Schema__Type__Index
23
+ from issues_fs.issues.storage.Path__Handler__Graph_Node import Path__Handler__Graph_Node
24
+
25
+
26
+ class Graph__Repository(Type_Safe): # Memory-FS based graph repository
27
+ memory_fs : Memory_FS # Storage abstraction
28
+ path_handler : Path__Handler__Graph_Node # Path generation
29
+ storage_fs : Storage_FS = None # Set from memory_fs
30
+
31
+ def __init__(self, **kwargs):
32
+ super().__init__(**kwargs)
33
+ if self.memory_fs:
34
+ self.storage_fs = self.memory_fs.storage_fs
35
+
36
+ # ═══════════════════════════════════════════════════════════════════════════════
37
+ # Node Operations - Phase 1: Dual File Support
38
+ # ═══════════════════════════════════════════════════════════════════════════════
39
+
40
+ @type_safe
41
+ def node_save(self, node: Schema__Node) -> bool: # Save node to issue.json
42
+ if not node.label:
43
+ return False
44
+
45
+ path_issue = self.path_handler.path_for_issue_json(node_type = node.node_type, # Always write to issue.json
46
+ label = node.label )
47
+ data = node.json()
48
+ content = json_dumps(data, indent=2)
49
+ return self.storage_fs.file__save(path_issue, content.encode('utf-8'))
50
+
51
+ @type_safe
52
+ def node_load(self , # Load node from issue.json or node.json
53
+ node_type : Safe_Str__Node_Type ,
54
+ label : Safe_Str__Node_Label
55
+ ) -> Schema__Node:
56
+ path = self.get_issue_file_path(node_type, label) # Get actual file path (issue.json or node.json)
57
+ if path is None:
58
+ return None
59
+
60
+ content = self.storage_fs.file__str(path)
61
+ if not content:
62
+ return None
63
+
64
+ data = json_loads(content)
65
+ if data is None:
66
+ return None
67
+
68
+ return Schema__Node.from_json(data)
69
+
70
+ @type_safe
71
+ def node_delete(self , # Delete node from storage
72
+ node_type : Safe_Str__Node_Type ,
73
+ label : Safe_Str__Node_Label
74
+ ) -> bool:
75
+ deleted_any = False
76
+ path_issue = self.path_handler.path_for_issue_json(node_type, label) # Delete issue.json if exists
77
+ path_node = self.path_handler.path_for_node_json(node_type, label) # Delete node.json if exists
78
+
79
+ if self.storage_fs.file__exists(path_issue):
80
+ self.storage_fs.file__delete(path_issue)
81
+ deleted_any = True
82
+
83
+ if self.storage_fs.file__exists(path_node):
84
+ self.storage_fs.file__delete(path_node)
85
+ deleted_any = True
86
+
87
+ return deleted_any
88
+
89
+ @type_safe
90
+ def node_exists(self , # Check if node exists (either file)
91
+ node_type : Safe_Str__Node_Type ,
92
+ label : Safe_Str__Node_Label
93
+ ) -> bool:
94
+ return self.get_issue_file_path(node_type, label) is not None
95
+
96
+ # ═══════════════════════════════════════════════════════════════════════════════
97
+ # File Path Resolution - Phase 1: Prefer issue.json over node.json
98
+ # ═══════════════════════════════════════════════════════════════════════════════
99
+
100
+ @type_safe
101
+ def get_issue_file_path(self , # Get actual issue file path
102
+ node_type : Safe_Str__Node_Type , # Prefers issue.json, falls back to node.json
103
+ label : Safe_Str__Node_Label
104
+ ) -> str:
105
+ path_issue = self.path_handler.path_for_issue_json(node_type, label) # Check issue.json first
106
+ if self.storage_fs.file__exists(path_issue):
107
+ return path_issue
108
+
109
+ path_node = self.path_handler.path_for_node_json(node_type, label) # Fall back to node.json
110
+ if self.storage_fs.file__exists(path_node):
111
+ return path_node
112
+
113
+ return None # Neither exists
114
+
115
+ # ═══════════════════════════════════════════════════════════════════════════════
116
+ # Node Listing Operations
117
+ # ═══════════════════════════════════════════════════════════════════════════════
118
+
119
+ @type_safe
120
+ def nodes_list_labels(self , # List all node labels for a type
121
+ node_type : Safe_Str__Node_Type
122
+ ) -> List[Safe_Str__Node_Label]:
123
+ type_folder = self.path_handler.path_for_type_folder(node_type)
124
+ all_paths = self.storage_fs.files__paths()
125
+
126
+ labels = set() # Use set to avoid duplicates
127
+ prefix = f"{type_folder}/"
128
+
129
+ for path in all_paths:
130
+ if path.startswith(prefix) is False:
131
+ continue
132
+
133
+ relative = path[len(prefix):] # Remove prefix
134
+ parts = relative.split('/')
135
+
136
+ if len(parts) >= 2:
137
+ label = parts[0] # First part is label folder
138
+ filename = parts[1] # Second part is filename
139
+
140
+ if filename in ('issue.json', 'node.json'): # Check for either file
141
+ try:
142
+ labels.add(Safe_Str__Node_Label(label))
143
+ except Exception:
144
+ pass # Skip invalid label formats
145
+
146
+ return list(labels)
147
+
148
+ # ═══════════════════════════════════════════════════════════════════════════════
149
+ # Type Index Operations
150
+ # ═══════════════════════════════════════════════════════════════════════════════
151
+
152
+ @type_safe
153
+ def type_index_load(self , # Load per-type index
154
+ node_type : Safe_Str__Node_Type
155
+ ) -> Schema__Type__Index:
156
+ path = self.path_handler.path_for_type_index(node_type)
157
+ if self.storage_fs.file__exists(path) is False:
158
+ return Schema__Type__Index(node_type=node_type)
159
+
160
+ content = self.storage_fs.file__str(path)
161
+ if not content:
162
+ return Schema__Type__Index(node_type=node_type)
163
+
164
+ data = json_loads(content)
165
+ if data is None:
166
+ return Schema__Type__Index(node_type=node_type)
167
+
168
+ return Schema__Type__Index.from_json(data)
169
+
170
+ @type_safe
171
+ def type_index_save(self , # Save per-type index
172
+ index : Schema__Type__Index
173
+ ) -> bool:
174
+ path = self.path_handler.path_for_type_index(index.node_type)
175
+ data = index.json()
176
+ content = json_dumps(data, indent=2)
177
+ return self.storage_fs.file__save(path, content.encode('utf-8'))
178
+
179
+ # ═══════════════════════════════════════════════════════════════════════════════
180
+ # Global Index Operations
181
+ # ═══════════════════════════════════════════════════════════════════════════════
182
+
183
+ def global_index_load(self) -> Schema__Global__Index: # Load global index
184
+ path = self.path_handler.path_for_global_index()
185
+ if self.storage_fs.file__exists(path) is False:
186
+ return Schema__Global__Index()
187
+
188
+ content = self.storage_fs.file__str(path)
189
+ if not content:
190
+ return Schema__Global__Index()
191
+
192
+ data = json_loads(content)
193
+ if data is None:
194
+ return Schema__Global__Index()
195
+
196
+ return Schema__Global__Index.from_json(data)
197
+
198
+ def global_index_save(self, index: Schema__Global__Index) -> bool: # Save global index
199
+ path = self.path_handler.path_for_global_index()
200
+ data = index.json()
201
+ content = json_dumps(data, indent=2)
202
+ return self.storage_fs.file__save(path, content.encode('utf-8'))
203
+
204
+ # ═══════════════════════════════════════════════════════════════════════════════
205
+ # Config Operations - Node Types
206
+ # ═══════════════════════════════════════════════════════════════════════════════
207
+
208
+ def node_types_load(self) -> List[Schema__Node__Type]: # Load all node types
209
+ path = self.path_handler.path_for_node_types()
210
+ if self.storage_fs.file__exists(path) is False:
211
+ return []
212
+
213
+ content = self.storage_fs.file__str(path)
214
+ if not content:
215
+ return []
216
+
217
+ data = json_loads(content)
218
+ if data is None or 'types' not in data:
219
+ return []
220
+
221
+ types = []
222
+ for item in data['types']:
223
+ types.append(Schema__Node__Type.from_json(item))
224
+ return types
225
+
226
+ def node_types_save(self, types: List[Schema__Node__Type]) -> bool: # Save all node types
227
+ path = self.path_handler.path_for_node_types()
228
+ data = {'types': [t.json() for t in types]}
229
+ content = json_dumps(data, indent=2)
230
+ return self.storage_fs.file__save(path, content.encode('utf-8'))
231
+
232
+ # ═══════════════════════════════════════════════════════════════════════════════
233
+ # Config Operations - Link Types
234
+ # ═══════════════════════════════════════════════════════════════════════════════
235
+
236
+ def link_types_load(self) -> List[Schema__Link__Type]: # Load all link types
237
+ path = self.path_handler.path_for_link_types()
238
+ if self.storage_fs.file__exists(path) is False:
239
+ return []
240
+
241
+ content = self.storage_fs.file__str(path)
242
+ if not content:
243
+ return []
244
+
245
+ data = json_loads(content)
246
+ if data is None or 'link_types' not in data:
247
+ return []
248
+
249
+ types = []
250
+ for item in data['link_types']:
251
+ types.append(Schema__Link__Type.from_json(item))
252
+ return types
253
+
254
+ def link_types_save(self, types: List[Schema__Link__Type]) -> bool: # Save all link types
255
+ path = self.path_handler.path_for_link_types()
256
+ data = {'link_types': [t.json() for t in types]}
257
+ content = json_dumps(data, indent=2)
258
+ return self.storage_fs.file__save(path, content.encode('utf-8'))
259
+
260
+ # ═══════════════════════════════════════════════════════════════════════════════
261
+ # Attachment Operations
262
+ # ═══════════════════════════════════════════════════════════════════════════════
263
+
264
+ @type_safe
265
+ def attachment_save(self , # Save attachment (raw bytes)
266
+ node_type : Safe_Str__Node_Type ,
267
+ label : Safe_Str__Node_Label ,
268
+ filename : str ,
269
+ data : bytes
270
+ ) -> bool:
271
+ path = self.path_handler.path_for_attachment(node_type = node_type ,
272
+ label = label ,
273
+ filename = filename )
274
+ return self.storage_fs.file__save(path, data)
275
+
276
+ @type_safe
277
+ def attachment_load(self , # Load attachment
278
+ node_type : Safe_Str__Node_Type ,
279
+ label : Safe_Str__Node_Label ,
280
+ filename : str
281
+ ) -> bytes:
282
+ path = self.path_handler.path_for_attachment(node_type = node_type ,
283
+ label = label ,
284
+ filename = filename )
285
+ if self.storage_fs.file__exists(path) is False:
286
+ return None
287
+ return self.storage_fs.file__bytes(path)
288
+
289
+ @type_safe
290
+ def attachment_delete(self , # Delete attachment
291
+ node_type : Safe_Str__Node_Type ,
292
+ label : Safe_Str__Node_Label ,
293
+ filename : str
294
+ ) -> bool:
295
+ path = self.path_handler.path_for_attachment(node_type = node_type ,
296
+ label = label ,
297
+ filename = filename )
298
+ if self.storage_fs.file__exists(path):
299
+ return self.storage_fs.file__delete(path)
300
+ return False
301
+
302
+ # ═══════════════════════════════════════════════════════════════════════════════
303
+ # Utility Operations
304
+ # ═══════════════════════════════════════════════════════════════════════════════
305
+
306
+ def clear_storage(self) -> None: # Clear all data (for tests)
307
+ self.storage_fs.clear()