Skip to content

Instantly share code, notes, and snippets.

@blakeNaccarato
Last active January 13, 2024 01:08
Show Gist options
  • Save blakeNaccarato/6ab1e90d472e77d8b2557b8e62543abd to your computer and use it in GitHub Desktop.
Save blakeNaccarato/6ab1e90d472e77d8b2557b8e62543abd to your computer and use it in GitHub Desktop.
Convert a single-card Trello JSON export to Markdown.

make_markdown.py

Convert a single-card Trello JSON export to Markdown.

Usage

Download this Gist, install the $PYTHON_VERSION seen in setup.ps1, and run setup.ps1 if you have cross-platform PowerShell installed, or equivalently in a terminal of your choice:

  • If, for example, $PYTHON_VERSION is 3.11, run py -3.11 -m venv .venv in Windows or on UNIX-like/MacOS systems with the Python Launcher installed.
  • Activate the virtual environment with .venv/scripts/activate on Windows or .venv/bin/activate on UNIX-like/MacOS systems.
  • Install requirements with pip install -r requirements.txt.
  • Run Python scripts/modules in this Gist like python <module>.py or python -m <module>.

Refer to additional details in the template from which this Gist is derived, including ground-up setup instructions, if needed.

Details

Provide your own card.json, a JSON export of a single Trello card that has checklists and comments. Script make_model.py generates a model of that card, and make_markdown.py uses that model to generate card.md.

