From 2e494aa87bc956b5b6f43ba9749804e088e472eb Mon Sep 17 00:00:00 2001 From: pseusys Date: Fri, 17 Feb 2023 22:41:08 +0100 Subject: [PATCH] documentation added, `black` linter added --- .github/workflows/build_image.yml | 2 +- .github/workflows/codestyle.yml | 4 +- Makefile | 3 +- requirements.txt | 3 +- sources/graphics_chart_drawer.py | 6 +-- sources/graphics_list_formatter.py | 60 ++++++++++++++++++++---- sources/main.py | 34 ++++++++++---- sources/manager_download.py | 52 ++++++++++----------- sources/manager_environment.py | 61 +++++++++++++----------- sources/manager_github.py | 72 ++++++++++++++++++++--------- sources/manager_localization.py | 20 +++++++- sources/yearly_commit_calculator.py | 18 +++++++- 12 files changed, 230 insertions(+), 105 deletions(-) diff --git a/.github/workflows/build_image.yml b/.github/workflows/build_image.yml index c9a0e34..4ae01df 100644 --- a/.github/workflows/build_image.yml +++ b/.github/workflows/build_image.yml @@ -6,7 +6,7 @@ on: jobs: publish-server-image: name: Publish 'waka-readme-stats' image - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Checkout 🛎️ diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index aa66ef2..bc13463 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -6,7 +6,7 @@ on: jobs: lint: name: Run codestyle check - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout 🛎️ @@ -21,4 +21,4 @@ jobs: run: pip install -r requirements.txt - name: Run Codestyle ✔️ - run: flake8 --max-line-length=160 --exclude venv,assets . \ No newline at end of file + run: flake8 --max-line-length=160 --exclude venv,assets . && black --line-length=160 --check --exclude='/venv/|/assets/' . \ No newline at end of file diff --git a/Makefile b/Makefile index 9c92749..88ec394 100644 --- a/Makefile +++ b/Makefile @@ -38,9 +38,10 @@ run-container: .PHONY: run-container -lint: +lint: venv @ # Run flake8 linter flake8 --max-line-length=160 --exclude venv,assets . + black --line-length=160 --check --exclude='/venv/|/assets/' . .PHONY: lint clean: diff --git a/requirements.txt b/requirements.txt index 8d33d56..c191e13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,6 @@ numpy~=1.24 httpx~=0.23 PyYAML~=6.0 -# Codestyle checking module: +# Codestyle checking modules: flake8~=6.0 +black~=23.1 diff --git a/sources/graphics_chart_drawer.py b/sources/graphics_chart_drawer.py index 4aae8cd..786091e 100644 --- a/sources/graphics_chart_drawer.py +++ b/sources/graphics_chart_drawer.py @@ -7,8 +7,8 @@ import matplotlib.pyplot as plt from manager_download import DownloadManager as DM -MAX_LANGUAGES = 5 -GRAPH_PATH = "assets/bar_graph.png" +MAX_LANGUAGES = 5 # Number of top languages to add to chart, for each year quarter +GRAPH_PATH = "assets/bar_graph.png" # Chart saving path. async def create_loc_graph(yearly_data: Dict, save_path: str): @@ -27,7 +27,7 @@ async def create_loc_graph(yearly_data: Dict, save_path: str): languages_all_loc = dict() for i, y in enumerate(sorted(yearly_data.keys())): for q in yearly_data[y].keys(): - langs = sorted(yearly_data[y][q].keys(), key=lambda l: yearly_data[y][q][l], reverse=True)[0:MAX_LANGUAGES] + langs = sorted(yearly_data[y][q].keys(), key=lambda n: yearly_data[y][q][n], reverse=True)[0:MAX_LANGUAGES] for lang in langs: if lang not in languages_all_loc: diff --git a/sources/graphics_list_formatter.py b/sources/graphics_list_formatter.py index d5b926c..ddb949e 100644 --- a/sources/graphics_list_formatter.py +++ b/sources/graphics_list_formatter.py @@ -10,24 +10,39 @@ from manager_github import GitHubManager as GHM from manager_localization import LocalizationManager as LM -DAY_TIME_EMOJI = ["🌞", "🌆", "🌃", "🌙"] -DAY_TIME_NAMES = ["Morning", "Daytime", "Evening", "Night"] -WEEK_DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] +DAY_TIME_EMOJI = ["🌞", "🌆", "🌃", "🌙"] # Emojis, representing different times of day. +DAY_TIME_NAMES = ["Morning", "Daytime", "Evening", "Night"] # Localization identifiers for different times of day. +WEEK_DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] # Localization identifiers for different days of week. class Symbol(Enum): + """ + Symbol version enum. + Allows to retrieve symbols pairs by calling `Symbol.get_symbols(version)`. + """ + VERSION_1 = "█", "░" VERSION_2 = "⣿", "⣀" VERSION_3 = "⬛", "⬜" @staticmethod def get_symbols(version: int) -> Tuple[str, str]: + """ + Retrieves symbols pair for specified version. + + :param version: Required symbols version. + :returns: Two strings for filled and empty symbol value in a tuple. + """ return Symbol[f"VERSION_{version}"].value def make_graph(percent: float): """ - Make progress graph from API graph + Make text progress bar. + Length of the progress bar is 25 characters. + + :param percent: Completion percent of the progress bar. + :return: The string progress bar representation. """ done_block, empty_block = Symbol.get_symbols(EM.SYMBOL_VERSION) percent_quart = round(percent / 4) @@ -36,7 +51,20 @@ def make_graph(percent: float): def make_list(data: List = None, names: List[str] = None, texts: List[str] = None, percents: List[float] = None, top_num: int = 5, sort: bool = True) -> str: """ - Make List + Make list of text progress bars with supportive info. + Each row has the following structure: [name of the measure] [quantity description (with words)] [progress bar] [total percentage]. + Name of the measure: up to 25 characters. + Quantity description: how many _things_ were found, up to 20 characters. + Progress bar: measure percentage, 25 characters. + Total percentage: floating point percentage. + + :param data: list of dictionaries, each of them containing a measure (name, text and percent). + :param names: list of names (names of measure), overloads data if defined. + :param texts: list of texts (quantity descriptions), overloads data if defined. + :param percents: list of percents (total percentages), overloads data if defined. + :param top_num: how many measures to display, default: 5. + :param sort: if measures should be sorted by total percentage, default: True. + :returns: The string representation of the list. """ if data is not None: names = [value for item in data for key, value in item.items() if key == "name"] if names is None else names @@ -46,10 +74,16 @@ def make_list(data: List = None, names: List[str] = None, texts: List[str] = Non data = list(zip(names, texts, percents)) top_data = sorted(data[:top_num], key=lambda record: record[2]) if sort else data[:top_num] data_list = [f"{n[:25]}{' ' * (25 - len(n))}{t}{' ' * (20 - len(t))}{make_graph(p)} {p:05.2f} % " for n, t, p in top_data] - return '\n'.join(data_list) + return "\n".join(data_list) async def make_commit_day_time_list(time_zone: str) -> str: + """ + Calculate commit-related info, how many commits were made, and at what time of day and day of week. + + :param time_zone: User time zone. + :returns: string representation of statistics. + """ stats = str() result = await DM.get_remote_graphql("repos_contributed_to", username=GHM.USER.login) @@ -62,8 +96,8 @@ async def make_commit_day_time_list(time_zone: str) -> str: result = await DM.get_remote_graphql("repo_committed_dates", owner=repository["owner"]["login"], name=repository["name"], id=GHM.USER.node_id) committed_dates = result["data"]["repository"]["defaultBranchRef"]["target"]["history"]["edges"] - for committedDate in committed_dates: - local_date = datetime.strptime(committedDate["node"]["committedDate"], "%Y-%m-%dT%H:%M:%SZ") + for committed_date in committed_dates: + local_date = datetime.strptime(committed_date["node"]["committedDate"], "%Y-%m-%dT%H:%M:%SZ") date = local_date.replace(tzinfo=utc).astimezone(timezone(time_zone)) day_times[date.hour // 6] += 1 @@ -74,14 +108,14 @@ async def make_commit_day_time_list(time_zone: str) -> str: day_times = day_times[1:] + day_times[:1] dt_names = [f"{DAY_TIME_EMOJI[i]} {LM.t(DAY_TIME_NAMES[i])}" for i in range(len(day_times))] - dt_texts = [f'{day_time} commits' for day_time in day_times] + dt_texts = [f"{day_time} commits" for day_time in day_times] dt_percents = [round((day_time / sum_day) * 100, 2) for day_time in day_times] title = LM.t("I am an Early") if sum(day_times[0:2]) >= sum(day_times[2:4]) else LM.t("I am a Night") stats += f"**{title}** \n\n```text\n{make_list(names=dt_names, texts=dt_texts, percents=dt_percents, top_num=7, sort=False)}\n```\n" if EM.SHOW_DAYS_OF_WEEK: wd_names = [LM.t(week_day) for week_day in WEEK_DAY_NAMES] - wd_texts = [f'{week_day} commits' for week_day in week_days] + wd_texts = [f"{week_day} commits" for week_day in week_days] wd_percents = [round((week_day / sum_week) * 100, 2) for week_day in week_days] title = LM.t("I am Most Productive on") % wd_names[wd_percents.index(max(wd_percents))] stats += f"📅 **{title}** \n\n```text\n{make_list(names=wd_names, texts=wd_texts, percents=wd_percents, top_num=7, sort=False)}\n```\n" @@ -90,6 +124,12 @@ async def make_commit_day_time_list(time_zone: str) -> str: def make_language_per_repo_list(repositories: Dict) -> str: + """ + Calculate language-related info, how many repositories in what language user has. + + :param repositories: User repositories. + :returns: string representation of statistics. + """ language_count = dict() repos_with_language = [repo for repo in repositories["data"]["user"]["repositories"]["edges"] if repo["node"]["primaryLanguage"] is not None] for repo in repos_with_language: diff --git a/sources/main.py b/sources/main.py index 027e032..9bb43e8 100644 --- a/sources/main.py +++ b/sources/main.py @@ -7,7 +7,7 @@ from urllib.parse import quote from humanize import intword, naturalsize, intcomma, precisedelta -from manager_download import init_download_manager, DownloadManager as DM, close_download_manager +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 @@ -17,6 +17,12 @@ from graphics_list_formatter import make_list, make_commit_day_time_list, make_l async def get_waka_time_stats() -> str: + """ + Collects user info from wakatime. + Info includes most common commit time, timezone, language, editors, projects and OSs. + + :returns: String representation of the info. + """ stats = str() data = await DM.get_remote_json("waka_latest") @@ -52,7 +58,13 @@ async def get_waka_time_stats() -> str: return stats -async def get_short_github_info(): +async def get_short_github_info() -> str: + """ + Collects user info from GitHub public profile. + The stats include: disk usage, contributions number, whether the user has opted to hire, public and private repositories number. + + :returns: String representation of the info. + """ stats = f"**🐱 {LM.t('My GitHub Data')}** \n\n" if GHM.USER.disk_usage is None: @@ -64,7 +76,7 @@ async def get_short_github_info(): data = await DM.get_remote_json("github_stats") if len(data["years"]) > 0: - contributions = LM.t('Contributions in the year') % (intcomma(data["years"][0]['total']), data["years"][0]['year']) + contributions = LM.t("Contributions in the year") % (intcomma(data["years"][0]["total"]), data["years"][0]["year"]) stats += f"> 🏆 {contributions}\n > \n" opted_to_hire = GHM.USER.hireable @@ -90,7 +102,10 @@ async def get_short_github_info(): async def get_stats() -> str: """ - Gets API data and returns markdown progress + Creates new README.md content from all the acquired statistics from all places. + The readme includes data from wakatime, contributed lines of code number, GitHub profile info and last updated date. + + :returns: String representation of README.md contents. """ stats = str() repositories = await DM.get_remote_graphql("user_repository_list", username=GHM.USER.login, id=GHM.USER.node_id) @@ -125,8 +140,7 @@ async def get_stats() -> str: await create_loc_graph(yearly_data, GRAPH_PATH) GHM.update_chart(GRAPH_PATH) chart_path = f"{GHM.USER.login}/{GHM.USER.login}/{GHM.branch()}/{GRAPH_PATH}" - stats += '**' + LM.t('Timeline') + '**\n\n' - stats += f"![Lines of Code chart](https://raw.githubusercontent.com/{chart_path})\n\n" + stats += f"**{LM.t('Timeline')}**\n\n![Lines of Code chart](https://raw.githubusercontent.com/{chart_path})\n\n" if EM.SHOW_UPDATED_DATE: stats += f"\n Last Updated on {datetime.now().strftime(EM.UPDATED_DATE_FORMAT)} UTC" @@ -135,16 +149,20 @@ async def get_stats() -> str: async def main(): + """ + Application main function. + Initializes all managers, collects user info and updates README.md if necessary. + """ init_github_manager() await init_download_manager() init_localization_manager() if GHM.update_readme(await get_stats()): print("Readme updated!") - await close_download_manager() + await DM.close_remote_resources() -if __name__ == '__main__': +if __name__ == "__main__": start_time = datetime.now() run(main()) run_delta = datetime.now() - start_time diff --git a/sources/manager_download.py b/sources/manager_download.py index 97e1c7c..538d0a3 100644 --- a/sources/manager_download.py +++ b/sources/manager_download.py @@ -1,3 +1,4 @@ +from asyncio import Task from hashlib import md5 from json import dumps from string import Template @@ -11,6 +12,7 @@ from manager_github import GitHubManager as GHM GITHUB_API_QUERIES = { + # Query to collect info about all user repositories, including: is it a fork, name and owner login. "repos_contributed_to": """ { user(login: "$username") { @@ -25,6 +27,7 @@ GITHUB_API_QUERIES = { } } }""", + # Query to collect info about all commits in user repositories, including: commit date. "repo_committed_dates": """ { repository(owner: "$owner", name: "$name") { @@ -43,6 +46,7 @@ GITHUB_API_QUERIES = { } } }""", + # Query to collect info about all repositories user created or collaborated on, including: name, primary language and owner login. "user_repository_list": """ { user(login: "$username") { @@ -62,6 +66,7 @@ GITHUB_API_QUERIES = { } } """, + # Query to collect info about user commits to given repository, including: commit date, additions and deletions numbers. "repo_commit_list": """ { repository(owner: "$owner", name: "$name") { @@ -90,7 +95,7 @@ GITHUB_API_QUERIES = { } } } -""" +""", } @@ -100,23 +105,15 @@ async def init_download_manager(): - Setup headers for GitHub GraphQL requests. - Launch static queries in background. """ - await DownloadManager.load_remote_resources({ - "linguist": "https://cdn.jsdelivr.net/gh/github/linguist@master/lib/linguist/languages.yml", - "waka_latest": f"https://wakatime.com/api/v1/users/current/stats/last_7_days?api_key={EM.WAKATIME_API_KEY}", - "waka_all": f"https://wakatime.com/api/v1/users/current/all_time_since_today?api_key={EM.WAKATIME_API_KEY}", - "github_stats": f"https://github-contributions.vercel.app/api/v1/{GHM.USER.login}" - }, { - "Authorization": f"Bearer {EM.GH_TOKEN}" - }) - - -async def close_download_manager(): - """ - Initialize download manager: - - Setup headers for GitHub GraphQL requests. - - Launch static queries in background. - """ - await DownloadManager.close_remote_resources("linguist", "waka_latest", "waka_all", "github_stats") + await DownloadManager.load_remote_resources( + { + "linguist": "https://cdn.jsdelivr.net/gh/github/linguist@master/lib/linguist/languages.yml", + "waka_latest": f"https://wakatime.com/api/v1/users/current/stats/last_7_days?api_key={EM.WAKATIME_API_KEY}", + "waka_all": f"https://wakatime.com/api/v1/users/current/all_time_since_today?api_key={EM.WAKATIME_API_KEY}", + "github_stats": f"https://github-contributions.vercel.app/api/v1/{GHM.USER.login}", + }, + {"Authorization": f"Bearer {EM.GH_TOKEN}"}, + ) class DownloadManager: @@ -130,6 +127,7 @@ class DownloadManager: DownloadManager launches all static queries asynchronously upon initialization and caches their results. It also executes dynamic queries upon request and caches result. """ + _client = AsyncClient(timeout=60.0) _REMOTE_RESOURCES_CACHE = dict() @@ -145,14 +143,16 @@ class DownloadManager: DownloadManager._client.headers = github_headers @staticmethod - async def close_remote_resources(*resource: str): + async def close_remote_resources(): """ - Prepare DownloadManager to launch GitHub API queries and launch all static queries. - :param resources: Dictionary of static queries, "IDENTIFIER": "URL". - :param github_headers: Dictionary of headers for GitHub API queries. + Close DownloadManager and cancel all un-awaited static web queries. + Await all queries that could not be cancelled. """ - for resource in [DownloadManager._REMOTE_RESOURCES_CACHE[r] for r in resource if isinstance(DownloadManager._REMOTE_RESOURCES_CACHE[r], Awaitable)]: - resource.cancel() + for resource in DownloadManager._REMOTE_RESOURCES_CACHE.values(): + if isinstance(resource, Task): + resource.cancel() + elif isinstance(resource, Awaitable): + await resource @staticmethod async def _get_remote_resource(resource: str, convertor: Optional[Callable[[bytes], Dict]]) -> Dict: @@ -208,9 +208,7 @@ class DownloadManager: """ key = f"{query}_{md5(dumps(kwargs, sort_keys=True).encode('utf-8')).digest()}" if key not in DownloadManager._REMOTE_RESOURCES_CACHE: - res = await DownloadManager._client.post("https://api.github.com/graphql", json={ - "query": Template(GITHUB_API_QUERIES[query]).substitute(kwargs) - }) + res = await DownloadManager._client.post("https://api.github.com/graphql", json={"query": Template(GITHUB_API_QUERIES[query]).substitute(kwargs)}) DownloadManager._REMOTE_RESOURCES_CACHE[key] = res else: res = DownloadManager._REMOTE_RESOURCES_CACHE[key] diff --git a/sources/manager_environment.py b/sources/manager_environment.py index 85f38d5..85b9ae5 100644 --- a/sources/manager_environment.py +++ b/sources/manager_environment.py @@ -2,35 +2,44 @@ from os import getenv, environ class EnvironmentManager: - _TRUTHY = ['true', '1', 't', 'y', 'yes'] + """ + Class for handling all environmental variables used by the action. + There are only two required variables: `INPUT_GH_TOKEN` and `INPUT_WAKATIME_API_KEY`. + The others have a provided default value. + For all boolean variables a 'truthy'-list is checked (not only true/false, but also 1, t, y and yes are accepted). + List variable `IGNORED_REPOS` is split and parsed. + Integer variable `SYMBOL_VERSION` is parsed. + """ - GH_TOKEN = environ['INPUT_GH_TOKEN'] - WAKATIME_API_KEY = environ['INPUT_WAKATIME_API_KEY'] + _TRUTHY = ["true", "1", "t", "y", "yes"] + + GH_TOKEN = environ["INPUT_GH_TOKEN"] + WAKATIME_API_KEY = environ["INPUT_WAKATIME_API_KEY"] SECTION_NAME = getenv("INPUT_SECTION_NAME", "waka") - BRANCH_NAME = getenv('INPUT_PUSH_BRANCH_NAME', "") + BRANCH_NAME = getenv("INPUT_PUSH_BRANCH_NAME", "") - SHOW_OS = getenv('INPUT_SHOW_OS', "False").lower() in _TRUTHY - SHOW_PROJECTS = getenv('INPUT_SHOW_PROJECTS', "True").lower() in _TRUTHY - SHOW_EDITORS = getenv('INPUT_SHOW_EDITORS', "True").lower() in _TRUTHY - SHOW_TIMEZONE = getenv('INPUT_SHOW_TIMEZONE', "True").lower() in _TRUTHY - SHOW_COMMIT = getenv('INPUT_SHOW_COMMIT', "True").lower() in _TRUTHY - SHOW_LANGUAGE = getenv('INPUT_SHOW_LANGUAGE', "True").lower() in _TRUTHY - SHOW_LINES_OF_CODE = getenv('INPUT_SHOW_LINES_OF_CODE', "False").lower() in _TRUTHY - SHOW_LANGUAGE_PER_REPO = getenv('INPUT_SHOW_LANGUAGE_PER_REPO', "True").lower() in _TRUTHY - SHOW_LOC_CHART = getenv('INPUT_SHOW_LOC_CHART', "True").lower() in _TRUTHY - SHOW_DAYS_OF_WEEK = getenv('INPUT_SHOW_DAYS_OF_WEEK', "True").lower() in _TRUTHY - SHOW_PROFILE_VIEWS = getenv('INPUT_SHOW_PROFILE_VIEWS', "True").lower() in _TRUTHY - SHOW_SHORT_INFO = getenv('INPUT_SHOW_SHORT_INFO', "True").lower() in _TRUTHY - SHOW_UPDATED_DATE = getenv('INPUT_SHOW_UPDATED_DATE', "True").lower() in _TRUTHY - SHOW_TOTAL_CODE_TIME = getenv('INPUT_SHOW_TOTAL_CODE_TIME', "True").lower() in _TRUTHY + SHOW_OS = getenv("INPUT_SHOW_OS", "False").lower() in _TRUTHY + SHOW_PROJECTS = getenv("INPUT_SHOW_PROJECTS", "True").lower() in _TRUTHY + SHOW_EDITORS = getenv("INPUT_SHOW_EDITORS", "True").lower() in _TRUTHY + SHOW_TIMEZONE = getenv("INPUT_SHOW_TIMEZONE", "True").lower() in _TRUTHY + SHOW_COMMIT = getenv("INPUT_SHOW_COMMIT", "True").lower() in _TRUTHY + SHOW_LANGUAGE = getenv("INPUT_SHOW_LANGUAGE", "True").lower() in _TRUTHY + SHOW_LINES_OF_CODE = getenv("INPUT_SHOW_LINES_OF_CODE", "False").lower() in _TRUTHY + SHOW_LANGUAGE_PER_REPO = getenv("INPUT_SHOW_LANGUAGE_PER_REPO", "True").lower() in _TRUTHY + SHOW_LOC_CHART = getenv("INPUT_SHOW_LOC_CHART", "True").lower() in _TRUTHY + SHOW_DAYS_OF_WEEK = getenv("INPUT_SHOW_DAYS_OF_WEEK", "True").lower() in _TRUTHY + SHOW_PROFILE_VIEWS = getenv("INPUT_SHOW_PROFILE_VIEWS", "True").lower() in _TRUTHY + SHOW_SHORT_INFO = getenv("INPUT_SHOW_SHORT_INFO", "True").lower() in _TRUTHY + SHOW_UPDATED_DATE = getenv("INPUT_SHOW_UPDATED_DATE", "True").lower() in _TRUTHY + SHOW_TOTAL_CODE_TIME = getenv("INPUT_SHOW_TOTAL_CODE_TIME", "True").lower() in _TRUTHY - COMMIT_BY_ME = getenv('INPUT_COMMIT_BY_ME', "False").lower() in _TRUTHY - COMMIT_MESSAGE = getenv('INPUT_COMMIT_MESSAGE', "Updated with Dev Metrics") - COMMIT_USERNAME = getenv('INPUT_COMMIT_USERNAME', "") - COMMIT_EMAIL = getenv('INPUT_COMMIT_EMAIL', "") + COMMIT_BY_ME = getenv("INPUT_COMMIT_BY_ME", "False").lower() in _TRUTHY + COMMIT_MESSAGE = getenv("INPUT_COMMIT_MESSAGE", "Updated with Dev Metrics") + COMMIT_USERNAME = getenv("INPUT_COMMIT_USERNAME", "") + COMMIT_EMAIL = getenv("INPUT_COMMIT_EMAIL", "") - LOCALE = getenv('INPUT_LOCALE', "en") - 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')) + LOCALE = getenv("INPUT_LOCALE", "en") + 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")) diff --git a/sources/manager_github.py b/sources/manager_github.py index 654dac2..45ce2e3 100644 --- a/sources/manager_github.py +++ b/sources/manager_github.py @@ -8,6 +8,8 @@ from manager_environment import EnvironmentManager as EM def init_github_manager(): """ + Initialize GitHub manager. + Current user, user readme repo and readme file are downloaded. """ GitHubManager.prepare_github_env() print(f"Current user: {GitHubManager.USER.login}") @@ -16,61 +18,79 @@ def init_github_manager(): class GitHubManager: USER: AuthenticatedUser REPO: Repository - README: ContentFile - README_CONTENTS: str + _README: ContentFile + _README_CONTENTS: str - _START_COMMENT = f'' - _END_COMMENT = f'' + _START_COMMENT = f"" + _END_COMMENT = f"" _README_REGEX = f"{_START_COMMENT}[\\s\\S]+{_END_COMMENT}" @staticmethod def prepare_github_env(): """ + Download and store for future use: + - Current GitHub user. + - Named repo of the user [username]/[username]. + - README.md file of this repo. + - Parsed contents of the file. """ github = Github(EM.GH_TOKEN) GitHubManager.USER = github.get_user() - GitHubManager.REPO = github.get_repo(f"{GitHubManager.USER.login}/{GitHubManager.USER.login}") - GitHubManager.README = GitHubManager.REPO.get_readme() - GitHubManager.README_CONTENTS = str(b64decode(GitHubManager.README.content), 'utf-8') + GitHubManager._REPO = github.get_repo(f"{GitHubManager.USER.login}/{GitHubManager.USER.login}") + GitHubManager._README = GitHubManager._REPO.get_readme() + GitHubManager._README_CONTENTS = str(b64decode(GitHubManager._README.content), "utf-8") @staticmethod - def _generate_new_readme(stats: str): + def _generate_new_readme(stats: str) -> str: """ - Generate a new Readme.md + Generates new README.md file, inserts its contents between start and end tags. + + :param stats: contents to insert. + :returns: new README.md string. """ readme_stats = f"{GitHubManager._START_COMMENT}\n{stats}\n{GitHubManager._END_COMMENT}" - return sub(GitHubManager._README_REGEX, readme_stats, GitHubManager.README_CONTENTS) + return sub(GitHubManager._README_REGEX, readme_stats, GitHubManager._README_CONTENTS) @staticmethod - def _get_author(): + def _get_author() -> InputGitAuthor: """ + Gets GitHub commit author specified by environmental variables. + It is the user himself or a 'readme-bot'. + + :returns: Commit author. """ if EM.COMMIT_BY_ME: - return InputGitAuthor( - GitHubManager.USER.login or EM.COMMIT_USERNAME, - GitHubManager.USER.email or EM.COMMIT_EMAIL - ) + return InputGitAuthor(GitHubManager.USER.login or EM.COMMIT_USERNAME, GitHubManager.USER.email or EM.COMMIT_EMAIL) else: - return InputGitAuthor( - EM.COMMIT_USERNAME or 'readme-bot', - EM.COMMIT_EMAIL or '41898282+github-actions[bot]@users.noreply.github.com' - ) + return InputGitAuthor(EM.COMMIT_USERNAME or "readme-bot", EM.COMMIT_EMAIL or "41898282+github-actions[bot]@users.noreply.github.com") @staticmethod def branch() -> str: + """ + Gets name of branch to commit to specified by environmental variables. + It is the default branch (regularly, 'main' or 'master') or a branch specified by user. + + :returns: Commit author. + """ return GitHubManager.REPO.default_branch if EM.BRANCH_NAME == "" else EM.BRANCH_NAME @staticmethod def update_readme(stats: str) -> bool: + """ + Updates readme with given data if necessary. + Uses commit author, commit message and branch name specified by environmental variables. + + :returns: whether the README.md file was updated or not. + """ new_readme = GitHubManager._generate_new_readme(stats) - if new_readme != GitHubManager.README_CONTENTS: + if new_readme != GitHubManager._README_CONTENTS: GitHubManager.REPO.update_file( - path=GitHubManager.README.path, + path=GitHubManager._README.path, message=EM.COMMIT_MESSAGE, content=new_readme, - sha=GitHubManager.README.sha, + sha=GitHubManager._README.sha, branch=GitHubManager.branch(), - committer=GitHubManager._get_author() + committer=GitHubManager._get_author(), ) return True else: @@ -78,6 +98,12 @@ class GitHubManager: @staticmethod def update_chart(chart_path: str): + """ + Updates lines of code chart. + Uses commit author, commit message and branch name specified by environmental variables. + + :param chart_path: path to saved lines of code chart. + """ with open(chart_path, "rb") as input_file: data = input_file.read() try: diff --git a/sources/manager_localization.py b/sources/manager_localization.py index 3946869..497ec2e 100644 --- a/sources/manager_localization.py +++ b/sources/manager_localization.py @@ -7,19 +7,37 @@ from manager_environment import EnvironmentManager as EM def init_localization_manager(): """ + Initialize localization manager. + Load GUI translations JSON file. """ LocalizationManager.load_localization("translation.json") class LocalizationManager: + """ + Class for handling localization (and maybe other file IO in future). + Stores localization in dictionary. + """ + _LOCALIZATION: Dict[str, str] = dict() @staticmethod def load_localization(file: str): - with open(join(dirname(__file__), file), encoding='utf-8') as config_file: + """ + Read localization file and store locale defined with environmental variable. + + :param file: Localization file path, related to current file (in sources root). + """ + with open(join(dirname(__file__), file), encoding="utf-8") as config_file: data = load(config_file) LocalizationManager._LOCALIZATION = data[EM.LOCALE] @staticmethod def t(key: str) -> str: + """ + Translate string to current localization. + + :param key: Localization key. + :returns: Translation string. + """ return LocalizationManager._LOCALIZATION[key] diff --git a/sources/yearly_commit_calculator.py b/sources/yearly_commit_calculator.py index f72d3ac..891025a 100644 --- a/sources/yearly_commit_calculator.py +++ b/sources/yearly_commit_calculator.py @@ -8,6 +8,13 @@ from manager_github import GitHubManager as GHM async def calculate_yearly_commit_data(repositories: Dict) -> Dict: + """ + Calculate commit data by years. + Commit data includes difference between contribution additions and deletions in each quarter of each recorded year. + + :param repositories: user repositories info dictionary. + :returns: Commit quarter yearly data dictionary. + """ yearly_data = dict() total = len(repositories["data"]["user"]["repositories"]["edges"]) for ind, repo in enumerate(repositories["data"]["user"]["repositories"]["edges"]): @@ -17,7 +24,14 @@ async def calculate_yearly_commit_data(repositories: Dict) -> Dict: return yearly_data -async def update_yearly_data_with_commit_stats(repo_details: Dict, yearly_data: Dict) -> Dict: +async def update_yearly_data_with_commit_stats(repo_details: Dict, yearly_data: Dict): + """ + Updates yearly commit data with commits from given repository. + Skips update if the commit isn't related to any repository. + + :param repo_details: Dictionary with information about the given repository. + :param yearly_data: Yearly data dictionary to update. + """ commit_data = await DM.get_remote_graphql("repo_commit_list", owner=repo_details["owner"]["login"], name=repo_details["name"], id=GHM.USER.node_id) if commit_data["data"]["repository"] is None: @@ -36,4 +50,4 @@ async def update_yearly_data_with_commit_stats(repo_details: Dict, yearly_data: yearly_data[curr_year][quarter] = dict() if repo_details["primaryLanguage"]["name"] not in yearly_data[curr_year][quarter]: yearly_data[curr_year][quarter][repo_details["primaryLanguage"]["name"]] = 0 - yearly_data[curr_year][quarter][repo_details["primaryLanguage"]["name"]] += (commit["additions"] - commit["deletions"]) + yearly_data[curr_year][quarter][repo_details["primaryLanguage"]["name"]] += commit["additions"] - commit["deletions"]