Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c34fb893a6 | ||
|
1fc26a4121 | ||
|
82c9408d6d | ||
|
9bee9ba11b | ||
|
b42a071671 | ||
|
e8541dbe4e | ||
|
da0f4f1847 | ||
|
d26ed33e7a | ||
|
a50019231d | ||
|
080a8c9b63 | ||
|
fbc9196645 | ||
|
59f35b046b | ||
|
60fa45f3f0 | ||
|
60fe3b9f48 | ||
|
29dba6dd79 | ||
|
ac0bb21462 | ||
|
413150be53 | ||
|
b2db3c3280 | ||
|
6e66f34e5a | ||
|
de673c4749 | ||
|
ce472c9c93 | ||
|
8514942821 | ||
|
d2c91885c3 | ||
|
bd7707fc5a | ||
|
72af24c8af | ||
|
8ffb95d479 |
@@ -1,4 +1,4 @@
|
||||
name: WakaReadme
|
||||
name: UnitTests
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,16 +11,15 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python -
|
||||
export PATH="$HOME/.poetry/bin:${PATH}"
|
||||
source $HOME/.poetry/env
|
||||
poetry install
|
||||
- name: Run unit tests
|
||||
run: |
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -112,7 +112,8 @@ celerybeat.pid
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
*.env
|
||||
env.sh
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
@@ -156,3 +157,6 @@ cython_debug/
|
||||
|
||||
# asdf
|
||||
.tool-versions
|
||||
|
||||
# ruff
|
||||
.ruff_cache
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Contributing
|
||||
|
||||

|
||||

|
||||
|
||||
> First off, thank you! Please follow along.
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
|
||||
where `WakaReadmeDev` is the docker container name. Then execute `bash` in the container:
|
||||
|
||||
```console
|
||||
$ docker exec -it 'WakaReadmeDev' bash
|
||||
```
|
||||
```console
|
||||
$ docker exec -it 'WakaReadmeDev' bash
|
||||
```
|
||||
|
||||
3. For development, you can attach code editor of your choice to this container.
|
||||
4. Export environnement variables with edits, as required:
|
||||
@@ -58,6 +58,7 @@
|
||||
INPUT_REPOSITORY='<REPOSITORY SLUG>'
|
||||
INPUT_COMMIT_MESSAGE='<COMMIT MESSAGE>'
|
||||
INPUT_SHOW_TITLE='True'
|
||||
INPUT_SECTION_NAME='waka'
|
||||
INPUT_BLOCKS='->'
|
||||
INPUT_SHOW_TIME='True'
|
||||
INPUT_SHOW_TOTAL='True'
|
||||
@@ -70,7 +71,8 @@
|
||||
```console
|
||||
# poetry shell
|
||||
# set -a && . ./.env && set +a # optional
|
||||
(venv)# python -m main --dev
|
||||
(waka-readme-py3_11)# python -m main --dev
|
||||
(waka-readme-py3_11)# python -m unittest discover # run tests
|
||||
```
|
||||
|
||||
5. Later, to remove stop and remove the container:
|
||||
@@ -92,7 +94,7 @@
|
||||
> ```json
|
||||
> {
|
||||
> "terminal.integrated.commandsToSkipShell": [
|
||||
> "-workbench.action.quickOpenView"
|
||||
> "-workbench.action.quickOpenView"
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
@@ -109,7 +111,7 @@
|
||||
|
||||
```console
|
||||
$ poetry shell
|
||||
(venv)$ poetry install
|
||||
(waka-readme-py3_11)$ poetry install
|
||||
```
|
||||
|
||||
to create and activate a virtual environnement and install dependencies.
|
||||
@@ -123,6 +125,7 @@
|
||||
INPUT_REPOSITORY='<REPOSITORY SLUG>'
|
||||
INPUT_COMMIT_MESSAGE='<COMMIT MESSAGE>'
|
||||
INPUT_SHOW_TITLE='True'
|
||||
INPUT_SECTION_NAME='waka'
|
||||
INPUT_BLOCKS='->'
|
||||
INPUT_SHOW_TIME='True'
|
||||
INPUT_SHOW_TOTAL='True'
|
||||
@@ -133,6 +136,7 @@
|
||||
3. Execute program in development mode with:
|
||||
|
||||
```console
|
||||
$ set -a && . ./.env && set +a # optional
|
||||
(venv)$ python -m main --dev
|
||||
(waka-readme-py3_11)$ set -a && . ./.env && set +a # optional
|
||||
(waka-readme-py3_11)$ python -m main --dev
|
||||
(waka-readme-py3_11)$ python -m unittest discover # run tests
|
||||
```
|
||||
|
80
README.md
80
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
</center>
|
||||
|
||||
# Dev Metrics in Readme [](https://travis-ci.com/athul/waka-readme)
|
||||
# Dev Metrics in Readme [](https://github.com/athul/waka-readme/actions/workflows/testing.yml) 
|
||||
|
||||
[WakaTime](https://wakatime.com) weekly metrics on your profile readme.
|
||||
|
||||
@@ -28,47 +28,49 @@ Alternatively, you can also fetch data from WakaTime compatible services like [W
|
||||
|
||||
## Prep Work
|
||||
|
||||
A GitHub repository and a README file is required. We'll be making use of readme in the [profile repository][profile_readme]\*.
|
||||
A GitHub repository and a `README.md` file is required. We'll be making use of readme in the [profile repository][profile_readme]\*.
|
||||
|
||||
- Save the README file after copy-pasting the following special comments. Your dev-metics will show up in between.
|
||||
- Save the `README.md` file after copy-pasting the following special comments. Your dev-metics will show up in between.
|
||||
|
||||
```md
|
||||
```md
|
||||
<!--START_SECTION:waka-->
|
||||
<!--END_SECTION:waka-->
|
||||
```
|
||||
|
||||
<!--START_SECTION:waka-->
|
||||
<!--END_SECTION:waka-->
|
||||
`<!--START_SECTION: -->` and `<!--END_SECTION: -->` are placeholders and must be retained as is. Whereas "`waka`" can be replaced by any alphanumeric string. See [#Tweaks](#tweaks) section for more.
|
||||
|
||||
```
|
||||
- Navigate to your repo's `Settings > Secrets` and add a new secret _named_ `WAKATIME_API_KEY` with your API key as it's _value_.
|
||||
|
||||
- Navigate to your repo's `Settings > Secrets` and add a new secret *named* `WAKATIME_API_KEY` with your API key as it's *value*.
|
||||
> Or use the url <https://github.com/USERNAME/USERNAME/settings/secrets/actions/new> by replacing the `USERNAME` with your own username.
|
||||
> ![new_secrets_actions][new_secrets_actions]
|
||||
> Or use the url <https://github.com/USERNAME/USERNAME/settings/secrets/actions/new> by replacing the `USERNAME` with your own username.
|
||||
>
|
||||
> ![new_secrets_actions][new_secrets_actions]
|
||||
|
||||
- If you're not using [profile repository][profile_readme], add another secret *named* `GH_TOKEN` and insert your [GitHub token][gh_access_token]\* in place of *value*.
|
||||
- If you're not using [profile repository][profile_readme], add another secret _named_ `GH_TOKEN` and insert your [GitHub token][gh_access_token]\* in place of _value_.
|
||||
|
||||
- Create a new workflow file (`waka-readme.yml`) inside `.github/workflows/` folder of your repository. You can create it from a template using the *actions tab* of your repository too.
|
||||
- Create a new workflow file (`waka-readme.yml`) inside `.github/workflows/` folder of your repository. You can create it from a template using the _actions tab_ of your repository too.
|
||||
- Clear any existing contents, add the following lines and save the file.
|
||||
|
||||
```yml
|
||||
name: Waka Readme
|
||||
```yml
|
||||
name: Waka Readme
|
||||
|
||||
on:
|
||||
workflow_dispatch: # for manual workflow trigger
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # runs at every 12AM UTC
|
||||
on:
|
||||
workflow_dispatch: # for manual workflow trigger
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # runs at every 12AM UTC
|
||||
|
||||
jobs:
|
||||
update-readme:
|
||||
name: WakaReadme DevMetrics
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: athul/waka-readme@master
|
||||
with:
|
||||
WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }}
|
||||
# following flags are required, only if this is not on
|
||||
# profile readme, remove the leading `#` to use them
|
||||
#GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
#REPOSITORY: <gh_username/gh_username>
|
||||
```
|
||||
jobs:
|
||||
update-readme:
|
||||
name: WakaReadme DevMetrics
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: athul/waka-readme@master
|
||||
with:
|
||||
WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }}
|
||||
# following flags are required, only if this is not on
|
||||
# profile readme, remove the leading `#` to use them
|
||||
#GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
#REPOSITORY: <gh_username/gh_username>
|
||||
```
|
||||
|
||||
## Tweaks
|
||||
|
||||
@@ -78,13 +80,17 @@ There are many flags that you can tweak to suit your taste!
|
||||
| ------------------ | -------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `API_BASE_URL` | `https://wakatime.com/api` | `https://wakatime.com/api`, `https://wakapi.dev/api`, `https://hakatime.mtx-dev.xyz/api` | Integration with WakaTime compatible services like [Wakapi][wakapi] & [Hakatime][hakatime] are possible |
|
||||
| `REPOSITORY` | `<gh_username>/<gh_username>` | `<gh_username>/<repo_name>` | Waka-readme stats will appear on the provided repository |
|
||||
| `COMMIT_MESSAGE` | `Updated waka-readme graph with new metrics` | anything else! | Messaged used when committing updated stats |
|
||||
| `SECTION_NAME` | `waka` | Any alphanumeric string | The generator will look for this section to fill up the readme. |
|
||||
| `COMMIT_MESSAGE` | `Updated waka-readme graph with new metrics` | Any string | Messaged used when committing updated stats |
|
||||
| `CODE_LANG` | `txt` | `python` `ruby` `json` , you can use other languages also | Language syntax to format the generated text, to get colored text. |
|
||||
| `SHOW_TITLE` | `false` | `false`, `true` | Add title to waka-readme stats blob |
|
||||
| `BLOCKS` | `░▒▓█` | `░▒▓█`, `⣀⣄⣤⣦⣶⣷⣿`, `-#`, you can be creative! | Ascii art used to build stats graph |
|
||||
| `BLOCKS` | `░▒▓█` | `░▒▓█`, `⣀⣄⣤⣦⣶⣷⣿`, `-#`, you can be creative! | Ascii art used to build stats graph |
|
||||
| `TIME_RANGE` | `last_7_days` | `last_7_days`, `last_30_days`, `last_6_months`, `last_year`, `all_time` | String representing a dispensation from which stats are aggregated |
|
||||
| `SHOW_TIME` | `true` | `false`, `true` | Displays the amount of time spent for each language |
|
||||
| `SHOW_TOTAL` | `false` | `false`, `true` | Show total coding time |
|
||||
| `SHOW_MASKED_TIME` | `false` | `false`, `true` | Adds total coding time including unclassified languages (overrides: `SHOW_TOTAL`) |
|
||||
| `LANG_COUNT` | `5` | Any reasonable number | Number of languages to be displayed |
|
||||
| `STOP_AT_OTHER` | `false` | `false`, `true` | Stop when language marked as `Other` is retrieved (overrides: `LANG_COUNT`) |
|
||||
|
||||
# Example
|
||||
|
||||
@@ -97,7 +103,7 @@ on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Runs at 12am UTC
|
||||
- cron: '0 0 * * *'
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
update-readme:
|
||||
@@ -112,6 +118,7 @@ jobs:
|
||||
TIME_RANGE: all_time
|
||||
SHOW_TIME: true
|
||||
SHOW_MASKED_TIME: true
|
||||
LANG_COUNT: 10
|
||||
```
|
||||
|
||||
**`README.md`**
|
||||
@@ -131,14 +138,13 @@ Other 47 hrs 58 mins >------------------------ 03.05 %
|
||||
|
||||
## Why only the language stats (and not other data) from the API?
|
||||
|
||||
I am a fan of minimal designs and the profile readme is a great way to show off your skills and interests. The WakaTime API, gets us a **lot of data** about a person's **coding activity including the editors and Operating Systems you used and the projects you worked on**. Some of these projects maybe secretive and should not be shown out to the public. Using up more data via the Wakatime API will clutter the profile readme and hinder your chances on displaying what you provide **value to the community** like the pinned Repositories. I believe that **Coding Stats is nerdiest of all** since you can tell the community that you are ***exercising these languages or learning a new language***, this will also show that you spend some amount of time to learn and exercise your development skills. That's what matters in the end :heart:
|
||||
I am a fan of minimal designs and the profile readme is a great way to show off your skills and interests. The WakaTime API, gets us a **lot of data** about a person's **coding activity including the editors and Operating Systems you used and the projects you worked on**. Some of these projects maybe secretive and should not be shown out to the public. Using up more data via the Wakatime API will clutter the profile readme and hinder your chances on displaying what you provide **value to the community** like the pinned Repositories. I believe that **Coding Stats is nerdiest of all** since you can tell the community that you are **_exercising these languages or learning a new language_**, this will also show that you spend some amount of time to learn and exercise your development skills. That's what matters in the end :heart:
|
||||
|
||||
---
|
||||
|
||||
<sup>*</sup>`REPOSITORY` flag and `GH_TOKEN` secret are required you're not using profile readme.
|
||||
<sup>\*</sup>`REPOSITORY` flag and `GH_TOKEN` secret are required you're not using profile readme.
|
||||
|
||||
[//]: #(Links)
|
||||
|
||||
[wakapi]: https://wakapi.dev
|
||||
[hakatime]: https://github.com/mujx/hakatime
|
||||
[waka_plugins]: https://wakatime.com/plugins
|
||||
|
20
action.yml
20
action.yml
@@ -34,16 +34,31 @@ inputs:
|
||||
default: "false"
|
||||
required: false
|
||||
|
||||
SECTION_NAME:
|
||||
description: "Section name for data to appear in readme"
|
||||
required: false
|
||||
default: "waka"
|
||||
|
||||
BLOCKS:
|
||||
description: "Add the progress blocks of your choice"
|
||||
default: "░▒▓█"
|
||||
required: false
|
||||
|
||||
CODE_LANG:
|
||||
description: "Add syntax formatter for generated code"
|
||||
default: "txt"
|
||||
required: false
|
||||
|
||||
TIME_RANGE:
|
||||
description: "Time range of the queried statistics"
|
||||
default: "last_7_days"
|
||||
required: false
|
||||
|
||||
LANG_COUNT:
|
||||
description: "Maximum number of languages to be shown"
|
||||
default: "5"
|
||||
required: false
|
||||
|
||||
SHOW_TIME:
|
||||
description: "Displays the amount of time spent for each language"
|
||||
default: "true"
|
||||
@@ -58,6 +73,11 @@ inputs:
|
||||
description: "Displays total coding time including unclassified languages"
|
||||
default: "false"
|
||||
required: false
|
||||
|
||||
STOP_AT_OTHER:
|
||||
description: "Stop data retrieval when language marked 'Other' is reached"
|
||||
default: "false"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "docker"
|
||||
|
@@ -11,7 +11,7 @@ ENV PYTHONFAULTHANDLER=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100 \
|
||||
# poetry:
|
||||
# POETRY_VERSION=1.1.14 \
|
||||
# POETRY_VERSION= \
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
POETRY_CACHE_DIR=/var/cache/pypoetry \
|
||||
PATH=${PATH}:/root/.local/bin
|
||||
|
@@ -9,7 +9,7 @@ ENV PYTHONFAULTHANDLER=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100 \
|
||||
# poetry:
|
||||
# POETRY_VERSION=1.1.14 \
|
||||
# POETRY_VERSION= \
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
POETRY_CACHE_DIR=/var/cache/pypoetry \
|
||||
PATH=${PATH}:/root/.local/bin
|
||||
|
706
main.py
706
main.py
@@ -1,25 +1,20 @@
|
||||
"""
|
||||
WakaReadme : WakaTime progress visualizer
|
||||
=========================================
|
||||
"""WakaReadme : WakaTime progress visualizer.
|
||||
|
||||
Wakatime Metrics on your Profile Readme.
|
||||
|
||||
Title:
|
||||
------
|
||||
|
||||
```txt
|
||||
From: 15 February, 2022 - To: 22 February, 2022
|
||||
````
|
||||
|
||||
Byline:
|
||||
-------
|
||||
|
||||
```txt
|
||||
Total: 34 hrs 43 mins
|
||||
```
|
||||
|
||||
Body:
|
||||
-----
|
||||
|
||||
```txt
|
||||
Python 27 hrs 29 mins ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣀⣀⣀⣀⣀ 77.83 %
|
||||
@@ -29,7 +24,7 @@ TOML 1 hr 48 mins ⣿⣤⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀
|
||||
Other 35 mins ⣦⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀ 01.68 %
|
||||
```
|
||||
|
||||
#### Contents = Title + Byline + Body
|
||||
Contents := Title + Byline + Body
|
||||
"""
|
||||
|
||||
# standard
|
||||
@@ -45,385 +40,408 @@ import re
|
||||
import os
|
||||
|
||||
# external
|
||||
# # requests
|
||||
from requests.exceptions import RequestException
|
||||
from requests import get as rq_get
|
||||
# # github
|
||||
from github import GithubException, Github
|
||||
# # faker
|
||||
from faker import Faker
|
||||
|
||||
|
||||
# pylint: disable=logging-fstring-interpolation
|
||||
|
||||
################### data ###################
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WakaConstants:
|
||||
"""
|
||||
WakaConstants
|
||||
-------------
|
||||
"""
|
||||
prefix_length: int = 16
|
||||
graph_length: int = 25
|
||||
start_comment: str = '<!--START_SECTION:waka-->'
|
||||
end_comment: str = '<!--END_SECTION:waka-->'
|
||||
waka_block_pattern: str = f'{start_comment}[\\s\\S]+{end_comment}'
|
||||
################### setup ###################
|
||||
|
||||
|
||||
class WakaInput:
|
||||
"""
|
||||
WakaInput Env Vars
|
||||
------------------
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
WakaInput Initialize
|
||||
--------------------
|
||||
"""
|
||||
# mapped environment variables
|
||||
# # required
|
||||
self.gh_token: str = os.getenv('INPUT_GH_TOKEN')
|
||||
self.waka_key: str = os.getenv('INPUT_WAKATIME_API_KEY')
|
||||
self.api_base_url: str = os.getenv(
|
||||
'INPUT_API_BASE_URL', 'https://wakatime.com/api'
|
||||
)
|
||||
self.repository: str = os.getenv('INPUT_REPOSITORY')
|
||||
# # depends
|
||||
self.commit_message: str = os.getenv(
|
||||
'INPUT_COMMIT_MESSAGE', 'Updated WakaReadme graph with new metrics'
|
||||
)
|
||||
# # optional
|
||||
self.show_title: str | bool = os.getenv('INPUT_SHOW_TITLE', 'False')
|
||||
self.block_style: str = os.getenv('INPUT_BLOCKS', '░▒▓█')
|
||||
self.time_range: str = os.getenv('INPUT_TIME_RANGE', 'last_7_days')
|
||||
self.show_time: str | bool = os.getenv('INPUT_SHOW_TIME', 'False')
|
||||
self.show_total_time: str | bool = os.getenv(
|
||||
'INPUT_SHOW_TOTAL', 'False'
|
||||
)
|
||||
self.show_masked_time: str | bool = os.getenv(
|
||||
'INPUT_SHOW_MASKED_TIME', 'False'
|
||||
)
|
||||
|
||||
def validate_input(self) -> bool:
|
||||
"""
|
||||
WakaInput Validate
|
||||
------------------
|
||||
"""
|
||||
|
||||
if not (self.gh_token and self.waka_key and self.api_base_url and self.repository):
|
||||
logger.error('Invalid required input(s), refer README')
|
||||
return False
|
||||
|
||||
if len(self.commit_message) < 1:
|
||||
logger.error(
|
||||
'Commit message length must be greater than 1 character long'
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
self.show_title: bool = strtobool(self.show_title)
|
||||
self.show_time: bool = strtobool(self.show_time)
|
||||
self.show_total_time: bool = strtobool(self.show_total_time)
|
||||
self.show_masked_time: bool = strtobool(self.show_masked_time)
|
||||
except (ValueError, AttributeError) as err:
|
||||
logger.error(err)
|
||||
return False
|
||||
|
||||
if len(self.block_style) < 2:
|
||||
logger.warning(
|
||||
'Block length should be greater than 2 characters long'
|
||||
)
|
||||
logger.debug('Using default blocks: ░▒▓█')
|
||||
|
||||
# 'all_time' is un-documented, should it be used?
|
||||
if self.time_range not in {
|
||||
'last_7_days', 'last_30_days', 'last_6_months', 'last_year', 'all_time'
|
||||
}:
|
||||
logger.warning('Invalid time range')
|
||||
logger.debug('Using default time range: last_7_days')
|
||||
self.time_range: str = 'last_7_days'
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def strtobool(val: str) -> bool:
|
||||
"""
|
||||
strtobool
|
||||
---------
|
||||
|
||||
PEP 632 https://www.python.org/dev/peps/pep-0632/ is depreciating distutils
|
||||
|
||||
Following code is somewhat shamelessly copied from the original source.
|
||||
|
||||
Convert a string representation of truth to True or False.
|
||||
|
||||
- True values are `'y', 'yes', 't', 'true', 'on', and '1'`
|
||||
- False values are `'n', 'no', 'f', 'false', 'off', and '0'`
|
||||
- Raises `ValueError` if `val` is anything else.
|
||||
"""
|
||||
val = val.lower()
|
||||
|
||||
if val in {'y', 'yes', 't', 'true', 'on', '1'}:
|
||||
return True
|
||||
|
||||
if val in {'n', 'no', 'f', 'false', 'off', '0'}:
|
||||
return False
|
||||
|
||||
raise ValueError(f'invalid truth value for {val}')
|
||||
|
||||
|
||||
################### logic ###################
|
||||
|
||||
def make_title(dawn: str, dusk: str, /) -> str:
|
||||
"""
|
||||
WakaReadme Title
|
||||
----------------
|
||||
|
||||
Makes title for WakaReadme.
|
||||
"""
|
||||
logger.debug('Making title')
|
||||
if not (dawn or dusk):
|
||||
logger.error('Cannot find start/end date')
|
||||
sys.exit(1)
|
||||
api_dfm, msg_dfm = '%Y-%m-%dT%H:%M:%SZ', '%d %B %Y'
|
||||
try:
|
||||
start_date = datetime.strptime(dawn, api_dfm).strftime(msg_dfm)
|
||||
end_date = datetime.strptime(dusk, api_dfm).strftime(msg_dfm)
|
||||
except ValueError as err:
|
||||
logger.error(err)
|
||||
sys.exit(1)
|
||||
logger.debug('Title was made\n')
|
||||
return f'From: {start_date} - To: {end_date}'
|
||||
|
||||
|
||||
def make_graph(
|
||||
block_style: str, percent: float, gr_len: str, /,
|
||||
*, lg_nm: str = ''
|
||||
) -> str:
|
||||
"""
|
||||
WakaReadme Graph
|
||||
----------------
|
||||
|
||||
Makes time graph from the API's data.
|
||||
"""
|
||||
logger.debug(f'Generating graph for "{lg_nm or "..."}"')
|
||||
markers: int = len(block_style) - 1
|
||||
proportion: float = percent / 100 * gr_len
|
||||
graph_bar: str = block_style[-1] * int(proportion + 0.5 / markers)
|
||||
remainder_block: int = int(
|
||||
(proportion - len(graph_bar)) * markers + 0.5
|
||||
)
|
||||
graph_bar += block_style[remainder_block] if remainder_block > 0 else ''
|
||||
graph_bar += block_style[0] * (gr_len - len(graph_bar))
|
||||
logger.debug(f'{lg_nm or "..."} graph generated')
|
||||
return graph_bar
|
||||
|
||||
|
||||
def prep_content(stats: dict | None, /) -> str:
|
||||
"""
|
||||
WakaReadme Prepare Markdown
|
||||
---------------------------
|
||||
|
||||
Prepared markdown content from the fetched statistics
|
||||
```
|
||||
"""
|
||||
contents: str = ''
|
||||
|
||||
# Check if any data exists
|
||||
if not (lang_info := stats.get('languages')):
|
||||
logger.debug('The data seems to be empty, please wait for a day')
|
||||
contents += 'No activity tracked'
|
||||
return contents
|
||||
|
||||
# make title
|
||||
if wk_i.show_title:
|
||||
contents += make_title(stats.get('start'), stats.get('end')) + '\n\n'
|
||||
|
||||
# make byline
|
||||
if wk_i.show_masked_time and (
|
||||
total_time := stats.get('human_readable_total_including_other_language')
|
||||
):
|
||||
# overrides 'human_readable_total'
|
||||
contents += f'Total Time: {total_time}\n\n'
|
||||
elif wk_i.show_total_time and (
|
||||
total_time := stats.get('human_readable_total')
|
||||
):
|
||||
contents += f'Total Time: {total_time}\n\n'
|
||||
|
||||
# make content
|
||||
logger.debug('Making contents')
|
||||
pad_len = len(
|
||||
# comment if it feels way computationally expensive
|
||||
max((str(l.get('name')) for l in lang_info), key=len)
|
||||
# and then don't for get to set pad_len to say 13 :)
|
||||
)
|
||||
for idx, lang in enumerate(lang_info):
|
||||
lang_name: str = lang.get('name')
|
||||
# >>> add languages to filter here <<<
|
||||
# if lang_name in {...}: continue
|
||||
lang_time: str = lang.get('text') if wk_i.show_time else ''
|
||||
lang_ratio: float = lang.get('percent')
|
||||
lang_bar: str = make_graph(
|
||||
wk_i.block_style, lang_ratio, wk_c.graph_length,
|
||||
lg_nm=lang_name
|
||||
)
|
||||
contents += (
|
||||
f'{lang_name.ljust(pad_len)} ' +
|
||||
f'{lang_time: <16}{lang_bar} ' +
|
||||
f'{lang_ratio:.2f}'.zfill(5) + ' %\n'
|
||||
)
|
||||
if idx >= 5 or lang_name == 'Other':
|
||||
break
|
||||
|
||||
logger.debug('Contents were made\n')
|
||||
return contents.rstrip('\n')
|
||||
|
||||
|
||||
def fetch_stats() -> Any:
|
||||
"""
|
||||
WakaReadme Fetch Stats
|
||||
----------------------
|
||||
|
||||
Returns statistics as JSON string
|
||||
"""
|
||||
attempts, statistic = 4, {}
|
||||
encoded_key: str = str(b64encode(bytes(wk_i.waka_key, 'utf-8')), 'utf-8')
|
||||
logger.debug(
|
||||
f'Pulling WakaTime stats from {" ".join(wk_i.time_range.split("_"))}'
|
||||
)
|
||||
|
||||
while attempts > 0:
|
||||
resp_message, fake_ua = None, cryptogenic.choice(
|
||||
[str(fake.user_agent()) for _ in range(5)]
|
||||
)
|
||||
# making a request
|
||||
if (resp := rq_get(
|
||||
url=f'{wk_i.api_base_url.rstrip("/")}/v1/users/current/stats/{wk_i.time_range}',
|
||||
headers={
|
||||
'Authorization': f'Basic {encoded_key}',
|
||||
'User-Agent': fake_ua,
|
||||
},
|
||||
)).status_code != 200:
|
||||
resp_message = f'• {resp.json().get("message")}'
|
||||
logger.debug(
|
||||
f'API response #{5 - attempts}: {resp.status_code} • {resp.reason} {resp_message or ""}'
|
||||
)
|
||||
if resp.status_code == 200 and (statistic := resp.json()):
|
||||
logger.debug('Fetched WakaTime statistics')
|
||||
break
|
||||
logger.debug(f'Retrying in {30 * (5 - attempts )}s ...')
|
||||
sleep(30 * (5 - attempts))
|
||||
attempts -= 1
|
||||
|
||||
if err := (statistic.get('error') or statistic.get('errors')):
|
||||
logger.error(err)
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
return statistic.get('data')
|
||||
|
||||
|
||||
def churn(old_readme: str, /) -> str | None:
|
||||
"""
|
||||
WakaReadme Churn
|
||||
----------------
|
||||
|
||||
Composes WakaTime stats within markdown code snippet
|
||||
"""
|
||||
# getting content
|
||||
if not (waka_stats := fetch_stats()):
|
||||
logger.error('Unable to fetch data, please rerun workflow')
|
||||
sys.exit(1)
|
||||
|
||||
# processing content
|
||||
generated_content = prep_content(waka_stats)
|
||||
print(generated_content, '\n', sep='')
|
||||
new_readme = re.sub(
|
||||
pattern=wk_c.waka_block_pattern,
|
||||
repl=f'{wk_c.start_comment}\n\n```text\n{generated_content}\n```\n\n{wk_c.end_comment}',
|
||||
string=old_readme
|
||||
)
|
||||
if len(sys.argv) == 2 and sys.argv[1] == '--dev':
|
||||
logger.debug('Detected run in `dev` mode.')
|
||||
# to avoid accidentally writing back to Github
|
||||
# when developing and testing WakaReadme
|
||||
return None
|
||||
return None if new_readme == old_readme else new_readme
|
||||
|
||||
|
||||
def genesis() -> None:
|
||||
"""Run Program"""
|
||||
logger.debug('Connecting to GitHub')
|
||||
gh_connect = Github(wk_i.gh_token)
|
||||
gh_repo = gh_connect.get_repo(wk_i.repository)
|
||||
readme_file = gh_repo.get_readme()
|
||||
logger.debug('Decoding readme contents\n')
|
||||
readme_contents = str(readme_file.decoded_content, encoding='utf-8')
|
||||
if new_content := churn(readme_contents):
|
||||
logger.debug('WakaReadme stats has changed')
|
||||
gh_repo.update_file(
|
||||
path=readme_file.path,
|
||||
message=wk_i.commit_message,
|
||||
content=new_content,
|
||||
sha=readme_file.sha
|
||||
)
|
||||
logger.info('Stats updated successfully')
|
||||
return
|
||||
logger.info('WakaReadme was not updated')
|
||||
|
||||
|
||||
################### driver ###################
|
||||
print()
|
||||
# hush existing loggers
|
||||
# pylint: disable = no-member # see: https://stackoverflow.com/q/20965287
|
||||
for lgr_name in logger.root.manager.loggerDict:
|
||||
# to disable log propagation completely set '.propagate = False'
|
||||
logger.getLogger(lgr_name).setLevel(logger.WARNING)
|
||||
# pylint: enable = no-member
|
||||
# somehow github.Requester gets missed out from loggerDict
|
||||
logger.getLogger('github.Requester').setLevel(logger.WARNING)
|
||||
logger.getLogger("github.Requester").setLevel(logger.WARNING)
|
||||
# configure logger
|
||||
logger.basicConfig(
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
format='[%(asctime)s] ln. %(lineno)-3d %(levelname)-8s %(message)s',
|
||||
level=logger.DEBUG
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
format="[%(asctime)s] ln. %(lineno)-3d %(levelname)-8s %(message)s",
|
||||
level=logger.DEBUG,
|
||||
)
|
||||
try:
|
||||
if len(sys.argv) == 2 and sys.argv[1] == '--dev':
|
||||
if len(sys.argv) == 2 and sys.argv[1] == "--dev":
|
||||
# get env-vars from .env file for development
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# comment this out to disable colored logging
|
||||
from loguru import logger
|
||||
|
||||
# load from .env before class def gets parsed
|
||||
load_dotenv()
|
||||
except ImportError as im_err:
|
||||
logger.warning(im_err)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
################### lib-func ###################
|
||||
|
||||
|
||||
def strtobool(val: str | bool):
|
||||
"""Strtobool.
|
||||
|
||||
PEP 632 https://www.python.org/dev/peps/pep-0632/ is depreciating distutils.
|
||||
This is from the official source code with slight modifications.
|
||||
|
||||
Converts a string representation of truth to `True` or `False`.
|
||||
|
||||
Args:
|
||||
val:
|
||||
Value to be converted to bool.
|
||||
|
||||
Returns:
|
||||
(Literal[True]):
|
||||
If `val` is any of 'y', 'yes', 't', 'true', 'on', or '1'.
|
||||
(Literal[False]):
|
||||
If `val` is any of 'n', 'no', 'f', 'false', 'off', and '0'.
|
||||
|
||||
Raises:
|
||||
ValueError: If `val` is anything else.
|
||||
"""
|
||||
if isinstance(val, bool):
|
||||
return val
|
||||
|
||||
val = val.lower()
|
||||
|
||||
if val in {"y", "yes", "t", "true", "on", "1"}:
|
||||
return True
|
||||
|
||||
if val in {"n", "no", "f", "false", "off", "0"}:
|
||||
return False
|
||||
|
||||
raise ValueError(f"invalid truth value for {val}")
|
||||
|
||||
|
||||
################### data ###################
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class WakaInput:
|
||||
"""WakaReadme Input Env Variables."""
|
||||
|
||||
# constants
|
||||
prefix_length: int = 16
|
||||
graph_length: int = 25
|
||||
|
||||
# mapped environment variables
|
||||
# # required
|
||||
gh_token: str | None = os.getenv("INPUT_GH_TOKEN")
|
||||
waka_key: str | None = os.getenv("INPUT_WAKATIME_API_KEY")
|
||||
api_base_url: str | None = os.getenv("INPUT_API_BASE_URL", "https://wakatime.com/api")
|
||||
repository: str | None = os.getenv("INPUT_REPOSITORY")
|
||||
# # depends
|
||||
commit_message: str = os.getenv(
|
||||
"INPUT_COMMIT_MESSAGE", "Updated WakaReadme graph with new metrics"
|
||||
)
|
||||
code_lang: str = os.getenv("INPUT_CODE_LANG", "txt")
|
||||
_section_name: str = os.getenv("INPUT_SECTION_NAME", "waka")
|
||||
start_comment: str = f"<!--START_SECTION:{_section_name}-->"
|
||||
end_comment: str = f"<!--END_SECTION:{_section_name}-->"
|
||||
waka_block_pattern: str = f"{start_comment}[\\s\\S]+{end_comment}"
|
||||
# # optional
|
||||
show_title: str | bool = os.getenv("INPUT_SHOW_TITLE") or False
|
||||
block_style: str = os.getenv("INPUT_BLOCKS", "░▒▓█")
|
||||
time_range: str = os.getenv("INPUT_TIME_RANGE", "last_7_days")
|
||||
show_time: str | bool = os.getenv("INPUT_SHOW_TIME") or False
|
||||
show_total_time: str | bool = os.getenv("INPUT_SHOW_TOTAL") or False
|
||||
show_masked_time: str | bool = os.getenv("INPUT_SHOW_MASKED_TIME") or False
|
||||
language_count: str | int = os.getenv("INPUT_LANG_COUNT") or 5
|
||||
stop_at_other: str | bool = os.getenv("INPUT_STOP_AT_OTHER") or False
|
||||
|
||||
def validate_input(self):
|
||||
"""Validate Input Env Variables."""
|
||||
logger.debug("Validating input variables")
|
||||
if not self.gh_token or not self.waka_key or not self.api_base_url or not self.repository:
|
||||
logger.error("Invalid inputs")
|
||||
logger.info("Refer https://github.com/athul/waka-readme")
|
||||
return False
|
||||
|
||||
if len(self.commit_message) < 1:
|
||||
logger.error("Commit message length must be greater than 1 character long")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.show_title = strtobool(self.show_title)
|
||||
self.show_time = strtobool(self.show_time)
|
||||
self.show_total_time = strtobool(self.show_total_time)
|
||||
self.show_masked_time = strtobool(self.show_masked_time)
|
||||
self.stop_at_other = strtobool(self.stop_at_other)
|
||||
except (ValueError, AttributeError) as err:
|
||||
logger.error(err)
|
||||
return False
|
||||
|
||||
if not self._section_name.isalnum():
|
||||
logger.warning("Section name must be in any of [[a-z][A-Z][0-9]]")
|
||||
logger.debug("Using default section name: waka")
|
||||
self._section_name = "waka"
|
||||
self.start_comment = f"<!--START_SECTION:{self._section_name}-->"
|
||||
self.end_comment = f"<!--END_SECTION:{self._section_name}-->"
|
||||
self.waka_block_pattern = f"{self.start_comment}[\\s\\S]+{self.end_comment}"
|
||||
|
||||
if len(self.block_style) < 2:
|
||||
logger.warning("Graph block must be longer than 2 characters")
|
||||
logger.debug("Using default blocks: ░▒▓█")
|
||||
self.block_style = "░▒▓█"
|
||||
|
||||
if self.time_range not in {
|
||||
"last_7_days",
|
||||
"last_30_days",
|
||||
"last_6_months",
|
||||
"last_year",
|
||||
"all_time",
|
||||
}: # "all_time" is un-documented, should it be used?
|
||||
logger.warning("Invalid time range")
|
||||
logger.debug("Using default time range: last_7_days")
|
||||
self.time_range = "last_7_days"
|
||||
|
||||
try:
|
||||
self.language_count = int(self.language_count)
|
||||
if self.language_count < -1:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
logger.warning("Invalid language count")
|
||||
logger.debug("Using default language count: 5")
|
||||
self.language_count = 5
|
||||
|
||||
logger.debug("Input validation complete\n")
|
||||
return True
|
||||
|
||||
|
||||
################### logic ###################
|
||||
|
||||
|
||||
def make_title(dawn: str | None, dusk: str | None, /):
|
||||
"""WakaReadme Title.
|
||||
|
||||
Makes title for WakaReadme.
|
||||
"""
|
||||
logger.debug("Making title")
|
||||
if not dawn or not dusk:
|
||||
logger.error("Cannot find start/end date\n")
|
||||
sys.exit(1)
|
||||
api_dfm, msg_dfm = "%Y-%m-%dT%H:%M:%SZ", "%d %B %Y"
|
||||
try:
|
||||
start_date = datetime.strptime(dawn, api_dfm).strftime(msg_dfm)
|
||||
end_date = datetime.strptime(dusk, api_dfm).strftime(msg_dfm)
|
||||
except ValueError as err:
|
||||
logger.error(f"{err}\n")
|
||||
sys.exit(1)
|
||||
|
||||
logger.debug("Title was made\n")
|
||||
return f"From: {start_date} - To: {end_date}"
|
||||
|
||||
|
||||
def make_graph(block_style: str, percent: float, gr_len: int, lg_nm: str = "", /):
|
||||
"""WakaReadme Graph.
|
||||
|
||||
Makes time graph from the API's data.
|
||||
"""
|
||||
logger.debug(f"Generating graph for '{lg_nm or '...'}'")
|
||||
markers = len(block_style) - 1
|
||||
proportion = percent / 100 * gr_len
|
||||
graph_bar = block_style[-1] * int(proportion + 0.5 / markers)
|
||||
remainder_block = int((proportion - len(graph_bar)) * markers + 0.5)
|
||||
graph_bar += block_style[remainder_block] if remainder_block > 0 else ""
|
||||
graph_bar += block_style[0] * (gr_len - len(graph_bar))
|
||||
|
||||
logger.debug(f"'{lg_nm or '...'}' graph generated")
|
||||
return graph_bar
|
||||
|
||||
|
||||
def prep_content(stats: dict[str, Any], language_count: int = 5, stop_at_other: bool = False, /):
|
||||
"""WakaReadme Prepare Markdown.
|
||||
|
||||
Prepared markdown content from the fetched statistics.
|
||||
```
|
||||
"""
|
||||
logger.debug("Making contents")
|
||||
contents = ""
|
||||
|
||||
# make title
|
||||
if wk_i.show_title:
|
||||
contents += make_title(stats.get("start"), stats.get("end")) + "\n\n"
|
||||
|
||||
# make byline
|
||||
if wk_i.show_masked_time and (
|
||||
total_time := stats.get("human_readable_total_including_other_language")
|
||||
):
|
||||
# overrides "human_readable_total"
|
||||
contents += f"Total Time: {total_time}\n\n"
|
||||
elif wk_i.show_total_time and (total_time := stats.get("human_readable_total")):
|
||||
contents += f"Total Time: {total_time}\n\n"
|
||||
|
||||
lang_info: list[dict[str, int | float | str]] | None = []
|
||||
|
||||
# Check if any language data exists
|
||||
if not (lang_info := stats.get("languages")):
|
||||
logger.debug("The API data seems to be empty, please wait for a day")
|
||||
contents += "No activity tracked"
|
||||
return contents.rstrip("\n")
|
||||
|
||||
# make lang content
|
||||
pad_len = len(
|
||||
# comment if it feels way computationally expensive
|
||||
max((str(lng["name"]) for lng in lang_info), key=len)
|
||||
# and then don't for get to set pad_len to say 13 :)
|
||||
)
|
||||
if language_count == 0 and not stop_at_other:
|
||||
logger.debug(
|
||||
"Set INPUT_LANG_COUNT to -1 to retrieve all language"
|
||||
+ " or specify a positive number (ie. above 0)"
|
||||
)
|
||||
return contents.rstrip("\n")
|
||||
|
||||
for idx, lang in enumerate(lang_info):
|
||||
lang_name = str(lang["name"])
|
||||
# >>> add languages to filter here <<<
|
||||
# if lang_name in {...}: continue
|
||||
lang_time = str(lang["text"]) if wk_i.show_time else ""
|
||||
lang_ratio = float(lang["percent"])
|
||||
lang_bar = make_graph(wk_i.block_style, lang_ratio, wk_i.graph_length, lang_name)
|
||||
contents += (
|
||||
f"{lang_name.ljust(pad_len)} "
|
||||
+ f"{lang_time: <16}{lang_bar} "
|
||||
+ f"{lang_ratio:.2f}".zfill(5)
|
||||
+ " %\n"
|
||||
)
|
||||
if language_count == -1:
|
||||
continue
|
||||
if stop_at_other and (lang_name == "Other"):
|
||||
break
|
||||
if idx + 1 >= language_count > 0: # idx starts at 0
|
||||
break
|
||||
|
||||
logger.debug("Contents were made\n")
|
||||
return contents.rstrip("\n")
|
||||
|
||||
|
||||
def fetch_stats():
|
||||
"""WakaReadme Fetch Stats.
|
||||
|
||||
Returns statistics as JSON string.
|
||||
"""
|
||||
attempts = 4
|
||||
statistic: dict[str, dict[str, Any]] = {}
|
||||
encoded_key = str(b64encode(bytes(str(wk_i.waka_key), "utf-8")), "utf-8")
|
||||
logger.debug(f"Pulling WakaTime stats from {' '.join(wk_i.time_range.split('_'))}")
|
||||
while attempts > 0:
|
||||
resp_message, fake_ua = "", cryptogenic.choice([str(fake.user_agent()) for _ in range(5)])
|
||||
# making a request
|
||||
if (
|
||||
resp := rq_get(
|
||||
url=f"{str(wk_i.api_base_url).rstrip('/')}/v1/users/current/stats/{wk_i.time_range}",
|
||||
headers={
|
||||
"Authorization": f"Basic {encoded_key}",
|
||||
"User-Agent": fake_ua,
|
||||
},
|
||||
timeout=(30.0 * (5 - attempts)),
|
||||
)
|
||||
).status_code != 200:
|
||||
resp_message += f" • {conn_info}" if (conn_info := resp.json().get("message")) else ""
|
||||
logger.debug(
|
||||
f"API response #{5 - attempts}: {resp.status_code} •" + f" {resp.reason}{resp_message}"
|
||||
)
|
||||
if resp.status_code == 200 and (statistic := resp.json()):
|
||||
logger.debug("Fetched WakaTime statistics")
|
||||
break
|
||||
logger.debug(f"Retrying in {30 * (5 - attempts )}s ...")
|
||||
sleep(30 * (5 - attempts))
|
||||
attempts -= 1
|
||||
|
||||
if err := (statistic.get("error") or statistic.get("errors")):
|
||||
logger.error(f"{err}\n")
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
return statistic.get("data")
|
||||
|
||||
|
||||
def churn(old_readme: str, /):
|
||||
"""WakaReadme Churn.
|
||||
|
||||
Composes WakaTime stats within markdown code snippet.
|
||||
"""
|
||||
# check if placeholder pattern exists in readme
|
||||
if not re.findall(wk_i.waka_block_pattern, old_readme):
|
||||
logger.warning(f"Can't find `{wk_i.waka_block_pattern}` pattern in readme")
|
||||
return None
|
||||
# getting contents
|
||||
if not (waka_stats := fetch_stats()):
|
||||
logger.error("Unable to fetch data, please rerun workflow\n")
|
||||
sys.exit(1)
|
||||
# preparing contents
|
||||
try:
|
||||
generated_content = prep_content(
|
||||
waka_stats, int(wk_i.language_count), bool(wk_i.stop_at_other)
|
||||
)
|
||||
except (AttributeError, KeyError, ValueError) as err:
|
||||
logger.error(f"Unable to read API data | {err}\n")
|
||||
sys.exit(1)
|
||||
print(generated_content, "\n", sep="")
|
||||
# substituting old contents
|
||||
new_readme = re.sub(
|
||||
pattern=wk_i.waka_block_pattern,
|
||||
repl=f"{wk_i.start_comment}\n\n```{wk_i.code_lang}\n{generated_content}\n```\n\n{wk_i.end_comment}",
|
||||
string=old_readme,
|
||||
)
|
||||
if len(sys.argv) == 2 and sys.argv[1] == "--dev":
|
||||
logger.debug("Detected run in `dev` mode.")
|
||||
# to avoid accidentally writing back to Github
|
||||
# when developing and testing WakaReadme
|
||||
return None
|
||||
|
||||
return None if new_readme == old_readme else new_readme
|
||||
|
||||
|
||||
def genesis():
|
||||
"""Run Program."""
|
||||
logger.debug("Connecting to GitHub")
|
||||
gh_connect = Github(wk_i.gh_token)
|
||||
# since a validator is being used casting to string here is okay
|
||||
gh_repo = gh_connect.get_repo(str(wk_i.repository))
|
||||
readme_file = gh_repo.get_readme()
|
||||
logger.debug("Decoding readme contents\n")
|
||||
readme_contents = str(readme_file.decoded_content, encoding="utf-8")
|
||||
if new_content := churn(readme_contents):
|
||||
logger.debug("WakaReadme stats has changed")
|
||||
gh_repo.update_file(
|
||||
path=readme_file.path,
|
||||
message=wk_i.commit_message,
|
||||
content=new_content,
|
||||
sha=readme_file.sha,
|
||||
)
|
||||
logger.info("Stats updated successfully")
|
||||
return
|
||||
|
||||
logger.info("WakaReadme was not updated")
|
||||
|
||||
|
||||
################### driver ###################
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# faker data preparation
|
||||
fake = Faker()
|
||||
Faker.seed(0)
|
||||
cryptogenic = SystemRandom()
|
||||
|
||||
# initial waka-readme setup
|
||||
logger.debug('Initialize WakaReadme')
|
||||
wk_c = WakaConstants()
|
||||
logger.debug("Initialize WakaReadme")
|
||||
wk_i = WakaInput()
|
||||
if not wk_i.validate_input():
|
||||
logger.error('Environment variables are misconfigured')
|
||||
logger.error("Environment variables are misconfigured\n")
|
||||
sys.exit(1)
|
||||
logger.debug('Input validation complete')
|
||||
|
||||
# run
|
||||
try:
|
||||
genesis()
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
logger.error('Interrupt signal received')
|
||||
logger.error("Interrupt signal received\n")
|
||||
sys.exit(1)
|
||||
except (GithubException, RequestException) as rq_exp:
|
||||
logger.critical(rq_exp)
|
||||
logger.critical(f"{rq_exp}\n")
|
||||
sys.exit(1)
|
||||
print('\nThanks for using WakaReadme!')
|
||||
print("\nThanks for using WakaReadme!\n")
|
||||
|
1261
poetry.lock
generated
1261
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
4
poetry.toml
Normal file
4
poetry.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[virtualenvs]
|
||||
prefer-active-python = true
|
||||
in-project = true
|
||||
path = ".venv"
|
@@ -1,25 +1,68 @@
|
||||
####################
|
||||
# Metadata #
|
||||
####################
|
||||
|
||||
[tool.poetry]
|
||||
name = "waka-readme"
|
||||
version = "0.1.7"
|
||||
version = "0.2.1"
|
||||
description = "Wakatime Weekly Metrics on your Profile Readme."
|
||||
authors = ["Athul Cyriac Ajay <athul8720@gmail.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
#packages = [{include = "waka_readme"}]
|
||||
|
||||
|
||||
####################
|
||||
# Dependencies #
|
||||
####################
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
requests = "^2.28.1"
|
||||
PyGithub = "^1.57"
|
||||
Faker = "^15.3.2"
|
||||
faker = "^18.10.1"
|
||||
pygithub = "^1.58.2"
|
||||
requests = "^2.31.0"
|
||||
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
autopep8 = "^2.0.0"
|
||||
pylint = "^2.15.6"
|
||||
python-dotenv = "^0.21.0"
|
||||
loguru = "^0.6.0"
|
||||
bandit = "^1.7.4"
|
||||
loguru = "^0.7.0"
|
||||
python-dotenv = "^1.0.0"
|
||||
|
||||
[tool.poetry.group.tooling]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.tooling.dependencies]
|
||||
bandit = "^1.7.5"
|
||||
black = "^23.3.0"
|
||||
ruff = "^0.0.272"
|
||||
|
||||
|
||||
####################
|
||||
# Build System #
|
||||
####################
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
|
||||
####################
|
||||
# Configurations #
|
||||
####################
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ["py311"]
|
||||
|
||||
[tool.bandit]
|
||||
exclude_dirs = [".github", ".pytest_cache", ".tox", ".vscode", "site", "tests"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.ruff.isort]
|
||||
force-sort-within-sections = true
|
||||
relative-imports-order = "closest-to-furthest"
|
||||
|
@@ -1,18 +1,20 @@
|
||||
'''
|
||||
Tests for the main.py
|
||||
'''
|
||||
"""Unit Tests."""
|
||||
|
||||
# standard
|
||||
from importlib import import_module
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass # , field
|
||||
from itertools import product
|
||||
# from inspect import cleandoc
|
||||
# from json import loads
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# from pathlib import Path
|
||||
# from inspect import cleandoc
|
||||
# from typing import Any
|
||||
# from json import load
|
||||
|
||||
try:
|
||||
prime = import_module('main')
|
||||
prime = import_module("main")
|
||||
# works when running as
|
||||
# python -m unittest discover
|
||||
except ImportError as err:
|
||||
@@ -22,68 +24,75 @@ except ImportError as err:
|
||||
|
||||
@dataclass
|
||||
class TestData:
|
||||
"""Test Data"""
|
||||
"""Test Data."""
|
||||
|
||||
# for future tests
|
||||
# waka_json: dict | None = None
|
||||
bar_percent: tuple[float] | None = None
|
||||
graph_blocks: tuple[str] | None = None
|
||||
waka_graphs: tuple[list[str]] | None = None
|
||||
dummy_readme: str = ''
|
||||
# waka_json: dict[str, dict[str, Any]] = field(
|
||||
# default_factory=lambda: {}
|
||||
# )
|
||||
bar_percent: tuple[int | float, ...] | None = None
|
||||
graph_blocks: tuple[str, ...] | None = None
|
||||
waka_graphs: tuple[list[str], ...] | None = None
|
||||
dummy_readme: str = ""
|
||||
|
||||
def populate(self) -> None:
|
||||
"""Populate Test Data"""
|
||||
"""Populate Test Data."""
|
||||
# for future tests
|
||||
# with open(file='tests/sample_data.json', mode='rt', encoding='utf-8') as wkf:
|
||||
# self.waka_json = loads(wkf.read())
|
||||
# with open(
|
||||
# file=Path(__file__).parent / "sample_data.json",
|
||||
# encoding="utf-8",
|
||||
# mode="rt",
|
||||
# ) as wkf:
|
||||
# self.waka_json = load(wkf)
|
||||
|
||||
self.bar_percent = (
|
||||
0, 100, 49.999, 50, 25, 75, 3.14, 9.901, 87.334, 87.333, 4.666, 4.667
|
||||
)
|
||||
self.bar_percent = (0, 100, 49.999, 50, 25, 75, 3.14, 9.901, 87.334, 87.333, 4.666, 4.667)
|
||||
|
||||
self.graph_blocks = ("░▒▓█", "⚪⚫", "⓪①②③④⑤⑥⑦⑧⑨⑩")
|
||||
|
||||
self.waka_graphs = ([
|
||||
"░░░░░░░░░░░░░░░░░░░░░░░░░",
|
||||
"█████████████████████████",
|
||||
"████████████▒░░░░░░░░░░░░",
|
||||
"████████████▓░░░░░░░░░░░░",
|
||||
"██████▒░░░░░░░░░░░░░░░░░░",
|
||||
"██████████████████▓░░░░░░",
|
||||
"▓░░░░░░░░░░░░░░░░░░░░░░░░",
|
||||
"██▒░░░░░░░░░░░░░░░░░░░░░░",
|
||||
"██████████████████████░░░",
|
||||
"█████████████████████▓░░░",
|
||||
"█░░░░░░░░░░░░░░░░░░░░░░░░",
|
||||
"█▒░░░░░░░░░░░░░░░░░░░░░░░"
|
||||
],
|
||||
self.waka_graphs = (
|
||||
[
|
||||
"⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪",
|
||||
"⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪"
|
||||
],
|
||||
"░░░░░░░░░░░░░░░░░░░░░░░░░",
|
||||
"█████████████████████████",
|
||||
"████████████▒░░░░░░░░░░░░",
|
||||
"████████████▓░░░░░░░░░░░░",
|
||||
"██████▒░░░░░░░░░░░░░░░░░░",
|
||||
"██████████████████▓░░░░░░",
|
||||
"▓░░░░░░░░░░░░░░░░░░░░░░░░",
|
||||
"██▒░░░░░░░░░░░░░░░░░░░░░░",
|
||||
"██████████████████████░░░",
|
||||
"█████████████████████▓░░░",
|
||||
"█░░░░░░░░░░░░░░░░░░░░░░░░",
|
||||
"█▒░░░░░░░░░░░░░░░░░░░░░░░",
|
||||
],
|
||||
[
|
||||
"⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑤⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑤⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩③⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑧⓪⓪⓪⓪⓪⓪",
|
||||
"⑧⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑤⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑧⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑧⓪⓪⓪",
|
||||
"⑩②⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩②⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪"
|
||||
])
|
||||
"⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪",
|
||||
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪",
|
||||
"⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
"⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
|
||||
],
|
||||
[
|
||||
"⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑤⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑤⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩③⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑧⓪⓪⓪⓪⓪⓪",
|
||||
"⑧⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑤⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑧⓪⓪⓪",
|
||||
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑧⓪⓪⓪",
|
||||
"⑩②⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
"⑩②⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
|
||||
],
|
||||
)
|
||||
|
||||
# self.dummy_readme = cleandoc("""
|
||||
# My Test Readme Start
|
||||
@@ -94,41 +103,43 @@ class TestData:
|
||||
|
||||
|
||||
class TestMain(unittest.TestCase):
|
||||
"""Testing Main Module"""
|
||||
"""Testing Main Module."""
|
||||
|
||||
def test_make_graph(self) -> None:
|
||||
"""Test graph maker"""
|
||||
"""Test graph maker."""
|
||||
if not tds.graph_blocks or not tds.waka_graphs or not tds.bar_percent:
|
||||
raise AssertionError("Data population failed")
|
||||
|
||||
for (idx, grb), (jdy, bpc) in product(
|
||||
enumerate(tds.graph_blocks), enumerate(tds.bar_percent)
|
||||
):
|
||||
self.assertEqual(
|
||||
prime.make_graph(grb, bpc, 25),
|
||||
tds.waka_graphs[idx][jdy]
|
||||
)
|
||||
self.assertEqual(prime.make_graph(grb, bpc, 25), tds.waka_graphs[idx][jdy])
|
||||
|
||||
def test_make_title(self) -> None:
|
||||
"""Test title maker"""
|
||||
"""Test title maker."""
|
||||
self.assertRegex(
|
||||
prime.make_title('2022-01-11T23:18:19Z', '2021-12-09T10:22:06Z'),
|
||||
r'From: \d{2} \w{3,9} \d{4} - To: \d{2} \w{3,9} \d{4}'
|
||||
prime.make_title("2022-01-11T23:18:19Z", "2021-12-09T10:22:06Z"),
|
||||
r"From: \d{2} \w{3,9} \d{4} - To: \d{2} \w{3,9} \d{4}",
|
||||
)
|
||||
|
||||
# Known test limits
|
||||
# # prep_content() and churn():
|
||||
# requires additional modifications such as changing
|
||||
# globally passed values to parametrically passing them
|
||||
# # fetch_stats(): would required HTTP Authentication
|
||||
def test_strtobool(self) -> None:
|
||||
"""Test string to bool."""
|
||||
self.assertTrue(prime.strtobool("Yes"))
|
||||
self.assertFalse(prime.strtobool("nO"))
|
||||
self.assertTrue(prime.strtobool(True))
|
||||
self.assertRaises(AttributeError, prime.strtobool, None)
|
||||
self.assertRaises(ValueError, prime.strtobool, "yo!")
|
||||
self.assertRaises(AttributeError, prime.strtobool, 20.5)
|
||||
|
||||
|
||||
tds = TestData()
|
||||
tds.populate()
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.path.insert(0, os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..')
|
||||
))
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
import main as prime
|
||||
|
||||
# works when running as
|
||||
# python tests/test_main.py
|
||||
except ImportError as im_er:
|
||||
|
Reference in New Issue
Block a user