nod-cli 0.1.0rc1__tar.gz
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_cli-0.1.0rc1/.github/workflows/publish.yml +22 -0
- nod_cli-0.1.0rc1/.gitignore +8 -0
- nod_cli-0.1.0rc1/LICENSE +21 -0
- nod_cli-0.1.0rc1/PKG-INFO +111 -0
- nod_cli-0.1.0rc1/README.md +80 -0
- nod_cli-0.1.0rc1/migrations/README.md +3 -0
- nod_cli-0.1.0rc1/pyproject.toml +56 -0
- nod_cli-0.1.0rc1/src/nod/__init__.py +1 -0
- nod_cli-0.1.0rc1/src/nod/application/__init__.py +0 -0
- nod_cli-0.1.0rc1/src/nod/application/services.py +195 -0
- nod_cli-0.1.0rc1/src/nod/cli/__init__.py +0 -0
- nod_cli-0.1.0rc1/src/nod/cli/app.py +518 -0
- nod_cli-0.1.0rc1/src/nod/domain/__init__.py +0 -0
- nod_cli-0.1.0rc1/src/nod/domain/enums.py +45 -0
- nod_cli-0.1.0rc1/src/nod/domain/errors.py +22 -0
- nod_cli-0.1.0rc1/src/nod/domain/models.py +86 -0
- nod_cli-0.1.0rc1/src/nod/infrastructure/__init__.py +0 -0
- nod_cli-0.1.0rc1/src/nod/infrastructure/database.py +37 -0
- nod_cli-0.1.0rc1/src/nod/infrastructure/git.py +38 -0
- nod_cli-0.1.0rc1/src/nod/infrastructure/models.py +138 -0
- nod_cli-0.1.0rc1/src/nod/infrastructure/repositories.py +173 -0
- nod_cli-0.1.0rc1/src/nod/mcp/__init__.py +0 -0
- nod_cli-0.1.0rc1/src/nod/mcp/server.py +177 -0
- nod_cli-0.1.0rc1/tests/test_cli_workflow.py +16 -0
- nod_cli-0.1.0rc1/tests/test_domain.py +11 -0
- nod_cli-0.1.0rc1/tests/test_views.py +85 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
pypi:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.13"
|
|
20
|
+
- run: python -m pip install --upgrade build
|
|
21
|
+
- run: python -m build
|
|
22
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
nod_cli-0.1.0rc1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 neatnettech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nod-cli
|
|
3
|
+
Version: 0.1.0rc1
|
|
4
|
+
Summary: Git-native project management for humans and coding agents.
|
|
5
|
+
Project-URL: Homepage, https://github.com/neatnettech/nod
|
|
6
|
+
Project-URL: Repository, https://github.com/neatnettech/nod
|
|
7
|
+
Author: neatnettech
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cli,git,kanban,mcp,project-management
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Requires-Dist: alembic<2,>=1.16
|
|
21
|
+
Requires-Dist: mcp<2,>=1.9
|
|
22
|
+
Requires-Dist: networkx<4,>=3.4
|
|
23
|
+
Requires-Dist: pydantic<3,>=2.11
|
|
24
|
+
Requires-Dist: rich<15,>=14
|
|
25
|
+
Requires-Dist: sqlalchemy<3,>=2.0
|
|
26
|
+
Requires-Dist: typer<1,>=0.16
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest-asyncio<2,>=1; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest<9,>=8; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Nod
|
|
33
|
+
|
|
34
|
+
Git-native project management for humans and coding agents.
|
|
35
|
+
|
|
36
|
+
Nod lives in your repository: project state is stored in `.nod/nod.db`, so
|
|
37
|
+
the plan travels with the code. Humans drive it through a focused CLI,
|
|
38
|
+
coding agents drive the same state through an MCP server.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# released version from PyPI (both nod and nod-mcp on PATH)
|
|
44
|
+
pipx install nod-cli
|
|
45
|
+
|
|
46
|
+
# bleeding edge, straight from main
|
|
47
|
+
pipx install git+https://github.com/neatnettech/nod.git
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
After a PyPI release, update with `pipx upgrade nod-cli`. For git installs,
|
|
51
|
+
run `pipx reinstall nod-cli` to pick up new commits.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python -m venv .venv
|
|
57
|
+
source .venv/bin/activate
|
|
58
|
+
pip install -e ".[dev]"
|
|
59
|
+
|
|
60
|
+
mkdir demo && cd demo
|
|
61
|
+
git init
|
|
62
|
+
nod init
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Concepts
|
|
66
|
+
|
|
67
|
+
* **Modules** group work by area: `nod module add "Vault"`
|
|
68
|
+
* **Cycles** are timeboxed plans: `nod cycle add "Sprint 1" --start 2026-10-01 --end 2026-10-14`
|
|
69
|
+
* **Work items** are epics, stories, tasks, and bugs:
|
|
70
|
+
`nod task add "Seed categories" --module vault --cycle sprint-1 --estimate 2h`
|
|
71
|
+
* **Dependencies** order the work: `nod depends OSS-2 OSS-1`
|
|
72
|
+
|
|
73
|
+
## Views
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
nod task list # tabular work items
|
|
77
|
+
nod module list # modules
|
|
78
|
+
nod cycle list # cycles
|
|
79
|
+
nod board # kanban grouped by status
|
|
80
|
+
nod timeline # cycles over time with estimates
|
|
81
|
+
nod graph # ASCII dependency diagram
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The dependency graph renders as a tree of prerequisite arrows with status
|
|
85
|
+
glyphs and branch names:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
OSS-1 StateBadge component ✓ DONE ⎇ feat/oss-1-statebadge
|
|
89
|
+
OSS-3 Seed categories ○ TODO
|
|
90
|
+
├─► OSS-4 Vault home ◐ IN_PROGRESS (depends on this)
|
|
91
|
+
└─► OSS-6 Note editor ○ TODO (depends on this)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Every view accepts `--json` for scripting and agents.
|
|
95
|
+
|
|
96
|
+
## Agents
|
|
97
|
+
|
|
98
|
+
`nod-mcp` exposes the same state as MCP tools: work item create/update/list,
|
|
99
|
+
project info, the board, the timeline, and the dependency graph. Point your
|
|
100
|
+
MCP client at `nod-mcp` from inside the repository.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pip install -e ".[dev]"
|
|
106
|
+
pytest
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Releases: tag a version (`v0.1.0-rc.1`, then `v0.1.0`) and push it. The
|
|
110
|
+
`publish` workflow builds the version from the tag and publishes to PyPI via
|
|
111
|
+
trusted publishing. Requires Python 3.13+.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Nod
|
|
2
|
+
|
|
3
|
+
Git-native project management for humans and coding agents.
|
|
4
|
+
|
|
5
|
+
Nod lives in your repository: project state is stored in `.nod/nod.db`, so
|
|
6
|
+
the plan travels with the code. Humans drive it through a focused CLI,
|
|
7
|
+
coding agents drive the same state through an MCP server.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# released version from PyPI (both nod and nod-mcp on PATH)
|
|
13
|
+
pipx install nod-cli
|
|
14
|
+
|
|
15
|
+
# bleeding edge, straight from main
|
|
16
|
+
pipx install git+https://github.com/neatnettech/nod.git
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
After a PyPI release, update with `pipx upgrade nod-cli`. For git installs,
|
|
20
|
+
run `pipx reinstall nod-cli` to pick up new commits.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m venv .venv
|
|
26
|
+
source .venv/bin/activate
|
|
27
|
+
pip install -e ".[dev]"
|
|
28
|
+
|
|
29
|
+
mkdir demo && cd demo
|
|
30
|
+
git init
|
|
31
|
+
nod init
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Concepts
|
|
35
|
+
|
|
36
|
+
* **Modules** group work by area: `nod module add "Vault"`
|
|
37
|
+
* **Cycles** are timeboxed plans: `nod cycle add "Sprint 1" --start 2026-10-01 --end 2026-10-14`
|
|
38
|
+
* **Work items** are epics, stories, tasks, and bugs:
|
|
39
|
+
`nod task add "Seed categories" --module vault --cycle sprint-1 --estimate 2h`
|
|
40
|
+
* **Dependencies** order the work: `nod depends OSS-2 OSS-1`
|
|
41
|
+
|
|
42
|
+
## Views
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
nod task list # tabular work items
|
|
46
|
+
nod module list # modules
|
|
47
|
+
nod cycle list # cycles
|
|
48
|
+
nod board # kanban grouped by status
|
|
49
|
+
nod timeline # cycles over time with estimates
|
|
50
|
+
nod graph # ASCII dependency diagram
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The dependency graph renders as a tree of prerequisite arrows with status
|
|
54
|
+
glyphs and branch names:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
OSS-1 StateBadge component ✓ DONE ⎇ feat/oss-1-statebadge
|
|
58
|
+
OSS-3 Seed categories ○ TODO
|
|
59
|
+
├─► OSS-4 Vault home ◐ IN_PROGRESS (depends on this)
|
|
60
|
+
└─► OSS-6 Note editor ○ TODO (depends on this)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Every view accepts `--json` for scripting and agents.
|
|
64
|
+
|
|
65
|
+
## Agents
|
|
66
|
+
|
|
67
|
+
`nod-mcp` exposes the same state as MCP tools: work item create/update/list,
|
|
68
|
+
project info, the board, the timeline, and the dependency graph. Point your
|
|
69
|
+
MCP client at `nod-mcp` from inside the repository.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install -e ".[dev]"
|
|
75
|
+
pytest
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Releases: tag a version (`v0.1.0-rc.1`, then `v0.1.0`) and push it. The
|
|
79
|
+
`publish` workflow builds the version from the tag and publishes to PyPI via
|
|
80
|
+
trusted publishing. Requires Python 3.13+.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nod-cli"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Git-native project management for humans and coding agents."
|
|
5
|
+
requires-python = ">=3.13"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
authors = [{ name = "neatnettech" }]
|
|
9
|
+
keywords = ["project-management", "cli", "kanban", "mcp", "git"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: MacOS :: MacOS X",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"alembic>=1.16,<2",
|
|
22
|
+
"mcp>=1.9,<2",
|
|
23
|
+
"networkx>=3.4,<4",
|
|
24
|
+
"pydantic>=2.11,<3",
|
|
25
|
+
"rich>=14,<15",
|
|
26
|
+
"sqlalchemy>=2.0,<3",
|
|
27
|
+
"typer>=0.16,<1",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/neatnettech/nod"
|
|
32
|
+
Repository = "https://github.com/neatnettech/nod"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8,<9",
|
|
37
|
+
"pytest-asyncio>=1,<2",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
nod = "nod.cli.app:app"
|
|
42
|
+
nod-mcp = "nod.mcp.server:main"
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
46
|
+
build-backend = "hatchling.build"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.version]
|
|
49
|
+
source = "vcs"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/nod"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
asyncio_mode = "auto"
|
|
@@ -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
|
|
File without changes
|