From 13a4f601e25211a94086ac46ffc2aeb73e5dd58b Mon Sep 17 00:00:00 2001 From: devfle <52854338+devfle@users.noreply.github.com> Date: Tue, 6 Dec 2022 15:10:31 +0000 Subject: [PATCH] feat: add project files --- README.md | 72 ++++++++++++++++++++++++++++- action.yml | 54 ++++++++++++++++++++++ graphql_query.py | 14 ++++++ main.py | 55 ++++++++++++++++++++++ readme_level.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 6 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 action.yml create mode 100644 graphql_query.py create mode 100644 main.py create mode 100644 readme_level.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index 9de73e6..e5ee705 100644 --- a/README.md +++ b/README.md @@ -1 +1,71 @@ -# readme-level-up \ No newline at end of file +# Read Me Level Up + +Readme level up is a small action that converts interactions made on GitHub into experience points. If you have enough experience points your level increases. The action suits perfectly for the profile page on Github. + +01 + + +# Setup Action + +I explain here how to set up the action. The Github profile readme serves as an example. But any other readme can be used as well. + +1. Add the following comments to your readme file: + +```text + + +``` + +2. create a new workflow with the following example content. + + +# Example Action Setup + +```yml +name: Update Readme Level +on: + workflow_dispatch: + schedule: + - cron: '0 08 * * *' +jobs: + update-readme: + runs-on: ubuntu-latest + steps: + - name: checkout project + uses: actions/checkout@v3 + - name: update markdown file + uses: devfle/readme-level-up@main + with: + github_username: GITHUB_USERNAME + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: commit markdown file + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git commit -a -m "update readme" + - name: push changes + uses: ad-m/github-push-action@v0.6.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: main +``` + + + +# Environment Variables + +All available environment variables and their default values can be found in the [action.yml file.](../main/action.yml) + +# Todos +I started the project because I had a fun idea and wanted to learn Python. Some things still need to be done: +- add types to all vars +- add error management +- add more data to level calculation +- add more options as env vars +- and more... + +If you have any ideas about what could be optimized, feel free to create an issue. + +# Contribution Guide + +coming soon diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..446670e --- /dev/null +++ b/action.yml @@ -0,0 +1,54 @@ +name: 'github-level-system' +author: 'devfle' +description: 'Readme level up is a small action that converts interactions made on GitHub into experience points.' + +inputs: + github_token: + description: 'A Github token that allows to communicate with the GitHub GraphQL API. The default should be sufficient.' + required: false + default: ${{ github.token }} + github_username: + description: 'Your GitHub username to communicate with the GitHub API' + required: true + default: '' + progress_bar_char_length: + description: 'The character length of the Progress bar' + required: false + default: 30 + empty_bar: + description: 'The character for the empty state of the progress bar' + required: false + default: '░' + filled_bar: + description: 'The character for the filled state of the progress bar' + required: false + default: '█' + readme_path: + description: 'The path to the markdown file to be edited' + required: false + default: './README.md' + +runs: + using: 'composite' + steps: + - name: setup python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: install packages + run: pip install -r $GITHUB_ACTION_PATH/requirements.txt + shell: bash + - name: run script + run: python $GITHUB_ACTION_PATH/main.py + shell: bash + env: + INPUT_GITHUB_TOKEN: ${{ inputs.github_token }} + INPUT_GITHUB_USERNAME: ${{ inputs.github_username }} + INPUT_PROGRESS_BAR_CHAR_LENGTH: ${{ inputs.progress_bar_char_length }} + INPUT_EMPTY_BAR: ${{ inputs.empty_bar }} + INPUT_FILLED_BAR: ${{ inputs.filled_bar }} + INPUT_README_PATH: ${{ inputs.readme_path }} + +branding: + icon: 'arrow-up-circle' + color: 'blue' diff --git a/graphql_query.py b/graphql_query.py new file mode 100644 index 0000000..303de29 --- /dev/null +++ b/graphql_query.py @@ -0,0 +1,14 @@ +"""Query for github graphql api""" +from os import getenv + +QUERY = f""" + {{ + user(login: "{getenv("INPUT_GITHUB_USERNAME")}") {{ + contributionsCollection {{ + contributionCalendar {{ + totalContributions + }} + }} + }} + }} + """ diff --git a/main.py b/main.py new file mode 100644 index 0000000..6adcc48 --- /dev/null +++ b/main.py @@ -0,0 +1,55 @@ +"""The main file of this project.""" +from os import getenv +from re import sub +from operator import itemgetter +from readme_level import ReadmeLevel + + +readme_instance: ReadmeLevel = ReadmeLevel() + + +def draw_progress_bar() -> str: + """Draws the progress bar""" + progress_bar_length: int = int(getenv("INPUT_PROGRESS_BAR_CHAR_LENGTH")) + + progress_bar_content = { + "empty_bar": getenv("INPUT_EMPTY_BAR"), + "filled_bar": getenv("INPUT_FILLED_BAR") + } + + progress_bar: str = "" + filled_progress: int = round(progress_bar_length * (50 / 100), 0) + + for index in range(progress_bar_length): + + if index <= filled_progress: + progress_bar += progress_bar_content["filled_bar"] + + if index > filled_progress: + progress_bar += progress_bar_content["empty_bar"] + + return progress_bar + + +user_level, to_next_lvl = itemgetter("current_level", + "percentage_level")(readme_instance.calc_current_level()) + +readme_path: str = getenv("INPUT_README_PATH") +start_section: str = "" +end_section: str = "" +search_pattern: str = fr"{start_section}[\s\S]*?{end_section}" +replace_str: str = (f"{start_section}\n" + "```text\n" + f"level: { user_level } { draw_progress_bar() } {round(to_next_lvl, 2)}%\n" + "```\n" + f"{end_section}") + + +# update readme +with open(readme_path, mode="r", encoding="utf-8") as readme_file: + readme_content = readme_file.read() + +changed_readme = sub(search_pattern, repl=replace_str, string=readme_content) + +with open(readme_path, mode="w", encoding="utf-8") as readme_file: + readme_file.write(changed_readme) diff --git a/readme_level.py b/readme_level.py new file mode 100644 index 0000000..af94ccf --- /dev/null +++ b/readme_level.py @@ -0,0 +1,116 @@ +"""Module that contains all the logic about the levelsystem.""" +from os import getenv +from logging import error +from requests import post +from graphql_query import QUERY + + +class ReadmeLevel: + """Class that contains all the logic about the levelsystem.""" + + # static variables + max_level: int = 99 + ep_to_next_level: int = 100 + current_ep: int = 0 + current_level: int = 1 + + contribution_count: int = 0 + project_count: int = 0 + discussion_count: int = 0 + star_count: int = 0 + follower_count: int = 0 + + contribution_ep: int = 20 + project_ep: int = 5 + discussion_ep: int = 10 + star_ep: int = 40 + follower_ep: int = 50 + + def __init__(self) -> None: + pass + + def fetch_user_data(self) -> dict[str, int] | None: + """Fetches the user data from github api""" + + if not getenv("INPUT_GITHUB_TOKEN"): + raise TypeError("github token is not a string") + + + auth_header = {"Authorization": "Bearer " + getenv("INPUT_GITHUB_TOKEN")} + response = post("https://api.github.com/graphql", + json={"query": QUERY}, headers=auth_header, timeout=2) + + if response.status_code == 200: + response_data = response.json() + user_data = (response_data["data"]["user"] + ["contributionsCollection"]["contributionCalendar"]) + + return user_data + + error("request to github api failed") + return None + + def _update_user_data(self) -> None: + """Updates the user data from current object""" + + user_stats = self.fetch_user_data() + + if not user_stats: + error("failed to update user data, because fetched user data were empty") + + key_mapper = { + "totalContributions": "contribution_count", + "projects": "project_count", + "follower": "follower_count", + "discussions": "discussion_count" + } + + for key, value in user_stats.items(): + setattr(self, key_mapper[key], value) + + def calc_current_ep(self) -> int: + """Calculates the current user experience points""" + + # update data first + self._update_user_data() + + # calc the current experience points + self.current_ep = ( + self.contribution_count * self.contribution_ep + + self.project_count * self.project_ep + + self.discussion_count * self.discussion_ep + + self.follower_count * self.follower_ep) + + return self.current_ep + + def calc_current_level(self) -> dict[str, int]: + """Calculates user level.""" + + # get current user experience points + # maybe we should use return value instead of attributes? + self.calc_current_ep() + + while self.current_ep >= self.ep_to_next_level: + + if self.current_level > self.max_level: + self.current_level = self.max_level + break + + # increase user level + self.current_level += 1 + self.current_ep -= self.ep_to_next_level + self.ep_to_next_level += 100 + + percentage_level = self.percentage_ep_level( + self.current_ep, self.ep_to_next_level) + + return { + "current_level": self.current_level, + "current_ep": self.current_ep, + "ep_to_next_level": self.ep_to_next_level, + "percentage_level": percentage_level + } + + def percentage_ep_level(self, current_ep: int, ep_to_next_level: int) -> float: + """Helper function that calcs the percentage value to the next level""" + return current_ep / ep_to_next_level * 100 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5e77405 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests==2.28.1 \ No newline at end of file