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,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)
|