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,106 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Graph__Repository__Factory - Factory for creating graph repositories
3
+ # Provides easy setup for different storage backends (Memory, Local Disk, S3, etc.)
4
+ # ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ from memory_fs.Memory_FS import Memory_FS
7
+ from memory_fs.helpers.Memory_FS__In_Memory import Memory_FS__In_Memory
8
+ from memory_fs.storage_fs.providers.Storage_FS__Local_Disk import Storage_FS__Local_Disk
9
+ from memory_fs.storage_fs.providers.Storage_FS__Sqlite import Storage_FS__Sqlite
10
+ from memory_fs.storage_fs.providers.Storage_FS__Zip import Storage_FS__Zip
11
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
12
+ from issues_fs.schemas.enums.Enum__Graph__Storage__Backend import Enum__Graph__Storage__Backend
13
+ from issues_fs.issues.graph_services.Graph__Repository import Graph__Repository
14
+ from issues_fs.issues.storage.Path__Handler__Graph_Node import Path__Handler__Graph_Node
15
+
16
+
17
+ class Graph__Repository__Factory(Type_Safe): # Factory for graph repositories
18
+
19
+ # ═══════════════════════════════════════════════════════════════════════════════
20
+ # Memory Backend (for tests)
21
+ # ═══════════════════════════════════════════════════════════════════════════════
22
+
23
+ @classmethod
24
+ def create_memory(cls) -> Graph__Repository: # Create in-memory repository
25
+ memory_fs = Memory_FS__In_Memory()
26
+ path_handler = Path__Handler__Graph_Node()
27
+ return Graph__Repository(memory_fs = memory_fs ,
28
+ path_handler = path_handler)
29
+
30
+ # ═══════════════════════════════════════════════════════════════════════════════
31
+ # Local Disk Backend (for development / Git workflows)
32
+ # ═══════════════════════════════════════════════════════════════════════════════
33
+
34
+ @classmethod
35
+ def create_local_disk(cls , # Create local disk repository
36
+ root_path : str
37
+ ) -> Graph__Repository:
38
+ memory_fs = Memory_FS()
39
+ storage = Storage_FS__Local_Disk(root_path=str(root_path))
40
+ memory_fs.storage_fs = storage
41
+ path_handler = Path__Handler__Graph_Node()
42
+ return Graph__Repository(memory_fs = memory_fs ,
43
+ path_handler = path_handler)
44
+
45
+ # ═══════════════════════════════════════════════════════════════════════════════
46
+ # SQLite Backend (for embedded use)
47
+ # ═══════════════════════════════════════════════════════════════════════════════
48
+
49
+ @classmethod
50
+ def create_sqlite(cls , # Create SQLite repository
51
+ db_path : str
52
+ ) -> Graph__Repository:
53
+ memory_fs = Memory_FS()
54
+ storage = Storage_FS__Sqlite(db_path=str(db_path))
55
+ memory_fs.storage_fs = storage
56
+ path_handler = Path__Handler__Graph_Node()
57
+ return Graph__Repository(memory_fs = memory_fs ,
58
+ path_handler = path_handler)
59
+
60
+ # ═══════════════════════════════════════════════════════════════════════════════
61
+ # ZIP Backend (for archives/export)
62
+ # ═══════════════════════════════════════════════════════════════════════════════
63
+
64
+ @classmethod
65
+ def create_zip(cls , # Create ZIP repository
66
+ zip_path : str
67
+ ) -> Graph__Repository:
68
+ memory_fs = Memory_FS()
69
+ storage = Storage_FS__Zip(zip_path=str(zip_path))
70
+ memory_fs.storage_fs = storage
71
+ path_handler = Path__Handler__Graph_Node()
72
+ return Graph__Repository(memory_fs = memory_fs ,
73
+ path_handler = path_handler)
74
+
75
+ # ═══════════════════════════════════════════════════════════════════════════════
76
+ # Generic Factory Method
77
+ # ═══════════════════════════════════════════════════════════════════════════════
78
+
79
+ @classmethod
80
+ def create(cls , # Generic factory
81
+ backend : Enum__Graph__Storage__Backend ,
82
+ root_path : str = None ,
83
+ db_path : str = None ,
84
+ zip_path : str = None
85
+ ) -> Graph__Repository:
86
+
87
+ if backend == Enum__Graph__Storage__Backend.MEMORY:
88
+ return cls.create_memory()
89
+
90
+ elif backend == Enum__Graph__Storage__Backend.LOCAL_DISK:
91
+ if root_path is None:
92
+ raise ValueError("root_path required for LOCAL_DISK backend")
93
+ return cls.create_local_disk(root_path=root_path)
94
+
95
+ elif backend == Enum__Graph__Storage__Backend.SQLITE:
96
+ if db_path is None:
97
+ raise ValueError("db_path required for SQLITE backend")
98
+ return cls.create_sqlite(db_path=db_path)
99
+
100
+ elif backend == Enum__Graph__Storage__Backend.ZIP:
101
+ if zip_path is None:
102
+ raise ValueError("zip_path required for ZIP backend")
103
+ return cls.create_zip(zip_path=zip_path)
104
+
105
+ else:
106
+ raise ValueError(f"Unknown backend: {backend}")
@@ -0,0 +1,227 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════════
2
+ # Link__Service - Business logic for relationship operations
3
+ # Handles bidirectional link creation and deletion between nodes
4
+ # ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ from typing import Optional
7
+ from osbot_utils.type_safe.Type_Safe import Type_Safe
8
+ from osbot_utils.type_safe.primitives.domains.identifiers.safe_int.Timestamp_Now import Timestamp_Now
9
+ from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
10
+
11
+ from issues_fs.schemas.graph.Safe_Str__Graph_Types import Safe_Str__Node_Type, Safe_Str__Node_Label, Safe_Str__Link_Verb
12
+ from issues_fs.schemas.graph.Schema__Link__Create__Request import Schema__Link__Create__Request
13
+ from issues_fs.schemas.graph.Schema__Link__Create__Response import Schema__Link__Create__Response
14
+ from issues_fs.schemas.graph.Schema__Link__Delete__Response import Schema__Link__Delete__Response
15
+ from issues_fs.schemas.graph.Schema__Link__List__Response import Schema__Link__List__Response
16
+ from issues_fs.schemas.graph.Schema__Node__Link import Schema__Node__Link
17
+ from issues_fs.schemas.graph.Schema__Link__Type import Schema__Link__Type
18
+ from issues_fs.issues.graph_services.Graph__Repository import Graph__Repository
19
+
20
+
21
+ class Link__Service(Type_Safe): # Link business logic service
22
+ repository : Graph__Repository # Data access layer
23
+
24
+ # ═══════════════════════════════════════════════════════════════════════════════
25
+ # Query Operations
26
+ # ═══════════════════════════════════════════════════════════════════════════════
27
+
28
+ def list_links(self , # List links for a node
29
+ node_type : Safe_Str__Node_Type ,
30
+ label : Safe_Str__Node_Label
31
+ ) -> Schema__Link__List__Response:
32
+ node = self.repository.node_load(node_type = node_type ,
33
+ label = label )
34
+ if node is None:
35
+ return Schema__Link__List__Response(success = False ,
36
+ message = f'Node not found: {label}')
37
+
38
+ return Schema__Link__List__Response(success = True ,
39
+ links = node.links )
40
+
41
+ # ═══════════════════════════════════════════════════════════════════════════════
42
+ # Create Link Operations
43
+ # ═══════════════════════════════════════════════════════════════════════════════
44
+
45
+ @type_safe
46
+ def create_link(self , # Create bidirectional link
47
+ source_type : Safe_Str__Node_Type ,
48
+ source_label : Safe_Str__Node_Label ,
49
+ request : Schema__Link__Create__Request # todo: see if the source_type and source_label should not be defined inside the Schema__Link__Create__Request class
50
+ ) -> Schema__Link__Create__Response:
51
+ # Load source node
52
+ source_node = self.repository.node_load(node_type = source_type ,
53
+ label = source_label )
54
+ if source_node is None:
55
+ return Schema__Link__Create__Response(success = False ,
56
+ message = f'Source not found: {source_label}')
57
+
58
+ # Parse target label to get type (e.g., "Bug-27" → "bug", "Bug-27")
59
+ target_type, target_label = self.parse_label(request.target_label)
60
+ if target_type is None:
61
+ return Schema__Link__Create__Response(success = False ,
62
+ message = f'Invalid target label: {request.target_label}')
63
+
64
+ # Load target node
65
+ target_node = self.repository.node_load(node_type = target_type ,
66
+ label = target_label )
67
+ if target_node is None:
68
+ return Schema__Link__Create__Response(success = False ,
69
+ message = f'Target not found: {target_label}')
70
+
71
+ # Find link type definition
72
+ link_type_def = self.find_link_type(request.verb)
73
+ if link_type_def is None:
74
+ return Schema__Link__Create__Response(success = False ,
75
+ message = f'Unknown link type: {request.verb}')
76
+
77
+ # Validate source/target types are allowed
78
+ if str(source_type) not in [str(t) for t in link_type_def.source_types]:
79
+ return Schema__Link__Create__Response(success = False ,
80
+ message = f'{source_type} cannot use verb {request.verb}')
81
+
82
+ if str(target_type) not in [str(t) for t in link_type_def.target_types]:
83
+ return Schema__Link__Create__Response(success = False ,
84
+ message = f'{request.verb} cannot target {target_type}')
85
+
86
+ now = Timestamp_Now()
87
+
88
+ # Create forward link (source → target)
89
+ source_link = Schema__Node__Link(link_type_id = link_type_def.link_type_id ,
90
+ verb = request.verb ,
91
+ target_id = target_node.node_id ,
92
+ target_label = target_label ,
93
+ created_at = now )
94
+
95
+ # Create inverse link (target → source)
96
+ target_link = Schema__Node__Link(link_type_id = link_type_def.link_type_id ,
97
+ verb = link_type_def.inverse_verb ,
98
+ target_id = source_node.node_id ,
99
+ target_label = source_label ,
100
+ created_at = now )
101
+
102
+ # Check for duplicate links
103
+ for existing in source_node.links:
104
+ if str(existing.target_id) == str(target_node.node_id) and str(existing.verb) == str(request.verb):
105
+ return Schema__Link__Create__Response(success = False ,
106
+ message = 'Link already exists')
107
+
108
+ # Add links to nodes
109
+ source_node.links.append(source_link)
110
+ source_node.updated_at = now
111
+
112
+ target_node.links.append(target_link)
113
+ target_node.updated_at = now
114
+
115
+ # Save both nodes
116
+ if self.repository.node_save(source_node) is False:
117
+ return Schema__Link__Create__Response(success = False ,
118
+ message = 'Failed to save source' )
119
+
120
+ if self.repository.node_save(target_node) is False:
121
+ # Rollback source (remove link we just added)
122
+ source_node.links.remove(source_link)
123
+ self.repository.node_save(source_node)
124
+ return Schema__Link__Create__Response(success = False ,
125
+ message = 'Failed to save target' )
126
+
127
+ return Schema__Link__Create__Response(success = True ,
128
+ source_link = source_link ,
129
+ target_link = target_link )
130
+
131
+ # ═══════════════════════════════════════════════════════════════════════════════
132
+ # Delete Link Operations
133
+ # ═══════════════════════════════════════════════════════════════════════════════
134
+
135
+ def delete_link(self , # Delete bidirectional link
136
+ source_type : Safe_Str__Node_Type ,
137
+ source_label : Safe_Str__Node_Label ,
138
+ target_label : Safe_Str__Node_Label
139
+ ) -> Schema__Link__Delete__Response:
140
+ # Load source node
141
+ source_node = self.repository.node_load(node_type = source_type ,
142
+ label = source_label )
143
+ if source_node is None:
144
+ return Schema__Link__Delete__Response(success = False ,
145
+ deleted = False ,
146
+ message = f'Source not found: {source_label}')
147
+
148
+ # Find and remove the link from source
149
+ link_to_remove = None
150
+ for link in source_node.links:
151
+ if str(link.target_label) == str(target_label):
152
+ link_to_remove = link
153
+ break
154
+
155
+ if link_to_remove is None:
156
+ return Schema__Link__Delete__Response(success = False ,
157
+ deleted = False ,
158
+ message = 'Link not found')
159
+
160
+ # Parse target label
161
+ target_type, parsed_target_label = self.parse_label(target_label)
162
+ if target_type is None:
163
+ return Schema__Link__Delete__Response(success = False ,
164
+ deleted = False ,
165
+ message = f'Invalid target label: {target_label}')
166
+
167
+ # Load target node
168
+ target_node = self.repository.node_load(node_type = target_type ,
169
+ label = parsed_target_label)
170
+ if target_node is None:
171
+ return Schema__Link__Delete__Response(success = False ,
172
+ deleted = False ,
173
+ message = f'Target not found: {target_label}')
174
+
175
+ # Remove link from source
176
+ source_node.links.remove(link_to_remove)
177
+ source_node.updated_at = Timestamp_Now()
178
+
179
+ # Find and remove inverse link from target
180
+ inverse_to_remove = None
181
+ for link in target_node.links:
182
+ if str(link.target_label) == str(source_label):
183
+ inverse_to_remove = link
184
+ break
185
+
186
+ if inverse_to_remove:
187
+ target_node.links.remove(inverse_to_remove)
188
+ target_node.updated_at = Timestamp_Now()
189
+
190
+ # Save both nodes
191
+ self.repository.node_save(source_node)
192
+ if inverse_to_remove:
193
+ self.repository.node_save(target_node)
194
+
195
+ return Schema__Link__Delete__Response(success = True ,
196
+ deleted = True ,
197
+ source_label = source_label ,
198
+ target_label = target_label )
199
+
200
+ # ═══════════════════════════════════════════════════════════════════════════════
201
+ # Helper Methods
202
+ # ═══════════════════════════════════════════════════════════════════════════════
203
+
204
+ def find_link_type(self , # Find link type by verb
205
+ verb : Safe_Str__Link_Verb
206
+ ) -> Optional[Schema__Link__Type]:
207
+ link_types = self.repository.link_types_load()
208
+ for lt in link_types:
209
+ if str(lt.verb) == str(verb):
210
+ return lt
211
+ return None
212
+
213
+ def parse_label(self , # Parse label to (type, label)
214
+ label : Safe_Str__Node_Label
215
+ ) -> tuple:
216
+ # "Bug-27" → ("bug", "Bug-27")
217
+ # "Task-15" → ("task", "Task-15")
218
+ label_str = str(label)
219
+ if '-' not in label_str:
220
+ return (None, None)
221
+
222
+ parts = label_str.split('-', 1)
223
+ if len(parts) != 2:
224
+ return (None, None)
225
+
226
+ node_type = parts[0].lower() # "Bug" → "bug"
227
+ return (Safe_Str__Node_Type(node_type), label)