flowtask-sdk 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.
- flowtask_sdk-0.1.0/PKG-INFO +203 -0
- flowtask_sdk-0.1.0/README.md +192 -0
- flowtask_sdk-0.1.0/flowtask/__init__.py +17 -0
- flowtask_sdk-0.1.0/flowtask/client.py +217 -0
- flowtask_sdk-0.1.0/flowtask/exceptions.py +28 -0
- flowtask_sdk-0.1.0/flowtask/models.py +163 -0
- flowtask_sdk-0.1.0/flowtask_sdk.egg-info/PKG-INFO +203 -0
- flowtask_sdk-0.1.0/flowtask_sdk.egg-info/SOURCES.txt +11 -0
- flowtask_sdk-0.1.0/flowtask_sdk.egg-info/dependency_links.txt +1 -0
- flowtask_sdk-0.1.0/flowtask_sdk.egg-info/requires.txt +4 -0
- flowtask_sdk-0.1.0/flowtask_sdk.egg-info/top_level.txt +1 -0
- flowtask_sdk-0.1.0/pyproject.toml +19 -0
- flowtask_sdk-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowtask-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the FlowTask visual task manager API
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests>=2.28.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
11
|
+
|
|
12
|
+
# FlowTask Python SDK
|
|
13
|
+
|
|
14
|
+
A Python client for the FlowTask API — programmatically create projects, tasks, and dependency graphs.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install -e sdk/python
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or from the SDK directory:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd sdk/python
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from flowtask import FlowTask
|
|
33
|
+
|
|
34
|
+
# Connect with your API token (create one in the FlowTask UI → Key icon → API Tokens)
|
|
35
|
+
ft = FlowTask(token="ft_your_token_here", base_url="http://localhost:3000")
|
|
36
|
+
|
|
37
|
+
# Create a project
|
|
38
|
+
project = ft.create_project("Sprint 1", color="#6366F1")
|
|
39
|
+
|
|
40
|
+
# Create tasks
|
|
41
|
+
design = project.create_task("Design System", color="#8B5CF6")
|
|
42
|
+
build = project.create_task("Build Components", color="#3B82F6")
|
|
43
|
+
test = project.create_task("Write Tests", color="#10B981")
|
|
44
|
+
deploy = project.create_task("Deploy", color="#EF4444")
|
|
45
|
+
|
|
46
|
+
# Create dependencies (diamond-shaped DAG)
|
|
47
|
+
build.add_dependency(design) # Design must be done before Build
|
|
48
|
+
test.add_dependency(design) # Design must be done before Test
|
|
49
|
+
deploy.add_dependency(build) # Build must be done before Deploy
|
|
50
|
+
deploy.add_dependency(test) # Test must be done before Deploy
|
|
51
|
+
|
|
52
|
+
# See what's actionable right now
|
|
53
|
+
for task in ft.actual_tasks():
|
|
54
|
+
print(f"→ {task.title}")
|
|
55
|
+
# → Design System (the only task with no blockers)
|
|
56
|
+
|
|
57
|
+
# Complete a task and see what unblocks
|
|
58
|
+
design.complete()
|
|
59
|
+
for task in ft.actual_tasks():
|
|
60
|
+
print(f"→ {task.title}")
|
|
61
|
+
# → Build Components
|
|
62
|
+
# → Write Tests
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API Reference
|
|
66
|
+
|
|
67
|
+
### FlowTask Client
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
ft = FlowTask(token="ft_...", base_url="https://your-app.railway.app")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Projects
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# List all projects
|
|
77
|
+
projects = ft.list_projects()
|
|
78
|
+
|
|
79
|
+
# Create a project
|
|
80
|
+
project = ft.create_project("Name", description="...", color="#6366F1")
|
|
81
|
+
|
|
82
|
+
# Load full project (tasks + dependencies)
|
|
83
|
+
tasks, dependencies = project.load()
|
|
84
|
+
|
|
85
|
+
# Update
|
|
86
|
+
project.update(name="New Name", color="#EF4444")
|
|
87
|
+
|
|
88
|
+
# Delete
|
|
89
|
+
project.delete()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Tasks
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# Create a task in a project
|
|
96
|
+
task = project.create_task(
|
|
97
|
+
"Task Title",
|
|
98
|
+
description="Details...",
|
|
99
|
+
color="#3B82F6",
|
|
100
|
+
due_date="2026-04-01T00:00:00Z",
|
|
101
|
+
assignee="Alice",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Update fields
|
|
105
|
+
task.update(title="New Title", description="Updated")
|
|
106
|
+
|
|
107
|
+
# Change status
|
|
108
|
+
task.set_status("in_progress") # or "pending", "completed"
|
|
109
|
+
task.complete() # shorthand for completed
|
|
110
|
+
|
|
111
|
+
# Delete
|
|
112
|
+
task.delete()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Dependencies
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# Make task_b depend on task_a (task_a must be done first)
|
|
119
|
+
task_b.add_dependency(task_a)
|
|
120
|
+
|
|
121
|
+
# Or use the client directly
|
|
122
|
+
dep = ft.create_dependency(
|
|
123
|
+
enabling_task_id=task_a.id,
|
|
124
|
+
dependent_task_id=task_b.id
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Remove a dependency
|
|
128
|
+
ft.delete_dependency(dep.id)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Tags
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# Create a tag
|
|
135
|
+
tag = ft.create_tag("frontend", color="#3B82F6")
|
|
136
|
+
|
|
137
|
+
# Assign to a task
|
|
138
|
+
task.add_tag(tag.id)
|
|
139
|
+
|
|
140
|
+
# Remove from a task
|
|
141
|
+
task.remove_tag(tag.id)
|
|
142
|
+
|
|
143
|
+
# List all tags
|
|
144
|
+
tags = ft.list_tags()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Smart Features
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# Get actionable tasks (all dependencies completed)
|
|
151
|
+
actionable = ft.actual_tasks()
|
|
152
|
+
for task in actionable:
|
|
153
|
+
print(f"[{task.project_id}] {task.title} — {task.status}")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Error Handling
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from flowtask import FlowTask, CycleDetectedError, ValidationError, NotFoundError
|
|
160
|
+
|
|
161
|
+
ft = FlowTask(token="ft_...")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
ft.create_dependency(task_a.id, task_b.id)
|
|
165
|
+
except CycleDetectedError as e:
|
|
166
|
+
print(f"Would create a cycle: {e.message}")
|
|
167
|
+
except ValidationError as e:
|
|
168
|
+
print(f"Invalid input: {e.message}")
|
|
169
|
+
except NotFoundError as e:
|
|
170
|
+
print(f"Not found: {e.message}")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Example: Import Tasks from CSV
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
import csv
|
|
177
|
+
from flowtask import FlowTask
|
|
178
|
+
|
|
179
|
+
ft = FlowTask(token="ft_...", base_url="http://localhost:3000")
|
|
180
|
+
project = ft.create_project("Imported Project")
|
|
181
|
+
|
|
182
|
+
with open("tasks.csv") as f:
|
|
183
|
+
for row in csv.DictReader(f):
|
|
184
|
+
project.create_task(
|
|
185
|
+
title=row["title"],
|
|
186
|
+
description=row.get("description", ""),
|
|
187
|
+
color=row.get("color"),
|
|
188
|
+
due_date=row.get("due_date"),
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Example: Daily Standup Report
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from flowtask import FlowTask
|
|
196
|
+
|
|
197
|
+
ft = FlowTask(token="ft_...", base_url="http://localhost:3000")
|
|
198
|
+
|
|
199
|
+
print("📋 What I can work on today:")
|
|
200
|
+
for task in ft.actual_tasks():
|
|
201
|
+
due = f" (due {task.due_date[:10]})" if task.due_date else ""
|
|
202
|
+
print(f" → {task.title}{due}")
|
|
203
|
+
```
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# FlowTask Python SDK
|
|
2
|
+
|
|
3
|
+
A Python client for the FlowTask API — programmatically create projects, tasks, and dependency graphs.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e sdk/python
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or from the SDK directory:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd sdk/python
|
|
15
|
+
pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from flowtask import FlowTask
|
|
22
|
+
|
|
23
|
+
# Connect with your API token (create one in the FlowTask UI → Key icon → API Tokens)
|
|
24
|
+
ft = FlowTask(token="ft_your_token_here", base_url="http://localhost:3000")
|
|
25
|
+
|
|
26
|
+
# Create a project
|
|
27
|
+
project = ft.create_project("Sprint 1", color="#6366F1")
|
|
28
|
+
|
|
29
|
+
# Create tasks
|
|
30
|
+
design = project.create_task("Design System", color="#8B5CF6")
|
|
31
|
+
build = project.create_task("Build Components", color="#3B82F6")
|
|
32
|
+
test = project.create_task("Write Tests", color="#10B981")
|
|
33
|
+
deploy = project.create_task("Deploy", color="#EF4444")
|
|
34
|
+
|
|
35
|
+
# Create dependencies (diamond-shaped DAG)
|
|
36
|
+
build.add_dependency(design) # Design must be done before Build
|
|
37
|
+
test.add_dependency(design) # Design must be done before Test
|
|
38
|
+
deploy.add_dependency(build) # Build must be done before Deploy
|
|
39
|
+
deploy.add_dependency(test) # Test must be done before Deploy
|
|
40
|
+
|
|
41
|
+
# See what's actionable right now
|
|
42
|
+
for task in ft.actual_tasks():
|
|
43
|
+
print(f"→ {task.title}")
|
|
44
|
+
# → Design System (the only task with no blockers)
|
|
45
|
+
|
|
46
|
+
# Complete a task and see what unblocks
|
|
47
|
+
design.complete()
|
|
48
|
+
for task in ft.actual_tasks():
|
|
49
|
+
print(f"→ {task.title}")
|
|
50
|
+
# → Build Components
|
|
51
|
+
# → Write Tests
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## API Reference
|
|
55
|
+
|
|
56
|
+
### FlowTask Client
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
ft = FlowTask(token="ft_...", base_url="https://your-app.railway.app")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Projects
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# List all projects
|
|
66
|
+
projects = ft.list_projects()
|
|
67
|
+
|
|
68
|
+
# Create a project
|
|
69
|
+
project = ft.create_project("Name", description="...", color="#6366F1")
|
|
70
|
+
|
|
71
|
+
# Load full project (tasks + dependencies)
|
|
72
|
+
tasks, dependencies = project.load()
|
|
73
|
+
|
|
74
|
+
# Update
|
|
75
|
+
project.update(name="New Name", color="#EF4444")
|
|
76
|
+
|
|
77
|
+
# Delete
|
|
78
|
+
project.delete()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Tasks
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# Create a task in a project
|
|
85
|
+
task = project.create_task(
|
|
86
|
+
"Task Title",
|
|
87
|
+
description="Details...",
|
|
88
|
+
color="#3B82F6",
|
|
89
|
+
due_date="2026-04-01T00:00:00Z",
|
|
90
|
+
assignee="Alice",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Update fields
|
|
94
|
+
task.update(title="New Title", description="Updated")
|
|
95
|
+
|
|
96
|
+
# Change status
|
|
97
|
+
task.set_status("in_progress") # or "pending", "completed"
|
|
98
|
+
task.complete() # shorthand for completed
|
|
99
|
+
|
|
100
|
+
# Delete
|
|
101
|
+
task.delete()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Dependencies
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# Make task_b depend on task_a (task_a must be done first)
|
|
108
|
+
task_b.add_dependency(task_a)
|
|
109
|
+
|
|
110
|
+
# Or use the client directly
|
|
111
|
+
dep = ft.create_dependency(
|
|
112
|
+
enabling_task_id=task_a.id,
|
|
113
|
+
dependent_task_id=task_b.id
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Remove a dependency
|
|
117
|
+
ft.delete_dependency(dep.id)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Tags
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# Create a tag
|
|
124
|
+
tag = ft.create_tag("frontend", color="#3B82F6")
|
|
125
|
+
|
|
126
|
+
# Assign to a task
|
|
127
|
+
task.add_tag(tag.id)
|
|
128
|
+
|
|
129
|
+
# Remove from a task
|
|
130
|
+
task.remove_tag(tag.id)
|
|
131
|
+
|
|
132
|
+
# List all tags
|
|
133
|
+
tags = ft.list_tags()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Smart Features
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Get actionable tasks (all dependencies completed)
|
|
140
|
+
actionable = ft.actual_tasks()
|
|
141
|
+
for task in actionable:
|
|
142
|
+
print(f"[{task.project_id}] {task.title} — {task.status}")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Error Handling
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from flowtask import FlowTask, CycleDetectedError, ValidationError, NotFoundError
|
|
149
|
+
|
|
150
|
+
ft = FlowTask(token="ft_...")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
ft.create_dependency(task_a.id, task_b.id)
|
|
154
|
+
except CycleDetectedError as e:
|
|
155
|
+
print(f"Would create a cycle: {e.message}")
|
|
156
|
+
except ValidationError as e:
|
|
157
|
+
print(f"Invalid input: {e.message}")
|
|
158
|
+
except NotFoundError as e:
|
|
159
|
+
print(f"Not found: {e.message}")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Example: Import Tasks from CSV
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
import csv
|
|
166
|
+
from flowtask import FlowTask
|
|
167
|
+
|
|
168
|
+
ft = FlowTask(token="ft_...", base_url="http://localhost:3000")
|
|
169
|
+
project = ft.create_project("Imported Project")
|
|
170
|
+
|
|
171
|
+
with open("tasks.csv") as f:
|
|
172
|
+
for row in csv.DictReader(f):
|
|
173
|
+
project.create_task(
|
|
174
|
+
title=row["title"],
|
|
175
|
+
description=row.get("description", ""),
|
|
176
|
+
color=row.get("color"),
|
|
177
|
+
due_date=row.get("due_date"),
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Example: Daily Standup Report
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from flowtask import FlowTask
|
|
185
|
+
|
|
186
|
+
ft = FlowTask(token="ft_...", base_url="http://localhost:3000")
|
|
187
|
+
|
|
188
|
+
print("📋 What I can work on today:")
|
|
189
|
+
for task in ft.actual_tasks():
|
|
190
|
+
due = f" (due {task.due_date[:10]})" if task.due_date else ""
|
|
191
|
+
print(f" → {task.title}{due}")
|
|
192
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .client import FlowTask
|
|
2
|
+
from .models import Project, Task, Dependency, Tag
|
|
3
|
+
from .exceptions import FlowTaskError, AuthenticationError, ValidationError, NotFoundError, CycleDetectedError
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
__all__ = [
|
|
7
|
+
"FlowTask",
|
|
8
|
+
"Project",
|
|
9
|
+
"Task",
|
|
10
|
+
"Dependency",
|
|
11
|
+
"Tag",
|
|
12
|
+
"FlowTaskError",
|
|
13
|
+
"AuthenticationError",
|
|
14
|
+
"ValidationError",
|
|
15
|
+
"NotFoundError",
|
|
16
|
+
"CycleDetectedError",
|
|
17
|
+
]
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""FlowTask Python SDK — programmatic access to the FlowTask API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import requests as _requests
|
|
7
|
+
|
|
8
|
+
from .models import Project, Task, Dependency, Tag
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
FlowTaskError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
CycleDetectedError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
ERROR_MAP = {
|
|
18
|
+
"UNAUTHORIZED": AuthenticationError,
|
|
19
|
+
"VALIDATION_ERROR": ValidationError,
|
|
20
|
+
"NOT_FOUND": NotFoundError,
|
|
21
|
+
"CYCLE_DETECTED": CycleDetectedError,
|
|
22
|
+
"FORBIDDEN": AuthenticationError,
|
|
23
|
+
"CONFLICT": ValidationError,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FlowTask:
|
|
28
|
+
"""Client for the FlowTask API.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
ft = FlowTask(token="ft_your_token", base_url="https://your-app.railway.app")
|
|
32
|
+
|
|
33
|
+
# Create a project
|
|
34
|
+
project = ft.create_project("Sprint 1", color="#6366F1")
|
|
35
|
+
|
|
36
|
+
# Create tasks
|
|
37
|
+
design = project.create_task("Design", color="#8B5CF6")
|
|
38
|
+
build = project.create_task("Build", color="#3B82F6")
|
|
39
|
+
|
|
40
|
+
# Create dependency: Design must be done before Build
|
|
41
|
+
build.add_dependency(design)
|
|
42
|
+
|
|
43
|
+
# See what's actionable
|
|
44
|
+
for task in ft.actual_tasks():
|
|
45
|
+
print(f"→ {task.title}")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, token: str, base_url: str = "http://localhost:3000"):
|
|
49
|
+
self.base_url = base_url.rstrip("/")
|
|
50
|
+
self._session = _requests.Session()
|
|
51
|
+
self._session.headers.update(
|
|
52
|
+
{
|
|
53
|
+
"Authorization": f"Bearer {token}",
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _request(self, method: str, path: str, json: dict = None) -> dict:
|
|
59
|
+
url = f"{self.base_url}/api{path}"
|
|
60
|
+
resp = self._session.request(method, url, json=json)
|
|
61
|
+
data = resp.json()
|
|
62
|
+
|
|
63
|
+
if not resp.ok:
|
|
64
|
+
error_data = data.get("error", {})
|
|
65
|
+
code = error_data.get("code", "UNKNOWN")
|
|
66
|
+
message = error_data.get("message", "Unknown error")
|
|
67
|
+
exc_class = ERROR_MAP.get(code, FlowTaskError)
|
|
68
|
+
raise exc_class(message=message, code=code, status=resp.status_code)
|
|
69
|
+
|
|
70
|
+
return data.get("data", data)
|
|
71
|
+
|
|
72
|
+
# ─── Projects ──────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def list_projects(self) -> list[Project]:
|
|
75
|
+
"""List all projects."""
|
|
76
|
+
data = self._request("GET", "/projects")
|
|
77
|
+
return [Project.from_dict(p, client=self) for p in data]
|
|
78
|
+
|
|
79
|
+
def create_project(
|
|
80
|
+
self,
|
|
81
|
+
name: str,
|
|
82
|
+
description: Optional[str] = None,
|
|
83
|
+
color: Optional[str] = None,
|
|
84
|
+
) -> Project:
|
|
85
|
+
"""Create a new project."""
|
|
86
|
+
body = {"name": name}
|
|
87
|
+
if description is not None:
|
|
88
|
+
body["description"] = description
|
|
89
|
+
if color is not None:
|
|
90
|
+
body["color"] = color
|
|
91
|
+
data = self._request("POST", "/projects", json=body)
|
|
92
|
+
return Project.from_dict(data, client=self)
|
|
93
|
+
|
|
94
|
+
def get_project(self, project_id: str) -> Project:
|
|
95
|
+
"""Get a project by ID."""
|
|
96
|
+
data = self._request("GET", f"/projects/{project_id}")
|
|
97
|
+
return Project.from_dict(data.get("project", data), client=self)
|
|
98
|
+
|
|
99
|
+
def get_project_full(self, project_id: str) -> tuple[list[Task], list[Dependency]]:
|
|
100
|
+
"""Load a project's full data: tasks and dependencies."""
|
|
101
|
+
data = self._request("GET", f"/projects/{project_id}")
|
|
102
|
+
tasks = [Task.from_dict(t, client=self) for t in data.get("tasks", [])]
|
|
103
|
+
deps = [Dependency.from_dict(d) for d in data.get("dependencies", [])]
|
|
104
|
+
return tasks, deps
|
|
105
|
+
|
|
106
|
+
def update_project(self, project_id: str, **kwargs) -> Project:
|
|
107
|
+
"""Update a project. Accepts: name, description, color."""
|
|
108
|
+
body = {}
|
|
109
|
+
for key in ("name", "description", "color"):
|
|
110
|
+
if key in kwargs:
|
|
111
|
+
body[key] = kwargs[key]
|
|
112
|
+
data = self._request("PUT", f"/projects/{project_id}", json=body)
|
|
113
|
+
return Project.from_dict(data, client=self)
|
|
114
|
+
|
|
115
|
+
def delete_project(self, project_id: str) -> None:
|
|
116
|
+
"""Delete a project and all its tasks."""
|
|
117
|
+
self._request("DELETE", f"/projects/{project_id}")
|
|
118
|
+
|
|
119
|
+
# ─── Tasks ─────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
def create_task(
|
|
122
|
+
self,
|
|
123
|
+
project_id: str,
|
|
124
|
+
title: str,
|
|
125
|
+
description: Optional[str] = None,
|
|
126
|
+
color: Optional[str] = None,
|
|
127
|
+
due_date: Optional[str] = None,
|
|
128
|
+
assignee: Optional[str] = None,
|
|
129
|
+
position_x: Optional[float] = None,
|
|
130
|
+
position_y: Optional[float] = None,
|
|
131
|
+
) -> Task:
|
|
132
|
+
"""Create a task in a project."""
|
|
133
|
+
body: dict = {"title": title}
|
|
134
|
+
if description is not None:
|
|
135
|
+
body["description"] = description
|
|
136
|
+
if color is not None:
|
|
137
|
+
body["color"] = color
|
|
138
|
+
if due_date is not None:
|
|
139
|
+
body["dueDate"] = due_date
|
|
140
|
+
if assignee is not None:
|
|
141
|
+
body["assignee"] = assignee
|
|
142
|
+
if position_x is not None:
|
|
143
|
+
body["positionX"] = position_x
|
|
144
|
+
if position_y is not None:
|
|
145
|
+
body["positionY"] = position_y
|
|
146
|
+
data = self._request("POST", f"/projects/{project_id}/tasks", json=body)
|
|
147
|
+
return Task.from_dict(data, client=self)
|
|
148
|
+
|
|
149
|
+
def update_task(self, task_id: str, **kwargs) -> Task:
|
|
150
|
+
"""Update a task. Accepts: title, description, status, color, due_date, assignee."""
|
|
151
|
+
key_map = {"due_date": "dueDate", "position_x": "positionX", "position_y": "positionY", "sort_order": "sortOrder"}
|
|
152
|
+
body = {}
|
|
153
|
+
for key, value in kwargs.items():
|
|
154
|
+
api_key = key_map.get(key, key)
|
|
155
|
+
body[api_key] = value
|
|
156
|
+
data = self._request("PUT", f"/tasks/{task_id}", json=body)
|
|
157
|
+
return Task.from_dict(data, client=self)
|
|
158
|
+
|
|
159
|
+
def set_task_status(self, task_id: str, status: str) -> Task:
|
|
160
|
+
"""Set a task's status: 'pending', 'in_progress', or 'completed'."""
|
|
161
|
+
data = self._request("PATCH", f"/tasks/{task_id}/status", json={"status": status})
|
|
162
|
+
return Task.from_dict(data, client=self)
|
|
163
|
+
|
|
164
|
+
def delete_task(self, task_id: str) -> None:
|
|
165
|
+
"""Delete a task and its dependencies."""
|
|
166
|
+
self._request("DELETE", f"/tasks/{task_id}")
|
|
167
|
+
|
|
168
|
+
# ─── Dependencies ──────────────────────────────────
|
|
169
|
+
|
|
170
|
+
def create_dependency(self, enabling_task_id: str, dependent_task_id: str) -> Dependency:
|
|
171
|
+
"""Create a dependency: enabling_task must be done before dependent_task.
|
|
172
|
+
|
|
173
|
+
Raises CycleDetectedError if this would create a circular dependency.
|
|
174
|
+
"""
|
|
175
|
+
data = self._request(
|
|
176
|
+
"POST",
|
|
177
|
+
"/dependencies",
|
|
178
|
+
json={
|
|
179
|
+
"enablingTaskId": enabling_task_id,
|
|
180
|
+
"dependentTaskId": dependent_task_id,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
return Dependency.from_dict(data)
|
|
184
|
+
|
|
185
|
+
def delete_dependency(self, dependency_id: str) -> None:
|
|
186
|
+
"""Remove a dependency."""
|
|
187
|
+
self._request("DELETE", f"/dependencies/{dependency_id}")
|
|
188
|
+
|
|
189
|
+
# ─── Tags ──────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
def list_tags(self) -> list[Tag]:
|
|
192
|
+
"""List all tags."""
|
|
193
|
+
data = self._request("GET", "/tags")
|
|
194
|
+
return [Tag.from_dict(t) for t in data]
|
|
195
|
+
|
|
196
|
+
def create_tag(self, name: str, color: Optional[str] = None) -> Tag:
|
|
197
|
+
"""Create a tag."""
|
|
198
|
+
body: dict = {"name": name}
|
|
199
|
+
if color is not None:
|
|
200
|
+
body["color"] = color
|
|
201
|
+
data = self._request("POST", "/tags", json=body)
|
|
202
|
+
return Tag.from_dict(data)
|
|
203
|
+
|
|
204
|
+
def assign_tag(self, task_id: str, tag_id: str) -> None:
|
|
205
|
+
"""Assign a tag to a task."""
|
|
206
|
+
self._request("POST", f"/tasks/{task_id}/tags/{tag_id}", json={})
|
|
207
|
+
|
|
208
|
+
def remove_tag(self, task_id: str, tag_id: str) -> None:
|
|
209
|
+
"""Remove a tag from a task."""
|
|
210
|
+
self._request("DELETE", f"/tasks/{task_id}/tags/{tag_id}")
|
|
211
|
+
|
|
212
|
+
# ─── Smart Features ────────────────────────────────
|
|
213
|
+
|
|
214
|
+
def actual_tasks(self) -> list[Task]:
|
|
215
|
+
"""Get actionable tasks — tasks where all dependencies are completed."""
|
|
216
|
+
data = self._request("GET", "/actual-tasks")
|
|
217
|
+
return [Task.from_dict(t, client=self) for t in data]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class FlowTaskError(Exception):
|
|
2
|
+
"""Base exception for FlowTask SDK."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, message: str, code: str = "UNKNOWN", status: int = 500):
|
|
5
|
+
self.message = message
|
|
6
|
+
self.code = code
|
|
7
|
+
self.status = status
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthenticationError(FlowTaskError):
|
|
12
|
+
"""Raised when authentication fails (invalid or missing token)."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ValidationError(FlowTaskError):
|
|
17
|
+
"""Raised when input validation fails."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NotFoundError(FlowTaskError):
|
|
22
|
+
"""Raised when a resource is not found."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CycleDetectedError(FlowTaskError):
|
|
27
|
+
"""Raised when creating a dependency would form a cycle."""
|
|
28
|
+
pass
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Optional, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .client import FlowTask
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Tag:
|
|
11
|
+
id: str
|
|
12
|
+
name: str
|
|
13
|
+
color: str
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_dict(cls, data: dict) -> Tag:
|
|
17
|
+
return cls(id=data["id"], name=data["name"], color=data["color"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Dependency:
|
|
22
|
+
id: str
|
|
23
|
+
enabling_task_id: str
|
|
24
|
+
dependent_task_id: str
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_dict(cls, data: dict) -> Dependency:
|
|
28
|
+
return cls(
|
|
29
|
+
id=data["id"],
|
|
30
|
+
enabling_task_id=data["enablingTaskId"],
|
|
31
|
+
dependent_task_id=data["dependentTaskId"],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Task:
|
|
37
|
+
id: str
|
|
38
|
+
title: str
|
|
39
|
+
status: str
|
|
40
|
+
description: Optional[str] = None
|
|
41
|
+
color: Optional[str] = None
|
|
42
|
+
due_date: Optional[str] = None
|
|
43
|
+
assignee: Optional[str] = None
|
|
44
|
+
position_x: float = 0
|
|
45
|
+
position_y: float = 0
|
|
46
|
+
sort_order: int = 0
|
|
47
|
+
project_id: str = ""
|
|
48
|
+
tags: list[Tag] = field(default_factory=list)
|
|
49
|
+
created_at: str = ""
|
|
50
|
+
updated_at: str = ""
|
|
51
|
+
_client: Optional[FlowTask] = field(default=None, repr=False)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls, data: dict, client: Optional[FlowTask] = None) -> Task:
|
|
55
|
+
return cls(
|
|
56
|
+
id=data["id"],
|
|
57
|
+
title=data["title"],
|
|
58
|
+
status=data["status"],
|
|
59
|
+
description=data.get("description"),
|
|
60
|
+
color=data.get("color"),
|
|
61
|
+
due_date=data.get("dueDate"),
|
|
62
|
+
assignee=data.get("assignee"),
|
|
63
|
+
position_x=data.get("positionX", 0),
|
|
64
|
+
position_y=data.get("positionY", 0),
|
|
65
|
+
sort_order=data.get("sortOrder", 0),
|
|
66
|
+
project_id=data.get("projectId", ""),
|
|
67
|
+
tags=[Tag.from_dict(t) for t in data.get("tags", [])],
|
|
68
|
+
created_at=data.get("createdAt", ""),
|
|
69
|
+
updated_at=data.get("updatedAt", ""),
|
|
70
|
+
_client=client,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def update(self, **kwargs) -> Task:
|
|
74
|
+
"""Update this task's fields. Returns the updated task."""
|
|
75
|
+
if not self._client:
|
|
76
|
+
raise RuntimeError("Task not bound to a client")
|
|
77
|
+
return self._client.update_task(self.id, **kwargs)
|
|
78
|
+
|
|
79
|
+
def set_status(self, status: str) -> Task:
|
|
80
|
+
"""Set task status: 'pending', 'in_progress', or 'completed'."""
|
|
81
|
+
if not self._client:
|
|
82
|
+
raise RuntimeError("Task not bound to a client")
|
|
83
|
+
return self._client.set_task_status(self.id, status)
|
|
84
|
+
|
|
85
|
+
def complete(self) -> Task:
|
|
86
|
+
"""Mark this task as completed."""
|
|
87
|
+
return self.set_status("completed")
|
|
88
|
+
|
|
89
|
+
def delete(self) -> None:
|
|
90
|
+
"""Delete this task."""
|
|
91
|
+
if not self._client:
|
|
92
|
+
raise RuntimeError("Task not bound to a client")
|
|
93
|
+
self._client.delete_task(self.id)
|
|
94
|
+
|
|
95
|
+
def add_dependency(self, depends_on: Task) -> Dependency:
|
|
96
|
+
"""Make this task depend on another task (depends_on must be done first)."""
|
|
97
|
+
if not self._client:
|
|
98
|
+
raise RuntimeError("Task not bound to a client")
|
|
99
|
+
return self._client.create_dependency(depends_on.id, self.id)
|
|
100
|
+
|
|
101
|
+
def add_tag(self, tag_id: str) -> None:
|
|
102
|
+
"""Assign a tag to this task."""
|
|
103
|
+
if not self._client:
|
|
104
|
+
raise RuntimeError("Task not bound to a client")
|
|
105
|
+
self._client.assign_tag(self.id, tag_id)
|
|
106
|
+
|
|
107
|
+
def remove_tag(self, tag_id: str) -> None:
|
|
108
|
+
"""Remove a tag from this task."""
|
|
109
|
+
if not self._client:
|
|
110
|
+
raise RuntimeError("Task not bound to a client")
|
|
111
|
+
self._client.remove_tag(self.id, tag_id)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class Project:
|
|
116
|
+
id: str
|
|
117
|
+
name: str
|
|
118
|
+
description: Optional[str] = None
|
|
119
|
+
color: str = "#6366F1"
|
|
120
|
+
sort_order: int = 0
|
|
121
|
+
task_count: int = 0
|
|
122
|
+
created_at: str = ""
|
|
123
|
+
updated_at: str = ""
|
|
124
|
+
_client: Optional[FlowTask] = field(default=None, repr=False)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def from_dict(cls, data: dict, client: Optional[FlowTask] = None) -> Project:
|
|
128
|
+
count = data.get("_count", {})
|
|
129
|
+
return cls(
|
|
130
|
+
id=data["id"],
|
|
131
|
+
name=data["name"],
|
|
132
|
+
description=data.get("description"),
|
|
133
|
+
color=data.get("color", "#6366F1"),
|
|
134
|
+
sort_order=data.get("sortOrder", 0),
|
|
135
|
+
task_count=count.get("tasks", 0) if isinstance(count, dict) else 0,
|
|
136
|
+
created_at=data.get("createdAt", ""),
|
|
137
|
+
updated_at=data.get("updatedAt", ""),
|
|
138
|
+
_client=client,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def create_task(self, title: str, **kwargs) -> Task:
|
|
142
|
+
"""Create a new task in this project."""
|
|
143
|
+
if not self._client:
|
|
144
|
+
raise RuntimeError("Project not bound to a client")
|
|
145
|
+
return self._client.create_task(self.id, title, **kwargs)
|
|
146
|
+
|
|
147
|
+
def load(self) -> tuple[list[Task], list[Dependency]]:
|
|
148
|
+
"""Load all tasks and dependencies for this project."""
|
|
149
|
+
if not self._client:
|
|
150
|
+
raise RuntimeError("Project not bound to a client")
|
|
151
|
+
return self._client.get_project_full(self.id)
|
|
152
|
+
|
|
153
|
+
def update(self, **kwargs) -> Project:
|
|
154
|
+
"""Update this project's fields."""
|
|
155
|
+
if not self._client:
|
|
156
|
+
raise RuntimeError("Project not bound to a client")
|
|
157
|
+
return self._client.update_project(self.id, **kwargs)
|
|
158
|
+
|
|
159
|
+
def delete(self) -> None:
|
|
160
|
+
"""Delete this project and all its tasks."""
|
|
161
|
+
if not self._client:
|
|
162
|
+
raise RuntimeError("Project not bound to a client")
|
|
163
|
+
self._client.delete_project(self.id)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowtask-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the FlowTask visual task manager API
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests>=2.28.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
11
|
+
|
|
12
|
+
# FlowTask Python SDK
|
|
13
|
+
|
|
14
|
+
A Python client for the FlowTask API — programmatically create projects, tasks, and dependency graphs.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install -e sdk/python
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or from the SDK directory:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd sdk/python
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from flowtask import FlowTask
|
|
33
|
+
|
|
34
|
+
# Connect with your API token (create one in the FlowTask UI → Key icon → API Tokens)
|
|
35
|
+
ft = FlowTask(token="ft_your_token_here", base_url="http://localhost:3000")
|
|
36
|
+
|
|
37
|
+
# Create a project
|
|
38
|
+
project = ft.create_project("Sprint 1", color="#6366F1")
|
|
39
|
+
|
|
40
|
+
# Create tasks
|
|
41
|
+
design = project.create_task("Design System", color="#8B5CF6")
|
|
42
|
+
build = project.create_task("Build Components", color="#3B82F6")
|
|
43
|
+
test = project.create_task("Write Tests", color="#10B981")
|
|
44
|
+
deploy = project.create_task("Deploy", color="#EF4444")
|
|
45
|
+
|
|
46
|
+
# Create dependencies (diamond-shaped DAG)
|
|
47
|
+
build.add_dependency(design) # Design must be done before Build
|
|
48
|
+
test.add_dependency(design) # Design must be done before Test
|
|
49
|
+
deploy.add_dependency(build) # Build must be done before Deploy
|
|
50
|
+
deploy.add_dependency(test) # Test must be done before Deploy
|
|
51
|
+
|
|
52
|
+
# See what's actionable right now
|
|
53
|
+
for task in ft.actual_tasks():
|
|
54
|
+
print(f"→ {task.title}")
|
|
55
|
+
# → Design System (the only task with no blockers)
|
|
56
|
+
|
|
57
|
+
# Complete a task and see what unblocks
|
|
58
|
+
design.complete()
|
|
59
|
+
for task in ft.actual_tasks():
|
|
60
|
+
print(f"→ {task.title}")
|
|
61
|
+
# → Build Components
|
|
62
|
+
# → Write Tests
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API Reference
|
|
66
|
+
|
|
67
|
+
### FlowTask Client
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
ft = FlowTask(token="ft_...", base_url="https://your-app.railway.app")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Projects
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# List all projects
|
|
77
|
+
projects = ft.list_projects()
|
|
78
|
+
|
|
79
|
+
# Create a project
|
|
80
|
+
project = ft.create_project("Name", description="...", color="#6366F1")
|
|
81
|
+
|
|
82
|
+
# Load full project (tasks + dependencies)
|
|
83
|
+
tasks, dependencies = project.load()
|
|
84
|
+
|
|
85
|
+
# Update
|
|
86
|
+
project.update(name="New Name", color="#EF4444")
|
|
87
|
+
|
|
88
|
+
# Delete
|
|
89
|
+
project.delete()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Tasks
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# Create a task in a project
|
|
96
|
+
task = project.create_task(
|
|
97
|
+
"Task Title",
|
|
98
|
+
description="Details...",
|
|
99
|
+
color="#3B82F6",
|
|
100
|
+
due_date="2026-04-01T00:00:00Z",
|
|
101
|
+
assignee="Alice",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Update fields
|
|
105
|
+
task.update(title="New Title", description="Updated")
|
|
106
|
+
|
|
107
|
+
# Change status
|
|
108
|
+
task.set_status("in_progress") # or "pending", "completed"
|
|
109
|
+
task.complete() # shorthand for completed
|
|
110
|
+
|
|
111
|
+
# Delete
|
|
112
|
+
task.delete()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Dependencies
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# Make task_b depend on task_a (task_a must be done first)
|
|
119
|
+
task_b.add_dependency(task_a)
|
|
120
|
+
|
|
121
|
+
# Or use the client directly
|
|
122
|
+
dep = ft.create_dependency(
|
|
123
|
+
enabling_task_id=task_a.id,
|
|
124
|
+
dependent_task_id=task_b.id
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Remove a dependency
|
|
128
|
+
ft.delete_dependency(dep.id)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Tags
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# Create a tag
|
|
135
|
+
tag = ft.create_tag("frontend", color="#3B82F6")
|
|
136
|
+
|
|
137
|
+
# Assign to a task
|
|
138
|
+
task.add_tag(tag.id)
|
|
139
|
+
|
|
140
|
+
# Remove from a task
|
|
141
|
+
task.remove_tag(tag.id)
|
|
142
|
+
|
|
143
|
+
# List all tags
|
|
144
|
+
tags = ft.list_tags()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Smart Features
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# Get actionable tasks (all dependencies completed)
|
|
151
|
+
actionable = ft.actual_tasks()
|
|
152
|
+
for task in actionable:
|
|
153
|
+
print(f"[{task.project_id}] {task.title} — {task.status}")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Error Handling
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from flowtask import FlowTask, CycleDetectedError, ValidationError, NotFoundError
|
|
160
|
+
|
|
161
|
+
ft = FlowTask(token="ft_...")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
ft.create_dependency(task_a.id, task_b.id)
|
|
165
|
+
except CycleDetectedError as e:
|
|
166
|
+
print(f"Would create a cycle: {e.message}")
|
|
167
|
+
except ValidationError as e:
|
|
168
|
+
print(f"Invalid input: {e.message}")
|
|
169
|
+
except NotFoundError as e:
|
|
170
|
+
print(f"Not found: {e.message}")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Example: Import Tasks from CSV
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
import csv
|
|
177
|
+
from flowtask import FlowTask
|
|
178
|
+
|
|
179
|
+
ft = FlowTask(token="ft_...", base_url="http://localhost:3000")
|
|
180
|
+
project = ft.create_project("Imported Project")
|
|
181
|
+
|
|
182
|
+
with open("tasks.csv") as f:
|
|
183
|
+
for row in csv.DictReader(f):
|
|
184
|
+
project.create_task(
|
|
185
|
+
title=row["title"],
|
|
186
|
+
description=row.get("description", ""),
|
|
187
|
+
color=row.get("color"),
|
|
188
|
+
due_date=row.get("due_date"),
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Example: Daily Standup Report
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from flowtask import FlowTask
|
|
196
|
+
|
|
197
|
+
ft = FlowTask(token="ft_...", base_url="http://localhost:3000")
|
|
198
|
+
|
|
199
|
+
print("📋 What I can work on today:")
|
|
200
|
+
for task in ft.actual_tasks():
|
|
201
|
+
due = f" (due {task.due_date[:10]})" if task.due_date else ""
|
|
202
|
+
print(f" → {task.title}{due}")
|
|
203
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
flowtask/__init__.py
|
|
4
|
+
flowtask/client.py
|
|
5
|
+
flowtask/exceptions.py
|
|
6
|
+
flowtask/models.py
|
|
7
|
+
flowtask_sdk.egg-info/PKG-INFO
|
|
8
|
+
flowtask_sdk.egg-info/SOURCES.txt
|
|
9
|
+
flowtask_sdk.egg-info/dependency_links.txt
|
|
10
|
+
flowtask_sdk.egg-info/requires.txt
|
|
11
|
+
flowtask_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flowtask
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flowtask-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the FlowTask visual task manager API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = [
|
|
13
|
+
"requests>=2.28.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=7.0",
|
|
19
|
+
]
|