|
from datetime import datetime, timedelta |
|
|
|
import markdown |
|
import requests |
|
|
|
|
|
class WorkActivityReport: |
|
def __init__(self, username, token, org_name=None): |
|
""" |
|
Initialize with GitHub username, token and optional organization name |
|
""" |
|
self.username = username |
|
self.headers = { |
|
'Authorization': f'token {token}', |
|
'Accept': 'application/vnd.github.v3+json' |
|
} |
|
self.base_url = 'https://api.github.com' |
|
self.org_name = org_name |
|
|
|
def get_user_events(self, days_back=7): |
|
""" |
|
Fetch user's events with detailed information |
|
""" |
|
events_url = f'{self.base_url}/users/{self.username}/events' |
|
events = [] |
|
page = 1 |
|
|
|
cutoff_date = datetime.now() - timedelta(days=days_back) |
|
|
|
while True: |
|
response = requests.get( |
|
events_url, |
|
headers=self.headers, |
|
params={'page': page, 'per_page': 100} |
|
) |
|
|
|
if response.status_code != 200: |
|
break |
|
|
|
page_events = response.json() |
|
if not page_events: |
|
break |
|
|
|
filtered_events = [ |
|
event for event in page_events |
|
if datetime.strptime(event['created_at'], '%Y-%m-%dT%H:%M:%SZ') > cutoff_date |
|
] |
|
|
|
events.extend(filtered_events) |
|
|
|
if not filtered_events and page_events: |
|
break |
|
|
|
page += 1 |
|
|
|
return events |
|
|
|
def format_comment_body(self, body): |
|
"""Format comment body with proper line breaks and indentation""" |
|
if not body: |
|
return "" |
|
|
|
lines = [] |
|
for line in body.splitlines(): |
|
# Check if line starts with a date pattern [YYYY-MM-DD] |
|
if line.strip().startswith('[20'): |
|
# Add extra newline before date-prefixed lines |
|
lines.append('') |
|
lines.append(line) |
|
else: |
|
lines.append(line) |
|
|
|
# Remove empty lines from the beginning |
|
while lines and not lines[0].strip(): |
|
lines.pop(0) |
|
|
|
return '\n '.join(lines) |
|
|
|
def get_repos_summary(self): |
|
""" |
|
Get summary of user's repositories |
|
""" |
|
repos_url = f'{self.base_url}/users/{self.username}/repos' |
|
repos = [] |
|
page = 1 |
|
|
|
while True: |
|
response = requests.get( |
|
repos_url, |
|
headers=self.headers, |
|
params={'page': page, 'per_page': 100} |
|
) |
|
|
|
if response.status_code != 200: |
|
break |
|
|
|
page_repos = response.json() |
|
if not page_repos: |
|
break |
|
|
|
repos.extend(page_repos) |
|
page += 1 |
|
|
|
return repos |
|
|
|
def generate_detailed_report(self, days_back=7): |
|
events = self.get_user_events(days_back) |
|
|
|
report = { |
|
'period': { |
|
'start': (datetime.now() - timedelta(days=days_back)).strftime('%Y-%m-%d'), |
|
'end': datetime.now().strftime('%Y-%m-%d') |
|
}, |
|
'summary': { |
|
'commits': 0, |
|
'prs_created': 0, |
|
'prs_reviewed': 0, |
|
'issues_created': 0, |
|
'issues_commented': 0, |
|
'pr_comments': 0, |
|
'code_reviews': 0, |
|
'discussion_comments': 0 |
|
}, |
|
'detailed_activity': { |
|
'commits': [], |
|
'pull_requests': [], |
|
'issues': [], |
|
'reviews': [], |
|
'issue_comments': [], |
|
'pr_comments': [], |
|
'discussion_comments': [] |
|
}, |
|
'projects_worked_on': set() |
|
} |
|
|
|
for event in events: |
|
repo_name = event['repo']['name'] |
|
if self.org_name and not repo_name.startswith(f"{self.org_name}/"): |
|
continue |
|
|
|
report['projects_worked_on'].add(repo_name) |
|
etype = event['type'] |
|
payload = event['payload'] |
|
|
|
if etype == 'PushEvent': |
|
commits = payload.get('commits', []) |
|
report['summary']['commits'] += len(commits) |
|
for commit in commits: |
|
sha = commit.get('sha', '') |
|
report['detailed_activity']['commits'].append({ |
|
'repo': repo_name, |
|
'message': commit.get('message', ''), |
|
'date': event.get('created_at', ''), |
|
'sha': sha, |
|
'url': f"https://github.com/{repo_name}/commit/{sha}" if sha else '' |
|
}) |
|
|
|
elif etype == 'PullRequestEvent': |
|
if payload.get('action') == 'opened': |
|
pr = payload.get('pull_request', {}) |
|
number = str(pr.get('number', '')) |
|
report['summary']['prs_created'] += 1 |
|
report['detailed_activity']['pull_requests'].append({ |
|
'repo': repo_name, |
|
'title': pr.get('title', ''), |
|
'state': pr.get('state', ''), |
|
'date': pr.get('created_at', ''), |
|
'number': number, |
|
'url': f"https://github.com/{repo_name}/pull/{number}" if number else '' |
|
}) |
|
|
|
elif etype == 'IssuesEvent': |
|
if payload.get('action') == 'opened': |
|
issue = payload.get('issue', {}) |
|
issue_url = issue.get('html_url', '') |
|
report['summary']['issues_created'] += 1 |
|
report['detailed_activity']['issues'].append({ |
|
'repo': repo_name, |
|
'title': issue.get('title', ''), |
|
'state': issue.get('state', ''), |
|
'date': issue.get('created_at', ''), |
|
'url': issue_url |
|
}) |
|
|
|
elif etype == 'PullRequestReviewEvent': |
|
pr = payload.get('pull_request', {}) |
|
review = payload.get('review', {}) |
|
pr_number = str(pr.get('number', '')) |
|
report['summary']['code_reviews'] += 1 |
|
report['detailed_activity']['reviews'].append({ |
|
'repo': repo_name, |
|
'pr_title': pr.get('title', ''), |
|
'state': review.get('state', ''), |
|
'date': review.get('submitted_at', ''), |
|
'number': pr_number, |
|
'url': f"https://github.com/{repo_name}/pull/{pr_number}" if pr_number else '' |
|
}) |
|
|
|
elif etype == 'IssueCommentEvent': |
|
issue = payload.get('issue', {}) |
|
comment = payload.get('comment', {}) |
|
issue_number = str(issue.get('number', '')) |
|
issue_url = f"https://github.com/{repo_name}/issues/{issue_number}" if issue_number else '' |
|
report['summary']['issues_commented'] += 1 |
|
report['detailed_activity']['issue_comments'].append({ |
|
'repo': repo_name, |
|
'issue_title': issue.get('title', ''), |
|
'comment_body': comment.get('body', ''), |
|
'date': comment.get('created_at', ''), |
|
'number': issue_number, |
|
'url': issue_url |
|
}) |
|
|
|
elif etype == 'PullRequestReviewCommentEvent': |
|
pr = payload.get('pull_request', {}) |
|
comment = payload.get('comment', {}) |
|
comment_url = comment.get('html_url', '') |
|
report['summary']['pr_comments'] += 1 |
|
report['detailed_activity']['pr_comments'].append({ |
|
'repo': repo_name, |
|
'pr_title': pr.get('title', ''), |
|
'comment_body': comment.get('body', ''), |
|
'date': comment.get('created_at', ''), |
|
'url': comment_url |
|
}) |
|
|
|
elif etype == 'DiscussionCommentEvent': |
|
discussion = payload.get('discussion', {}) |
|
comment = payload.get('comment', {}) |
|
comment_url = comment.get('html_url', '') |
|
report['summary']['discussion_comments'] += 1 |
|
report['detailed_activity']['discussion_comments'].append({ |
|
'repo': repo_name, |
|
'discussion_title': discussion.get('title', ''), |
|
'comment_body': comment.get('body', ''), |
|
'date': comment.get('created_at', ''), |
|
'url': comment_url |
|
}) |
|
|
|
return report |
|
|
|
def construct_github_url(self, repo, type_='repo', number=None): |
|
"""Construct GitHub URLs for different types of content""" |
|
base_url = f"https://github.com/{repo}" |
|
if type_ == 'repo': |
|
return base_url |
|
elif type_ == 'issue': |
|
return f"{base_url}/issues/{number}" |
|
elif type_ == 'pr': |
|
return f"{base_url}/pull/{number}" |
|
return base_url |
|
|
|
def generate_markdown_report(self, days_back=7): |
|
""" |
|
Generate a formatted markdown report with clickable URLs. |
|
""" |
|
report = self.generate_detailed_report(days_back) |
|
|
|
def format_activities(activities, format_str): |
|
return '\n'.join( |
|
format_str(activity) |
|
for activity in sorted(activities, key=lambda x: x['date'], reverse=True) |
|
) |
|
|
|
# Use a helper to indent multi-line strings: |
|
import textwrap |
|
|
|
def indent_comment(comment_body): |
|
return textwrap.indent(self.format_comment_body(comment_body), " ") |
|
|
|
md_content = [ |
|
f"# Work Activity Report", |
|
f"## Period: {report['period']['start']} to {report['period']['end']}", |
|
"\n### Summary", |
|
f"- Total Commits: {report['summary']['commits']}", |
|
f"- Pull Requests Created: {report['summary']['prs_created']}", |
|
f"- Code Reviews Performed: {report['summary']['code_reviews']}", |
|
f"- Issues Created: {report['summary']['issues_created']}", |
|
f"- Issue Comments: {report['summary']['issues_commented']}", |
|
f"- PR Comments: {report['summary']['pr_comments']}", |
|
f"- Discussion Comments: {report['summary']['discussion_comments']}", |
|
"", |
|
"### Projects Worked On", |
|
'\n'.join('- ' + project for project in sorted(report['projects_worked_on'])), |
|
"", |
|
"### Detailed Activity", |
|
"", |
|
"#### Recent Commits", |
|
format_activities( |
|
report['detailed_activity']['commits'], |
|
lambda |
|
c: f"- [{c['date'][:10]}] [{c['message'].split(chr(10))[0]}]({c['url']}) ({c['repo']})" |
|
), |
|
"", |
|
"#### Pull Requests Created", |
|
format_activities( |
|
report['detailed_activity']['pull_requests'], |
|
lambda |
|
pr: f"- [{pr['date'][:10]}] [{pr['title']}]({pr['url']}) ({pr['repo']}) - {pr['state']}" |
|
), |
|
"", |
|
"#### Code Reviews", |
|
format_activities( |
|
report['detailed_activity']['reviews'], |
|
lambda |
|
r: f"- [{r['date'][:10]}] Reviewed: [{r['pr_title']}]({r['url']}) ({r['repo']}) - {r['state']}" |
|
), |
|
"", |
|
"#### Issue Comments", |
|
'\n\n'.join( |
|
f"- [{comment['date'][:10]}] On issue: " |
|
f"[{comment['issue_title']}]({comment['url']}) ({comment['repo']})\n\n " |
|
f"{self.format_comment_body(comment['comment_body'])}" |
|
for comment in sorted( |
|
report['detailed_activity']['issue_comments'], |
|
key=lambda x: x['date'], |
|
reverse=True |
|
) |
|
), |
|
"", |
|
"#### Pull Request Comments", |
|
"\n\n".join( |
|
f"- [{c['date'][:10]}] On PR: " |
|
f"[{c['pr_title']}]({c['url']}) ({c['repo']})\n\n" |
|
f"{indent_comment(c['comment_body'])}" |
|
for c in sorted( |
|
report['detailed_activity']['pr_comments'], |
|
key=lambda x: x['date'], |
|
reverse=True |
|
) |
|
), |
|
"" |
|
] |
|
|
|
return '\n'.join(md_content) |
|
|
|
def save_report(self, days_back=7, output_format='both'): |
|
""" |
|
Save the report in specified format(s) |
|
""" |
|
md_content = self.generate_markdown_report(days_back) |
|
|
|
# Save markdown version |
|
with open('work_activity_report.md', 'w') as f: |
|
f.write(md_content) |
|
|
|
if output_format in ['html', 'both']: |
|
html_doc = markdown_content_to_html_content(md_content) |
|
with open('work_activity_report.html', 'w') as f: |
|
f.write(html_doc) |
|
|
|
|
|
def markdown_content_to_html_content(markdown_content: str) -> str: |
|
""" |
|
Convert markdown content to HTML content |
|
""" |
|
# Convert to HTML using markdown with extensions |
|
html_content = markdown.markdown(markdown_content, extensions=['tables', 'fenced_code']) |
|
html_doc = f""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Work Activity Report</title> |
|
<style> |
|
body {{ |
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; |
|
max-width: 1200px; |
|
margin: 40px auto; |
|
padding: 0 20px; |
|
line-height: 1.5; |
|
}} |
|
h1, h2, h3, h4 {{ color: #24292e; }} |
|
a {{ |
|
color: #0366d6; |
|
text-decoration: none; |
|
}} |
|
a:hover {{ |
|
text-decoration: underline; |
|
}} |
|
li {{ |
|
margin: 8px 0; |
|
color: #24292e; |
|
}} |
|
code {{ |
|
background-color: #f6f8fa; |
|
padding: 2px 5px; |
|
border-radius: 3px; |
|
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; |
|
}} |
|
pre {{ |
|
background-color: #f6f8fa; |
|
padding: 16px; |
|
border-radius: 6px; |
|
overflow: auto; |
|
}} |
|
</style> |
|
</head> |
|
<body> |
|
{html_content} |
|
</body> |
|
</html> |
|
""" |
|
return html_doc |
|
|
|
|
|
if __name__ == "__main__": |
|
# Replace these with your actual values |
|
username = "" |
|
token = "" |
|
org_name = "" # Optional, set to None if not needed |
|
|
|
reporter = WorkActivityReport(username, token, org_name) |
|
reporter.save_report(days_back=7, output_format='both') |