Skip to content

Instantly share code, notes, and snippets.

@HarryR
Last active September 6, 2024 05:51
Show Gist options
  • Save HarryR/ee1e4bad46d38f46a1d09adaa93bc119 to your computer and use it in GitHub Desktop.
Save HarryR/ee1e4bad46d38f46a1d09adaa93bc119 to your computer and use it in GitHub Desktop.
Monthly reporting for Kanbanflow, produces a Markdown summary of all tasks and the percentage time breakdown daily & across categories
#!/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
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(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(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(year, month)
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
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__":
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment