nod-cli 0.1.0rc1__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.
- nod/__init__.py +1 -0
- nod/application/__init__.py +0 -0
- nod/application/services.py +195 -0
- nod/cli/__init__.py +0 -0
- nod/cli/app.py +518 -0
- nod/domain/__init__.py +0 -0
- nod/domain/enums.py +45 -0
- nod/domain/errors.py +22 -0
- nod/domain/models.py +86 -0
- nod/infrastructure/__init__.py +0 -0
- nod/infrastructure/database.py +37 -0
- nod/infrastructure/git.py +38 -0
- nod/infrastructure/models.py +138 -0
- nod/infrastructure/repositories.py +173 -0
- nod/mcp/__init__.py +0 -0
- nod/mcp/server.py +177 -0
- nod_cli-0.1.0rc1.dist-info/METADATA +111 -0
- nod_cli-0.1.0rc1.dist-info/RECORD +21 -0
- nod_cli-0.1.0rc1.dist-info/WHEEL +4 -0
- nod_cli-0.1.0rc1.dist-info/entry_points.txt +3 -0
- nod_cli-0.1.0rc1.dist-info/licenses/LICENSE +21 -0
nod/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import re
|
|
3
|
+
from uuid import UUID, uuid4
|
|
4
|
+
|
|
5
|
+
import networkx as nx
|
|
6
|
+
from sqlalchemy import select
|
|
7
|
+
|
|
8
|
+
from nod.domain.enums import (
|
|
9
|
+
CycleStatus,
|
|
10
|
+
ModuleStatus,
|
|
11
|
+
Priority,
|
|
12
|
+
RelationType,
|
|
13
|
+
WorkItemStatus,
|
|
14
|
+
WorkItemType,
|
|
15
|
+
)
|
|
16
|
+
from nod.domain.errors import DependencyCycleError, NotFoundError, ValidationError
|
|
17
|
+
from nod.domain.models import Cycle, Module, WorkItem
|
|
18
|
+
from nod.infrastructure.models import WorkItemRelationModel
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def slugify(value: str) -> str:
|
|
22
|
+
value = value.strip().lower()
|
|
23
|
+
value = re.sub(r"[^a-z0-9]+", "-", value)
|
|
24
|
+
return value.strip("-")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Services:
|
|
28
|
+
def __init__(self, session, project):
|
|
29
|
+
from nod.infrastructure.repositories import (
|
|
30
|
+
CycleRepository,
|
|
31
|
+
ModuleRepository,
|
|
32
|
+
WorkItemRepository,
|
|
33
|
+
)
|
|
34
|
+
self.session = session
|
|
35
|
+
self.project = project
|
|
36
|
+
self.modules = ModuleRepository(session)
|
|
37
|
+
self.cycles = CycleRepository(session)
|
|
38
|
+
self.items = WorkItemRepository(session)
|
|
39
|
+
|
|
40
|
+
def create_module(self, name: str, description=None) -> Module:
|
|
41
|
+
if not name.strip():
|
|
42
|
+
raise ValidationError("Module name cannot be empty.")
|
|
43
|
+
module = Module(uuid4(), self.project.id, name.strip(), slugify(name), description=description)
|
|
44
|
+
self.modules.add(module)
|
|
45
|
+
self.session.commit()
|
|
46
|
+
return module
|
|
47
|
+
|
|
48
|
+
def update_module(self, ref: str, title=None, description=None, append=None, status=None, start=None, target=None):
|
|
49
|
+
module = self.modules.get(ref)
|
|
50
|
+
if not module:
|
|
51
|
+
raise NotFoundError(f"Module not found: {ref}")
|
|
52
|
+
if title is not None:
|
|
53
|
+
module.name = title.strip()
|
|
54
|
+
if description is not None:
|
|
55
|
+
module.description = description
|
|
56
|
+
if append is not None:
|
|
57
|
+
module.description = ((module.description or "").rstrip() + "\n\n" + append.strip()).strip()
|
|
58
|
+
if status is not None:
|
|
59
|
+
module.status = ModuleStatus(status)
|
|
60
|
+
self.modules.update(module)
|
|
61
|
+
self.session.commit()
|
|
62
|
+
return module
|
|
63
|
+
|
|
64
|
+
def create_cycle(self, name: str, start=None, end=None, description=None) -> Cycle:
|
|
65
|
+
if not name.strip():
|
|
66
|
+
raise ValidationError("Cycle name cannot be empty.")
|
|
67
|
+
if start and end and start > end:
|
|
68
|
+
raise ValidationError("Cycle start date cannot be after end date.")
|
|
69
|
+
cycle = Cycle(uuid4(), self.project.id, name.strip(), slugify(name), description=description, start_date=start, end_date=end)
|
|
70
|
+
self.cycles.add(cycle)
|
|
71
|
+
self.session.commit()
|
|
72
|
+
return cycle
|
|
73
|
+
|
|
74
|
+
def update_cycle(self, ref: str, title=None, description=None, append=None, status=None, start=None, end=None):
|
|
75
|
+
cycle = self.cycles.get(ref)
|
|
76
|
+
if not cycle:
|
|
77
|
+
raise NotFoundError(f"Cycle not found: {ref}")
|
|
78
|
+
new_start = start if start is not None else cycle.start_date
|
|
79
|
+
new_end = end if end is not None else cycle.end_date
|
|
80
|
+
if new_start and new_end and new_start > new_end:
|
|
81
|
+
raise ValidationError("Cycle start date cannot be after end date.")
|
|
82
|
+
if title is not None:
|
|
83
|
+
cycle.name = title.strip()
|
|
84
|
+
if description is not None:
|
|
85
|
+
cycle.description = description
|
|
86
|
+
if append is not None:
|
|
87
|
+
cycle.description = ((cycle.description or "").rstrip() + "\n\n" + append.strip()).strip()
|
|
88
|
+
if status is not None:
|
|
89
|
+
cycle.status = CycleStatus(status)
|
|
90
|
+
cycle.start_date, cycle.end_date = new_start, new_end
|
|
91
|
+
self.cycles.update(cycle)
|
|
92
|
+
self.session.commit()
|
|
93
|
+
return cycle
|
|
94
|
+
|
|
95
|
+
def create_task(self, title: str, description=None, type_=WorkItemType.TASK, priority=Priority.MEDIUM, module=None, cycle=None, estimate=None, status=None, branch=None):
|
|
96
|
+
if not title.strip():
|
|
97
|
+
raise ValidationError("Task title cannot be empty.")
|
|
98
|
+
sequence = self.items.next_sequence(self.project.id)
|
|
99
|
+
item = WorkItem(
|
|
100
|
+
uuid4(), self.project.id, sequence,
|
|
101
|
+
f"{self.project.identifier}-{sequence}", title.strip(),
|
|
102
|
+
description=description, type=type_, priority=priority,
|
|
103
|
+
module_id=module.id if module else None,
|
|
104
|
+
cycle_id=cycle.id if cycle else None,
|
|
105
|
+
estimate=estimate,
|
|
106
|
+
status=status or WorkItemStatus.TODO,
|
|
107
|
+
branch_name=branch,
|
|
108
|
+
)
|
|
109
|
+
self.items.add(item)
|
|
110
|
+
self.session.commit()
|
|
111
|
+
return item
|
|
112
|
+
|
|
113
|
+
def update_task(
|
|
114
|
+
self, identifier: str, title=None, description=None, append=None,
|
|
115
|
+
status=None, priority=None, module=None, cycle=None, branch=None, estimate=None
|
|
116
|
+
):
|
|
117
|
+
item = self.items.get(self.project.id, identifier)
|
|
118
|
+
if not item:
|
|
119
|
+
raise NotFoundError(f"Work Item not found: {identifier}")
|
|
120
|
+
if title is not None:
|
|
121
|
+
item.title = title.strip()
|
|
122
|
+
if description is not None:
|
|
123
|
+
item.description = description
|
|
124
|
+
if append is not None:
|
|
125
|
+
item.description = ((item.description or "").rstrip() + "\n\n" + append.strip()).strip()
|
|
126
|
+
if status is not None:
|
|
127
|
+
item.status = WorkItemStatus(status)
|
|
128
|
+
if priority is not None:
|
|
129
|
+
item.priority = Priority(priority)
|
|
130
|
+
if module is not None:
|
|
131
|
+
item.module_id = module.id
|
|
132
|
+
if cycle is not None:
|
|
133
|
+
item.cycle_id = cycle.id
|
|
134
|
+
if branch is not None:
|
|
135
|
+
item.branch_name = branch
|
|
136
|
+
if estimate is not None:
|
|
137
|
+
item.estimate = estimate
|
|
138
|
+
self.items.update(item)
|
|
139
|
+
self.session.commit()
|
|
140
|
+
return item
|
|
141
|
+
|
|
142
|
+
def add_dependency(self, source: str, target: str):
|
|
143
|
+
source_item = self.items.get(self.project.id, source)
|
|
144
|
+
target_item = self.items.get(self.project.id, target)
|
|
145
|
+
if not source_item or not target_item:
|
|
146
|
+
raise NotFoundError("Both Work Items must exist.")
|
|
147
|
+
|
|
148
|
+
graph = self._dependency_graph()
|
|
149
|
+
graph.add_edge(source_item.identifier, target_item.identifier)
|
|
150
|
+
if not nx.is_directed_acyclic_graph(graph):
|
|
151
|
+
raise DependencyCycleError("Dependency would create a cycle.")
|
|
152
|
+
|
|
153
|
+
self.session.add(WorkItemRelationModel(
|
|
154
|
+
source_work_item_id=str(source_item.id),
|
|
155
|
+
target_work_item_id=str(target_item.id),
|
|
156
|
+
relation_type=RelationType.DEPENDS_ON.value,
|
|
157
|
+
))
|
|
158
|
+
self.session.commit()
|
|
159
|
+
|
|
160
|
+
def _dependency_graph(self):
|
|
161
|
+
graph = nx.DiGraph()
|
|
162
|
+
rows = self.session.scalars(
|
|
163
|
+
select(WorkItemRelationModel).where(
|
|
164
|
+
WorkItemRelationModel.relation_type == RelationType.DEPENDS_ON.value
|
|
165
|
+
)
|
|
166
|
+
).all()
|
|
167
|
+
for row in rows:
|
|
168
|
+
source = self.session.get(__import__("nod.infrastructure.models", fromlist=["WorkItemModel"]).WorkItemModel, row.source_work_item_id)
|
|
169
|
+
target = self.session.get(__import__("nod.infrastructure.models", fromlist=["WorkItemModel"]).WorkItemModel, row.target_work_item_id)
|
|
170
|
+
if source and target:
|
|
171
|
+
graph.add_edge(source.identifier, target.identifier)
|
|
172
|
+
return graph
|
|
173
|
+
|
|
174
|
+
def graph(self):
|
|
175
|
+
from nod.infrastructure.models import WorkItemRelationModel, WorkItemModel
|
|
176
|
+
graph = nx.DiGraph()
|
|
177
|
+
items = self.items.list(self.project.id)
|
|
178
|
+
modules = {m.id: m.slug for m in self.modules.list()}
|
|
179
|
+
for item in items:
|
|
180
|
+
graph.add_node(
|
|
181
|
+
item.identifier,
|
|
182
|
+
title=item.title,
|
|
183
|
+
status=item.status.value,
|
|
184
|
+
priority=item.priority.value,
|
|
185
|
+
description=item.description or "",
|
|
186
|
+
branch=item.branch_name,
|
|
187
|
+
module=modules.get(item.module_id),
|
|
188
|
+
)
|
|
189
|
+
rows = self.session.scalars(select(WorkItemRelationModel)).all()
|
|
190
|
+
for row in rows:
|
|
191
|
+
source = self.session.get(WorkItemModel, row.source_work_item_id)
|
|
192
|
+
target = self.session.get(WorkItemModel, row.target_work_item_id)
|
|
193
|
+
if source and target:
|
|
194
|
+
graph.add_edge(source.identifier, target.identifier, relation=row.relation_type)
|
|
195
|
+
return graph
|
nod/cli/__init__.py
ADDED
|
File without changes
|