diff --git a/.env.example b/.env.example index 5200ba7..fbe85ad 100644 --- a/.env.example +++ b/.env.example @@ -18,5 +18,7 @@ INPUT_SHOW_DAYS_OF_WEEK=True INPUT_SHOW_LANGUAGE_PER_REPO=True INPUT_SHOW_UPDATED_DATE=True INPUT_UPDATED_DATE_FORMAT=%d/%m/%Y %H:%M:%S -INPUT_COMMIT_BY_ME=False +INPUT_COMMIT_BY_ME=True INPUT_COMMIT_MESSAGE=Updated with Dev Metrics +INPUT_DEBUG_LOGGING=True +DEBUG_RUN=True diff --git a/.github/workflows/build_image.yml b/.github/workflows/build_image.yml index 6a27942..338f66e 100644 --- a/.github/workflows/build_image.yml +++ b/.github/workflows/build_image.yml @@ -3,6 +3,10 @@ name: PUBLISH_IMAGE on: push: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + jobs: publish-server-image: name: Publish 'waka-readme-stats' image diff --git a/README.md b/README.md index 353aaaa..458609e 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,8 @@ CSS 2 repos █░░░░░░░░░░░░░░░░ | 2 | ⣿ | ⣀ | | 3 | ⬛ | ⬜ | +`DEBUG_LOGGING` flag can be set to increase action output verbosity, by default equals internal runner debug property + **Timeline** ![Chart not found](https://raw.githubusercontent.com/anmol098/anmol098/master/charts/bar_graph.png) diff --git a/action.yml b/action.yml index 1166b7a..91646ed 100644 --- a/action.yml +++ b/action.yml @@ -132,6 +132,11 @@ inputs: description: "Version of the symbol block and empty of the progress bar" default: "1" + DEBUG_LOGGING: + required: false + description: "Whether to enable action debug logging" + default: ${{ runner.debug }} + runs: using: 'docker' image: 'docker://wakareadmestats/waka-readme-stats:master' diff --git a/sources/main.py b/sources/main.py index fc131b4..9b474d8 100644 --- a/sources/main.py +++ b/sources/main.py @@ -5,12 +5,13 @@ from asyncio import run from datetime import datetime from urllib.parse import quote -from humanize import intword, naturalsize, intcomma, precisedelta +from humanize import intword, naturalsize, intcomma from manager_download import init_download_manager, DownloadManager as DM from manager_environment import EnvironmentManager as EM from manager_github import init_github_manager, GitHubManager as GHM from manager_localization import init_localization_manager, LocalizationManager as LM +from manager_debug import init_debug_manager, DebugManager as DBM from graphics_chart_drawer import create_loc_graph, GRAPH_PATH from yearly_commit_calculator import calculate_yearly_commit_data from graphics_list_formatter import make_list, make_commit_day_time_list, make_language_per_repo_list @@ -23,10 +24,12 @@ async def get_waka_time_stats() -> str: :returns: String representation of the info. """ + DBM.i("Adding short WakaTime stats...") stats = str() data = await DM.get_remote_json("waka_latest") if EM.SHOW_COMMIT: + DBM.i("Adding user commit day time info...") stats += f"{await make_commit_day_time_list(data['data']['timezone'])}\n\n" if EM.SHOW_TIMEZONE or EM.SHOW_LANGUAGE or EM.SHOW_EDITORS or EM.SHOW_PROJECTS or EM.SHOW_OS: @@ -34,27 +37,33 @@ async def get_waka_time_stats() -> str: stats += f"📊 **{LM.t('This Week I Spend My Time On')}** \n\n```text\n" if EM.SHOW_TIMEZONE: + DBM.i("Adding user timezone info...") time_zone = data["data"]["timezone"] stats += f"🕑︎ {LM.t('Timezone')}: {time_zone}\n\n" if EM.SHOW_LANGUAGE: + DBM.i("Adding user top languages info...") lang_list = no_activity if len(data["data"]["languages"]) == 0 else make_list(data["data"]["languages"]) stats += f"💬 {LM.t('Languages')}: \n{lang_list}\n\n" if EM.SHOW_EDITORS: + DBM.i("Adding user editors info...") edit_list = no_activity if len(data["data"]["editors"]) == 0 else make_list(data["data"]["editors"]) stats += f"🔥 {LM.t('Editors')}: \n{edit_list}\n\n" if EM.SHOW_PROJECTS: + DBM.i("Adding user projects info...") project_list = no_activity if len(data["data"]["projects"]) == 0 else make_list(data["data"]["projects"]) stats += f"🐱‍💻 {LM.t('Projects')}: \n{project_list}\n\n" if EM.SHOW_OS: + DBM.i("Adding user operating systems info...") os_list = no_activity if len(data["data"]["operating_systems"]) == 0 else make_list(data["data"]["operating_systems"]) stats += f"💻 {LM.t('operating system')}: \n{os_list}\n\n" stats = f"{stats[:-1]}```\n\n" + DBM.g("WakaTime stats added!") return stats @@ -65,38 +74,47 @@ async def get_short_github_info() -> str: :returns: String representation of the info. """ + DBM.i("Adding short GitHub info...") stats = f"**🐱 {LM.t('My GitHub Data')}** \n\n" + DBM.i("Adding user disk usage info...") if GHM.USER.disk_usage is None: disk_usage = LM.t("Used in GitHub's Storage") % "?" - print("Please add new github personal access token with user permission!") + DBM.p("Please add new github personal access token with user permission!") else: disk_usage = LM.t("Used in GitHub's Storage") % naturalsize(GHM.USER.disk_usage) stats += f"> 📦 {disk_usage} \n > \n" data = await DM.get_remote_json("github_stats") + DBM.i("Adding contributions info...") if len(data["years"]) > 0: contributions = LM.t("Contributions in the year") % (intcomma(data["years"][0]["total"]), data["years"][0]["year"]) stats += f"> 🏆 {contributions}\n > \n" + else: + DBM.p("GitHub contributions data unavailable!") + DBM.i("Adding opted for hire info...") opted_to_hire = GHM.USER.hireable if opted_to_hire: stats += f"> 💼 {LM.t('Opted to Hire')}\n > \n" else: stats += f"> 🚫 {LM.t('Not Opted to Hire')}\n > \n" + DBM.i("Adding public repositories info...") public_repo = GHM.USER.public_repos if public_repo != 1: stats += f"> 📜 {LM.t('public repositories') % public_repo} \n > \n" else: stats += f"> 📜 {LM.t('public repository') % public_repo} \n > \n" + DBM.i("Adding private repositories info...") private_repo = GHM.USER.owned_private_repos if GHM.USER.owned_private_repos is not None else 0 if public_repo != 1: stats += f"> 🔑 {LM.t('private repositories') % private_repo} \n > \n" else: stats += f"> 🔑 {LM.t('private repository') % private_repo} \n > \n" + DBM.g("Short GitHub info added!") return stats @@ -107,6 +125,8 @@ async def get_stats() -> str: :returns: String representation of README.md contents. """ + DBM.i("Collecting stats for README...") + stats = str() repositories = await DM.get_remote_graphql("user_repository_list", username=GHM.USER.login, id=GHM.USER.node_id) @@ -114,16 +134,20 @@ async def get_stats() -> str: yearly_data = await calculate_yearly_commit_data(repositories) else: yearly_data = (None, dict()) + DBM.w("User yearly data not needed, skipped.") if EM.SHOW_TOTAL_CODE_TIME: + DBM.i("Adding total code time info...") data = await DM.get_remote_json("waka_all") stats += f"![Code Time](http://img.shields.io/badge/{quote('Code Time')}-{quote(str(data['data']['text']))}-blue)\n\n" if EM.SHOW_PROFILE_VIEWS: + DBM.i("Adding profile views info...") data = GHM.REPO.get_views_traffic(per="week") stats += f"![Profile Views](http://img.shields.io/badge/{quote(LM.t('Profile Views'))}-{data['count']}-blue)\n\n" if EM.SHOW_LINES_OF_CODE: + DBM.i("Adding lines of code info...") total_loc = sum([yearly_data[y][q][d] for y in yearly_data.keys() for q in yearly_data[y].keys() for d in yearly_data[y][q].keys()]) data = f"{intword(total_loc)} {LM.t('Lines of code')}" stats += f"![Lines of code](https://img.shields.io/badge/{quote(LM.t('From Hello World I have written'))}-{quote(data)}-blue)\n\n" @@ -134,6 +158,7 @@ async def get_stats() -> str: stats += await get_waka_time_stats() if EM.SHOW_LANGUAGE_PER_REPO: + DBM.i("Adding language per repository info...") stats += f"{make_language_per_repo_list(repositories)}\n\n" if EM.SHOW_LOC_CHART: @@ -143,8 +168,10 @@ async def get_stats() -> str: stats += f"**{LM.t('Timeline')}**\n\n![Lines of Code chart](https://raw.githubusercontent.com/{chart_path})\n\n" if EM.SHOW_UPDATED_DATE: + DBM.i("Adding last updated time...") stats += f"\n Last Updated on {datetime.now().strftime(EM.UPDATED_DATE_FORMAT)} UTC" + DBM.g("Stats for README collected!") return stats @@ -156,14 +183,18 @@ async def main(): init_github_manager() await init_download_manager() init_localization_manager() + DBM.i("Managers initialized.") if GHM.update_readme(await get_stats()): - print("Readme updated!") + DBM.g("Readme updated!") await DM.close_remote_resources() if __name__ == "__main__": + init_debug_manager() start_time = datetime.now() + DBM.g("Program execution started at $date.", date=start_time) run(main()) - run_delta = datetime.now() - start_time - print(f"Program processed in {precisedelta(run_delta, minimum_unit='microseconds')}.") + end_time = datetime.now() + DBM.g("Program execution finished at $date.", date=end_time) + DBM.p("Program finished in $time.", time=end_time - start_time) diff --git a/sources/manager_debug.py b/sources/manager_debug.py new file mode 100644 index 0000000..7ff18e6 --- /dev/null +++ b/sources/manager_debug.py @@ -0,0 +1,65 @@ +from datetime import datetime +from logging import getLogger, Logger, StreamHandler +from string import Template +from typing import Dict + +from humanize import precisedelta + +from manager_environment import EnvironmentManager as EM + + +def init_debug_manager(): + """ + Initialize download manager: + - Setup headers for GitHub GraphQL requests. + - Launch static queries in background. + """ + DebugManager.create_logger("DEBUG" if EM.DEBUG_LOGGING else "ERROR") + + +class DebugManager: + _COLOR_RESET = "\u001B[0m" + _COLOR_RED = "\u001B[31m" + _COLOR_GREEN = "\u001B[32m" + _COLOR_BLUE = "\u001B[34m" + _COLOR_YELLOW = "\u001B[33m" + + _DATE_TEMPLATE = "date" + _TIME_TEMPLATE = "time" + + _logger: Logger + + @staticmethod + def create_logger(level: str): + DebugManager._logger = getLogger(__name__) + DebugManager._logger.setLevel(level) + DebugManager._logger.addHandler(StreamHandler()) + + @staticmethod + def _process_template(message: str, kwargs: Dict) -> str: + if DebugManager._DATE_TEMPLATE in kwargs: + kwargs[DebugManager._DATE_TEMPLATE] = f"{datetime.strftime(kwargs[DebugManager._DATE_TEMPLATE], '%d-%m-%Y %H:%M:%S:%f')}" + if DebugManager._TIME_TEMPLATE in kwargs: + kwargs[DebugManager._TIME_TEMPLATE] = precisedelta(kwargs[DebugManager._TIME_TEMPLATE], minimum_unit="microseconds") + + return Template(message).substitute(kwargs) + + @staticmethod + def g(message: str, **kwargs): + message = DebugManager._process_template(message, kwargs) + DebugManager._logger.info(f"{DebugManager._COLOR_GREEN}{message}{DebugManager._COLOR_RESET}") + + @staticmethod + def i(message: str, **kwargs): + message = DebugManager._process_template(message, kwargs) + DebugManager._logger.debug(f"{DebugManager._COLOR_BLUE}{message}{DebugManager._COLOR_RESET}") + + @staticmethod + def w(message: str, **kwargs): + message = DebugManager._process_template(message, kwargs) + DebugManager._logger.warning(f"{DebugManager._COLOR_YELLOW}{message}{DebugManager._COLOR_RESET}") + + @staticmethod + def p(message: str, **kwargs): + message = DebugManager._process_template(message, kwargs) + DebugManager._logger.error(message) diff --git a/sources/manager_download.py b/sources/manager_download.py index 4259d21..0505644 100644 --- a/sources/manager_download.py +++ b/sources/manager_download.py @@ -9,6 +9,7 @@ from yaml import safe_load from manager_environment import EnvironmentManager as EM from manager_github import GitHubManager as GHM +from manager_debug import DebugManager as DBM GITHUB_API_QUERIES = { @@ -68,6 +69,7 @@ GITHUB_API_QUERIES = { owner { login } + isPrivate } pageInfo { endCursor @@ -188,16 +190,21 @@ class DownloadManager: By default `response.json()` is used. :return: Response dictionary. """ + DBM.i(f"\tMaking a remote API query named '{resource}'...") if isinstance(DownloadManager._REMOTE_RESOURCES_CACHE[resource], Awaitable): res = await DownloadManager._REMOTE_RESOURCES_CACHE[resource] DownloadManager._REMOTE_RESOURCES_CACHE[resource] = res - if res.status_code == 200: - if convertor is None: - return res.json() - else: - return convertor(res.content) + DBM.g(f"\tQuery '{resource}' finished, result saved!") + else: + res = DownloadManager._REMOTE_RESOURCES_CACHE[resource] + DBM.g(f"\tQuery '{resource}' loaded from cache!") + if res.status_code == 200: + if convertor is None: + return res.json() else: - raise Exception(f"Query '{res.url}' failed to run by returning code of {res.status_code}: {res.json()}") + return convertor(res.content) + else: + raise Exception(f"Query '{res.url}' failed to run by returning code of {res.status_code}: {res.json()}") @staticmethod async def get_remote_json(resource: str) -> Dict: diff --git a/sources/manager_environment.py b/sources/manager_environment.py index 85b9ae5..a38a782 100644 --- a/sources/manager_environment.py +++ b/sources/manager_environment.py @@ -43,3 +43,6 @@ class EnvironmentManager: UPDATED_DATE_FORMAT = getenv("INPUT_UPDATED_DATE_FORMAT", "%d/%m/%Y %H:%M:%S") IGNORED_REPOS = getenv("INPUT_IGNORED_REPOS", "").replace(" ", "").split(",") SYMBOL_VERSION = int(getenv("INPUT_SYMBOL_VERSION")) + + DEBUG_LOGGING = getenv("INPUT_DEBUG_LOGGING", "0").lower() in _TRUTHY + DEBUG_RUN = getenv("DEBUG_RUN", "False").lower() in _TRUTHY diff --git a/sources/manager_github.py b/sources/manager_github.py index 931d8ca..e4bd7fb 100644 --- a/sources/manager_github.py +++ b/sources/manager_github.py @@ -4,6 +4,7 @@ from re import sub from github import Github, AuthenticatedUser, Repository, ContentFile, InputGitAuthor, UnknownObjectException from manager_environment import EnvironmentManager as EM +from manager_debug import DebugManager as DBM def init_github_manager(): @@ -12,7 +13,7 @@ def init_github_manager(): Current user, user readme repo and readme file are downloaded. """ GitHubManager.prepare_github_env() - print(f"Current user: {GitHubManager.USER.login}") + DBM.i(f"Current user: {GitHubManager.USER.login}") class GitHubManager: @@ -82,6 +83,7 @@ class GitHubManager: :returns: whether the README.md file was updated or not. """ + DBM.i("Updating README...") new_readme = GitHubManager._generate_new_readme(stats) if new_readme != GitHubManager._README_CONTENTS: GitHubManager.REPO.update_file( @@ -92,8 +94,10 @@ class GitHubManager: branch=GitHubManager.branch(), committer=GitHubManager._get_author(), ) + DBM.g("README updated!") return True else: + DBM.w("README update not needed!") return False @staticmethod @@ -104,10 +108,13 @@ class GitHubManager: :param chart_path: path to saved lines of code chart. """ + DBM.i("Updating lines of code chart...") with open(chart_path, "rb") as input_file: data = input_file.read() try: contents = GitHubManager.REPO.get_contents(chart_path) GitHubManager.REPO.update_file(contents.path, "Charts Updated", data, contents.sha, committer=GitHubManager._get_author()) + DBM.g("Lines of code chart updated!") except UnknownObjectException: GitHubManager.REPO.create_file(chart_path, "Charts Added", data, committer=GitHubManager._get_author()) + DBM.g("Lines of code chart created!") diff --git a/sources/yearly_commit_calculator.py b/sources/yearly_commit_calculator.py index 458c433..49bce72 100644 --- a/sources/yearly_commit_calculator.py +++ b/sources/yearly_commit_calculator.py @@ -5,6 +5,7 @@ from typing import Dict from manager_download import DownloadManager as DM from manager_environment import EnvironmentManager as EM from manager_github import GitHubManager as GHM +from manager_debug import DebugManager as DBM async def calculate_yearly_commit_data(repositories: Dict) -> Dict: @@ -15,12 +16,15 @@ async def calculate_yearly_commit_data(repositories: Dict) -> Dict: :param repositories: user repositories info dictionary. :returns: Commit quarter yearly data dictionary. """ + DBM.i("Calculating yearly commit data...") yearly_data = dict() total = len(repositories["data"]["user"]["repositories"]["nodes"]) for ind, repo in enumerate(repositories["data"]["user"]["repositories"]["nodes"]): if repo["name"] not in EM.IGNORED_REPOS: - print(f"{ind + 1}/{total}", "Retrieving repo:", repo["owner"]["login"], repo["name"]) + repo_name = "private" if repo["isPrivate"] else f"{repo['owner']['login']}/{repo['name']}" + DBM.i(f"\t{ind + 1}/{total} Retrieving repo: {repo_name}") await update_yearly_data_with_commit_stats(repo, yearly_data) + DBM.g("Yearly commit data calculated!") return yearly_data @@ -35,7 +39,7 @@ async def update_yearly_data_with_commit_stats(repo_details: Dict, yearly_data: owner = repo_details["owner"]["login"] branch_data = await DM.get_remote_graphql("repo_branch_list", owner=owner, name=repo_details["name"]) if branch_data["data"]["repository"] is None: - print(f"\tSkipping repo: {repo_details['name']}") + DBM.w(f"\t\tSkipping repo: {repo_details['name']}") return dict() for branch in branch_data["data"]["repository"]["refs"]["nodes"]: