From 5a97f16a35edaf8fcdb05f3b641fd64df26a94a4 Mon Sep 17 00:00:00 2001 From: Krishna Prajapati Date: Fri, 3 Nov 2023 20:53:43 +0530 Subject: [PATCH] Contribution graph --- preload/contribution_graph.py | 77 +++++++++++ preload/examples/badge.py | 72 ++++++++++- scripts/getdata.py | 177 -------------------------- scripts/gui/get_contribution_graph.py | 100 +++++++++++++++ scripts/gui/main.py | 9 ++ 5 files changed, 254 insertions(+), 181 deletions(-) create mode 100644 preload/contribution_graph.py delete mode 100644 scripts/getdata.py create mode 100644 scripts/gui/get_contribution_graph.py diff --git a/preload/contribution_graph.py b/preload/contribution_graph.py new file mode 100644 index 0000000..8cffa14 --- /dev/null +++ b/preload/contribution_graph.py @@ -0,0 +1,77 @@ +import badger2040 +import badger_os + +CONTRIBUTION_GRAPH_PAGE = 0 #page 0 = latest 6 months; page 1 = previous 6 months + + + +class ContributionPage(object): + # Each contribution page contains title, subtitle and graph data + def __init__(self, title, subtitle, contribution_data): + self.title = title + self.subtitle = subtitle + self.contribution_data = contribution_data + + +#Drawing contribution graph +def draw_contribution_graph(display): + #Clearing the display + display.set_pen(15) + display.clear() + + display.set_pen(0) # Black + display.set_thickness(2) + display.set_font("sans") + + page_data = contribution_pages[CONTRIBUTION_GRAPH_PAGE] + + # Drawing title text + display.text(page_data.title, 8,16,badger2040.WIDTH-16,0.6) + # Drawing subtitle text + display.text(page_data.subtitle, 8,32,badger2040.WIDTH-16,0.5) + + # Drawing graph + + contributions_of_day = page_data.contribution_data + + # 26 weeks (6 months) + for week in range(26): + + # 7 days each week + for day_of_week in range(7): + + # Calculating position of each cell + x = 8 + (week * 11) + y = 50 + (day_of_week * 11) + index = (week*7)+day_of_week + + + # Cell value can be 0-5; + # 0 being no contributions, color with lightest pen (15) + # 5 being highest contributions, color with darkest pen (0) + display.set_pen(15 - ((contributions_of_day[index]+1) * 3)) + display.rectangle(x, y, 9, 9) + + +# Open the contributions data files corresponding to the pages +contribution_files = ["contribution_page_1.txt","contribution_page_2.txt"] +contribution_pages = [] + +# Load file data +for filename in contribution_files: + file = open(filename, "r") + page_title = file.readline() + page_subtitle = file.readline() + contribution_data = [] + + # Loading 182 days (26 weeks / 6 months) + for x in range(182): + # Read each line for each day's value + contribution_of_day_str = file.readline() + # If day's data does not exist, it may be partial week, set the value to -1 + contribution_of_day = int(contribution_of_day_str) if len(contribution_of_day_str)>0 else -1 + # Append the day's data + contribution_data.append(contribution_of_day) + + # Append the page data + contribution_pages.append(ContributionPage(page_title, page_subtitle, contribution_data)) \ No newline at end of file diff --git a/preload/examples/badge.py b/preload/examples/badge.py index 892bb24..ad3ee0a 100644 --- a/preload/examples/badge.py +++ b/preload/examples/badge.py @@ -1,5 +1,11 @@ import badger2040 import jpegdec +import contribution_graph +import badger_os +import json +import machine +import time +import os # Global Constants WIDTH = badger2040.WIDTH @@ -17,6 +23,9 @@ BADGE_BACKGROUND = "/badges/back.jpg" +CURRENT_PAGE = 0 # 0=Badge; 1=contribution graph + + # Will be replaced with badge.txt # "Universe 2023", first_name, lastname_name, company, title, pronouns to the file on separate lines. DEFAULT_TEXT = """Universe 2023 @@ -92,6 +101,39 @@ def draw_badge(): display.update() +def draw_page(): +# match case not available, using if-else + if CURRENT_PAGE == 0: + draw_badge() + elif CURRENT_PAGE == 1 or CURRENT_PAGE == 2: + contribution_graph.draw_contribution_graph(display) + display.update() + + +def exit_to_launcher(): + # # Changing state to set running application as launcher + + badger_os.state_clear_running() + # state = {"running": "launcher", "page": 0} + # # # Saving the state file + # try: + # with open("/state/launcher.json", "w") as f: + # f.write(json.dumps(state)) + # f.flush() + + # except OSError: + # # State file does not exist, create it + # import os + # try: + # os.stat("/state") + # except OSError: + # os.mkdir("/state") + # badger_os.state_save("launcher", state) + + # # Clear the display and reset device + display.clear() + display.update() + machine.reset() # ------------------------------ # Program setup @@ -145,12 +187,34 @@ def draw_badge(): # Main program # ------------------------------ -draw_badge() +draw_page() while True: - # Sometimes a button press or hold will keep the system - # powered *through* HALT, so latch the power back on. - display.keepalive() + # Buttons UP / DOWN to view previous / next page of contribution graph + if CURRENT_PAGE == 1 and (display.pressed(badger2040.BUTTON_DOWN) or display.pressed(badger2040.BUTTON_UP)): + contribution_graph.CONTRIBUTION_GRAPH_PAGE = 1 if contribution_graph.CONTRIBUTION_GRAPH_PAGE == 0 else 0 + draw_page() + + # Button A opens launcher + elif display.pressed(badger2040.BUTTON_A): + exit_to_launcher() + + # Button B sets current page to Badge + elif CURRENT_PAGE != 0 and display.pressed(badger2040.BUTTON_B): + CURRENT_PAGE = 0 + draw_page() + + # Button C sets current page to Contribution Graph + elif CURRENT_PAGE != 1 and display.pressed(badger2040.BUTTON_C): + CURRENT_PAGE = 1 + contribution_graph.CONTRIBUTION_GRAPH_PAGE = 0 + draw_page() + + # if incorrect button pressed, show popup info for 5 seconds + elif display.pressed(badger2040.BUTTON_A) or display.pressed(badger2040.BUTTON_B) or display.pressed(badger2040.BUTTON_C) or display.pressed(badger2040.BUTTON_UP) or display.pressed(badger2040.BUTTON_DOWN): + badger_os.warning(display, "a = launcher b = badge c = contributions") + time.sleep(5) + draw_page() # If on battery, halt the Badger to save power, it will wake up if any of the front buttons are pressed display.halt() diff --git a/scripts/getdata.py b/scripts/getdata.py deleted file mode 100644 index 0fe748c..0000000 --- a/scripts/getdata.py +++ /dev/null @@ -1,177 +0,0 @@ -""" MIT License - -Copyright (c) 2022 Krishna Prajapati - @KrisPrajapati - -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. """ - -import urllib.request -import requests -from lxml import etree -from io import StringIO -import os -import datetime -import qrcode -import shutil -from lib import convert -import argparse - -first_name = "" -last_name = "" -username = "" - -# Parse HTML into DOM and fetch name & username -def parse_profile_data(html): - global username - parser = etree.HTMLParser() - dom_root = etree.parse(StringIO(html.decode("unicode_escape")), parser) - name_nodes = dom_root.xpath("//span[@itemprop='name']") - username_nodes = dom_root.xpath("//span[@itemprop='additionalName']") - - if len(name_nodes) == 0 or len(username_nodes) == 0: - print("An error occured") - quit() - - name = name_nodes[0].text.strip() if args.name is None else args.name - username = username_nodes[0].text.strip() - - names = name.split(" ") - first_name = names[0] - last_name = names[1] if len(names) > 1 else "" - - print(f"Welcome, {name}!") - print(f"@{username}") - print("\nCreating Badge") - badge_filename = f"generated/badge.txt" - os.makedirs(os.path.dirname(badge_filename), exist_ok=True) - with open(badge_filename, "w") as badge_file: - badge_file.write(f"UNIVERSE 2023\n{first_name}\n{last_name}\n@{username}") - badge_file.close() - - -# Date format for contribution page -def dateformat(dateStr): - date_components = dateStr.split("-") - date = datetime.datetime(int(date_components[0]), int(date_components[1]), int(date_components[2])) - return date.strftime("%-d %b %Y") - -# Parse and fetch contribution graph data -def parse_contributions_data(html): - parser = etree.HTMLParser() - dom_root = etree.parse(StringIO(html.decode("unicode_escape")), parser) - - #Fetch each rect element inside the svg - - rect_nodes = dom_root.xpath("//svg[@class='js-calendar-graph-svg']//rect") - - #Offset as latest week may be partial - latest_week = dom_root.xpath("//svg[@class='js-calendar-graph-svg']/g/g[last()]//rect") - week_position_offset = 7 - len(latest_week) - - - # Graph has 369 days - # however we can display only 182 days per page (i.e. 364 days) - # so ignore the earliest 5 days - - # Writing latest 6 months to page 1 - with open(f"generated/contribution_page_1.txt", "w") as contributions_file: - total = 0 - graph_data = "" - start_date = rect_nodes[187+week_position_offset].xpath("string(@data-date)") - end_date = rect_nodes[len(rect_nodes)-1].xpath("string(@data-date)") - for i in range(187+week_position_offset,len(rect_nodes)): - contributions = 0 - if rect_nodes[i].xpath("string(@data-count)") != "": - contributions = int(rect_nodes[i].xpath("string(@data-count)")) - total += contributions - data_level = rect_nodes[i].xpath("string(@data-level)") - graph_data+=f"\n{data_level}" - - contributions_file.write(f"{total} contributions between\n{dateformat(start_date)} - {dateformat(end_date)}") - contributions_file.write(graph_data) - contributions_file.close() - - - # Writing earlier 6 months to page 2 - with open(f"generated/contribution_page_2.txt", "w") as contributions_file: - total = 0 - graph_data = "" - start_date = rect_nodes[4+week_position_offset].xpath("string(@data-date)") - end_date = rect_nodes[186+week_position_offset].xpath("string(@data-date)") - - for i in range(4+week_position_offset,186+week_position_offset): - contributions = 0 - if rect_nodes[i].xpath("string(@data-count)") != "": - contributions = int(rect_nodes[i].xpath("string(@data-count)")) - total += contributions - data_level = rect_nodes[i].xpath("string(@data-level)") - graph_data+=f"\n{data_level}" - contributions_file.write(f"{total} contributions between\n{dateformat(start_date)} - {dateformat(end_date)}") - contributions_file.write(graph_data) - contributions_file.close() - -# Generate QR code -def generate_qr_code(): - qr = qrcode.QRCode(version=1,error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=3, border=3,) - qr.add_data(f"https://github.com/{username}") - png = qr.make_image() - pngfilename = f"generated/gh_qrcode.png" - png.save(pngfilename) - convert.convert(pngfilename) - os.remove(pngfilename) - -#--------------------------- -# MAIN PROGRAM ENTRY POINT -#--------------------------- - -parser = argparse.ArgumentParser() -parser.add_argument('--handle', type=str, required=False) -parser.add_argument('--name', type=str, required=False) -args = parser.parse_args() - -entered_username = input("\nEnter your GitHub username: ") if args.handle is None else args.handle -print("\nFetching Profile...\n") - -profile_request = requests.get(f"http://github.com/{entered_username}") - - -if profile_request.status_code == 200: - profile_html = profile_request.content - parse_profile_data(profile_html) - generate_qr_code() - -elif profile_request.status_code == 404: - print("Profile not found") - quit() -else: - print("Error fetching profile") - quit() - - -print("\nFetching Contribution Graph...") -contributions_request = requests.get(f"http://github.com/users/{entered_username}/contributions") - - -if contributions_request.status_code == 200: - print("\nProcessing Contribution Graph...") - contributions_html = contributions_request.content - parse_contributions_data(contributions_html) - -else: - print("Error fetching contribution graph") - quit() diff --git a/scripts/gui/get_contribution_graph.py b/scripts/gui/get_contribution_graph.py new file mode 100644 index 0000000..e36bd37 --- /dev/null +++ b/scripts/gui/get_contribution_graph.py @@ -0,0 +1,100 @@ +""" MIT License + +Copyright (c) 2022 Krishna Prajapati - @KrisPrajapati + +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. """ + +import urllib.request +import requests +from lxml import etree +from io import StringIO +import os +import datetime +import qrcode +import shutil +import argparse + + +# Date format for contribution page +def dateformat(dateStr): + date_components = dateStr.split("-") + date = datetime.datetime(int(date_components[0]), int(date_components[1]), int(date_components[2])) + return date.strftime("%-d %b %Y") + +# Parse and fetch contribution graph data +def parse_contributions_data(html): + parser = etree.HTMLParser() + dom_root = etree.parse(StringIO(html.decode("unicode_escape")), parser) + + # Writing latest 6 months to page 1 (Week 26 to 52) + write_contribution_file(filename="contribution_page_1.txt", start_week=26, end_week=52, dom_root=dom_root) + + # Writing earlier 6 months to page 2 (Week 0 to 25) + write_contribution_file(filename="contribution_page_2.txt", start_week=0, end_week=25, dom_root=dom_root) + +def write_contribution_file(filename, start_week, end_week, dom_root): + with open(f"generated/{filename}", "w") as contributions_file: + graph_data = "" + contributions = 0 + start_date = first_day_of_week(dom_root,start_week) + end_date = last_day_of_week(dom_root,end_week) + + for week in range(start_week,end_week+1): + contributions+=contribution_count_for_week(dom_root,week) + graph_data+=graph_levels_for_week(dom_root,week) + + contributions_file.write(f"{contributions} contributions between\n{dateformat(start_date)} - {dateformat(end_date)}") + contributions_file.write(graph_data) + contributions_file.close() + +def graph_levels_for_week(dom,week): + nodes = dom.xpath(f'//table[@class="ContributionCalendar-grid js-calendar-graph-table"]//td[@data-ix="{week}"]') + data = "" + for i in range(0,len(nodes)): + data+=f"\n{nodes[i].xpath('string(@data-level)')}" + return data + +def contribution_count_for_week(dom, week): + count = 0 + nodes = dom.xpath(f'//table[@class="ContributionCalendar-grid js-calendar-graph-table"]//td[@data-ix="{week}"]//text()') + for i in range(0,len(nodes)): + num = [int(s) for s in nodes[i].split() if s.isdigit()] + count+=sum(num) + return count + +def first_day_of_week(dom,week): + return dom.xpath(f'string((//table[@class="ContributionCalendar-grid js-calendar-graph-table"]//td[@data-ix="{week}"])[1]/@data-date)') + +def last_day_of_week(dom,week): + return dom.xpath(f'string((//table[@class="ContributionCalendar-grid js-calendar-graph-table"]//td[@data-ix="{week}"])[last()]/@data-date)') + + +def fetch_contribution_graph(handle): + print("\nFetching Contribution Graph...") + contributions_request = requests.get(f"http://github.com/users/{handle}/contributions") + + + if contributions_request.status_code == 200: + print("\nProcessing Contribution Graph...") + contributions_html = contributions_request.content + parse_contributions_data(contributions_html) + + else: + print("Error fetching contribution graph") + quit() diff --git a/scripts/gui/main.py b/scripts/gui/main.py index 91f0c1f..05324c3 100644 --- a/scripts/gui/main.py +++ b/scripts/gui/main.py @@ -9,6 +9,7 @@ import os import time import sys +from get_contribution_graph import fetch_contribution_graph script_dir = os.path.dirname(os.path.abspath(__file__)) root_path = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) @@ -138,6 +139,7 @@ def create_badge(self, scanned): title = scan_data[4].upper() pronouns = scan_data[5].upper() + # Depending on keyboard mapping, the @ symbol as the first character # of the handle may have been entered as " # If so, replace it with @ @@ -155,7 +157,14 @@ def create_badge(self, scanned): f"Universe 2023\n{first_name}\n{last_name}\n{company}\n{title}\n{pronouns}\n{handle}\n") badge_file.close() + # Fetch contribution graph + # try: + fetch_contribution_graph(handle[1:]) + # except: + # print("Could not fetch contribution graph") + _transfer_folder(os.path.join(root_path,"generated")) + # Reboot the badge self.set_state("rebooting")