2024-09-28 10:57:27 +00:00
|
|
|
"""Favro Data Model.
|
|
|
|
|
|
|
|
Based on the descriptions on the [Favro API Specification
|
|
|
|
Page](https://favro.com/developer/), and observed behaviour.
|
|
|
|
"""
|
2024-09-28 12:13:51 +00:00
|
|
|
|
2024-09-26 21:07:46 +00:00
|
|
|
import dataclasses
|
|
|
|
import datetime
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
2024-10-02 11:34:49 +00:00
|
|
|
def map_opt(mapper, value):
|
|
|
|
if value is not None:
|
|
|
|
return mapper(value)
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2024-10-02 14:23:15 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CollectionId:
|
|
|
|
raw_id: int
|
|
|
|
|
|
|
|
|
2024-09-26 21:07:46 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class SeqId:
|
|
|
|
raw_id: int
|
|
|
|
|
2024-09-26 21:08:37 +00:00
|
|
|
|
2024-09-26 21:07:46 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CardId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-09-26 21:08:37 +00:00
|
|
|
|
2024-09-26 21:07:46 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
2024-10-01 14:40:44 +00:00
|
|
|
class CardCommonId:
|
2024-09-26 21:07:46 +00:00
|
|
|
raw_id: str
|
|
|
|
|
2024-09-26 21:08:37 +00:00
|
|
|
|
2024-09-26 21:07:46 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class UserId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-09-26 21:08:37 +00:00
|
|
|
|
2024-09-26 21:07:46 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class OrganizationId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-10-01 14:14:51 +00:00
|
|
|
|
2024-10-01 14:06:06 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CustomFieldId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-10-01 14:14:51 +00:00
|
|
|
|
2024-10-01 14:06:06 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class WidgetCommonId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-10-01 14:14:51 +00:00
|
|
|
|
2024-10-01 14:06:06 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CustomFieldItemId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-10-02 08:32:14 +00:00
|
|
|
|
2024-10-01 14:40:44 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class TaskId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-10-02 08:32:14 +00:00
|
|
|
|
2024-10-01 14:40:44 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class TaskListId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-09-26 21:08:37 +00:00
|
|
|
|
2024-09-28 10:37:19 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CardAssignment:
|
|
|
|
user: UserId
|
|
|
|
completed: bool
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'CardAssignment':
|
|
|
|
return CardAssignment(
|
2024-09-28 10:54:34 +00:00
|
|
|
UserId(json['userId']),
|
|
|
|
json['completed'],
|
2024-09-28 10:37:19 +00:00
|
|
|
)
|
|
|
|
|
2024-09-28 10:54:34 +00:00
|
|
|
|
2024-09-28 10:53:34 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class UserInfo:
|
|
|
|
user_id: UserId
|
|
|
|
name: str
|
|
|
|
email: str
|
|
|
|
organization_role: str
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'UserInfo':
|
|
|
|
return UserInfo(
|
2024-10-01 11:23:22 +00:00
|
|
|
map_opt(UserId, json.get('userId')),
|
|
|
|
json.get('name'),
|
|
|
|
json.get('email'),
|
|
|
|
json.get('organizationRole'),
|
2024-09-28 10:53:34 +00:00
|
|
|
)
|
|
|
|
|
2024-09-28 10:54:34 +00:00
|
|
|
|
2024-09-28 10:37:19 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class TagId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-09-28 10:54:34 +00:00
|
|
|
|
2024-09-28 10:53:34 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class TagInfo:
|
|
|
|
tag_id: TagId
|
|
|
|
organization_id: OrganizationId
|
|
|
|
name: str
|
|
|
|
color: str | None
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'TagInfo':
|
|
|
|
return TagInfo(
|
2024-09-28 10:54:34 +00:00
|
|
|
TagId(json['tagId']),
|
|
|
|
OrganizationId(json['organizationId']),
|
|
|
|
json['name'],
|
|
|
|
json.get('color'),
|
2024-09-28 10:53:34 +00:00
|
|
|
)
|
|
|
|
|
2024-10-01 14:14:51 +00:00
|
|
|
|
2024-10-01 14:06:06 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CustomFieldItem:
|
2024-10-01 14:14:51 +00:00
|
|
|
"""Custom field item object.
|
2024-10-01 14:06:06 +00:00
|
|
|
Fields:
|
|
|
|
- customFieldItemId: The id of the custom field item.
|
|
|
|
- name: The name of the custom field item.
|
|
|
|
"""
|
2024-10-01 14:14:51 +00:00
|
|
|
|
2024-10-01 14:06:06 +00:00
|
|
|
custom_field_item_id: CustomFieldItemId
|
|
|
|
name: str
|
|
|
|
|
2024-10-02 11:34:49 +00:00
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'CustomFieldItem':
|
|
|
|
return CustomFieldItem(
|
|
|
|
custom_field_item_id=CustomFieldItemId(json['customFieldItemId']),
|
|
|
|
name=json['name'],
|
|
|
|
)
|
|
|
|
|
2024-10-01 14:14:51 +00:00
|
|
|
|
2024-10-01 14:06:06 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CustomFieldInfo:
|
2024-10-01 14:14:51 +00:00
|
|
|
"""Custom field object.
|
2024-10-01 14:06:06 +00:00
|
|
|
|
|
|
|
Fields:
|
|
|
|
- organizationId: The id of the organization that this custom field exists in.
|
|
|
|
- customFieldId: The id of the custom field.
|
|
|
|
- widgetCommonId: The shared id of the widget if the custom field is local to that widget.
|
|
|
|
- type: field type. Refer to allowed custom field types.
|
|
|
|
- name: name of the custom field.
|
|
|
|
- enabled: True if the custom field is currently enabled for the organization.
|
|
|
|
- customFieldItems: The list of items that this custom field can have in case it is a selectable one.
|
|
|
|
"""
|
2024-10-01 14:14:51 +00:00
|
|
|
|
|
|
|
organization_id: OrganizationId
|
|
|
|
custom_field_id: CustomFieldId
|
|
|
|
widget_common_id: WidgetCommonId
|
2024-10-01 14:06:06 +00:00
|
|
|
type: str
|
|
|
|
name: str
|
|
|
|
enabled: bool
|
|
|
|
custom_field_items: list[CustomFieldItem]
|
|
|
|
|
2024-10-02 11:34:49 +00:00
|
|
|
def get_field_item(self, field_item_id: CustomFieldItemId) -> CustomFieldItem | None:
|
|
|
|
for item in self.custom_field_items:
|
|
|
|
if item.custom_field_item_id == field_item_id:
|
|
|
|
return item
|
|
|
|
return None
|
|
|
|
|
2024-10-01 14:06:06 +00:00
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'CustomFieldInfo':
|
2024-10-01 14:40:44 +00:00
|
|
|
return CustomFieldInfo(
|
2024-10-02 08:32:14 +00:00
|
|
|
organization_id=OrganizationId(json['organizationId']),
|
|
|
|
custom_field_id=CustomFieldId(json['customFieldId']),
|
2024-10-02 11:34:49 +00:00
|
|
|
widget_common_id=map_opt(WidgetCommonId,json.get('widgetCommonId')),
|
2024-10-02 08:32:14 +00:00
|
|
|
type=json['type'],
|
|
|
|
name=json['name'],
|
|
|
|
enabled=json['enabled'],
|
2024-10-02 11:54:30 +00:00
|
|
|
custom_field_items=[CustomFieldItem.from_json(f) for f in json.get('customFieldItems', [])],
|
2024-10-01 14:40:44 +00:00
|
|
|
)
|
2024-10-01 14:06:06 +00:00
|
|
|
|
2024-09-28 10:54:34 +00:00
|
|
|
|
2024-09-28 12:10:13 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CustomField:
|
|
|
|
custom_field_id: CustomFieldId
|
2024-10-02 11:34:49 +00:00
|
|
|
value: list[CustomFieldItemId] | CustomFieldItemId
|
2024-10-02 11:54:30 +00:00
|
|
|
color: str
|
2024-09-28 12:10:13 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'CustomField':
|
2024-10-02 11:54:30 +00:00
|
|
|
value = json.get('value')
|
|
|
|
if value is None:
|
|
|
|
typed_value = []
|
|
|
|
elif isinstance(value, str):
|
2024-10-02 11:34:49 +00:00
|
|
|
typed_value = CustomFieldItemId(value)
|
|
|
|
else:
|
|
|
|
typed_value = [CustomFieldItemId(v) for v in value]
|
2024-09-28 12:10:13 +00:00
|
|
|
return CustomField(
|
2024-10-02 11:34:49 +00:00
|
|
|
custom_field_id = CustomFieldId(json['customFieldId']),
|
|
|
|
value = typed_value,
|
2024-10-02 11:54:30 +00:00
|
|
|
color = json.get('color'),
|
2024-09-28 12:10:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-09-28 10:37:19 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CardDependency:
|
|
|
|
card_id: CardId
|
2024-10-01 14:40:44 +00:00
|
|
|
card_common_id: CardCommonId
|
2024-09-28 10:37:19 +00:00
|
|
|
is_before: bool
|
|
|
|
reverse_card_id: CardId
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'CardDependency':
|
|
|
|
return CardDependency(
|
2024-09-28 10:54:34 +00:00
|
|
|
CardId(json['cardId']),
|
2024-10-01 14:40:44 +00:00
|
|
|
CardCommonId(json['cardCommonId']),
|
2024-09-28 10:54:34 +00:00
|
|
|
json['isBefore'],
|
|
|
|
CardId(json['reverseCardId']),
|
2024-09-28 10:37:19 +00:00
|
|
|
)
|
|
|
|
|
2024-10-02 08:32:14 +00:00
|
|
|
|
2024-10-01 14:40:44 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class Task:
|
|
|
|
task_id: TaskId
|
|
|
|
task_list_id: TaskListId
|
|
|
|
organization_id: OrganizationId
|
|
|
|
card_common_id: CardCommonId
|
|
|
|
name: str
|
|
|
|
completed: bool
|
|
|
|
position: int
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'Task':
|
|
|
|
return Task(
|
2024-10-02 08:32:14 +00:00
|
|
|
task_id=json['taskId'],
|
|
|
|
task_list_id=json['taskListId'],
|
|
|
|
organization_id=json['organizationId'],
|
|
|
|
card_common_id=json['cardCommonId'],
|
|
|
|
name=json['name'],
|
|
|
|
completed=json['completed'],
|
|
|
|
position=json['position'],
|
2024-10-01 14:40:44 +00:00
|
|
|
)
|
2024-09-28 10:54:34 +00:00
|
|
|
|
2024-10-02 14:23:15 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class Collection:
|
|
|
|
collection_id: CollectionId
|
|
|
|
organization_id: OrganizationId
|
|
|
|
name: str
|
|
|
|
shared_to_users: list[dict[str,str]] # TODO
|
|
|
|
public_sharing: str
|
|
|
|
background: str
|
|
|
|
is_archived: bool
|
|
|
|
widget_common_id: WidgetCommonId
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'Collection':
|
|
|
|
return Collection(
|
|
|
|
collection_id=CollectionId(json['collectionId']),
|
|
|
|
organization_id=OrganizationId(json['collectionId']),
|
|
|
|
name=json['name'],
|
|
|
|
shared_to_users=json['sharedToUsers'],
|
|
|
|
public_sharing=json['publicSharing'],
|
|
|
|
background=json['background'],
|
|
|
|
is_archived=json['archived'],
|
|
|
|
widget_common_id=map_opt(WidgetCommonId,json.get('widgetCommonId')),
|
|
|
|
)
|
2024-10-02 08:32:14 +00:00
|
|
|
|
2024-09-26 21:07:46 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class Card:
|
|
|
|
card_id: CardId
|
|
|
|
seq_id: SeqId
|
2024-10-01 14:40:44 +00:00
|
|
|
common_id: CardCommonId
|
2024-09-26 21:07:46 +00:00
|
|
|
organization_id: OrganizationId
|
|
|
|
is_archived: bool
|
|
|
|
name: str
|
2024-09-28 10:37:19 +00:00
|
|
|
dependencies: list[CardDependency]
|
|
|
|
assignments: list[CardAssignment]
|
2024-09-28 12:10:13 +00:00
|
|
|
custom_fields: list[CustomField]
|
2024-09-28 10:37:19 +00:00
|
|
|
tags: list[TagId]
|
2024-09-26 21:07:46 +00:00
|
|
|
todo_list_user_id: UserId | None
|
|
|
|
todo_list_completed: bool | None
|
|
|
|
creator_user_id: UserId
|
|
|
|
creation_date: datetime.datetime
|
2024-10-01 11:15:49 +00:00
|
|
|
start_date: datetime.date | None
|
|
|
|
due_date: datetime.date | None
|
2024-09-28 12:10:13 +00:00
|
|
|
attachments: list[dict]
|
2024-09-26 21:07:46 +00:00
|
|
|
detailed_description: str | None
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'Card':
|
2024-10-02 11:54:30 +00:00
|
|
|
card_id = CardId(json['cardId'])
|
|
|
|
seq_id = SeqId(json['sequentialId'])
|
2024-09-26 21:07:46 +00:00
|
|
|
return Card(
|
2024-10-02 11:54:30 +00:00
|
|
|
card_id=card_id,
|
|
|
|
seq_id=seq_id,
|
2024-10-01 14:40:44 +00:00
|
|
|
common_id=CardCommonId(json['cardCommonId']),
|
2024-09-26 21:08:37 +00:00
|
|
|
detailed_description=json.get('detailedDescription'),
|
|
|
|
is_archived=json['archived'],
|
|
|
|
organization_id=OrganizationId(json['organizationId']),
|
|
|
|
name=json['name'],
|
2024-10-01 11:15:49 +00:00
|
|
|
todo_list_user_id=map_opt(UserId, json.get('todoListUserId')),
|
2024-09-26 21:08:37 +00:00
|
|
|
todo_list_completed=json.get('todoListCompleted'),
|
2024-09-28 12:13:51 +00:00
|
|
|
dependencies=[
|
|
|
|
CardDependency.from_json(dep) for dep in json['dependencies']
|
|
|
|
],
|
2024-09-28 10:37:19 +00:00
|
|
|
tags=[TagId(tag) for tag in json['tags']],
|
2024-09-26 21:08:37 +00:00
|
|
|
creator_user_id=UserId(json['createdByUserId']),
|
2024-10-01 14:14:51 +00:00
|
|
|
creation_date=map_opt(
|
2024-10-01 14:15:03 +00:00
|
|
|
datetime.datetime.fromisoformat,
|
|
|
|
json.get('createdAt'),
|
2024-10-01 14:14:51 +00:00
|
|
|
),
|
|
|
|
start_date=map_opt(datetime.datetime.fromisoformat, json.get('startDate')),
|
|
|
|
due_date=map_opt(datetime.datetime.fromisoformat, json.get('dueDate')),
|
2024-09-28 10:54:34 +00:00
|
|
|
assignments=[CardAssignment.from_json(ass) for ass in json['assignments']],
|
2024-09-28 12:13:51 +00:00
|
|
|
custom_fields=[
|
|
|
|
CustomField.from_json(field) for field in json['customFields']
|
|
|
|
],
|
|
|
|
attachments=json['attachments'], # TODO
|
2024-09-26 21:07:46 +00:00
|
|
|
)
|