vikunja-python 0.1.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.
- vikunja_python/__init__.py +0 -0
- vikunja_python/cli/__init__.py +0 -0
- vikunja_python/cli/main.py +264 -0
- vikunja_python/core/__init__.py +0 -0
- vikunja_python/core/client.py +106 -0
- vikunja_python/core/models/__init__.py +377 -0
- vikunja_python/core/models/api_token.py +131 -0
- vikunja_python/core/models/auth.py +34 -0
- vikunja_python/core/models/base.py +193 -0
- vikunja_python/core/models/bulk_assignees.py +98 -0
- vikunja_python/core/models/filter.py +134 -0
- vikunja_python/core/models/label.py +230 -0
- vikunja_python/core/models/link_sharing.py +138 -0
- vikunja_python/core/models/migration.py +404 -0
- vikunja_python/core/models/phase6_medium.py +74 -0
- vikunja_python/core/models/project.py +217 -0
- vikunja_python/core/models/relation.py +199 -0
- vikunja_python/core/models/task.py +261 -0
- vikunja_python/core/models/task_expansion.py +252 -0
- vikunja_python/core/models/user.py +838 -0
- vikunja_python/core/models/webhook.py +270 -0
- vikunja_python/mcp/__init__.py +0 -0
- vikunja_python/mcp/server.py +678 -0
- vikunja_python-0.1.0.dist-info/METADATA +16 -0
- vikunja_python-0.1.0.dist-info/RECORD +28 -0
- vikunja_python-0.1.0.dist-info/WHEEL +5 -0
- vikunja_python-0.1.0.dist-info/entry_points.txt +3 -0
- vikunja_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Models for Vikunja API
|
|
3
|
+
|
|
4
|
+
Covers: models.Project and related request/response types
|
|
5
|
+
API endpoints: /projects, /projects/{id}
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, field_validator
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Optional, Any
|
|
11
|
+
from .base import VikunjaBaseModel, User
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# View Configuration Models (nested in Project)
|
|
16
|
+
# ============================================================================
|
|
17
|
+
|
|
18
|
+
class ViewFilter(VikunjaBaseModel):
|
|
19
|
+
"""
|
|
20
|
+
Filter configuration for a project view.
|
|
21
|
+
|
|
22
|
+
Used in: ProjectView.filter
|
|
23
|
+
"""
|
|
24
|
+
s: Optional[str] = Field(None, description="Search term")
|
|
25
|
+
sort_by: Optional[list[str]] = Field(None, description="Fields to sort by")
|
|
26
|
+
order_by: Optional[str] = Field(None, description="Sort order (asc/desc)")
|
|
27
|
+
filter: Optional[str] = Field(None, description="Filter expression (e.g., 'done = false')")
|
|
28
|
+
filter_include_nulls: bool = Field(False, description="Include null values in filter")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BucketConfiguration(VikunjaBaseModel):
|
|
32
|
+
"""
|
|
33
|
+
Bucket configuration for Kanban/board views.
|
|
34
|
+
|
|
35
|
+
Used in: ProjectView.bucket_configuration
|
|
36
|
+
|
|
37
|
+
UNKNOWN: Full structure not yet verified from API response
|
|
38
|
+
TODO: Extract from actual API call with project that has buckets configured
|
|
39
|
+
"""
|
|
40
|
+
# UNKNOWN: Field definitions pending API verification
|
|
41
|
+
mode: Optional[str] = Field(None, description="Configuration mode (none/manual)")
|
|
42
|
+
# TODO: Add remaining fields after API inspection
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProjectView(VikunjaBaseModel):
|
|
46
|
+
"""
|
|
47
|
+
View configuration within a project.
|
|
48
|
+
|
|
49
|
+
Vikunja supports multiple view types per project:
|
|
50
|
+
- list: List view
|
|
51
|
+
- gantt: Gantt chart view
|
|
52
|
+
- table: Table/spreadsheet view
|
|
53
|
+
- kanban: Kanban board with buckets
|
|
54
|
+
|
|
55
|
+
Used in: Project.views[]
|
|
56
|
+
"""
|
|
57
|
+
id: int = Field(..., description="Unique view identifier")
|
|
58
|
+
title: str = Field(..., description="View name/title")
|
|
59
|
+
project_id: int = Field(..., description="Parent project ID")
|
|
60
|
+
view_kind: str = Field(..., description="View type: list, gantt, table, kanban")
|
|
61
|
+
|
|
62
|
+
# Filter configuration
|
|
63
|
+
filter: Optional[ViewFilter] = Field(None, description="View filter settings")
|
|
64
|
+
|
|
65
|
+
# Positioning
|
|
66
|
+
position: int = Field(0, description="Display order within project")
|
|
67
|
+
|
|
68
|
+
# Bucket configuration (for Kanban views)
|
|
69
|
+
bucket_configuration_mode: str = Field("none", description="Bucket mode: none, manual, automatic")
|
|
70
|
+
bucket_configuration: Optional[BucketConfiguration] = Field(None, description="Bucket settings")
|
|
71
|
+
default_bucket_id: int = Field(0, description="Default bucket for new tasks")
|
|
72
|
+
done_bucket_id: int = Field(0, description="Bucket for completed tasks")
|
|
73
|
+
|
|
74
|
+
# Timestamps
|
|
75
|
+
created: datetime = Field(..., description="Creation timestamp")
|
|
76
|
+
updated: datetime = Field(..., description="Last update timestamp")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ============================================================================
|
|
80
|
+
# Project Entity Model (18 fields from API)
|
|
81
|
+
# ============================================================================
|
|
82
|
+
|
|
83
|
+
class Project(VikunjaBaseModel):
|
|
84
|
+
"""
|
|
85
|
+
Project model representing a project in Vikunja.
|
|
86
|
+
|
|
87
|
+
API endpoint: GET /projects, GET /projects/{id}
|
|
88
|
+
|
|
89
|
+
All 18 fields discovered from actual API response at v2.3.0:
|
|
90
|
+
https://vikunja.ok9.io/api/v1/projects
|
|
91
|
+
|
|
92
|
+
Nested Objects:
|
|
93
|
+
- owner: User object (project creator)
|
|
94
|
+
- views[]: Array of ProjectView objects
|
|
95
|
+
- background_information: null or object (Unsplash background data)
|
|
96
|
+
|
|
97
|
+
Special Values:
|
|
98
|
+
- parent_project_id: 0 = root level project
|
|
99
|
+
- position: 65536 for default Inbox project
|
|
100
|
+
- max_permission: null or int (0=read, 1=write, 2=admin)
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# Core Identification (5 fields)
|
|
104
|
+
id: int = Field(..., description="Unique project identifier")
|
|
105
|
+
title: str = Field(..., max_length=255, description="Project title")
|
|
106
|
+
description: Optional[str] = Field(None, max_length=10000, description="Project description (markdown)")
|
|
107
|
+
identifier: str = Field("", description="Short identifier/code for project")
|
|
108
|
+
hex_color: str = Field("", description="Project color (without #)")
|
|
109
|
+
|
|
110
|
+
# Hierarchy (2 fields)
|
|
111
|
+
parent_project_id: int = Field(0, description="Parent project ID (0 = root level)")
|
|
112
|
+
owner: User = Field(..., description="Project owner/creator")
|
|
113
|
+
|
|
114
|
+
# Status Flags (2 fields)
|
|
115
|
+
is_archived: bool = Field(False, description="Archived status")
|
|
116
|
+
is_favorite: bool = Field(False, description="Marked as favorite/starred")
|
|
117
|
+
|
|
118
|
+
# Positioning (1 field)
|
|
119
|
+
position: int = Field(0, description="Display order (65536 for default Inbox)")
|
|
120
|
+
|
|
121
|
+
# Settings & Configuration (4 fields)
|
|
122
|
+
background_information: Optional[Any] = Field(None, description="Background image info (Unsplash data)")
|
|
123
|
+
background_blur_hash: str = Field("", description="Blur hash for background preview")
|
|
124
|
+
views: list[ProjectView] = Field(default_factory=list, description="Configured views for this project")
|
|
125
|
+
max_permission: Optional[int] = Field(None, ge=0, le=2, description="Max permission level (0=read, 1=write, 2=admin)")
|
|
126
|
+
|
|
127
|
+
# Timestamps (2 fields)
|
|
128
|
+
created: datetime = Field(..., description="Creation timestamp")
|
|
129
|
+
updated: datetime = Field(..., description="Last update timestamp")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ============================================================================
|
|
133
|
+
# Project Request Models (for Create/Update operations)
|
|
134
|
+
# ============================================================================
|
|
135
|
+
|
|
136
|
+
class ProjectCreateRequest(VikunjaBaseModel):
|
|
137
|
+
"""
|
|
138
|
+
Request body for creating a new project.
|
|
139
|
+
|
|
140
|
+
Required fields: title
|
|
141
|
+
Optional fields: All other project properties
|
|
142
|
+
|
|
143
|
+
API endpoint: PUT /projects
|
|
144
|
+
"""
|
|
145
|
+
title: str = Field(..., min_length=1, max_length=255, description="Project title (required)")
|
|
146
|
+
|
|
147
|
+
# Optional fields - only send if explicitly set
|
|
148
|
+
description: Optional[str] = Field(None, max_length=10000)
|
|
149
|
+
identifier: Optional[str] = None
|
|
150
|
+
hex_color: Optional[str] = None
|
|
151
|
+
|
|
152
|
+
# Hierarchy
|
|
153
|
+
parent_project_id: Optional[int] = Field(None, description="Parent project ID (omit for root)")
|
|
154
|
+
|
|
155
|
+
def model_dump_for_api(self) -> dict:
|
|
156
|
+
"""Convert to dict, excluding None values."""
|
|
157
|
+
return self.model_dump(exclude_none=True)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ProjectUpdateRequest(VikunjaBaseModel):
|
|
161
|
+
"""
|
|
162
|
+
Request body for updating an existing project.
|
|
163
|
+
|
|
164
|
+
All fields optional - only provided fields are updated.
|
|
165
|
+
|
|
166
|
+
API endpoint: POST /projects/{id} (PATCH semantics)
|
|
167
|
+
"""
|
|
168
|
+
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
169
|
+
description: Optional[str] = Field(None, max_length=10000)
|
|
170
|
+
identifier: Optional[str] = None
|
|
171
|
+
hex_color: Optional[str] = None
|
|
172
|
+
is_archived: Optional[bool] = None
|
|
173
|
+
is_favorite: Optional[bool] = None
|
|
174
|
+
|
|
175
|
+
# Hierarchy (moving projects)
|
|
176
|
+
parent_project_id: Optional[int] = None
|
|
177
|
+
|
|
178
|
+
def model_dump_for_api(self) -> dict:
|
|
179
|
+
"""Convert to dict, excluding None values."""
|
|
180
|
+
return self.model_dump(exclude_none=True)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ============================================================================
|
|
184
|
+
# Project List Request/Response Models
|
|
185
|
+
# ============================================================================
|
|
186
|
+
|
|
187
|
+
class ProjectListRequest(VikunjaBaseModel):
|
|
188
|
+
"""
|
|
189
|
+
Query parameters for listing projects.
|
|
190
|
+
|
|
191
|
+
API endpoint: GET /projects
|
|
192
|
+
|
|
193
|
+
Pagination headers returned:
|
|
194
|
+
x-pagination-total-pages
|
|
195
|
+
x-pagination-result-count
|
|
196
|
+
"""
|
|
197
|
+
# Pagination
|
|
198
|
+
page: int = Field(1, ge=1, description="Page number (1-based)")
|
|
199
|
+
per_page: int = Field(50, ge=1, le=100, description="Items per page")
|
|
200
|
+
|
|
201
|
+
# Filtering
|
|
202
|
+
search: Optional[str] = Field(None, description="Search term for project title/description")
|
|
203
|
+
sort_by: Optional[str] = Field(None, description="Field to sort by (e.g., 'title', 'updated')")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ProjectListResponse(VikunjaBaseModel):
|
|
207
|
+
"""
|
|
208
|
+
Response for project list operations.
|
|
209
|
+
|
|
210
|
+
Includes pagination metadata from response headers.
|
|
211
|
+
"""
|
|
212
|
+
success: bool = Field(True, description="Request succeeded")
|
|
213
|
+
projects: list[Project] = Field(default_factory=list, description="List of projects")
|
|
214
|
+
total_count: Optional[int] = Field(None, description="Total number of projects (across all pages)")
|
|
215
|
+
page: Optional[int] = Field(None, description="Current page number")
|
|
216
|
+
per_page: Optional[int] = Field(None, description="Items per page")
|
|
217
|
+
total_pages: Optional[int] = Field(None, description="Total number of pages")
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task Relationship Models for Vikunja API
|
|
3
|
+
|
|
4
|
+
Covers: models.TaskRelation, models.RelationKind, and related types
|
|
5
|
+
API endpoints: /tasks/{id}/relations (implied - relationships are part of task CRUD)
|
|
6
|
+
|
|
7
|
+
Relationship Types (12 kinds):
|
|
8
|
+
- subtask/parenttask: Hierarchical parent-child relationships
|
|
9
|
+
- blocking/blocked: Dependency relationships (A blocks B means B can't start until A done)
|
|
10
|
+
- precedes/follows: Sequential ordering (A precedes B means A comes before B)
|
|
11
|
+
- related: Generic association
|
|
12
|
+
- duplicateof/duplicates: Duplicate task tracking
|
|
13
|
+
- copiedfrom/copiedto: Source/destination of task copies
|
|
14
|
+
|
|
15
|
+
Usage Examples:
|
|
16
|
+
# Create a subtask relationship (Task 2 is a subtask of Task 1)
|
|
17
|
+
relation = TaskRelation(
|
|
18
|
+
task_id=2, # This task (the child/subtask)
|
|
19
|
+
other_task_id=1, # Related task (the parent)
|
|
20
|
+
relation_kind="subtask" # This task IS A subtask of other_task
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Create a blocking relationship (Task 1 blocks Task 2)
|
|
24
|
+
relation = TaskRelation(
|
|
25
|
+
task_id=1, # This task (the blocker)
|
|
26
|
+
other_task_id=2, # Blocked task (depends on this one)
|
|
27
|
+
relation_kind="blocking" # This task BLOCKS the other
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from pydantic import Field
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from typing import Optional, Literal
|
|
34
|
+
from .base import VikunjaBaseModel, User
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ============================================================================
|
|
38
|
+
# Relation Kind Enum (12 types)
|
|
39
|
+
# ============================================================================
|
|
40
|
+
|
|
41
|
+
RelationKind = Literal[
|
|
42
|
+
"unknown", # Default/undefined relationship
|
|
43
|
+
"subtask", # This task IS A subtask of other_task (child → parent)
|
|
44
|
+
"parenttask", # This task HAS OTHER as subtask (parent → child)
|
|
45
|
+
"related", # Generic association (bidirectional)
|
|
46
|
+
"duplicateof", # This task IS A DUPLICATE OF other_task
|
|
47
|
+
"duplicates", # This task HAS OTHER AS duplicate (reverse of duplicateof)
|
|
48
|
+
"blocking", # This task BLOCKS other_task (other can't start until this done)
|
|
49
|
+
"blocked", # This task IS BLOCKED BY other_task (depends on other)
|
|
50
|
+
"precedes", # This task COMES BEFORE other_task (sequential)
|
|
51
|
+
"follows", # This task COMES AFTER other_task (sequential)
|
|
52
|
+
"copiedfrom", # This task WAS COPIED FROM other_task (source)
|
|
53
|
+
"copiedto", # This task IS A COPY IN other_task (destination)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Human-readable descriptions for each relation kind
|
|
57
|
+
RELATION_KIND_DESCRIPTIONS: dict[str, str] = {
|
|
58
|
+
"unknown": "Undefined relationship type",
|
|
59
|
+
"subtask": "This task is a subtask of the other task (child relationship)",
|
|
60
|
+
"parenttask": "This task has the other task as a subtask (parent relationship)",
|
|
61
|
+
"related": "Generic association between tasks",
|
|
62
|
+
"duplicateof": "This task is a duplicate of the other task",
|
|
63
|
+
"duplicates": "The other task is a duplicate of this task",
|
|
64
|
+
"blocking": "This task blocks the other task (other depends on this)",
|
|
65
|
+
"blocked": "This task is blocked by the other task (this depends on other)",
|
|
66
|
+
"precedes": "This task must be completed before the other task",
|
|
67
|
+
"follows": "This task comes after the other task in sequence",
|
|
68
|
+
"copiedfrom": "This task was copied from the other task (original)",
|
|
69
|
+
"copiedto": "This task is a copy of the other task (duplicate)",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ============================================================================
|
|
74
|
+
# Task Relation Model
|
|
75
|
+
# ============================================================================
|
|
76
|
+
|
|
77
|
+
class TaskRelation(VikunjaBaseModel):
|
|
78
|
+
"""
|
|
79
|
+
Relationship between two tasks.
|
|
80
|
+
|
|
81
|
+
API endpoint: Part of task CRUD, managed via /tasks/{id}/relations
|
|
82
|
+
|
|
83
|
+
Key Concept: The relationship is FROM this task TO another task.
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
# Task 5 is a subtask of Task 1
|
|
87
|
+
relation = TaskRelation(
|
|
88
|
+
task_id=5, # This task (the subtask)
|
|
89
|
+
other_task_id=1, # Parent task
|
|
90
|
+
relation_kind="subtask" # "I am a subtask of other"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Task 3 blocks Task 7 (Task 7 can't start until Task 3 done)
|
|
94
|
+
relation = TaskRelation(
|
|
95
|
+
task_id=3, # This task (the blocker)
|
|
96
|
+
other_task_id=7, # Blocked task
|
|
97
|
+
relation_kind="blocking" # "I block the other task"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Task 2 is blocked by Task 4 (Task 2 depends on Task 4)
|
|
101
|
+
relation = TaskRelation(
|
|
102
|
+
task_id=2, # This task (the one waiting)
|
|
103
|
+
other_task_id=4, # Blocking task
|
|
104
|
+
relation_kind="blocked" # "I am blocked by the other task"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
Common Patterns:
|
|
108
|
+
- Subtask hierarchy: Use "subtask" from child to parent
|
|
109
|
+
- Dependencies: Use "blocking" for blocker, "blocked" for dependent
|
|
110
|
+
- Sequence: Use "precedes" for earlier task, "follows" for later
|
|
111
|
+
|
|
112
|
+
Fields from API spec (models.TaskRelation):
|
|
113
|
+
- task_id: The "base" task (this task)
|
|
114
|
+
- other_task_id: The related task
|
|
115
|
+
- relation_kind: Type of relationship
|
|
116
|
+
- created: Auto-set timestamp
|
|
117
|
+
- created_by: User who created the relation
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
# Core Identification (2 fields)
|
|
121
|
+
id: Optional[int] = Field(None, description="Unique relation identifier (auto-assigned)")
|
|
122
|
+
task_id: int = Field(..., description="This task's ID (the 'base' task)")
|
|
123
|
+
|
|
124
|
+
# Relationship Definition (2 fields)
|
|
125
|
+
other_task_id: int = Field(..., description="ID of the related task")
|
|
126
|
+
relation_kind: RelationKind = Field(..., description="Type of relationship")
|
|
127
|
+
|
|
128
|
+
# Metadata (2 fields - auto-managed by server)
|
|
129
|
+
created: Optional[datetime] = Field(None, description="Creation timestamp (read-only)")
|
|
130
|
+
created_by: Optional[User] = Field(None, description="User who created this relation")
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def relationship_description(self) -> str:
|
|
134
|
+
"""Human-readable description of this relationship."""
|
|
135
|
+
kind_desc = RELATION_KIND_DESCRIPTIONS.get(self.relation_kind, "Unknown relationship")
|
|
136
|
+
return f"Task {self.task_id} {kind_desc.lower()} (related task: {self.other_task_id})"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ============================================================================
|
|
140
|
+
# Task Relation Request Models
|
|
141
|
+
# ============================================================================
|
|
142
|
+
|
|
143
|
+
class TaskRelationCreateRequest(VikunjaBaseModel):
|
|
144
|
+
"""
|
|
145
|
+
Request body for creating a task relationship.
|
|
146
|
+
|
|
147
|
+
Required fields: task_id, other_task_id, relation_kind
|
|
148
|
+
|
|
149
|
+
API endpoint: POST /tasks/{id}/relations (implied)
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
# Make Task 5 a subtask of Task 1
|
|
153
|
+
req = TaskRelationCreateRequest(
|
|
154
|
+
task_id=5,
|
|
155
|
+
other_task_id=1,
|
|
156
|
+
relation_kind="subtask"
|
|
157
|
+
)
|
|
158
|
+
"""
|
|
159
|
+
task_id: int = Field(..., description="This task's ID (the base task)")
|
|
160
|
+
other_task_id: int = Field(..., description="Related task's ID")
|
|
161
|
+
relation_kind: RelationKind = Field(..., description="Type of relationship")
|
|
162
|
+
|
|
163
|
+
def model_dump_for_api(self) -> dict:
|
|
164
|
+
"""Convert to dict for API submission."""
|
|
165
|
+
return self.model_dump(exclude_none=True)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TaskRelationUpdateRequest(VikunjaBaseModel):
|
|
169
|
+
"""
|
|
170
|
+
Request body for updating a task relationship.
|
|
171
|
+
|
|
172
|
+
Only provided fields are updated.
|
|
173
|
+
|
|
174
|
+
API endpoint: POST /tasks/{id}/relations/{relation_id} (implied)
|
|
175
|
+
"""
|
|
176
|
+
other_task_id: Optional[int] = None
|
|
177
|
+
relation_kind: Optional[RelationKind] = None
|
|
178
|
+
|
|
179
|
+
def model_dump_for_api(self) -> dict:
|
|
180
|
+
"""Convert to dict, excluding None values."""
|
|
181
|
+
return self.model_dump(exclude_none=True)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ============================================================================
|
|
185
|
+
# Task Relation Response Models
|
|
186
|
+
# ============================================================================
|
|
187
|
+
|
|
188
|
+
class TaskRelationListResponse(VikunjaBaseModel):
|
|
189
|
+
"""Response for listing task relationships."""
|
|
190
|
+
success: bool = Field(True, description="Request succeeded")
|
|
191
|
+
relations: list[TaskRelation] = Field(default_factory=list, description="List of task relations")
|
|
192
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TaskRelationGetResponse(VikunjaBaseModel):
|
|
196
|
+
"""Response for getting a single task relationship."""
|
|
197
|
+
success: bool = Field(True, description="Request succeeded")
|
|
198
|
+
relation: Optional[TaskRelation] = Field(None, description="The task relation")
|
|
199
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task Models for Vikunja API
|
|
3
|
+
|
|
4
|
+
Covers: models.Task and related request/response types
|
|
5
|
+
API endpoints: /tasks, /projects/{id}/tasks, /tasks/{id}
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, field_validator, model_validator
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Optional, Any
|
|
11
|
+
from .base import VikunjaBaseModel, User, Label
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# Task Entity Model (27 fields from API)
|
|
16
|
+
# ============================================================================
|
|
17
|
+
|
|
18
|
+
class Task(VikunjaBaseModel):
|
|
19
|
+
"""
|
|
20
|
+
Task model representing a single task in Vikunja.
|
|
21
|
+
|
|
22
|
+
API endpoint: GET /tasks, GET /tasks/{id}
|
|
23
|
+
|
|
24
|
+
All 27 fields discovered from actual API response at v2.3.0:
|
|
25
|
+
https://vikunja.ok9.io/api/v1/tasks
|
|
26
|
+
|
|
27
|
+
Date Format: ISO 8601 with timezone (e.g., "2026-04-27T14:51:09-05:00")
|
|
28
|
+
Default Dates: "0001-01-01T00:00:00Z" represents null/unset
|
|
29
|
+
|
|
30
|
+
Nested Objects:
|
|
31
|
+
- assignees[]: User objects
|
|
32
|
+
- labels[]: Full Label objects
|
|
33
|
+
- created_by: User object
|
|
34
|
+
- related_tasks: dict (relation types)
|
|
35
|
+
|
|
36
|
+
Nullable Fields (return null in API):
|
|
37
|
+
- attachments, reminders, reactions
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Core Identification (6 fields)
|
|
41
|
+
id: int = Field(..., description="Unique task identifier")
|
|
42
|
+
title: str = Field(..., max_length=255, description="Task title (required)")
|
|
43
|
+
description: Optional[str] = Field(None, max_length=10000, description="Task description (markdown)")
|
|
44
|
+
identifier: str = Field(..., description="Task identifier within project (e.g., '#1')")
|
|
45
|
+
index: float = Field(0.0, description="Task index for ordering within view/bucket")
|
|
46
|
+
project_id: int = Field(..., description="Parent project ID")
|
|
47
|
+
|
|
48
|
+
# Status Fields (3 fields)
|
|
49
|
+
done: bool = Field(False, description="Completion status")
|
|
50
|
+
done_at: Optional[datetime] = Field(None, description="Timestamp when task was marked done")
|
|
51
|
+
percent_done: float = Field(0.0, ge=0.0, le=100.0, description="Completion percentage (0-100)")
|
|
52
|
+
|
|
53
|
+
# Priority & Importance (3 fields)
|
|
54
|
+
priority: int = Field(0, ge=0, le=5, description="Task priority (0-5, higher = more important)")
|
|
55
|
+
is_favorite: bool = Field(False, description="Marked as favorite/starred")
|
|
56
|
+
hex_color: str = Field("", description="Custom hex color for task (without #)")
|
|
57
|
+
|
|
58
|
+
# Date/Time Fields (6 fields) - All ISO 8601 with timezone
|
|
59
|
+
created: datetime = Field(..., description="Creation timestamp")
|
|
60
|
+
updated: datetime = Field(..., description="Last update timestamp")
|
|
61
|
+
due_date: Optional[datetime] = Field(None, description="Due date/time")
|
|
62
|
+
start_date: Optional[datetime] = Field(None, description="Start date/time")
|
|
63
|
+
end_date: Optional[datetime] = Field(None, description="End date/time")
|
|
64
|
+
|
|
65
|
+
# Note: "0001-01-01T00:00:00Z" is returned for unset dates - handle in validator
|
|
66
|
+
|
|
67
|
+
# Recurrence Fields (2 fields)
|
|
68
|
+
repeat_after: int = Field(0, ge=0, description="Recurrence interval in seconds")
|
|
69
|
+
repeat_mode: int = Field(0, description="Recurrence mode (1=after completion, 2=after original due date)")
|
|
70
|
+
|
|
71
|
+
# Positioning Fields (3 fields) - For Kanban/board views
|
|
72
|
+
bucket_id: int = Field(0, description="Bucket ID in board view")
|
|
73
|
+
position: float = Field(0.0, description="Position within bucket")
|
|
74
|
+
|
|
75
|
+
# Nested Objects (4 fields)
|
|
76
|
+
assignees: Optional[list[User]] = Field(default_factory=list, description="Users assigned to this task")
|
|
77
|
+
labels: Optional[list[Label]] = Field(default_factory=list, description="Labels/tags on this task")
|
|
78
|
+
created_by: Optional[User] = Field(None, description="User who created this task")
|
|
79
|
+
related_tasks: Optional[dict[str, Any]] = Field(default_factory=dict, description="Related tasks by relation type")
|
|
80
|
+
subtasks: Optional[list['Task']] = Field(default_factory=list, description="Subtasks of this task (requires expand=subtasks)")
|
|
81
|
+
|
|
82
|
+
# Nullable Fields (3 fields) - May be null in API response
|
|
83
|
+
attachments: Optional[list[Any]] = Field(None, description="File attachments (requires expand=attachments)")
|
|
84
|
+
reminders: Optional[list[Any]] = Field(None, description="Task reminders (requires expand=reminders)")
|
|
85
|
+
reactions: Optional[list[Any]] = Field(None, description="Emoji reactions (requires expand=reactions)")
|
|
86
|
+
|
|
87
|
+
# Custom Fields (1 field) - UNKNOWN: Full structure not verified
|
|
88
|
+
custom_fields: Optional[dict[str, Any]] = Field(None, description="Custom field values")
|
|
89
|
+
|
|
90
|
+
# Expanded Fields (not in base task summary, but available via expand)
|
|
91
|
+
comment_count: Optional[int] = Field(None, description="Count of comments on this task")
|
|
92
|
+
is_unread: Optional[bool] = Field(None, description="Whether the task is unread for the current user")
|
|
93
|
+
buckets: Optional[list[dict[str, Any]]] = Field(None, description="Kanban buckets this task belongs to")
|
|
94
|
+
|
|
95
|
+
@model_validator(mode='after')
|
|
96
|
+
def populate_subtasks_from_relations(self) -> 'Task':
|
|
97
|
+
"""Populate subtasks field from related_tasks['subtask'] if expanded."""
|
|
98
|
+
if not self.subtasks and self.related_tasks and "subtask" in self.related_tasks:
|
|
99
|
+
rels = self.related_tasks["subtask"]
|
|
100
|
+
if rels and isinstance(rels, list) and len(rels) > 0:
|
|
101
|
+
# API might return dicts or Task objects
|
|
102
|
+
# Use type(self) to avoid circular imports during definition
|
|
103
|
+
cls = type(self)
|
|
104
|
+
self.subtasks = [cls(**r) if isinstance(r, dict) else r for r in rels]
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
@field_validator('start_date', 'end_date', mode='before')
|
|
108
|
+
@classmethod
|
|
109
|
+
def handle_default_dates(cls, v):
|
|
110
|
+
"""Convert '0001-01-01T00:00:00Z' to None for unset dates."""
|
|
111
|
+
if v == "0001-01-01T00:00:00Z":
|
|
112
|
+
return None
|
|
113
|
+
return v
|
|
114
|
+
|
|
115
|
+
@field_validator('due_date', 'done_at', mode='before')
|
|
116
|
+
@classmethod
|
|
117
|
+
def parse_datetime(cls, v):
|
|
118
|
+
"""Parse ISO 8601 datetime strings."""
|
|
119
|
+
if v is None or v == "":
|
|
120
|
+
return None
|
|
121
|
+
# Handle timezone offset format (e.g., -05:00)
|
|
122
|
+
if isinstance(v, str):
|
|
123
|
+
try:
|
|
124
|
+
# Python 3.11+ handles timezone offsets natively
|
|
125
|
+
return datetime.fromisoformat(v)
|
|
126
|
+
except ValueError:
|
|
127
|
+
return None
|
|
128
|
+
return v
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ============================================================================
|
|
132
|
+
# Task Request Models (for Create/Update operations)
|
|
133
|
+
# ============================================================================
|
|
134
|
+
|
|
135
|
+
class TaskCreateRequest(VikunjaBaseModel):
|
|
136
|
+
"""
|
|
137
|
+
Request body for creating a new task.
|
|
138
|
+
|
|
139
|
+
Required fields: title
|
|
140
|
+
Optional fields: All other task properties
|
|
141
|
+
|
|
142
|
+
API endpoint: POST /tasks, POST /projects/{id}/tasks
|
|
143
|
+
"""
|
|
144
|
+
title: str = Field(..., min_length=1, max_length=255, description="Task title (required)")
|
|
145
|
+
|
|
146
|
+
# Optional fields - only send if explicitly set
|
|
147
|
+
description: Optional[str] = Field(None, max_length=10000)
|
|
148
|
+
priority: Optional[int] = Field(None, ge=0, le=5)
|
|
149
|
+
due_date: Optional[datetime] = None
|
|
150
|
+
start_date: Optional[datetime] = None
|
|
151
|
+
end_date: Optional[datetime] = None
|
|
152
|
+
done: Optional[bool] = Field(None, description="Set initial completion status")
|
|
153
|
+
|
|
154
|
+
# Recurrence
|
|
155
|
+
repeat_after: Optional[int] = Field(None, ge=0)
|
|
156
|
+
repeat_mode: Optional[int] = Field(None)
|
|
157
|
+
|
|
158
|
+
# Assignments (IDs, not full objects)
|
|
159
|
+
assignee_ids: Optional[list[int]] = Field(None, description="User IDs to assign")
|
|
160
|
+
label_ids: Optional[list[int]] = Field(None, description="Label IDs to apply")
|
|
161
|
+
|
|
162
|
+
# Positioning
|
|
163
|
+
bucket_id: Optional[int] = None
|
|
164
|
+
project_id: Optional[int] = None
|
|
165
|
+
|
|
166
|
+
def model_dump_for_api(self) -> dict:
|
|
167
|
+
"""
|
|
168
|
+
Convert to dict, excluding None values.
|
|
169
|
+
|
|
170
|
+
Vikunja API expects only provided fields - don't send nulls.
|
|
171
|
+
"""
|
|
172
|
+
return self.model_dump(exclude_none=True)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TaskUpdateRequest(VikunjaBaseModel):
|
|
176
|
+
"""
|
|
177
|
+
Request body for updating an existing task.
|
|
178
|
+
|
|
179
|
+
All fields optional - only provided fields are updated.
|
|
180
|
+
|
|
181
|
+
API endpoint: POST /tasks/{id} (PATCH semantics)
|
|
182
|
+
"""
|
|
183
|
+
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
184
|
+
description: Optional[str] = Field(None, max_length=10000)
|
|
185
|
+
priority: Optional[int] = Field(None, ge=0, le=5)
|
|
186
|
+
done: Optional[bool] = None
|
|
187
|
+
due_date: Optional[datetime] = None
|
|
188
|
+
start_date: Optional[datetime] = None
|
|
189
|
+
end_date: Optional[datetime] = None
|
|
190
|
+
percent_done: Optional[float] = Field(None, ge=0.0, le=100.0)
|
|
191
|
+
is_favorite: Optional[bool] = None
|
|
192
|
+
hex_color: Optional[str] = None
|
|
193
|
+
|
|
194
|
+
# Recurrence
|
|
195
|
+
repeat_after: Optional[int] = Field(None, ge=0)
|
|
196
|
+
repeat_mode: Optional[int] = None
|
|
197
|
+
|
|
198
|
+
# Positioning
|
|
199
|
+
bucket_id: Optional[int] = None
|
|
200
|
+
position: Optional[float] = None
|
|
201
|
+
|
|
202
|
+
def model_dump_for_api(self) -> dict:
|
|
203
|
+
"""Convert to dict, excluding None values."""
|
|
204
|
+
return self.model_dump(exclude_none=True)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ============================================================================
|
|
208
|
+
# Task List Request/Response Models
|
|
209
|
+
# ============================================================================
|
|
210
|
+
|
|
211
|
+
class TaskListRequest(VikunjaBaseModel):
|
|
212
|
+
"""
|
|
213
|
+
Query parameters for listing tasks.
|
|
214
|
+
|
|
215
|
+
API endpoint: GET /tasks, GET /projects/{id}/tasks
|
|
216
|
+
|
|
217
|
+
Pagination headers returned:
|
|
218
|
+
x-pagination-total-pages
|
|
219
|
+
x-pagination-result-count
|
|
220
|
+
"""
|
|
221
|
+
# Filtering
|
|
222
|
+
project_id: Optional[int] = Field(None, description="Filter by project ID")
|
|
223
|
+
filter: Optional[str] = Field(None, description="Vikunja filter syntax (e.g., 'done = false')")
|
|
224
|
+
filter_timezone: Optional[str] = Field(None, description="Timezone for date filters")
|
|
225
|
+
filter_include_nulls: bool = Field(False, description="Include null-valued fields in filter")
|
|
226
|
+
|
|
227
|
+
# Pagination
|
|
228
|
+
page: int = Field(1, ge=1, description="Page number (1-based)")
|
|
229
|
+
per_page: int = Field(50, ge=1, le=100, description="Items per page")
|
|
230
|
+
|
|
231
|
+
# Sorting
|
|
232
|
+
sort_by: Optional[list[str]] = Field(None, description="Fields to sort by (e.g., ['due_date', 'priority'])")
|
|
233
|
+
order_by: Optional[str] = Field(None, pattern="^(asc|desc)$", description="Sort order")
|
|
234
|
+
|
|
235
|
+
# Expansion (fetch nested objects)
|
|
236
|
+
expand: Optional[list[str]] = Field(
|
|
237
|
+
None,
|
|
238
|
+
description=(
|
|
239
|
+
"Expand nested objects to include metadata in summaries. "
|
|
240
|
+
"Valid: subtasks, comments, reactions, buckets, comment_count, is_unread. "
|
|
241
|
+
"Invalid (412): attachments, reminders, assignees — explicitly marked as do-not-use for listing. "
|
|
242
|
+
"Note: list_tasks() returns summaries only (no full descriptions). Use get_task() for full details."
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class TaskListResponse(VikunjaBaseModel):
|
|
248
|
+
"""
|
|
249
|
+
Response for task list operations.
|
|
250
|
+
|
|
251
|
+
Includes pagination metadata from response headers.
|
|
252
|
+
"""
|
|
253
|
+
success: bool = Field(True, description="Request succeeded")
|
|
254
|
+
tasks: list[Task] = Field(default_factory=list, description="List of tasks")
|
|
255
|
+
total_count: Optional[int] = Field(None, description="Total number of tasks (across all pages)")
|
|
256
|
+
page: Optional[int] = Field(None, description="Current page number")
|
|
257
|
+
per_page: Optional[int] = Field(None, description="Items per page")
|
|
258
|
+
total_pages: Optional[int] = Field(None, description="Total number of pages")
|
|
259
|
+
|
|
260
|
+
# Rebuild model for recursive subtasks
|
|
261
|
+
Task.model_rebuild()
|