1
0
favro-sync/favro_sync/favro_data_model.py

328 lines
8.8 KiB
Python
Raw Normal View History

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
@dataclasses.dataclass(frozen=True)
class CardAssignment:
user: UserId
completed: bool
@staticmethod
def from_json(json: dict[str, Any]) -> 'CardAssignment':
return CardAssignment(
UserId(json['userId']),
json['completed'],
)
@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'),
)
@dataclasses.dataclass(frozen=True)
class TagId:
raw_id: str
@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(
TagId(json['tagId']),
OrganizationId(json['organizationId']),
json['name'],
json.get('color'),
)
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(
2024-10-10 12:02:02 +00:00
custom_field_item_id=CustomFieldItemId(json['customFieldItemId']),
name=json['name'],
2024-10-02 11:34:49 +00:00
)
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-10 12:02:02 +00:00
def get_field_item(
self, field_item_id: CustomFieldItemId,
) -> CustomFieldItem | None:
2024-10-02 11:34:49 +00:00
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-10 12:02:02 +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-10 12:02:02 +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 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-10 12:02:02 +00:00
custom_field_id=CustomFieldId(json['customFieldId']),
value=typed_value,
color=json.get('color'),
2024-09-28 12:10:13 +00:00
)
@dataclasses.dataclass(frozen=True)
class CardDependency:
card_id: CardId
2024-10-01 14:40:44 +00:00
card_common_id: CardCommonId
is_before: bool
reverse_card_id: CardId
@staticmethod
def from_json(json: dict[str, Any]) -> 'CardDependency':
return CardDependency(
CardId(json['cardId']),
2024-10-01 14:40:44 +00:00
CardCommonId(json['cardCommonId']),
json['isBefore'],
CardId(json['reverseCardId']),
)
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-10-10 12:02:02 +00:00
2024-10-02 14:23:15 +00:00
@dataclasses.dataclass(frozen=True)
class Collection:
collection_id: CollectionId
organization_id: OrganizationId
name: str
2024-10-10 12:02:02 +00:00
shared_to_users: list[dict[str, str]] # TODO
2024-10-02 14:23:15 +00:00
public_sharing: str
background: str
is_archived: bool
widget_common_id: WidgetCommonId
@staticmethod
def from_json(json: dict[str, Any]) -> 'Collection':
return Collection(
2024-10-10 12:02:02 +00:00
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 14:23:15 +00:00
)
2024-10-02 08:32:14 +00:00
2024-10-10 12:02:02 +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
dependencies: list[CardDependency]
assignments: list[CardAssignment]
2024-09-28 12:10:13 +00:00
custom_fields: list[CustomField]
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
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'],
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']
],
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')),
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
)