flowtask-sdk 0.2.0__tar.gz → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowtask-sdk
3
- Version: 0.2.0
3
+ Version: 0.5.0
4
4
  Summary: Python SDK for the FlowTask visual task manager API
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -1,8 +1,8 @@
1
1
  from .client import FlowTask
2
- from .models import Workspace, Project, Task, Dependency, Tag, Section, MyDayItem, SmartViewCounts
2
+ from .models import Workspace, Project, Task, Dependency, Tag, Section, MyDayItem, SmartViewCounts, WorkspaceMember, Note, NoteFolder
3
3
  from .exceptions import FlowTaskError, AuthenticationError, ValidationError, NotFoundError, CycleDetectedError
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.5.0"
6
6
  __all__ = [
7
7
  "FlowTask",
8
8
  "Workspace",
@@ -13,6 +13,9 @@ __all__ = [
13
13
  "Section",
14
14
  "MyDayItem",
15
15
  "SmartViewCounts",
16
+ "WorkspaceMember",
17
+ "Note",
18
+ "NoteFolder",
16
19
  "FlowTaskError",
17
20
  "AuthenticationError",
18
21
  "ValidationError",
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from typing import Optional
6
6
  import requests as _requests
7
7
 
8
- from .models import Workspace, Project, Task, Dependency, Tag, Section, MyDayItem, SmartViewCounts
8
+ from .models import Workspace, Project, Task, Dependency, Tag, Section, MyDayItem, SmartViewCounts, WorkspaceMember, Note, NoteFolder
9
9
  from .exceptions import (
10
10
  FlowTaskError,
11
11
  AuthenticationError,
@@ -345,6 +345,33 @@ class FlowTask:
345
345
  """Reorder sections by providing IDs in desired order."""
346
346
  self._request("PATCH", "/sections/reorder", json={"sectionIds": section_ids})
347
347
 
348
+ # ─── Sharing ───────────────────────────────────────
349
+
350
+ def invite_to_workspace(self, workspace_id: str, email: str, role: str = "editor") -> WorkspaceMember:
351
+ """Invite a user to a workspace by email. Returns the member or creates a pending invitation."""
352
+ data = self._request("POST", f"/workspaces/{workspace_id}/members", json={"email": email, "role": role})
353
+ if data.get("type") == "member" and data.get("member"):
354
+ return WorkspaceMember.from_dict(data["member"])
355
+ return WorkspaceMember(id="pending", user_id="", role=role, user_email=email)
356
+
357
+ def list_workspace_members(self, workspace_id: str) -> list[WorkspaceMember]:
358
+ """List all members of a workspace."""
359
+ data = self._request("GET", f"/workspaces/{workspace_id}/members")
360
+ return [WorkspaceMember.from_dict(m) for m in data.get("members", [])]
361
+
362
+ def update_workspace_member(self, workspace_id: str, member_id: str, role: str) -> WorkspaceMember:
363
+ """Change a member's role (owner only)."""
364
+ data = self._request("PUT", f"/workspaces/{workspace_id}/members/{member_id}", json={"role": role})
365
+ return WorkspaceMember.from_dict(data)
366
+
367
+ def remove_workspace_member(self, workspace_id: str, member_id: str) -> None:
368
+ """Remove a member from a workspace (owner only)."""
369
+ self._request("DELETE", f"/workspaces/{workspace_id}/members/{member_id}")
370
+
371
+ def leave_workspace(self, workspace_id: str) -> None:
372
+ """Leave a workspace (non-owners only)."""
373
+ self._request("DELETE", f"/workspaces/{workspace_id}/members/me")
374
+
348
375
  # ─── My Day ────────────────────────────────────────
349
376
 
350
377
  def get_my_day(self, date: Optional[str] = None) -> list[MyDayItem]:
@@ -406,6 +433,57 @@ class FlowTask:
406
433
  data = self._request("GET", "/actual-tasks")
407
434
  return [Task.from_dict(t, client=self) for t in data]
408
435
 
436
+ # ─── Notes ─────────────────────────────────────────
437
+
438
+ def list_notes(self, workspace_id: str) -> dict:
439
+ """List all notes and folders in a workspace."""
440
+ data = self._request("GET", f"/workspaces/{workspace_id}/notes")
441
+ folders = [NoteFolder.from_dict(f, client=self) for f in data.get("folders", [])]
442
+ unfoldered = [Note.from_dict(n, client=self) for n in data.get("unfolderedNotes", [])]
443
+ return {"folders": folders, "unfolderedNotes": unfoldered}
444
+
445
+ def create_note(
446
+ self,
447
+ workspace_id: str,
448
+ title: str,
449
+ content: str = "",
450
+ folder_id: Optional[str] = None,
451
+ ) -> Note:
452
+ """Create a note in a workspace. Content is markdown."""
453
+ body: dict = {"title": title, "content": content}
454
+ if folder_id:
455
+ body["folderId"] = folder_id
456
+ data = self._request("POST", f"/workspaces/{workspace_id}/notes", json=body)
457
+ return Note.from_dict(data, client=self)
458
+
459
+ def update_note(self, note_id: str, **kwargs) -> Note:
460
+ """Update a note. Accepts: title, content, folder_id."""
461
+ key_map = {"folder_id": "folderId"}
462
+ body = {}
463
+ for key, value in kwargs.items():
464
+ api_key = key_map.get(key, key)
465
+ body[api_key] = value
466
+ data = self._request("PUT", f"/notes/{note_id}", json=body)
467
+ return Note.from_dict(data, client=self)
468
+
469
+ def delete_note(self, note_id: str) -> None:
470
+ """Delete a note."""
471
+ self._request("DELETE", f"/notes/{note_id}")
472
+
473
+ def create_note_folder(self, workspace_id: str, name: str) -> NoteFolder:
474
+ """Create a note folder in a workspace."""
475
+ data = self._request("POST", f"/workspaces/{workspace_id}/folders", json={"name": name})
476
+ return NoteFolder.from_dict(data, client=self)
477
+
478
+ def update_note_folder(self, folder_id: str, name: str) -> NoteFolder:
479
+ """Rename a note folder."""
480
+ data = self._request("PUT", f"/folders/{folder_id}", json={"name": name})
481
+ return NoteFolder.from_dict(data, client=self)
482
+
483
+ def delete_note_folder(self, folder_id: str) -> None:
484
+ """Delete a note folder. Notes become unfoldered."""
485
+ self._request("DELETE", f"/folders/{folder_id}")
486
+
409
487
  # ─── Search ────────────────────────────────────────
410
488
 
411
489
  def search(self, query: str) -> list[Task]:
@@ -145,6 +145,26 @@ class Section:
145
145
  )
146
146
 
147
147
 
148
+ @dataclass
149
+ class WorkspaceMember:
150
+ id: str
151
+ user_id: str
152
+ role: str
153
+ user_email: str = ""
154
+ user_name: Optional[str] = None
155
+
156
+ @classmethod
157
+ def from_dict(cls, data: dict) -> WorkspaceMember:
158
+ user = data.get("user", {})
159
+ return cls(
160
+ id=data["id"],
161
+ user_id=data.get("userId", ""),
162
+ role=data.get("role", "editor"),
163
+ user_email=user.get("email", ""),
164
+ user_name=user.get("name"),
165
+ )
166
+
167
+
148
168
  @dataclass
149
169
  class Workspace:
150
170
  id: str
@@ -153,6 +173,8 @@ class Workspace:
153
173
  sort_order: int = 0
154
174
  project_count: int = 0
155
175
  task_count: int = 0
176
+ is_shared: bool = False
177
+ current_user_role: str = "owner"
156
178
  created_at: str = ""
157
179
  updated_at: str = ""
158
180
  _client: Optional[FlowTask] = field(default=None, repr=False)
@@ -167,6 +189,8 @@ class Workspace:
167
189
  sort_order=data.get("sortOrder", 0),
168
190
  project_count=count.get("projects", 0) if isinstance(count, dict) else 0,
169
191
  task_count=count.get("tasks", 0) if isinstance(count, dict) else 0,
192
+ is_shared=data.get("isShared", False),
193
+ current_user_role=data.get("currentUserRole", "owner"),
170
194
  created_at=data.get("createdAt", ""),
171
195
  updated_at=data.get("updatedAt", ""),
172
196
  _client=client,
@@ -220,6 +244,48 @@ class Workspace:
220
244
  raise RuntimeError("Workspace not bound to a client")
221
245
  self._client.delete_workspace(self.id)
222
246
 
247
+ def share(self, email: str, role: str = "editor") -> WorkspaceMember:
248
+ """Invite a user to this workspace by email."""
249
+ if not self._client:
250
+ raise RuntimeError("Workspace not bound to a client")
251
+ return self._client.invite_to_workspace(self.id, email, role)
252
+
253
+ def list_members(self) -> list[WorkspaceMember]:
254
+ """List all members of this workspace."""
255
+ if not self._client:
256
+ raise RuntimeError("Workspace not bound to a client")
257
+ return self._client.list_workspace_members(self.id)
258
+
259
+ def remove_member(self, member_id: str) -> None:
260
+ """Remove a member from this workspace."""
261
+ if not self._client:
262
+ raise RuntimeError("Workspace not bound to a client")
263
+ self._client.remove_workspace_member(self.id, member_id)
264
+
265
+ def leave(self) -> None:
266
+ """Leave this workspace (non-owners only)."""
267
+ if not self._client:
268
+ raise RuntimeError("Workspace not bound to a client")
269
+ self._client.leave_workspace(self.id)
270
+
271
+ def list_notes(self) -> dict:
272
+ """List all notes and folders in this workspace."""
273
+ if not self._client:
274
+ raise RuntimeError("Workspace not bound to a client")
275
+ return self._client.list_notes(self.id)
276
+
277
+ def create_note(self, title: str, content: str = "", folder_id: Optional[str] = None) -> Note:
278
+ """Create a note in this workspace."""
279
+ if not self._client:
280
+ raise RuntimeError("Workspace not bound to a client")
281
+ return self._client.create_note(self.id, title, content, folder_id)
282
+
283
+ def create_note_folder(self, name: str) -> NoteFolder:
284
+ """Create a note folder in this workspace."""
285
+ if not self._client:
286
+ raise RuntimeError("Workspace not bound to a client")
287
+ return self._client.create_note_folder(self.id, name)
288
+
223
289
 
224
290
  @dataclass
225
291
  class Project:
@@ -335,3 +401,63 @@ class SmartViewCounts:
335
401
  delegated=data.get("delegated", 0),
336
402
  completed=data.get("completed", 0),
337
403
  )
404
+
405
+
406
+ @dataclass
407
+ class NoteFolder:
408
+ id: str
409
+ name: str
410
+ sort_order: int = 0
411
+ workspace_id: str = ""
412
+ notes: list[Note] = field(default_factory=list)
413
+
414
+ @classmethod
415
+ def from_dict(cls, data: dict, client: Optional[FlowTask] = None) -> NoteFolder:
416
+ return cls(
417
+ id=data["id"],
418
+ name=data["name"],
419
+ sort_order=data.get("sortOrder", 0),
420
+ workspace_id=data.get("workspaceId", ""),
421
+ notes=[Note.from_dict(n, client=client) for n in data.get("notes", [])],
422
+ )
423
+
424
+
425
+ @dataclass
426
+ class Note:
427
+ id: str
428
+ title: str
429
+ content: str = ""
430
+ sort_order: int = 0
431
+ folder_id: Optional[str] = None
432
+ author_id: str = ""
433
+ workspace_id: str = ""
434
+ created_at: str = ""
435
+ updated_at: str = ""
436
+ _client: Optional[FlowTask] = field(default=None, repr=False)
437
+
438
+ @classmethod
439
+ def from_dict(cls, data: dict, client: Optional[FlowTask] = None) -> Note:
440
+ return cls(
441
+ id=data["id"],
442
+ title=data["title"],
443
+ content=data.get("content", ""),
444
+ sort_order=data.get("sortOrder", 0),
445
+ folder_id=data.get("folderId"),
446
+ author_id=data.get("authorId", ""),
447
+ workspace_id=data.get("workspaceId", ""),
448
+ created_at=data.get("createdAt", ""),
449
+ updated_at=data.get("updatedAt", ""),
450
+ _client=client,
451
+ )
452
+
453
+ def update(self, **kwargs) -> Note:
454
+ """Update this note. Accepts: title, content, folder_id."""
455
+ if not self._client:
456
+ raise RuntimeError("Note not bound to a client")
457
+ return self._client.update_note(self.id, **kwargs)
458
+
459
+ def delete(self) -> None:
460
+ """Delete this note."""
461
+ if not self._client:
462
+ raise RuntimeError("Note not bound to a client")
463
+ self._client.delete_note(self.id)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowtask-sdk
3
- Version: 0.2.0
3
+ Version: 0.5.0
4
4
  Summary: Python SDK for the FlowTask visual task manager API
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flowtask-sdk"
7
- version = "0.2.0"
7
+ version = "0.5.0"
8
8
  description = "Python SDK for the FlowTask visual task manager API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes