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.
@@ -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,4 @@
1
+ requests>=2.28.0
2
+
3
+ [dev]
4
+ pytest>=7.0
@@ -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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+