Last active
September 6, 2024 05:51
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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