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