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,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vikunja Pydantic Models - Base Configuration
|
|
3
|
+
|
|
4
|
+
All Vikunja API models inherit from VikunjaBaseModel for consistent configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Optional
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VikunjaBaseModel(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
Base model with common configuration for all Vikunja API models.
|
|
16
|
+
|
|
17
|
+
Configuration:
|
|
18
|
+
extra='ignore': Ignore undocumented fields from API (prevents crashes on upstream changes)
|
|
19
|
+
populate_by_name=True: Accept both snake_case and camelCase field names
|
|
20
|
+
from_attributes=True: Support ORM mode if needed for database integration
|
|
21
|
+
"""
|
|
22
|
+
model_config = ConfigDict(
|
|
23
|
+
extra='ignore', # Ignore undocumented API fields
|
|
24
|
+
populate_by_name=True, # Allow both naming conventions
|
|
25
|
+
from_attributes=True, # Support ORM mode
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ============================================================================
|
|
30
|
+
# Shared Nested Models (used across multiple entities)
|
|
31
|
+
# ============================================================================
|
|
32
|
+
|
|
33
|
+
class User(VikunjaBaseModel):
|
|
34
|
+
"""
|
|
35
|
+
User object as returned by the Vikunja API.
|
|
36
|
+
|
|
37
|
+
Appears in: assignees[], created_by, owner (projects), labels.created_by
|
|
38
|
+
"""
|
|
39
|
+
id: int = Field(..., description="Unique user identifier")
|
|
40
|
+
username: str = Field(..., description="Username")
|
|
41
|
+
email: Optional[str] = Field(None, description="Email address")
|
|
42
|
+
name: Optional[str] = Field(None, description="Display name (may be empty string)")
|
|
43
|
+
created: datetime = Field(..., description="Account creation timestamp (ISO 8601)")
|
|
44
|
+
updated: datetime = Field(..., description="Last update timestamp (ISO 8601)")
|
|
45
|
+
|
|
46
|
+
class Label(VikunjaBaseModel):
|
|
47
|
+
"""
|
|
48
|
+
Label (tag) assigned to tasks.
|
|
49
|
+
|
|
50
|
+
API endpoint: /labels
|
|
51
|
+
Used in: Task.labels[]
|
|
52
|
+
"""
|
|
53
|
+
id: int = Field(..., description="Unique label identifier")
|
|
54
|
+
title: str = Field(..., description="Label text/title")
|
|
55
|
+
description: Optional[str] = Field(None, description="Label description (markdown)")
|
|
56
|
+
hex_color: Optional[str] = Field(None, description="Hex color code")
|
|
57
|
+
|
|
58
|
+
@field_validator('hex_color')
|
|
59
|
+
@classmethod
|
|
60
|
+
def validate_hex_color(cls, v):
|
|
61
|
+
"""Validate and normalize hex color format (#RRGGBB)."""
|
|
62
|
+
if v is None or v == "":
|
|
63
|
+
return None
|
|
64
|
+
# Add # if missing
|
|
65
|
+
if not v.startswith('#'):
|
|
66
|
+
v = f'#{v}'
|
|
67
|
+
if not re.match(r'^#[0-9A-Fa-f]{6}$', v):
|
|
68
|
+
return None # Or raise error, but base model should be lenient
|
|
69
|
+
return v
|
|
70
|
+
created_by: Optional[User] = Field(None, description="User who created this label")
|
|
71
|
+
created: datetime = Field(..., description="Creation timestamp (ISO 8601)")
|
|
72
|
+
updated: datetime = Field(..., description="Last update timestamp (ISO 8601)")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TaskComment(VikunjaBaseModel):
|
|
76
|
+
"""
|
|
77
|
+
Comment on a task.
|
|
78
|
+
|
|
79
|
+
API endpoint: /tasks/{id}/comments
|
|
80
|
+
|
|
81
|
+
UNKNOWN: Full structure not yet verified from API response
|
|
82
|
+
TODO: Extract from actual API call with expand=comments
|
|
83
|
+
"""
|
|
84
|
+
# UNKNOWN: Field definitions pending API verification
|
|
85
|
+
id: Optional[int] = Field(None, description="Comment ID")
|
|
86
|
+
text: Optional[str] = Field(None, description="Comment text (markdown)")
|
|
87
|
+
# TODO: Add remaining fields after API inspection
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TaskAttachment(VikunjaBaseModel):
|
|
91
|
+
"""
|
|
92
|
+
File attachment on a task.
|
|
93
|
+
|
|
94
|
+
API endpoint: /tasks/{id}/attachments
|
|
95
|
+
|
|
96
|
+
UNKNOWN: Full structure not yet verified from API response
|
|
97
|
+
TODO: Extract from actual API call with expand=attachments
|
|
98
|
+
"""
|
|
99
|
+
# UNKNOWN: Field definitions pending API verification
|
|
100
|
+
id: Optional[int] = Field(None, description="Attachment ID")
|
|
101
|
+
filename: Optional[str] = Field(None, description="Original filename")
|
|
102
|
+
# TODO: Add remaining fields after API inspection
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TaskReminder(VikunjaBaseModel):
|
|
106
|
+
"""
|
|
107
|
+
Reminder for a task.
|
|
108
|
+
|
|
109
|
+
API endpoint: /tasks/{id}/reminders
|
|
110
|
+
|
|
111
|
+
UNKNOWN: Full structure not yet verified from API response
|
|
112
|
+
TODO: Extract from actual API call with expand=reminders
|
|
113
|
+
"""
|
|
114
|
+
# UNKNOWN: Field definitions pending API verification
|
|
115
|
+
id: Optional[int] = Field(None, description="Reminder ID")
|
|
116
|
+
reminder_at: Optional[datetime] = Field(None, description="When reminder triggers")
|
|
117
|
+
# TODO: Add remaining fields after API inspection
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ============================================================================
|
|
121
|
+
# Error Handling Models
|
|
122
|
+
# ============================================================================
|
|
123
|
+
|
|
124
|
+
class ErrorDetail(VikunjaBaseModel):
|
|
125
|
+
"""
|
|
126
|
+
Structured error response for LLM consumption.
|
|
127
|
+
|
|
128
|
+
Vikunja API returns errors in this format:
|
|
129
|
+
{"code": <int>, "message": "<string>"}
|
|
130
|
+
|
|
131
|
+
The 'code' field is an error code (e.g., 11 = invalid token).
|
|
132
|
+
"""
|
|
133
|
+
code: int = Field(..., description="Machine-readable error code")
|
|
134
|
+
message: str = Field(..., description="Human-readable error message")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Message(VikunjaBaseModel):
|
|
138
|
+
"""
|
|
139
|
+
Generic API message wrapper.
|
|
140
|
+
|
|
141
|
+
Used for simple success/error messages from the API.
|
|
142
|
+
"""
|
|
143
|
+
code: Optional[int] = Field(None, description="Error code if applicable")
|
|
144
|
+
message: str = Field(..., description="Message text")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ============================================================================
|
|
148
|
+
# Authentication Models
|
|
149
|
+
# ============================================================================
|
|
150
|
+
|
|
151
|
+
class Token(VikunjaBaseModel):
|
|
152
|
+
"""
|
|
153
|
+
Authentication token response.
|
|
154
|
+
|
|
155
|
+
Returned by: POST /login, POST /auth/openid/{provider}/callback
|
|
156
|
+
"""
|
|
157
|
+
# UNKNOWN: Exact fields not yet verified - placeholder based on JWT patterns
|
|
158
|
+
token: Optional[str] = Field(None, description="JWT token")
|
|
159
|
+
expires_at: Optional[datetime] = Field(None, description="Token expiration time")
|
|
160
|
+
# TODO: Verify exact fields from /login response
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ============================================================================
|
|
164
|
+
# Pagination Metadata (from API headers)
|
|
165
|
+
# ============================================================================
|
|
166
|
+
|
|
167
|
+
class PaginationInfo(VikunjaBaseModel):
|
|
168
|
+
"""
|
|
169
|
+
Pagination metadata from response headers.
|
|
170
|
+
|
|
171
|
+
Vikunja returns pagination info in headers:
|
|
172
|
+
x-pagination-total-pages: <int>
|
|
173
|
+
x-pagination-result-count: <int>
|
|
174
|
+
"""
|
|
175
|
+
total_pages: Optional[int] = Field(None, description="Total number of pages")
|
|
176
|
+
result_count: Optional[int] = Field(None, description="Number of items in current response")
|
|
177
|
+
# TODO: Add page/per_page if returned in body
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ============================================================================
|
|
181
|
+
# Generic Response Wrappers
|
|
182
|
+
# ============================================================================
|
|
183
|
+
|
|
184
|
+
class ListResponse(VikunjaBaseModel):
|
|
185
|
+
"""
|
|
186
|
+
Generic wrapper for list responses with pagination.
|
|
187
|
+
|
|
188
|
+
Usage: ListResponse[Task] for task lists
|
|
189
|
+
"""
|
|
190
|
+
success: bool = Field(True, description="Request succeeded")
|
|
191
|
+
items: list = Field(default_factory=list, description="List of items")
|
|
192
|
+
error: Optional[ErrorDetail] = Field(None, description="Error if request failed")
|
|
193
|
+
pagination: Optional[PaginationInfo] = Field(None, description="Pagination metadata")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bulk Assignment Models
|
|
3
|
+
|
|
4
|
+
Models for bulk task assignment operations.
|
|
5
|
+
Based on Vikunja API v2.3.0 specification.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class User(BaseModel):
|
|
17
|
+
"""User representation for bulk assignments."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(extra='ignore')
|
|
20
|
+
|
|
21
|
+
id: int = Field(..., description="The unique user ID")
|
|
22
|
+
username: str = Field(..., description="The username")
|
|
23
|
+
name: str = Field(..., description="The full name of the user")
|
|
24
|
+
email: Optional[str] = Field(None, description="The email address of the user")
|
|
25
|
+
created: datetime = Field(..., description="Account creation timestamp")
|
|
26
|
+
updated: datetime = Field(..., description="Last update timestamp")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BulkAssignees(BaseModel):
|
|
30
|
+
"""Request to bulk assign users to tasks.
|
|
31
|
+
|
|
32
|
+
Used when assigning multiple users to a single task or the same set of users
|
|
33
|
+
to multiple tasks.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(extra='ignore')
|
|
37
|
+
|
|
38
|
+
assignees: list[User] = Field(..., description="List of users to assign to the task(s)")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BulkAssigneesCreateRequest(BaseModel):
|
|
42
|
+
"""Request to create bulk assignments for a task."""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(extra='ignore')
|
|
45
|
+
|
|
46
|
+
assignees: list[int] = Field(
|
|
47
|
+
...,
|
|
48
|
+
description="List of user IDs to assign to the task"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BulkAssigneesResponse(BaseModel):
|
|
53
|
+
"""Response from a bulk assignment operation."""
|
|
54
|
+
|
|
55
|
+
model_config = ConfigDict(extra='ignore')
|
|
56
|
+
|
|
57
|
+
assignees: list[User] = Field(..., description="List of assigned users")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BulkTask(BaseModel):
|
|
61
|
+
"""Bulk task operation request.
|
|
62
|
+
|
|
63
|
+
Used for updating multiple tasks with the same field values at once.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(extra='ignore')
|
|
67
|
+
|
|
68
|
+
task_ids: list[int] = Field(
|
|
69
|
+
...,
|
|
70
|
+
description="List of task IDs to update"
|
|
71
|
+
)
|
|
72
|
+
fields: list[str] = Field(
|
|
73
|
+
...,
|
|
74
|
+
description="List of field names to update (e.g., 'title', 'priority', 'done')"
|
|
75
|
+
)
|
|
76
|
+
values: dict = Field(
|
|
77
|
+
default_factory=dict,
|
|
78
|
+
description="Map of field names to values. Only fields in the 'fields' list will be updated."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BulkTaskResponse(BaseModel):
|
|
83
|
+
"""Response from a bulk task operation."""
|
|
84
|
+
|
|
85
|
+
model_config = ConfigDict(extra='ignore')
|
|
86
|
+
|
|
87
|
+
tasks: list[dict] = Field(
|
|
88
|
+
default_factory=list,
|
|
89
|
+
description="List of updated task objects"
|
|
90
|
+
)
|
|
91
|
+
success_count: int = Field(
|
|
92
|
+
0,
|
|
93
|
+
description="Number of tasks successfully updated"
|
|
94
|
+
)
|
|
95
|
+
failure_count: int = Field(
|
|
96
|
+
0,
|
|
97
|
+
description="Number of tasks that failed to update"
|
|
98
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filter Models for Vikunja API
|
|
3
|
+
|
|
4
|
+
Covers: models.SavedFilter and related types
|
|
5
|
+
API endpoints: /filters, /filters/{id}
|
|
6
|
+
|
|
7
|
+
Note: GET /filters returns 405 (Method Not Allowed) - use PUT to create filters.
|
|
8
|
+
Filters are typically managed through the UI or project views.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Optional, Any
|
|
14
|
+
from .base import VikunjaBaseModel, User
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ============================================================================
|
|
18
|
+
# Task Collection (filter definition)
|
|
19
|
+
# ============================================================================
|
|
20
|
+
|
|
21
|
+
class TaskCollection(VikunjaBaseModel):
|
|
22
|
+
"""
|
|
23
|
+
Filter query definition for matching tasks.
|
|
24
|
+
|
|
25
|
+
Used in: SavedFilter.filters
|
|
26
|
+
|
|
27
|
+
Documentation: https://vikunja.io/docs/filters
|
|
28
|
+
|
|
29
|
+
Example filter syntax: "done = false && priority >= 3"
|
|
30
|
+
"""
|
|
31
|
+
filter: Optional[str] = Field(None, description="Filter expression (e.g., 'done = false && priority >= 3')")
|
|
32
|
+
filter_include_nulls: bool = Field(False, description="Include null values in results")
|
|
33
|
+
order_by: Optional[list[str]] = Field(None, description="Order direction: ['asc'] or ['desc']")
|
|
34
|
+
s: Optional[str] = Field(None, description="Search term")
|
|
35
|
+
sort_by: Optional[list[str]] = Field(None, description="Fields to sort by (e.g., ['due_date', 'priority'])")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ============================================================================
|
|
39
|
+
# Saved Filter Entity Model
|
|
40
|
+
# ============================================================================
|
|
41
|
+
|
|
42
|
+
class SavedFilter(VikunjaBaseModel):
|
|
43
|
+
"""
|
|
44
|
+
Saved filter model representing a user-created filter preset.
|
|
45
|
+
|
|
46
|
+
API endpoint: PUT /filters (create), GET /filters/{id}, POST /filters/{id}, DELETE /filters/{id}
|
|
47
|
+
|
|
48
|
+
Fields from API spec (models.SavedFilter):
|
|
49
|
+
- id: Unique numeric ID
|
|
50
|
+
- title: Filter name (1-250 chars, required)
|
|
51
|
+
- description: Optional description
|
|
52
|
+
- filters: TaskCollection defining the filter logic
|
|
53
|
+
- is_favorite: Whether filter is favorited
|
|
54
|
+
- owner: User who owns this filter
|
|
55
|
+
- created: Creation timestamp (auto-set, read-only)
|
|
56
|
+
- updated: Last update timestamp (auto-set, read-only)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Core Identification (2 fields)
|
|
60
|
+
id: Optional[int] = Field(None, description="Unique filter identifier (auto-assigned on create)")
|
|
61
|
+
title: str = Field(..., min_length=1, max_length=250, description="Filter name/title")
|
|
62
|
+
|
|
63
|
+
# Filter Definition (2 fields)
|
|
64
|
+
description: Optional[str] = Field(None, description="Filter description")
|
|
65
|
+
filters: Optional[TaskCollection] = Field(None, description="Filter logic definition")
|
|
66
|
+
|
|
67
|
+
# Status & Ownership (2 fields)
|
|
68
|
+
is_favorite: bool = Field(False, description="Marked as favorite")
|
|
69
|
+
owner: Optional[User] = Field(None, description="Filter owner")
|
|
70
|
+
|
|
71
|
+
# Timestamps (auto-managed by server) - 2 fields
|
|
72
|
+
created: Optional[datetime] = Field(None, description="Creation timestamp (read-only)")
|
|
73
|
+
updated: Optional[datetime] = Field(None, description="Last update timestamp (read-only)")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ============================================================================
|
|
77
|
+
# Filter Request Models
|
|
78
|
+
# ============================================================================
|
|
79
|
+
|
|
80
|
+
class SavedFilterCreateRequest(VikunjaBaseModel):
|
|
81
|
+
"""
|
|
82
|
+
Request body for creating a new saved filter.
|
|
83
|
+
|
|
84
|
+
Required fields: title, filters
|
|
85
|
+
Optional fields: description, is_favorite
|
|
86
|
+
|
|
87
|
+
API endpoint: PUT /filters
|
|
88
|
+
"""
|
|
89
|
+
title: str = Field(..., min_length=1, max_length=250, description="Filter name (required)")
|
|
90
|
+
filters: TaskCollection = Field(..., description="Filter logic definition")
|
|
91
|
+
|
|
92
|
+
# Optional fields
|
|
93
|
+
description: Optional[str] = Field(None, description="Filter description")
|
|
94
|
+
is_favorite: bool = Field(False, description="Mark as favorite")
|
|
95
|
+
|
|
96
|
+
def model_dump_for_api(self) -> dict:
|
|
97
|
+
"""Convert to dict, excluding None values."""
|
|
98
|
+
return self.model_dump(exclude_none=True)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SavedFilterUpdateRequest(VikunjaBaseModel):
|
|
102
|
+
"""
|
|
103
|
+
Request body for updating a saved filter.
|
|
104
|
+
|
|
105
|
+
All fields optional - only provided fields are updated.
|
|
106
|
+
|
|
107
|
+
API endpoint: POST /filters/{id}
|
|
108
|
+
"""
|
|
109
|
+
title: Optional[str] = Field(None, min_length=1, max_length=250)
|
|
110
|
+
description: Optional[str] = None
|
|
111
|
+
filters: Optional[TaskCollection] = None
|
|
112
|
+
is_favorite: Optional[bool] = None
|
|
113
|
+
|
|
114
|
+
def model_dump_for_api(self) -> dict:
|
|
115
|
+
"""Convert to dict, excluding None values."""
|
|
116
|
+
return self.model_dump(exclude_none=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ============================================================================
|
|
120
|
+
# Filter List/Get Response Models
|
|
121
|
+
# ============================================================================
|
|
122
|
+
|
|
123
|
+
class SavedFilterListResponse(VikunjaBaseModel):
|
|
124
|
+
"""Response for filter list operations."""
|
|
125
|
+
success: bool = Field(True, description="Request succeeded")
|
|
126
|
+
filters: list[SavedFilter] = Field(default_factory=list, description="List of saved filters")
|
|
127
|
+
error: Optional[Any] = Field(None, description="Error if request failed")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class SavedFilterGetResponse(VikunjaBaseModel):
|
|
131
|
+
"""Response for getting a single filter."""
|
|
132
|
+
success: bool = Field(True, description="Request succeeded")
|
|
133
|
+
filter: Optional[SavedFilter] = Field(None, description="The saved filter")
|
|
134
|
+
error: Optional[Any] = Field(None, description="Error if request failed")
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Label Models for Vikunja API
|
|
3
|
+
|
|
4
|
+
Covers: models.Label, LabelCreateRequest, LabelUpdateRequest, LabelListResponse
|
|
5
|
+
API endpoints: /labels (CRUD operations)
|
|
6
|
+
|
|
7
|
+
Labels are reusable tags that can be applied to tasks. Each label has:
|
|
8
|
+
- title (required): Display name shown on tasks
|
|
9
|
+
- hex_color: 7-character hex color code (#RRGGBB)
|
|
10
|
+
- description: Optional explanatory text
|
|
11
|
+
- created/updated: Auto-managed timestamps
|
|
12
|
+
- created_by: User who created the label
|
|
13
|
+
|
|
14
|
+
Usage Examples:
|
|
15
|
+
# Create a new label
|
|
16
|
+
label = LabelCreateRequest(
|
|
17
|
+
title="bug",
|
|
18
|
+
hex_color="#ff0000",
|
|
19
|
+
description="Bug reports and issues"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Update an existing label
|
|
23
|
+
update = LabelUpdateRequest(
|
|
24
|
+
title="critical-bug", # Rename the label
|
|
25
|
+
hex_color="#ff0000" # Keep same color
|
|
26
|
+
)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from pydantic import Field, field_validator
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from typing import Optional
|
|
32
|
+
import re
|
|
33
|
+
from .base import VikunjaBaseModel, User
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ============================================================================
|
|
37
|
+
# Label Model (Full structure from API spec)
|
|
38
|
+
# ============================================================================
|
|
39
|
+
|
|
40
|
+
class Label(VikunjaBaseModel):
|
|
41
|
+
"""
|
|
42
|
+
A reusable label/tag for categorizing tasks.
|
|
43
|
+
|
|
44
|
+
API endpoint: /labels
|
|
45
|
+
|
|
46
|
+
Labels are shared across projects and can be applied to multiple tasks.
|
|
47
|
+
Each label has a unique ID, title, color, and optional description.
|
|
48
|
+
|
|
49
|
+
Fields from API spec (models.Label):
|
|
50
|
+
- id: Unique identifier (auto-assigned)
|
|
51
|
+
- title: Display name (required, 1-250 chars)
|
|
52
|
+
- hex_color: Color in #RRGGBB format (max 7 chars)
|
|
53
|
+
- description: Optional explanatory text
|
|
54
|
+
- created: Auto-set creation timestamp
|
|
55
|
+
- updated: Auto-set update timestamp
|
|
56
|
+
- created_by: User who created the label
|
|
57
|
+
|
|
58
|
+
Example from API response:
|
|
59
|
+
{
|
|
60
|
+
"id": 1,
|
|
61
|
+
"title": "bug",
|
|
62
|
+
"hex_color": "#ff0000",
|
|
63
|
+
"description": "Bug reports and issues",
|
|
64
|
+
"created": "2024-01-15T10:30:00Z",
|
|
65
|
+
"updated": "2024-01-15T10:30:00Z",
|
|
66
|
+
"created_by": {...}
|
|
67
|
+
}
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Core Identification (2 fields)
|
|
71
|
+
id: Optional[int] = Field(None, description="Unique label ID (auto-assigned by server)")
|
|
72
|
+
title: str = Field(
|
|
73
|
+
...,
|
|
74
|
+
min_length=1,
|
|
75
|
+
max_length=250,
|
|
76
|
+
description="Label display name shown on tasks"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Visual Properties (2 fields)
|
|
80
|
+
hex_color: Optional[str] = Field(
|
|
81
|
+
None,
|
|
82
|
+
max_length=7,
|
|
83
|
+
description="Hex color code (#RRGGBB format)"
|
|
84
|
+
)
|
|
85
|
+
description: Optional[str] = Field(None, description="Optional label description")
|
|
86
|
+
|
|
87
|
+
# Metadata (3 fields - auto-managed by server)
|
|
88
|
+
created: Optional[datetime] = Field(None, description="Creation timestamp (read-only)")
|
|
89
|
+
updated: Optional[datetime] = Field(None, description="Last update timestamp (read-only)")
|
|
90
|
+
created_by: Optional[User] = Field(None, description="User who created this label")
|
|
91
|
+
|
|
92
|
+
@field_validator('hex_color')
|
|
93
|
+
@classmethod
|
|
94
|
+
def validate_hex_color(cls, v):
|
|
95
|
+
"""Validate and normalize hex color format (#RRGGBB)."""
|
|
96
|
+
if v is None or v == "":
|
|
97
|
+
return None
|
|
98
|
+
# Add # if missing
|
|
99
|
+
if not v.startswith('#'):
|
|
100
|
+
v = f'#{v}'
|
|
101
|
+
if not re.match(r'^#[0-9A-Fa-f]{6}$', v):
|
|
102
|
+
raise ValueError(f"Invalid hex color format: {v}. Expected #RRGGBB or RRGGBB")
|
|
103
|
+
return v
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ============================================================================
|
|
107
|
+
# Label Create Request Model
|
|
108
|
+
# ============================================================================
|
|
109
|
+
|
|
110
|
+
class LabelCreateRequest(VikunjaBaseModel):
|
|
111
|
+
"""
|
|
112
|
+
Request body for creating a new label.
|
|
113
|
+
|
|
114
|
+
Required fields: title (1-250 chars)
|
|
115
|
+
Optional fields: hex_color, description
|
|
116
|
+
|
|
117
|
+
API endpoint: POST /labels
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
req = LabelCreateRequest(
|
|
121
|
+
title="bug",
|
|
122
|
+
hex_color="#ff0000",
|
|
123
|
+
description="Bug reports and issues"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
Validation:
|
|
127
|
+
- title: 1-250 characters, required
|
|
128
|
+
- hex_color: Must be valid #RRGGBB format if provided
|
|
129
|
+
- description: Any length if provided
|
|
130
|
+
"""
|
|
131
|
+
title: str = Field(
|
|
132
|
+
...,
|
|
133
|
+
min_length=1,
|
|
134
|
+
max_length=250,
|
|
135
|
+
description="Label display name (required)"
|
|
136
|
+
)
|
|
137
|
+
hex_color: Optional[str] = Field(None, max_length=7, description="Hex color (#RRGGBB)")
|
|
138
|
+
description: Optional[str] = Field(None, description="Optional label description")
|
|
139
|
+
|
|
140
|
+
@field_validator('hex_color')
|
|
141
|
+
@classmethod
|
|
142
|
+
def validate_hex_color(cls, v):
|
|
143
|
+
"""Validate and normalize hex color format."""
|
|
144
|
+
if v is None or v == "":
|
|
145
|
+
return None
|
|
146
|
+
if not v.startswith('#'):
|
|
147
|
+
v = f'#{v}'
|
|
148
|
+
if not re.match(r'^#[0-9A-Fa-f]{6}$', v):
|
|
149
|
+
raise ValueError(f"Invalid hex color format: {v}. Expected #RRGGBB or RRGGBB")
|
|
150
|
+
return v
|
|
151
|
+
|
|
152
|
+
def model_dump_for_api(self) -> dict:
|
|
153
|
+
"""Convert to dict for API submission."""
|
|
154
|
+
return self.model_dump(exclude_none=True)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ============================================================================
|
|
158
|
+
# Label Update Request Model
|
|
159
|
+
# ============================================================================
|
|
160
|
+
|
|
161
|
+
class LabelUpdateRequest(VikunjaBaseModel):
|
|
162
|
+
"""
|
|
163
|
+
Request body for updating an existing label.
|
|
164
|
+
|
|
165
|
+
All fields are optional - only provided fields are updated.
|
|
166
|
+
|
|
167
|
+
API endpoint: POST /labels/{id} (implied)
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
# Rename a label, keep color
|
|
171
|
+
req = LabelUpdateRequest(
|
|
172
|
+
title="critical-bug" # Only title changes
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Update color only
|
|
176
|
+
req = LabelUpdateRequest(
|
|
177
|
+
hex_color="#ff6600" # Only color changes
|
|
178
|
+
)
|
|
179
|
+
"""
|
|
180
|
+
title: Optional[str] = Field(None, min_length=1, max_length=250, description="New label name")
|
|
181
|
+
hex_color: Optional[str] = Field(None, max_length=7, description="New hex color (#RRGGBB)")
|
|
182
|
+
description: Optional[str] = Field(None, description="New label description")
|
|
183
|
+
|
|
184
|
+
@field_validator('hex_color')
|
|
185
|
+
@classmethod
|
|
186
|
+
def validate_hex_color(cls, v):
|
|
187
|
+
"""Validate and normalize hex color format."""
|
|
188
|
+
if v is None or v == "":
|
|
189
|
+
return None
|
|
190
|
+
if not v.startswith('#'):
|
|
191
|
+
v = f'#{v}'
|
|
192
|
+
if not re.match(r'^#[0-9A-Fa-f]{6}$', v):
|
|
193
|
+
raise ValueError(f"Invalid hex color format: {v}. Expected #RRGGBB or RRGGBB")
|
|
194
|
+
return v
|
|
195
|
+
|
|
196
|
+
def model_dump_for_api(self) -> dict:
|
|
197
|
+
"""Convert to dict, excluding None values."""
|
|
198
|
+
return self.model_dump(exclude_none=True)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ============================================================================
|
|
202
|
+
# Label Response Models
|
|
203
|
+
# ============================================================================
|
|
204
|
+
|
|
205
|
+
class LabelListResponse(VikunjaBaseModel):
|
|
206
|
+
"""Response for listing labels."""
|
|
207
|
+
success: bool = Field(True, description="Request succeeded")
|
|
208
|
+
labels: list[Label] = Field(default_factory=list, description="List of labels")
|
|
209
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class LabelGetResponse(VikunjaBaseModel):
|
|
213
|
+
"""Response for getting a single label."""
|
|
214
|
+
success: bool = Field(True, description="Request succeeded")
|
|
215
|
+
label: Optional[Label] = Field(None, description="The requested label")
|
|
216
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class LabelCreateResponse(VikunjaBaseModel):
|
|
220
|
+
"""Response for creating a label."""
|
|
221
|
+
success: bool = Field(True, description="Request succeeded")
|
|
222
|
+
label: Optional[Label] = Field(None, description="The created label")
|
|
223
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class LabelUpdateResponse(VikunjaBaseModel):
|
|
227
|
+
"""Response for updating a label."""
|
|
228
|
+
success: bool = Field(True, description="Request succeeded")
|
|
229
|
+
label: Optional[Label] = Field(None, description="The updated label")
|
|
230
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|