diff --git a/favro_sync/__init__.py b/favro_sync/__init__.py index 5716e85..b7d8acb 100644 --- a/favro_sync/__init__.py +++ b/favro_sync/__init__.py @@ -17,13 +17,14 @@ Features: - Description - Tags - Assignees + - Dependencies - Change card features: - Title - Description - [Obsidian](https://obsidian.md/) compatibility: - - Mount within your vault. + - Mountable within your vault. - Link to cards by either card number or card title. - - Tags are directly integrated. + - Tags and dependencies are integrated. Limitations: @@ -58,4 +59,5 @@ Following features are work in progress: - [ ] Frontmatter: Dependencies. As vault links in Obsidian mode. - [ ] Allow users to toggle Obsidian mode, instead of being default. - [ ] Get the correct last-modified date. +- [ ] Improve cache behaviour. User and tags can have much longer cache times. """ diff --git a/favro_sync/favro_client.py b/favro_sync/favro_client.py index ed85e41..e4a3bc1 100644 --- a/favro_sync/favro_client.py +++ b/favro_sync/favro_client.py @@ -136,6 +136,10 @@ class FavroClient: return card return next(self.get_cards(seq_id=seq_id)) + def get_card_by_card_id(self, card_id: CardId) -> Card: + response = self.session.get(URL_UPDATE_CARD.format(card_id=card_id.raw_id)) + return Card.from_json(response.json()) + def get_user(self, user_id: UserId) -> UserInfo: response = self.session.get(URL_GET_USER.format(user_id=user_id.raw_id)) return UserInfo.from_json(response.json()) @@ -153,21 +157,22 @@ class FavroClient: def update_card_contents_locally( self, card_id: CardId, card_contents: CardContents, - ) -> Card: + ) -> Card | None: if card := self.cache.remove(card_id): card = dataclasses.replace( card, detailed_description=card_contents.description, name=card_contents.name, tags=[], # TODO? - assignments=[], + assignments=[], # TODO? + dependencies=[], # TODO ) self.cache.add_card(card) return card def update_card_contents( self, card_id: CardId, card_contents: CardContents, - ) -> Card: + ) -> Card | None: """Returns updated Card.""" if self.read_only == 'silent': logger.warning( diff --git a/favro_sync/favro_data_model.py b/favro_sync/favro_data_model.py index ab7aa2d..dcf635a 100644 --- a/favro_sync/favro_data_model.py +++ b/favro_sync/favro_data_model.py @@ -68,6 +68,11 @@ class TagId: raw_id: str +@dataclasses.dataclass(frozen=True) +class CustomFieldId: + raw_id: str + + @dataclasses.dataclass(frozen=True) class TagInfo: tag_id: TagId @@ -85,6 +90,19 @@ class TagInfo: ) +@dataclasses.dataclass(frozen=True) +class CustomField: + custom_field_id: CustomFieldId + value: list[str] # TODO + + @staticmethod + def from_json(json: dict[str, Any]) -> 'CustomField': + return CustomField( + CustomFieldId(json['customFieldId']), + json['value'], # TODO + ) + + @dataclasses.dataclass(frozen=True) class CardDependency: card_id: CardId @@ -112,12 +130,13 @@ class Card: name: str dependencies: list[CardDependency] assignments: list[CardAssignment] + custom_fields: list[CustomField] tags: list[TagId] todo_list_user_id: UserId | None todo_list_completed: bool | None creator_user_id: UserId creation_date: datetime.datetime - + attachments: list[dict] detailed_description: str | None @staticmethod @@ -135,9 +154,11 @@ class Card: name=json['name'], todo_list_user_id=todo_list_user_id, todo_list_completed=json.get('todoListCompleted'), - dependencies=json['dependencies'], + dependencies=[CardDependency.from_json(dep) for dep in json['dependencies']], tags=[TagId(tag) for tag in json['tags']], creator_user_id=UserId(json['createdByUserId']), creation_date=datetime.datetime.fromisoformat(json['createdAt']), assignments=[CardAssignment.from_json(ass) for ass in json['assignments']], + custom_fields=[CustomField.from_json(field) for field in json['customFields']], + attachments=json['attachments'], # TODO ) diff --git a/favro_sync/favro_fuse.py b/favro_sync/favro_fuse.py index 5d1eb75..2c0f64e 100644 --- a/favro_sync/favro_fuse.py +++ b/favro_sync/favro_fuse.py @@ -168,11 +168,16 @@ class FavroFuse(fuse.Fuse): self.favro_client.get_user(assignment.user).name for assignment in card.assignments ] + dependencies = [ + CARD_FILENAME_FORMAT.format(seq_id=self.favro_client.get_card_by_card_id(dep.card_id).seq_id.raw_id) + for dep in card.dependencies if dep.is_before + ] card_contents = CardContents( card.name, card.detailed_description, tags, assignments, + dependencies, ) return self.formatter.format_card_contents(card_contents) diff --git a/favro_sync/favro_markdown.py b/favro_sync/favro_markdown.py index 1a51226..4b8f76f 100644 --- a/favro_sync/favro_markdown.py +++ b/favro_sync/favro_markdown.py @@ -12,6 +12,16 @@ class CardContents: description: str | None tags: list[str] assignments: list[str] + card_dependencies: list[str] + + +def format_obsidian_link(text: str) -> str: + return f'"[[{text}]]"' + +def parse_obsidian_link(text: str) -> str: + if m := re.match(r'^\[\[(.*)\]\]$', text): + return m.group(1) + return text class CardFileFormatter: @@ -35,7 +45,13 @@ class CardFileFormatter: frontmatter_data['assignments'] = card.assignments if self.obsidian_mode: frontmatter_data['assignments'] = [ - f'"[[{name}]]"' for name in frontmatter_data['assignments'] + format_obsidian_link(name) for name in frontmatter_data['assignments'] + ] + if card.card_dependencies: + frontmatter_data['dependencies'] = card.card_dependencies + if self.obsidian_mode: + frontmatter_data['dependencies'] = [ + format_obsidian_link(name) for name in frontmatter_data['dependencies'] ] # Frontmatter @@ -78,15 +94,19 @@ class CardFileFormatter: document.children.remove(elem) break + tags: list[str] = fm.metadata.get('tags', []) + assignments: list[str] = fm.metadata.get('assignments', []) - for idx in range(0, len(assignments)): - if m := re.match(r'^\[\[(.*)\]\]$', assignments[idx]): - assignments[idx] = m.group(1) + assignments = [parse_obsidian_link(text) for text in assignments] + + card_dependencies: list[str] = fm.metadata.get('dependencies', []) + card_dependencies=[parse_obsidian_link(text) for text in card_dependencies] description = self.renderer.render_children(document).strip() return CardContents( name, description, - tags=fm.metadata.get('tags', []), + tags=tags, assignments=assignments, + card_dependencies=card_dependencies, ) diff --git a/test/test_client.py b/test/test_client.py index 61cbb54..3c961b5 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -1,5 +1,5 @@ from favro_sync import secrets -from favro_sync.favro_client import FavroClient, OrganizationId +from favro_sync.favro_client import FavroClient, OrganizationId, SeqId # TODO: Skip if no secrets @@ -18,5 +18,12 @@ def test_create_client(): def test_get_card(): client = test_create_client() - card = next(client.get_todo_list_cards()) + card = client.get_card(SeqId(11368)) + print(card) + assert card is not None + assert card.name is not None + assert card.detailed_description is not None + assert len(card.dependencies) == 1 + assert len(card.attachments) == 0 + assert len(card.custom_fields) == 2 diff --git a/test/test_markdown_parsing.py b/test/test_markdown_parsing.py index c425864..a84524b 100644 --- a/test/test_markdown_parsing.py +++ b/test/test_markdown_parsing.py @@ -27,6 +27,8 @@ tags: assignments: - "[[Gunnar Gunnarson]]" - "[[Alice Alicedottor]]" +dependencies: + - "[[PAR-9999]]" --- # Name of Card @@ -47,6 +49,6 @@ def test_parse_and_render_2(): card_contents = FORMATTER.parse_card_contents(EXAMPLE_TEXT_2) print(card_contents) - assert card_contents.name == 'Respond to Carstens comments, and perform any relevant fixing' + assert card_contents.name == 'Name of Card' assert '---' not in card_contents.description assert FORMATTER.format_card_contents(card_contents) == EXAMPLE_TEXT_2