Skip to content

Instantly share code, notes, and snippets.

@HarryR
Last active September 6, 2024 05:51

Revisions

  1. HarryR revised this gist Sep 6, 2024. 1 changed file with 23 additions and 11 deletions.
    34 changes: 23 additions & 11 deletions kanbanflow-monthly-report.py
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,9 @@
    #!/usr/bin/env python3
    import os
    import re
    import sys
    import requests
    import calendar
    from datetime import datetime, timedelta, date
    from typing import TypedDict
    from collections import defaultdict
    @@ -51,19 +54,19 @@ def api(*args, **params):
    resp = SESSION.get(url, params=params)
    return resp.json()

    def reporting_range():
    today = datetime.now()
    last_day_of_previous_month = today.replace(day=1) - timedelta(days=1)
    first_day = last_day_of_previous_month.replace(day=1)
    return first_day.strftime('%Y-%m-%d'), last_day_of_previous_month.strftime('%Y-%m-%d')
    def reporting_range(year:int, month:int):
    first_day = date(year, month, 1)
    _, last_day_num = calendar.monthrange(year, month)
    last_date = date(year, month, last_day_num)
    return first_day.strftime('%Y-%m-%d'), last_date.strftime('%Y-%m-%d')

    def main():
    def main(year:int, month:int):
    board: KBF_Board = api('board')
    color2name = {_['value']: _['name'] for _ in board['colors']}

    time_by_color: dict[str,int] = defaultdict(int)
    total_time = 0
    last_month_start, last_month_end = reporting_range()
    last_month_start, last_month_end = reporting_range(year, month)
    tasks_by_day: dict[str,list[KBF_Task]] = defaultdict(list[KBF_Task])
    daily_times: dict[str, int] = defaultdict(int)

    @@ -94,13 +97,22 @@ def main():
    print(f'### {day} ({d.strftime("%A")}, {daily_time_pct}%)')
    for t in tasks:
    time_pct = t['totalSecondsSpent'] / total_time
    print(f' * {color2name[t["color"]]}:', t['name'], f'({round(time_pct * 100, 1)}%)')
    daily_task_pct = round((t['totalSecondsSpent'] / daily_times[day]) * 100,1)
    print(f' * {color2name[t["color"]]}:', t['name'], f'({round(time_pct * 100, 1)}%m/{daily_task_pct}%d)')
    for st in t['subtasks']:
    print(' *', '[x]' if st['finished'] else '[ ]', st['name'])
    print()

    print()


    if __name__ == "__main__":
    main()
    month = datetime.now().month
    year = datetime.now().year
    if len(sys.argv) > 1:
    m = re.match(r'^((?P<month>[0-9]{1,2})|(?P<year>[0-9]{4})-(?P<month2>[0-9]{1,2}))$', sys.argv[1])
    x = m.groupdict()
    month = int(x['month'] or x['month2'])
    year = int(x.get('year',None) or year)
    if month < 1 or month > 12:
    print("Error: invalid month")
    sys.exit(1)
    main(year, month)
  2. HarryR revised this gist Sep 6, 2024. No changes.
  3. HarryR created this gist Sep 6, 2024.
    106 changes: 106 additions & 0 deletions kanbanflow-monthly-report.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,106 @@
    #!/usr/bin/env python3
    import os
    import requests
    from datetime import datetime, timedelta, date
    from typing import TypedDict
    from collections import defaultdict

    API_SECRET = os.getenv('KANBANFLOW_SECRET')

    SESSION = requests.Session()
    SESSION.auth = ('apiToken', API_SECRET)

    class KBF_Column(TypedDict):
    uniqueId: str
    name: str

    class KBF_Color(TypedDict):
    name: str
    value: str

    class KBF_Board(TypedDict):
    _id: str
    name: str
    columns: list[KBF_Column]
    colors: list[KBF_Color]

    class KBF_Subtask(TypedDict):
    name: str
    finished: str

    class KBF_Task(TypedDict):
    _id: str
    name: str
    description: str
    color: str
    columnId: str
    totalSecondsSpent: int
    totalSecondsEstimate: int
    responsibleUserId: str
    groupingDate: str
    subtasks: list[KBF_Subtask]

    class KBF_Tasks(TypedDict):
    columnId: str
    columnName: str
    tasksLimited: bool
    tasks: list[KBF_Task]

    def api(*args, **params):
    url = f'https://kanbanflow.com/api/v1/{"/".join(args)}'
    resp = SESSION.get(url, params=params)
    return resp.json()

    def reporting_range():
    today = datetime.now()
    last_day_of_previous_month = today.replace(day=1) - timedelta(days=1)
    first_day = last_day_of_previous_month.replace(day=1)
    return first_day.strftime('%Y-%m-%d'), last_day_of_previous_month.strftime('%Y-%m-%d')

    def main():
    board: KBF_Board = api('board')
    color2name = {_['value']: _['name'] for _ in board['colors']}

    time_by_color: dict[str,int] = defaultdict(int)
    total_time = 0
    last_month_start, last_month_end = reporting_range()
    tasks_by_day: dict[str,list[KBF_Task]] = defaultdict(list[KBF_Task])
    daily_times: dict[str, int] = defaultdict(int)

    all_tasks: list[KBF_Tasks] = api('tasks', limit=100, startGroupingDate=last_month_start, order='asc', columnName='Done')
    for tasks in all_tasks:
    for t in tasks['tasks']:
    if not t['groupingDate'].startswith(last_month_start[:8]):
    continue
    time_by_color[t['color']] += t['totalSecondsSpent']
    total_time += t['totalSecondsSpent']
    daily_times[t['groupingDate']] += t['totalSecondsSpent']
    tasks_by_day[t['groupingDate']].append(t)
    t['subtasks'] = api('tasks', t['_id'], 'subtasks')

    print('# Task report for', last_month_start, 'to', last_month_end)
    print()
    print('## Time Breakdown')
    for c, totalTimeForColor in time_by_color.items():
    time_pct = totalTimeForColor / total_time
    print(' *', color2name[c], f'({round(time_pct * 100, 1)}%)')

    print()
    print('## Daily Breakdown')
    print()
    for day, tasks in tasks_by_day.items():
    d = date.fromisoformat(day)
    daily_time_pct = round((daily_times[day] / total_time) * 100,1)
    print(f'### {day} ({d.strftime("%A")}, {daily_time_pct}%)')
    for t in tasks:
    time_pct = t['totalSecondsSpent'] / total_time
    print(f' * {color2name[t["color"]]}:', t['name'], f'({round(time_pct * 100, 1)}%)')
    for st in t['subtasks']:
    print(' *', '[x]' if st['finished'] else '[ ]', st['name'])
    print()

    print()


    if __name__ == "__main__":
    main()