taskulus 0.1.0__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.
Files changed (45) hide show
  1. taskulus-0.1.0/PKG-INFO +20 -0
  2. taskulus-0.1.0/README.md +3 -0
  3. taskulus-0.1.0/pyproject.toml +46 -0
  4. taskulus-0.1.0/setup.cfg +4 -0
  5. taskulus-0.1.0/src/taskulus/__init__.py +7 -0
  6. taskulus-0.1.0/src/taskulus/cache.py +147 -0
  7. taskulus-0.1.0/src/taskulus/cli.py +563 -0
  8. taskulus-0.1.0/src/taskulus/config.py +36 -0
  9. taskulus-0.1.0/src/taskulus/config_loader.py +79 -0
  10. taskulus-0.1.0/src/taskulus/daemon.py +37 -0
  11. taskulus-0.1.0/src/taskulus/daemon_client.py +170 -0
  12. taskulus-0.1.0/src/taskulus/daemon_paths.py +31 -0
  13. taskulus-0.1.0/src/taskulus/daemon_protocol.py +86 -0
  14. taskulus-0.1.0/src/taskulus/daemon_server.py +220 -0
  15. taskulus-0.1.0/src/taskulus/dependencies.py +291 -0
  16. taskulus-0.1.0/src/taskulus/dependency_tree.py +218 -0
  17. taskulus-0.1.0/src/taskulus/doctor.py +48 -0
  18. taskulus-0.1.0/src/taskulus/file_io.py +53 -0
  19. taskulus-0.1.0/src/taskulus/hierarchy.py +58 -0
  20. taskulus-0.1.0/src/taskulus/ids.py +100 -0
  21. taskulus-0.1.0/src/taskulus/index.py +98 -0
  22. taskulus-0.1.0/src/taskulus/issue_close.py +37 -0
  23. taskulus-0.1.0/src/taskulus/issue_comment.py +55 -0
  24. taskulus-0.1.0/src/taskulus/issue_creation.py +137 -0
  25. taskulus-0.1.0/src/taskulus/issue_delete.py +28 -0
  26. taskulus-0.1.0/src/taskulus/issue_display.py +29 -0
  27. taskulus-0.1.0/src/taskulus/issue_files.py +47 -0
  28. taskulus-0.1.0/src/taskulus/issue_listing.py +209 -0
  29. taskulus-0.1.0/src/taskulus/issue_lookup.py +49 -0
  30. taskulus-0.1.0/src/taskulus/issue_transfer.py +81 -0
  31. taskulus-0.1.0/src/taskulus/issue_update.py +94 -0
  32. taskulus-0.1.0/src/taskulus/maintenance.py +227 -0
  33. taskulus-0.1.0/src/taskulus/migration.py +277 -0
  34. taskulus-0.1.0/src/taskulus/models.py +122 -0
  35. taskulus-0.1.0/src/taskulus/project.py +143 -0
  36. taskulus-0.1.0/src/taskulus/queries.py +94 -0
  37. taskulus-0.1.0/src/taskulus/users.py +20 -0
  38. taskulus-0.1.0/src/taskulus/wiki.py +129 -0
  39. taskulus-0.1.0/src/taskulus/workflows.py +90 -0
  40. taskulus-0.1.0/src/taskulus.egg-info/PKG-INFO +20 -0
  41. taskulus-0.1.0/src/taskulus.egg-info/SOURCES.txt +43 -0
  42. taskulus-0.1.0/src/taskulus.egg-info/dependency_links.txt +1 -0
  43. taskulus-0.1.0/src/taskulus.egg-info/entry_points.txt +2 -0
  44. taskulus-0.1.0/src/taskulus.egg-info/requires.txt +10 -0
  45. taskulus-0.1.0/src/taskulus.egg-info/top_level.txt +1 -0
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: taskulus
3
+ Version: 0.1.0
4
+ Summary: A git-backed project management system.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click
8
+ Requires-Dist: jinja2
9
+ Requires-Dist: pydantic
10
+ Requires-Dist: pyyaml
11
+ Requires-Dist: pydantic-settings
12
+ Requires-Dist: behave
13
+ Requires-Dist: coverage
14
+ Requires-Dist: ruff
15
+ Requires-Dist: black
16
+ Requires-Dist: sphinx
17
+
18
+ # Taskulus (Python)
19
+
20
+ This file exists for packaging metadata in editable installs. For full project documentation, see the repository README.md.
@@ -0,0 +1,3 @@
1
+ # Taskulus (Python)
2
+
3
+ This file exists for packaging metadata in editable installs. For full project documentation, see the repository README.md.
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "taskulus"
7
+ version = "0.1.0"
8
+ description = "A git-backed project management system."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "click",
13
+ "jinja2",
14
+ "pydantic",
15
+ "pyyaml",
16
+ "pydantic-settings",
17
+ "behave",
18
+ "coverage",
19
+ "ruff",
20
+ "black",
21
+ "sphinx",
22
+ ]
23
+
24
+ [project.scripts]
25
+ tsk = "taskulus.cli:cli"
26
+
27
+ [tool.setuptools]
28
+ package-dir = { "" = "src" }
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.black]
34
+ line-length = 88
35
+ target-version = ["py311"]
36
+
37
+ [tool.ruff]
38
+ line-length = 88
39
+ target-version = "py311"
40
+
41
+ [tool.semantic_release]
42
+ version_toml = ["pyproject.toml:project.version"]
43
+ tag_format = "v{version}"
44
+ build_command = "python -m build"
45
+ allow_zero_version = true
46
+ major_on_zero = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """
2
+ Taskulus Python package.
3
+
4
+ :module: taskulus
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,147 @@
1
+ """Index cache utilities for Taskulus."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import dataclass
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional
11
+
12
+ from taskulus.index import IssueIndex
13
+ from taskulus.models import IssueData
14
+
15
+
16
+ @dataclass
17
+ class IndexCache:
18
+ """Serialized index cache representation."""
19
+
20
+ version: int
21
+ built_at: datetime
22
+ file_mtimes: Dict[str, float]
23
+ issues: List[IssueData]
24
+ reverse_deps: Dict[str, List[str]]
25
+
26
+
27
+ def _normalize_mtime(value: float) -> float:
28
+ """Normalize file modification times for cache consistency.
29
+
30
+ :param value: Raw modification time value.
31
+ :type value: float
32
+ :return: Rounded modification time to microsecond precision.
33
+ :rtype: float
34
+ """
35
+ return round(value, 6)
36
+
37
+
38
+ def collect_issue_file_mtimes(issues_directory: Path) -> Dict[str, float]:
39
+ """Collect file modification times for issues.
40
+
41
+ :param issues_directory: Directory containing issue files.
42
+ :type issues_directory: Path
43
+ :return: Mapping of filename to mtime.
44
+ :rtype: Dict[str, float]
45
+ """
46
+ mtimes: Dict[str, float] = {}
47
+ for entry in os.scandir(issues_directory):
48
+ if not entry.is_file() or not entry.name.endswith(".json"):
49
+ continue
50
+ mtimes[entry.name] = _normalize_mtime(entry.stat().st_mtime)
51
+ return mtimes
52
+
53
+
54
+ def load_cache_if_valid(
55
+ cache_path: Path, issues_directory: Path
56
+ ) -> Optional[IssueIndex]:
57
+ """Load cached index if the cache is valid.
58
+
59
+ :param cache_path: Path to cache file.
60
+ :type cache_path: Path
61
+ :param issues_directory: Directory containing issue files.
62
+ :type issues_directory: Path
63
+ :return: IssueIndex if cache is valid, otherwise None.
64
+ :rtype: Optional[IssueIndex]
65
+ """
66
+ if not cache_path.exists():
67
+ return None
68
+
69
+ payload = json.loads(cache_path.read_text(encoding="utf-8"))
70
+ file_mtimes = payload.get("file_mtimes", {})
71
+ current_mtimes = collect_issue_file_mtimes(issues_directory)
72
+ if file_mtimes != current_mtimes:
73
+ return None
74
+
75
+ issues = [IssueData.model_validate(item) for item in payload.get("issues", [])]
76
+ reverse_deps = payload.get("reverse_deps", {})
77
+ return build_index_from_cache(issues, reverse_deps)
78
+
79
+
80
+ def write_cache(
81
+ index: IssueIndex, cache_path: Path, file_mtimes: Dict[str, float]
82
+ ) -> None:
83
+ """Write an index cache file to disk.
84
+
85
+ :param index: Issue index to serialize.
86
+ :type index: IssueIndex
87
+ :param cache_path: Path to cache file.
88
+ :type cache_path: Path
89
+ :param file_mtimes: File modification time mapping.
90
+ :type file_mtimes: Dict[str, float]
91
+ """
92
+ cache = IndexCache(
93
+ version=1,
94
+ built_at=datetime.now(timezone.utc),
95
+ file_mtimes=file_mtimes,
96
+ issues=list(index.by_id.values()),
97
+ reverse_deps={
98
+ target: [issue.identifier for issue in issues]
99
+ for target, issues in index.reverse_dependencies.items()
100
+ },
101
+ )
102
+ payload = {
103
+ "version": cache.version,
104
+ "built_at": cache.built_at.isoformat().replace("+00:00", "Z"),
105
+ "file_mtimes": cache.file_mtimes,
106
+ "issues": [
107
+ issue.model_dump(by_alias=True, mode="json") for issue in cache.issues
108
+ ],
109
+ "reverse_deps": cache.reverse_deps,
110
+ }
111
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
112
+ cache_path.write_text(
113
+ json.dumps(payload, indent=2, sort_keys=False),
114
+ encoding="utf-8",
115
+ )
116
+
117
+
118
+ def build_index_from_cache(
119
+ issues: List[IssueData], reverse_deps: Dict[str, List[str]]
120
+ ) -> IssueIndex:
121
+ """Rebuild an IssueIndex from cached data.
122
+
123
+ :param issues: Issues from cache.
124
+ :type issues: List[IssueData]
125
+ :param reverse_deps: Reverse dependency mapping.
126
+ :type reverse_deps: Dict[str, List[str]]
127
+ :return: IssueIndex populated from cache.
128
+ :rtype: IssueIndex
129
+ """
130
+ index = IssueIndex()
131
+ for issue in issues:
132
+ index.by_id[issue.identifier] = issue
133
+ index.by_status.setdefault(issue.status, []).append(issue)
134
+ index.by_type.setdefault(issue.issue_type, []).append(issue)
135
+ if issue.parent is not None:
136
+ index.by_parent.setdefault(issue.parent, []).append(issue)
137
+ for label in issue.labels:
138
+ index.by_label.setdefault(label, []).append(issue)
139
+
140
+ for target, issue_ids in reverse_deps.items():
141
+ index.reverse_dependencies[target] = [
142
+ index.by_id[identifier]
143
+ for identifier in issue_ids
144
+ if identifier in index.by_id
145
+ ]
146
+
147
+ return index