__*
.*
pyproject.toml
!.gitignore
*card*
MIT License
Copyright (c) 2023 Blake Naccarato
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""Convert a single-card Trello JSON export to Markdown."""
import re
from json import loads
from pathlib import Path
from typer import Typer
from model import Model
app = Typer()
CARD = Path("card.json")
OUTPUT = Path("card.md")
any_heading = re.compile(r"^#+\s(.*$)", re.MULTILINE)
@app.command()
def main(card_path: Path = CARD, out: Path = OUTPUT, start: int = 2):
"""Generate a Markdown report from a card."""
card = Model(**loads(card_path.read_text(encoding="utf-8")))
level = start
output: list[str] = [f"{head(level)} {card.name}\n"]
for checklist in card.checklists:
level += 1
output.append(f"{head(level)} {checklist.name}\n")
output.extend(f"- [x] {item.name}" for item in checklist.check_items)
level -= 1
for comment in [a for a in reversed(card.actions) if a.type == "commentCard"]:
if not (text := comment.data.text):
continue
level += 1
heading, text = text.split("\n", maxsplit=1)
output.append(f"{head(level)} {heading.strip('# :.')}\n")
level += 1
text = any_heading.sub(rf"{head(level)} \g<1>", text)
output.extend([text, ""])
level -= 2
out.write_text("\n".join(output), encoding="utf-8")
def head(level: int) -> str:
return "#" * level
if __name__ == "__main__":
app()
"""Generate a model of a single-card Trello JSON export."""
from pathlib import Path
from datamodel_code_generator import InputFileType, PythonVersion, generate
from typer import Typer
app = Typer()
CARD = Path("card.json")
MODEL = Path("model.py")
@app.command()
def main(card: Path = CARD, model: Path = MODEL):
"""Generate data model from JSON file."""
generate(
card,
output=model,
input_file_type=InputFileType.Json,
snake_case_field=True,
target_python_version=PythonVersion.PY_311,
)
if __name__ == "__main__":
app()
# generated by datamodel-codegen:
# filename: card.json
# timestamp: 2023-12-23T02:13:28+00:00
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
class Trello(BaseModel):
board: int
card: int
class AttachmentsByType(BaseModel):
trello: Trello
class Badges(BaseModel):
attachments_by_type: AttachmentsByType = Field(..., alias="attachmentsByType")
location: bool
votes: int
viewing_member_voted: bool = Field(..., alias="viewingMemberVoted")
subscribed: bool
fogbugz: str
check_items: int = Field(..., alias="checkItems")
check_items_checked: int = Field(..., alias="checkItemsChecked")
check_items_earliest_due: None = Field(..., alias="checkItemsEarliestDue")
comments: int
attachments: int
description: bool
due: None
due_complete: bool = Field(..., alias="dueComplete")
start: str
class CheckItemState(BaseModel):
id_check_item: str = Field(..., alias="idCheckItem")
state: str
class DescData(BaseModel):
emoji: dict[str, Any]
class Label(BaseModel):
id: str
id_board: str = Field(..., alias="idBoard")
name: str
color: str
uses: int
class PerCard(BaseModel):
status: str
disable_at: int = Field(..., alias="disableAt")
warn_at: int = Field(..., alias="warnAt")
class Attachments(BaseModel):
per_card: PerCard = Field(..., alias="perCard")
class Checklists(BaseModel):
per_card: PerCard = Field(..., alias="perCard")
class Stickers(BaseModel):
per_card: PerCard = Field(..., alias="perCard")
class Limits(BaseModel):
attachments: Attachments
checklists: Checklists
stickers: Stickers
class Cover(BaseModel):
id_attachment: None = Field(..., alias="idAttachment")
color: None
id_uploaded_background: None = Field(..., alias="idUploadedBackground")
size: str
brightness: str
id_plugin: None = Field(..., alias="idPlugin")
class PerChecklist(BaseModel):
status: str
disable_at: int = Field(..., alias="disableAt")
warn_at: int = Field(..., alias="warnAt")
class CheckItems(BaseModel):
per_checklist: PerChecklist = Field(..., alias="perChecklist")
class Limits1(BaseModel):
check_items: CheckItems = Field(..., alias="checkItems")
class NameData(BaseModel):
emoji: dict[str, Any]
class CheckItem(BaseModel):
id: str
name: str
name_data: NameData = Field(..., alias="nameData")
pos: float
state: str
due: None
due_reminder: None = Field(..., alias="dueReminder")
id_member: None = Field(..., alias="idMember")
id_checklist: str = Field(..., alias="idChecklist")
class Checklist(BaseModel):
id: str
name: str
id_board: str = Field(..., alias="idBoard")
id_card: str = Field(..., alias="idCard")
pos: int
limits: Limits1
check_items: list[CheckItem] = Field(..., alias="checkItems")
creation_method: None = Field(..., alias="creationMethod")
class NonPublic(BaseModel):
full_name: str = Field(..., alias="fullName")
initials: str
avatar_hash: None = Field(..., alias="avatarHash")
class Member(BaseModel):
id: str
aa_id: str = Field(..., alias="aaId")
activity_blocked: bool = Field(..., alias="activityBlocked")
avatar_hash: str = Field(..., alias="avatarHash")
avatar_url: str = Field(..., alias="avatarUrl")
bio: str
bio_data: None = Field(..., alias="bioData")
confirmed: bool
full_name: str = Field(..., alias="fullName")
id_enterprise: None = Field(..., alias="idEnterprise")
id_enterprises_deactivated: list[Any] = Field(..., alias="idEnterprisesDeactivated")
id_member_referrer: None = Field(..., alias="idMemberReferrer")
id_prem_orgs_admin: list[Any] = Field(..., alias="idPremOrgsAdmin")
initials: str
member_type: str = Field(..., alias="memberType")
non_public: NonPublic = Field(..., alias="nonPublic")
non_public_available: bool = Field(..., alias="nonPublicAvailable")
products: list[Any]
url: str
username: str
status: str
class TextData(BaseModel):
emoji: dict[str, Any]
class Card(BaseModel):
id: str
name: str
id_short: int = Field(..., alias="idShort")
short_link: str = Field(..., alias="shortLink")
due_complete: bool | None = Field(None, alias="dueComplete")
closed: bool | None = None
id_labels: list[str] | None = Field(None, alias="idLabels")
id_list: str | None = Field(None, alias="idList")
pos: float | None = None
start: str | None = None
desc: str | None = None
due_reminder: int | None = Field(None, alias="dueReminder")
class Board(BaseModel):
id: str
name: str
short_link: str = Field(..., alias="shortLink")
class ListModel(BaseModel):
id: str
name: str
class Old(BaseModel):
due_complete: bool | None = Field(None, alias="dueComplete")
closed: bool | None = None
id_labels: list[str] | None = Field(None, alias="idLabels")
id_list: str | None = Field(None, alias="idList")
pos: int | None = None
start: None = None
name: str | None = None
desc: str | None = None
due_reminder: None = Field(None, alias="dueReminder")
class ListBefore(BaseModel):
id: str
name: str
class ListAfter(BaseModel):
id: str
name: str
class Attachment(BaseModel):
name: str
id: str
class Checklist1(BaseModel):
id: str
name: str
class CheckItem1(BaseModel):
id: str
name: str
state: str
text_data: TextData = Field(..., alias="textData")
class Member1(BaseModel):
id: str
name: str
class Data(BaseModel):
text: str | None = None
text_data: TextData | None = Field(None, alias="textData")
card: Card
board: Board
list: ListModel | None = None
old: Old | None = None
date_last_edited: str | None = Field(None, alias="dateLastEdited")
list_before: ListBefore | None = Field(None, alias="listBefore")
list_after: ListAfter | None = Field(None, alias="listAfter")
attachment: Attachment | None = None
checklist: Checklist1 | None = None
check_item: CheckItem1 | None = Field(None, alias="checkItem")
id_member: str | None = Field(None, alias="idMember")
member: Member1 | None = None
class Icon(BaseModel):
url: str
class AppCreator(BaseModel):
id: str
name: str | None = None
icon: Icon | None = None
class PerAction(BaseModel):
status: str
disable_at: int = Field(..., alias="disableAt")
warn_at: int = Field(..., alias="warnAt")
class UniquePerAction(BaseModel):
status: str
disable_at: int = Field(..., alias="disableAt")
warn_at: int = Field(..., alias="warnAt")
class Reactions(BaseModel):
per_action: PerAction = Field(..., alias="perAction")
unique_per_action: UniquePerAction = Field(..., alias="uniquePerAction")
class Limits2(BaseModel):
reactions: Reactions
class MemberCreator(BaseModel):
id: str
activity_blocked: bool = Field(..., alias="activityBlocked")
avatar_hash: str = Field(..., alias="avatarHash")
avatar_url: str = Field(..., alias="avatarUrl")
full_name: str = Field(..., alias="fullName")
id_member_referrer: None = Field(..., alias="idMemberReferrer")
initials: str
non_public: NonPublic = Field(..., alias="nonPublic")
non_public_available: bool = Field(..., alias="nonPublicAvailable")
username: str
class Member2(BaseModel):
id: str
activity_blocked: bool = Field(..., alias="activityBlocked")
avatar_hash: str = Field(..., alias="avatarHash")
avatar_url: str = Field(..., alias="avatarUrl")
full_name: str = Field(..., alias="fullName")
id_member_referrer: None = Field(..., alias="idMemberReferrer")
initials: str
non_public: NonPublic = Field(..., alias="nonPublic")
non_public_available: bool = Field(..., alias="nonPublicAvailable")
username: str
class Action(BaseModel):
id: str
id_member_creator: str = Field(..., alias="idMemberCreator")
data: Data
app_creator: AppCreator | None = Field(..., alias="appCreator")
type: str
date: str
limits: Limits2 | None
member_creator: MemberCreator = Field(..., alias="memberCreator")
member: Member2 | None = None
class Model(BaseModel):
id: str
address: None
badges: Badges
check_item_states: list[CheckItemState] = Field(..., alias="checkItemStates")
closed: bool
coordinates: None
creation_method: None = Field(..., alias="creationMethod")
due_complete: bool = Field(..., alias="dueComplete")
date_last_activity: str = Field(..., alias="dateLastActivity")
desc: str
desc_data: DescData = Field(..., alias="descData")
due: None
due_reminder: int = Field(..., alias="dueReminder")
email: str
id_board: str = Field(..., alias="idBoard")
id_checklists: list[str] = Field(..., alias="idChecklists")
id_labels: list[str] = Field(..., alias="idLabels")
id_list: str = Field(..., alias="idList")
id_members: list[str] = Field(..., alias="idMembers")
id_members_voted: list[Any] = Field(..., alias="idMembersVoted")
id_organization: str = Field(..., alias="idOrganization")
id_short: int = Field(..., alias="idShort")
id_attachment_cover: str = Field(..., alias="idAttachmentCover")
labels: list[Label]
limits: Limits
location_name: None = Field(..., alias="locationName")
manual_cover_attachment: bool = Field(..., alias="manualCoverAttachment")
name: str
node_id: str = Field(..., alias="nodeId")
pos: int
short_link: str = Field(..., alias="shortLink")
short_url: str = Field(..., alias="shortUrl")
static_map_url: None = Field(..., alias="staticMapUrl")
start: str
subscribed: bool
url: str
cover: Cover
is_template: bool = Field(..., alias="isTemplate")
card_role: None = Field(..., alias="cardRole")
checklists: list[Checklist]
custom_field_items: list[Any] = Field(..., alias="customFieldItems")
members: list[Member]
plugin_data: list[Any] = Field(..., alias="pluginData")
actions: list[Action]
# Dependencies for this Gist, installed with `setup.ps1`
datamodel-code-generator==0.25.2
pydantic==2.5.3
typer==0.9.0
<#
.SYNOPSIS
Copy the template and install requirements in a Python virtual environment.
#>
$PYTHON_VERSION = '3.11'
$tmp = New-TemporaryFile
$tmpdir = "$($tmp.Directory)/$($tmp.BaseName)"
git clone --depth 1 'https://github.com/blakeNaccarato/gist-template.git' $tmpdir
$template = "$tmpdir/template"
Get-ChildItem -File "$template/*" | Move-Item -Force
if (! (Test-Path '.vscode')) {New-Item -ItemType Directory '.vscode'}
Get-ChildItem -File "$template/.vscode/*" | Move-Item -Destination '.vscode' -Force
py "-$PYTHON_VERSION" -m 'venv' '.venv'
$activate_win = '.venv/scripts/activate'
if (Test-Path $activate_win) { . $activate_win } else { . '.venv/bin/activate' }
pip install --requirement 'requirements.txt'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment