Skip to content

Commit

Permalink
feat: add project files
Browse files Browse the repository at this point in the history
  • Loading branch information
devfle committed Dec 14, 2022
1 parent 7bf9925 commit 13a4f60
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 1 deletion.
72 changes: 71 additions & 1 deletion README.md
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
54 changes: 54 additions & 0 deletions action.yml
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'
14 changes: 14 additions & 0 deletions graphql_query.py
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
}}
}}
}}
}}
"""
55 changes: 55 additions & 0 deletions main.py
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)
116 changes: 116 additions & 0 deletions readme_level.py
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
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests==2.28.1

0 comments on commit 13a4f60

Please sign in to comment.