diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint_and_mypy.yml similarity index 77% rename from .github/workflows/pylint.yml rename to .github/workflows/pylint_and_mypy.yml index 66e6edb..b08e64a 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint_and_mypy.yml @@ -1,4 +1,4 @@ -name: Pylint +name: Pylint and MyPy on: [push] @@ -17,8 +17,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint pip install -r requirements.txt + pip install pylint mypy types-requests types-pytz - name: Analysing the code with pylint run: | pylint $(git ls-files '*.py') + - name: Type checking with mypy + run: | + mypy util generator diff --git a/README.md b/README.md index bd2657b..8032996 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [简体中文](README_zh-CN.md) | English - [![Deploy state](https://github.com/WCY-dt/my-github-2024/actions/workflows/deploy.yml/badge.svg)](https://github.com/WCY-dt/my-github-2024/actions/workflows/deploy.yml) [![Server Status](https://img.shields.io/badge/dynamic/json?logo=linux&color=brightgreen&label=Server%20status&query=%24.status&cacheSeconds=600&url=https%3A%2F%2F2024.ch3nyang.top%2Fstatus)](https://2024.ch3nyang.top) + [![Pylint](https://github.com/WCY-dt/my-github-2024/actions/workflows/pylint.yml/badge.svg)](https://github.com/WCY-dt/my-github-2024/actions/workflows/pylint.yml) [![Deploy state](https://github.com/WCY-dt/my-github-2024/actions/workflows/deploy.yml/badge.svg)](https://github.com/WCY-dt/my-github-2024/actions/workflows/deploy.yml) [![Server Status](https://img.shields.io/badge/dynamic/json?logo=linux&color=brightgreen&label=Server%20status&query=%24.status&cacheSeconds=600&url=https%3A%2F%2F2024.ch3nyang.top%2Fstatus)](https://2024.ch3nyang.top) 👉 Try it now: https://2024.ch3nyang.top diff --git a/README_zh-CN.md b/README_zh-CN.md index b638f04..b43da0e 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -5,7 +5,7 @@ [English](README.md) | 简体中文 - [![Deploy state](https://github.com/WCY-dt/my-github-2024/actions/workflows/deploy.yml/badge.svg)](https://github.com/WCY-dt/my-github-2024/actions/workflows/deploy.yml) [![Server Status](https://img.shields.io/badge/dynamic/json?logo=linux&color=brightgreen&label=Server%20status&query=%24.status&cacheSeconds=600&url=https%3A%2F%2F2024.ch3nyang.top%2Fstatus)](https://2024.ch3nyang.top) + [![Pylint and Mypy](https://github.com/WCY-dt/my-github-2024/actions/workflows/pylint_and_mypy.yml/badge.svg)](https://github.com/WCY-dt/my-github-2024/actions/workflows/pylint_and_mypy.yml) [![Deploy state](https://github.com/WCY-dt/my-github-2024/actions/workflows/deploy.yml/badge.svg)](https://github.com/WCY-dt/my-github-2024/actions/workflows/deploy.yml) [![Server Status](https://img.shields.io/badge/dynamic/json?logo=linux&color=brightgreen&label=Server%20status&query=%24.status&cacheSeconds=600&url=https%3A%2F%2F2024.ch3nyang.top%2Fstatus)](https://2024.ch3nyang.top) 👉 立即体验: https://2024.ch3nyang.top diff --git a/generator/fetch.py b/generator/fetch.py index 7e728f0..f3adb73 100644 --- a/generator/fetch.py +++ b/generator/fetch.py @@ -9,6 +9,7 @@ import logging from log.logging_config import setup_logging +from util import Github setup_logging() @@ -43,7 +44,7 @@ } -def fetch_github(github: object, year: int, skip_fetch: bool = False) -> tuple: +def fetch_github(github: Github, year: int, skip_fetch: bool = False) -> tuple: """ Fetches and processes GitHub data for a given year. diff --git a/util/__init__.py b/util/__init__.py index 79a6b8b..26e375f 100644 --- a/util/__init__.py +++ b/util/__init__.py @@ -10,6 +10,7 @@ import logging from datetime import timedelta from datetime import timezone as dt_timezone +from typing import Any import pytz @@ -27,7 +28,7 @@ class Github: Attributes: access_token (str): The Github access token. username (str): The Github username. - timezone (pytz.timezone): The timezone. + timezone (pytz.BaseTzInfo): The timezone. data (dict | list | None): The fetched data. Methods: @@ -54,6 +55,8 @@ class Github: result: Get the result data. """ + data: dict[str, Any] | None + def __init__( self, access_token: str, username: str, timezone: str = "Asia/Shanghai" ) -> None: @@ -67,10 +70,10 @@ def __init__( """ self.access_token: str = access_token self.username: str = username - self.timezone: pytz.timezone = self._parse_timezone(timezone) + self.timezone: pytz.BaseTzInfo = self._parse_timezone(timezone) self.data = None - def _parse_timezone(self, tz_str: str) -> pytz.timezone: + def _parse_timezone(self, tz_str: str) -> pytz.BaseTzInfo: """ Parse the timezone string to a pytz timezone object. @@ -78,7 +81,7 @@ def _parse_timezone(self, tz_str: str) -> pytz.timezone: tz_str (str): The timezone string. Returns: - pytz.timezone: The pytz timezone object. + pytz.BaseTzInfo: The pytz timezone object. """ try: return pytz.timezone(tz_str) @@ -86,7 +89,7 @@ def _parse_timezone(self, tz_str: str) -> pytz.timezone: if tz_str.startswith("+") or tz_str.startswith("-"): hours_offset = int(tz_str) timezone = dt_timezone(timedelta(hours=hours_offset)) - return pytz.timezone(timezone) + return pytz.timezone(str(timezone)) raise e @@ -186,7 +189,8 @@ def filter_json(self, key: dict) -> "Github": Returns: Github: The Github object with the filtered data. """ - self.data = gh_filter.json_key(self.data, key) + if self.data is not None: + self.data = gh_filter.json_key(self.data, key) return self def count_all(self) -> "Github": @@ -262,7 +266,7 @@ def write_to_file(self, filename: str) -> "Github": f.write(json.dumps(self.result, indent=4)) return self - def _get_result_structure(self, data: dict | list | None) -> dict: + def _get_result_structure(self, data: dict | list | None) -> dict | list | str: """ Get the result structure of the data. @@ -270,7 +274,7 @@ def _get_result_structure(self, data: dict | list | None) -> dict: data (dict | list): The data to get the result structure from. Returns: - dict: The result structure of the data. + dict | list | str: The result structure of the data. """ if isinstance(data, list): return [self._get_result_structure(data[0])] @@ -299,7 +303,7 @@ def result(self) -> dict | list | None: return self.data @result.setter - def result(self, value: dict | list | None) -> None: + def result(self, value: dict[str, Any] | None) -> None: """ Set the result data. diff --git a/util/gh_count.py b/util/gh_count.py index 4e6f332..1089c54 100644 --- a/util/gh_count.py +++ b/util/gh_count.py @@ -102,9 +102,9 @@ def commits_types_number(data: dict) -> dict: Returns: dict: The data containing the number of commits of each type. """ - commits_types = {} + commits_types: dict[str, int] = {} for repo in data["repos_details"]: - repo_commits_types = {} + repo_commits_types: dict[str, int] = {} for commit in repo["commits_details"]: commit_type = _get_commit_type(commit["message"]) @@ -127,7 +127,7 @@ def commits_types_number(data: dict) -> dict: def _count_monthly(items: list, key: str) -> dict: - monthly_count = {} + monthly_count: dict[int, int] = {} for item in items: month = datetime.fromisoformat(item[key]).month if month in monthly_count: @@ -172,7 +172,7 @@ def commits_monthly_number(data: dict) -> dict: def _count_weekly(items: list, key: str) -> dict: - weekly_count = {} + weekly_count: dict[int, int] = {} for item in items: weekday = datetime.fromisoformat(item[key]).weekday() if weekday in weekly_count: @@ -182,7 +182,7 @@ def _count_weekly(items: list, key: str) -> dict: return weekly_count -def _fill_weekly_list(weekly_count: dict) -> dict: +def _fill_weekly_list(weekly_count: dict) -> list: weekly_list = [] for i in range(7): if i in weekly_count: @@ -202,7 +202,7 @@ def commits_weekdaily_number(data: dict) -> dict: Returns: dict: The data containing the number of commits in each weekday. """ - commits_weekly = {} + commits_weekly: dict[int, int] = {} for repo in data["repos_details"]: repo_commits_weekly = _count_weekly(repo["commits_details"], "created_time") @@ -222,7 +222,7 @@ def commits_weekdaily_number(data: dict) -> dict: def _count_daily(items: list, key: str) -> dict: - daily_count = {} + daily_count: dict[str, int] = {} for item in items: date = datetime.fromisoformat(item[key]).date().isoformat() if date in daily_count: @@ -233,7 +233,7 @@ def _count_daily(items: list, key: str) -> dict: def _fill_daily_list(daily_count: dict) -> dict: - daily_list = {} + daily_list: dict[int, list[int]] = {} for year in range(2000, datetime.now().year + 2): days_in_year = ( 366 if year % 4 == 0 and year % 100 != 0 or year % 400 == 0 else 365 @@ -280,7 +280,7 @@ def commits_daily_number(data: dict) -> dict: def _count_hourly(items: list, key: str) -> dict: - hourly_count = {} + hourly_count: dict[int, int] = {} for item in items: hour = datetime.fromisoformat(item[key]).hour if hour in hourly_count: @@ -310,7 +310,7 @@ def commits_hourly_number(data: dict) -> dict: Returns: dict: The data containing the number of commits in each hour. """ - commits_hourly = {} + commits_hourly: dict[int, int] = {} for repo in data["repos_details"]: repo_commits_hourly = _count_hourly(repo["commits_details"], "created_time") @@ -355,8 +355,8 @@ def repos_languages_number(data: dict) -> dict: Returns: dict: The data containing the number of repositories using each language. """ - languages_num = {} - repos_languages_count = {} + languages_num: dict[str, int] = {} + repos_languages_count: dict[str, int] = {} for repo in data["repos_details"]: for key, value in repo["languages_num"].items(): if key in languages_num: diff --git a/util/gh_fetch.py b/util/gh_fetch.py index cb784e0..19ec585 100644 --- a/util/gh_fetch.py +++ b/util/gh_fetch.py @@ -2,35 +2,35 @@ This module fetches the Github user information. Functions: - get_github_info(username: str, token: str, timezone: pytz.timezone, year: int) -> dict: + get_github_info(username: str, token: str, timezone: pytz.BaseTzInfo, year: int) -> dict: Get the Github user information. _get_response(url: str, token: str, per_page: int = 100, accept: str = "application/vnd.github.v3+json", timeout: int = 10) -> requests.Response: Get the response from the Github API. - _parse_time(time_str: str, timezone: pytz.timezone) -> str: + _parse_time(time_str: str, timezone: pytz.BaseTzInfo) -> str: Parse the time string to the timezone. _paginate(func: callable) -> callable: Paginate the data from the Github API. - _get_user_issues(username: str, url: str, token: str, timezone: pytz.timezone, + _get_user_issues(username: str, url: str, token: str, timezone: pytz.BaseTzInfo, year: int) -> list: Get the user issues. - _get_user_prs(username: str, url: str, token: str, timezone: pytz.timezone, + _get_user_prs(username: str, url: str, token: str, timezone: pytz.BaseTzInfo, year: int) -> list: Get the user pull requests. - _get_user_repos(username: str, url: str, token: str, timezone: pytz.timezone, + _get_user_repos(username: str, url: str, token: str, timezone: pytz.BaseTzInfo, year: int) -> list: Get the user repositories. _get_user_repo_languages(url: str, token: str) -> dict: Get the user repository languages. - _get_user_commits(username: str, url: str, token: str, timezone: pytz.timezone, + _get_user_commits(username: str, url: str, token: str, timezone: pytz.BaseTzInfo, year: int) -> list: Get the user commits. """ @@ -39,6 +39,7 @@ import time import urllib.parse from datetime import datetime +from typing import Callable, Any import pytz import requests @@ -49,13 +50,13 @@ setup_logging() -def _parse_time(time_str: str, timezone: pytz.timezone) -> str: +def _parse_time(time_str: str, timezone: pytz.BaseTzInfo) -> str: """ Parse the time string to the timezone. Args: time_str (str): The time string. - timezone (pytz.timezone): The timezone. + timezone (pytz.BaseTzInfo): The timezone. Returns: str: The time string in the timezone. @@ -79,19 +80,19 @@ def _parse_time(time_str: str, timezone: pytz.timezone) -> str: return result -def _paginate(func: callable) -> callable: +def _paginate(func: Callable) -> Callable: """ Paginate the data from the Github API. Args: - func (callable): The function to paginate. + func (Callable): The function to paginate. Returns: - callable: The wrapper function to paginate the data. + Callable: The wrapper function to paginate the data. """ def wrapper( - username: str, url: str, token: str, timezone: pytz.timezone, year: int + username: str, url: str | None, token: str, timezone: pytz.BaseTzInfo, year: int ) -> list: """ Wrapper function to paginate the data. @@ -100,7 +101,7 @@ def wrapper( username (str): The Github username. url (str): The Github API URL to fetch the data. token (str): The Github access token. - timezone (pytz.timezone): The timezone. + timezone (pytz.BaseTzInfo): The timezone. year (int): The year to fetch the data. Returns: @@ -121,7 +122,7 @@ def wrapper( links = res.headers.get("Link") if links and cont: - next_url = None + next_url: str | None = None for link in links.split(","): if 'rel="next"' in link: next_url = link[link.find("<") + 1 : link.find(">")] @@ -164,7 +165,7 @@ def _get_response( parsed_url = urllib.parse.urlparse(url) query_params = urllib.parse.parse_qs(parsed_url.query) - query_params["per_page"] = str(per_page) + query_params["per_page"] = [str(per_page)] new_query_string = urllib.parse.urlencode(query_params, doseq=True) url = urllib.parse.urlunparse(parsed_url._replace(query=new_query_string)) @@ -179,7 +180,10 @@ def _get_response( response.raise_for_status() except Exception as e: logging.error("Failed to fetch data from %s: %s", url, e) - logging.error("Response: %s", response.text) + if response is not None: + logging.error("Response: %s", response.text) + else: + logging.error("Response is None") raise e else: return response @@ -188,7 +192,7 @@ def _get_response( def get_github_info( - username: str, token: str, timezone: pytz.timezone, year: int + username: str, token: str, timezone: pytz.BaseTzInfo, year: int ) -> dict: """ Get the Github user information. @@ -196,7 +200,7 @@ def get_github_info( Args: username (str): The Github username. token (str): The Github access token. - timezone (pytz.timezone): The timezone. + timezone (pytz.BaseTzInfo): The timezone. year (int): The year to fetch the data. Returns: @@ -246,8 +250,8 @@ def get_github_info( @_paginate def _get_user_issues( - username: str, url: str, token: str, timezone: pytz.timezone, year: int -) -> list: + username: str, url: str, token: str, timezone: pytz.BaseTzInfo, year: int +) -> tuple[list, requests.Response, bool]: """ Get the user issues. @@ -255,21 +259,22 @@ def _get_user_issues( username (str): The Github username. url (str): The Github API URL. token (str): The Github access token. - timezone (pytz.timezone): The timezone. + timezone (pytz.BaseTzInfo): The timezone. year (int): The year to fetch the data. Returns: - list: The user issues. + tuple[list, requests.Response, bool]: + The user issues, response, and continue flag. """ _ = username # Avoid pylint error response = _get_response(url, token) if response.status_code == 409: - return [], response + return [], response, False issues = response.json().get("items") - issues_details = [] + issues_details: list[dict[str, Any]] = [] for issue in issues: created_time = _parse_time(issue.get("created_at"), timezone) @@ -291,8 +296,8 @@ def _get_user_issues( @_paginate def _get_user_prs( - username: str, url: str, token: str, timezone: pytz.timezone, year: int -) -> list: + username: str, url: str, token: str, timezone: pytz.BaseTzInfo, year: int +) -> tuple[list, requests.Response, bool]: """ Get the user pull requests. @@ -300,20 +305,21 @@ def _get_user_prs( username (str): The Github username. url (str): The Github API URL. token (str): The Github access token. - timezone (pytz.timezone): The timezone. + timezone (pytz.BaseTzInfo): The timezone. year (int): The year to fetch the data. Returns: - list: The user pull requests. + tuple[list, requests.Response, bool]: + The user pull requests, response, and continue flag. """ _ = username # Avoid pylint error response = _get_response(url, token) if response.status_code == 409: - return [], response + return [], response, False prs = response.json().get("items") - prs_details = [] + prs_details: list[dict[str, Any]] = [] for pr in prs: created_time = _parse_time(pr.get("created_at"), timezone) @@ -344,8 +350,8 @@ def _get_user_prs( @_paginate def _get_user_repos( - username: str, url: str, token: str, timezone: pytz.timezone, year: int -) -> list: + username: str, url: str, token: str, timezone: pytz.BaseTzInfo, year: int +) -> tuple[list, requests.Response, bool]: """ Get the user repositories. @@ -353,18 +359,19 @@ def _get_user_repos( username (str): The Github username. url (str): The Github API URL. token (str): The Github access token. - timezone (pytz.timezone): The timezone. + timezone (pytz.BaseTzInfo): The timezone. year (int): The year to fetch the data. Returns: - list: The user repositories. + tuple[list, requests.Response, bool]: + The user repositories, response, and continue flag. """ response = _get_response(url, token) if response.status_code == 409: - return [], response + return [], response, False repos = response.json() - repos_details = [] + repos_details: list[dict[str, Any]] = [] for repo in repos: try: @@ -422,8 +429,8 @@ def _get_user_repo_languages(url: str, token: str) -> dict: @_paginate def _get_user_commits( - username: str, url: str, token: str, timezone: pytz.timezone, year: int -) -> list: + username: str, url: str, token: str, timezone: pytz.BaseTzInfo, year: int +) -> tuple[list, requests.Response, bool]: """ Get the user commits. @@ -431,19 +438,20 @@ def _get_user_commits( username (str): The Github username. url (str): The Github API URL. token (str): The Github access token. - timezone (pytz.timezone): The timezone. + timezone (pytz.BaseTzInfo): The timezone. year (int): The year to fetch the data. Returns: - list: The user commits. + tuple[list, requests.Response, bool]: + The user commits, response, and continue flag. """ response = _get_response(url, token) if response.status_code == 409: - return [], response + return [], response, False response.raise_for_status() commits = response.json() - commits_details = [] + commits_details: list[dict[str, Any]] = [] for commit in commits: committer = None