-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
311 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,71 @@ | ||
# readme-level-up | ||
# 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. | ||
|
||
<img width="875" alt="01" src="https://user-images.githubusercontent.com/52854338/206877769-3463c59a-af0b-4fab-bde4-aec71ecfb5c2.png"> | ||
|
||
|
||
# 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 | ||
<!--README_LEVEL_UP:START--> | ||
<!--README_LEVEL_UP:STOP--> | ||
``` | ||
|
||
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/[email protected] | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
"""Query for github graphql api""" | ||
from os import getenv | ||
|
||
QUERY = f""" | ||
{{ | ||
user(login: "{getenv("INPUT_GITHUB_USERNAME")}") {{ | ||
contributionsCollection {{ | ||
contributionCalendar {{ | ||
totalContributions | ||
}} | ||
}} | ||
}} | ||
}} | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = "<!--README_LEVEL_UP:START-->" | ||
end_section: str = "<!--README_LEVEL_UP:END-->" | ||
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
requests==2.28.1 |