Compare commits

..

No commits in common. "master" and "0.1.1" have entirely different histories.

21 changed files with 242 additions and 12822 deletions

View File

@ -1,28 +0,0 @@
# Do NOT edit this file, make
# a copy and rename as `.env`
INPUT_GH_TOKEN=
INPUT_WAKATIME_API_KEY=
# meta
INPUT_API_BASE_URL=
INPUT_REPOSITORY=
# content
INPUT_SHOW_TITLE=
INPUT_SECTION_NAME=
INPUT_BLOCKS=
INPUT_CODE_LANG=
INPUT_TIME_RANGE=
INPUT_LANG_COUNT=
INPUT_SHOW_TIME=
INPUT_SHOW_TOTAL=
INPUT_SHOW_MASKED_TIME=
INPUT_STOP_AT_OTHER=
INPUT_IGNORED_LANGUAGES=
# commit
INPUT_COMMIT_MESSAGE=
INPUT_TARGET_BRANCH=
INPUT_TARGET_PATH=
INPUT_COMMITTER_NAME=
INPUT_COMMITTER_EMAIL=
INPUT_AUTHOR_NAME=
INPUT_AUTHOR_EMAIL=

4
.github/FUNDING.yml vendored
View File

@ -1,4 +0,0 @@
# These are supported funding model platforms
ko_fi: athulcyriac
custom: https://www.buymeacoffee.com/athulca

View File

@ -1,24 +0,0 @@
name: WakaReadme CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
UnitTests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build docker image
run: |
# Clear existing cache
docker builder prune --force
# Build and run container (executes unit tests)
docker compose -p waka-readme -f ./compose.yml up --no-color --pull always --build --force-recreate
# Cleanup
docker compose -p waka-readme -f ./compose.yml down --rmi all

171
.gitignore vendored
View File

@ -1,171 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
poetry.lock
# pdm
# Similar to poetry.lock, it is generally recommended to include pdm.lock in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
# However, this project does not rely on pdm for production.
pdm.lock
.pdm-python
.pdm-build
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
*.env
env.sh
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# VSCode
.vscode/
# asdf/rtx
.tool-versions
.rtx.toml
# ruff
.ruff_cache

View File

@ -1,66 +0,0 @@
# Contributing
![python_ver](https://img.shields.io/badge/Python-%3E%3D3.12-blue.svg)
> First off, thank you! Please follow along.
**You need to _`fork`_ this repository & _`clone`_ it onto your system.** Inside the cloned folder, create a `.env` file with the following contents (without `# comments`):
```ini
INPUT_GH_TOKEN=EXAMPLE_GITHUB_PAT # required (for development)
INPUT_WAKATIME_API_KEY=EXAMPLE-WAKATIME-API-KEY # required
INPUT_API_BASE_URL=https://wakatime.com/api # required
INPUT_REPOSITORY=GITHUB_USERNAME/REPOSITORY_NAME # required
INPUT_COMMIT_MESSAGE=Updated WakaReadme graph with new metrics
INPUT_SHOW_TITLE=true
INPUT_SECTION_NAME=waka
INPUT_BLOCKS=->
INPUT_SHOW_TIME=true
INPUT_SHOW_TOTAL=true
INPUT_TIME_RANGE=last_7_days
INPUT_SHOW_MASKED_TIME=false
INPUT_LANG_COUNT=0
INPUT_STOP_AT_OTHER=true
INPUT_IGNORED_LANGUAGES=
```
**NEVER commit this `.env` file!**
## Using containers (recommended)
> Assumes that you already have latest version of either [`podman`](https://podman.io/) or [`docker`](https://www.docker.com/) (with [`compose`](https://docs.docker.com/compose/)) installed & configured.
>
> Replace `podman` with `docker` everywhere, if you're using the latter.
```sh
# Build and watch logs
$ podman-compose -p waka-readme -f ./docker-compose.yml up
# Cleanup
$ podman-compose -p waka-readme -f ./docker-compose.yml down
```
---
## Using virtual environments
> Assumes you've already installed & configured latest version of [python](https://www.python.org/).
1. Inside the cloned folder run the following commands to install dependencies
```sh
$ python -m venv .venv
$ . ./.venv/bin/activate
$ python -m pip install .
# ... install decencies ...
```
to activate virtual environment & install dependencies.
2. To test or execute the program in development, run:
```sh
(.venv)$ python -m unittest discover # run tests
(.venv)$ python -m main --dev # execute program in dev mode
```
> You can use any other virtual environment & dependency manager as well.

8
Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM python:latest
# Install dependencies.
ADD requirements.txt /requirements.txt
ADD main.py /main.py
RUN pip install -r requirements.txt
CMD ["python", "/main.py"]

25
LICENSE
View File

@ -1,21 +1,20 @@
MIT License
The MIT License (MIT)
Copyright (c) 2020 ATHUL CYRIAC AJAY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

13
Pipfile Normal file
View File

@ -0,0 +1,13 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
requests = "*"
pygithub = "*"
[requires]
python_version = "3.7"

85
Pipfile.lock generated Normal file
View File

@ -0,0 +1,85 @@
{
"_meta": {
"hash": {
"sha256": "ee0dbc310e64a4cc599d6b5e02bb22e3f33eb97ead8e5f1e315cf355fbef9367"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
],
"version": "==2020.6.20"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"deprecated": {
"hashes": [
"sha256:525ba66fb5f90b07169fdd48b6373c18f1ee12728ca277ca44567a367d9d7f74",
"sha256:a766c1dccb30c5f6eb2b203f87edd1d8588847709c78589e1521d769addc8218"
],
"version": "==1.2.10"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"pygithub": {
"hashes": [
"sha256:8375a058ec651cc0774244a3bc7395cf93617298735934cdd59e5bcd9a1df96e",
"sha256:d2d17d1e3f4474e070353f201164685a95b5a92f5ee0897442504e399c7bc249"
],
"index": "pypi",
"version": "==1.51"
},
"pyjwt": {
"hashes": [
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
],
"version": "==1.7.1"
},
"requests": {
"hashes": [
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
],
"index": "pypi",
"version": "==2.24.0"
},
"urllib3": {
"hashes": [
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
],
"version": "==1.25.9"
},
"wrapt": {
"hashes": [
"sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
],
"version": "==1.12.1"
}
},
"develop": {}
}

202
README.md
View File

@ -1,191 +1,75 @@
# Dev Metrics in Readme
fork from [waka-readme](https://github/athul/waka-readme)
edit for Gitea use.
[Wakatime](https://wakatime.com) Weekly Metrics on your Profile Readme:
![Project Preview](https://user-images.githubusercontent.com/8397274/87243943-e6b45c00-c457-11ea-94c9-2aa0bf241be8.png)
[WakaTime](https://wakatime.com) coding metrics on your profile readme.
## Update your Readme
## New to WakaTime?
Add a comment to your README like the follows
> Nope? Skip to [#Prep work](#prep-work).
```md
<!--START_SECTION:waka-->
<!--END_SECTION:waka-->
```
WakaTime gives you an idea of the time you spent on coding.
This helps you boost your productivity and competitive edge (aka _flex_ :muscle:).
The lines will be our entrypoints for our metrics.
1. Head over to <https://wakatime.com/> and create an account.
2. After logging in get your WakaTime API Key from <https://wakatime.com/api-key/>.
3. Install [WakaTime plugin][waka_plugins] in your favorite editor / IDE.
4. Paste in your API key to start telemetry.
## Using it
:information_source: **Info** | You can read [WakaTime help][waka_help] to know more about configurations.
Alternatively, you can fetch data from WakaTime compatible services such as [Wakapi][wakapi] or [Hakatime][hakatime].
- Get your Wakatime API Key from your [Account Settings in Wakatime](https://wakatime.com/settings/account) and save it as `WAKATIME_API_KEY = <your wakatime API Key>` in your Repository Secrets
## Prep Work
That's it. The Action runs everyday at 00.00 UTC
A Gitea repository and a `README.md` file is required. We'll be making use of readme in the [profile repository][profile_readme].
### Profile Repository
- Save the `README.md` file after copy-pasting the following special comments. Your dev-metics will show up in between.
If you're executing the workflow on your Profile Repository (`<username>/<username>`)
```md
<!--START_SECTION:waka-->
<!--END_SECTION:waka-->
```
**You wouldn't need an GitHub Access Token since GitHub Actions already makes one for you.**
`<!--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`:
- Go to `Secrets` (at `https://your_gitea_url/username/.profile/settings/actions/secrets` by replacing the `USERNAME` with your own username) and add a new secret "_Named_" `WAKATIME_API_KEY` with your API key as it's "_Secret_".
- Create a new workflow file named `waka-readme.yml` inside `.gitea/workflows/` folder of your profile repository.
- Clear all existing contents, add following lines and save the file.
```yml
name: Waka Readme
on:
# for manual workflow trigger
workflow_dispatch:
schedule:
# runs at 12 AM UTC (5:30 AM IST)
- cron: "0 0 * * *"
jobs:
update-readme:
name: WakaReadme DevMetrics
runs-on: ubuntu-latest
steps:
- uses: https://git.mamahaha.work/sangge/waka-readme@master # this action name
with:
WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }}
```
Refer [#Example](#example) section for a full blown workflow file.
## Tweaks
There are many flags that you can modify as you see fit.
### Meta Tweaks
| Environment flag | Options (`Default`, `Other`, ...) | Description |
| ---------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `API_BASE_URL` | `https://wakatime.com/api`, `https://wakapi.dev/api`, `https://hakatime.mtx-dev.xyz/api` | Use WakaTime compatible services like [Wakapi][wakapi] & [Hakatime][hakatime] |
| `REPOSITORY` | `<gh_username>/<gh_username>`, `<gh_username>/<repo_name>` | Waka-readme stats will appear on the provided repository |
### Content Tweaks
| Environment flag | Options (`Default`, `Other`, ...) | Description |
| ------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `SHOW_TITLE` | `false`, `true` | Add title to waka-readme stats blob |
| `SECTION_NAME` | `waka`, any alphanumeric string | The generator will look for section name to fill up the readme. |
| `BLOCKS` | `░▒▓█`, `⣀⣄⣤⣦⣶⣷⣿`, `-#`, `=>`, you can be creative | Ascii art used to build stats graph |
| `CODE_LANG` | `txt`, `python` `ruby` `json` , you can use other languages also | Language syntax based highlighted text |
| `TIME_RANGE` | `last_7_days`, `last_30_days`, `last_6_months`, `last_year`, `all_time` | String representing a dispensation from which stats are aggregated |
| `LANG_COUNT` | `5`, any plausible number | Number of languages to be displayed |
| `SHOW_TIME` | `true`, `false` | Displays the amount of time spent for each language |
| `SHOW_TOTAL` | `false`, `true` | Show total coding time |
| `SHOW_MASKED_TIME` | `false`, `true` | Adds total coding time including unclassified languages (overrides: `SHOW_TOTAL`) |
| `STOP_AT_OTHER` | `false`, `true` | Stop when language marked as `Other` is retrieved (overrides: `LANG_COUNT`) |
| `IGNORED_LANGUAGES` | <code> </code>, `Binary YAML JSON TOML` | Hide languages from your stats |
### Commit Tweaks
| Environment flag | Options (`Default`, `Other`, ...) |
| ----------------- | -------------------------------------------------------------------- |
| `COMMIT_MESSAGE` | `Updated waka-readme graph with new metrics`, any reasonable message |
| `TARGET_BRANCH` | `NOT_SET`, target branch name |
| `TARGET_PATH` | `NOT_SET`, `/path/to/target/file` |
| `COMMITTER_NAME` | `NOT_SET`, committer name |
| `COMMITTER_EMAIL` | `NOT_SET`, committer email |
| `AUTHOR_NAME` | `NOT_SET`, author name |
| `AUTHOR_EMAIL` | `NOT_SET`, author email |
The first option is the _default_ value of the _flag_, subsequent options are valid values available for the _flag_.
## Example
**`waka-readme.yml`**
Here is a sample workflow file for you to get started,
```yml
name: Waka Readme
on:
# for manual workflow trigger
workflow_dispatch:
schedule:
# runs at 12 AM UTC (5:30 AM IST)
- cron: "0 0 * * *"
# Runs at 12am UTC
- cron: '0 0 * * *'
jobs:
update-readme:
name: WakaReadme DevMetrics
name: Update this repo's README
runs-on: ubuntu-latest
steps:
# this action name
- uses: https://git.mamahaha.work/sangge/waka-readme@master # do NOT replace with anything else
- uses: athul/waka-readme@master
with:
GH_TOKEN: ${{ secrets.GH_TOKEN }} # optional if on profile readme
WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }} # required
### meta
API_BASE_URL: https://wakatime.com/api # optional
REPOSITORY: YOUR_GITEA_USERNAME/YOUR_REPOSITORY_NAME # optional
### content
SHOW_TITLE: true # optional
SECTION_NAME: waka # optional
BLOCKS: -> # optional
CODE_LANG: rust # optional
TIME_RANGE: all_time # optional
LANG_COUNT: 10 # optional
SHOW_TIME: true # optional
SHOW_TOTAL: true # optional
SHOW_MASKED_TIME: false # optional
STOP_AT_OTHER: true # optional
IGNORED_LANGUAGES: YAML JSON TOML # optional
### commit
COMMIT_MESSAGE: Updated waka-readme graph with new metrics # optional
TARGET_BRANCH: master # optional
TARGET_PATH: README.md # optional
COMMITTER_NAME: GitHubActionBot # optional
COMMITTER_EMAIL: action-bot@github.com # optional
AUTHOR_NAME: YOUR_NAME # optional
AUTHOR_EMAIL: YOUR@EMAIL.com # optional
# you can populate email-id with secretes instead
WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }}
```
_Rendered `markdown`:_
### Different Repository than Profile Repository
<!-- prettier-ignore-start -->
if you're executing the workflow on another repo other than `<username>/<username>`
```rust
From: 10 July 2020 - To: 06 August 2022
- You'll need to get a GitHub Access Token with a `repo` scope and save it in the Repo Secrets `GH_TOKEN = <Your GitHub Access Token>`
Total Time: 1,464 hrs 54 mins
Here is Sample Worflow File for running it
Python 859 hrs 29 mins >>>>>>>>>>>>>>----------- 54.68 %
Markdown 132 hrs 33 mins >>----------------------- 08.43 %
TeX 103 hrs 52 mins >>----------------------- 06.61 %
HTML 94 hrs 48 mins >>----------------------- 06.03 %
Nim 64 hrs 31 mins >------------------------ 04.11 %
Other 47 hrs 58 mins >------------------------ 03.05 %
```yml
name: Waka Readme
on:
schedule:
# Runs at 12am UTC
- cron: '0 0 * * *'
jobs:
update-readme:
name: Update Readme with Metrics
runs-on: ubuntu-latest
steps:
- uses: athul/waka-readme@master
with:
WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }}
GH_TOKEN: ${{ secrets.GH_TOKEN}}
USERNAME: <username> # optional, it will automaticially use the username that executing the workflow
```
<!-- prettier-ignore-end -->
## Notes
- Flags `REPOSITORY` and `GH_TOKEN` are required ONLY if, you are NOT using [profile readme][profile_readme].
- If you are using `GH_TOKEN`, make sure set the [fine grained token](https://github.com/settings/tokens?type=beta) scope to repository contents with `read-and-write` access. See [#141 (comment)](https://github.com/athul/waka-readme/issues/141#issuecomment-1679831949).
- `WAKATIME_API_KEY` is a **required** secret. All other environment variables are optional.
- The above example does NOT show proper default values, refer [#Tweaks](#tweaks) for the same.
- `IGNORED_LANGUAGES` is suggested for [.NET](https://dotnet.microsoft.com) users, as WakaTime assumes you're working with `Binary`, while debugging.
## 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:
[//]: #(Links)
[wakapi]: https://wakapi.dev
[hakatime]: https://github.com/mujx/hakatime
[waka_plugins]: https://wakatime.com/plugins
[waka_help]: https://wakatime.com/help/editors
[profile_readme]: https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/managing-your-profile-readme

View File

@ -1,106 +1,26 @@
name: "Waka - Readme"
author: "Athul Cyriac Ajay"
description: "WakaTime coding activity graph in your profile readme"
name: 'Waka - Readme'
author: Athil Cyriac Ajay
description: 'Add a Wakatime Coding Activity graph in your Readme'
inputs:
GITEA_TOKEN:
description: "GitHub access token with Repo scope"
GH_TOKEN:
description: 'GitHub access token with Repo scope'
required: true
default: ${{ github.token }}
required: true
WAKATIME_API_KEY:
description: "Your Wakatime/Wakapi/Hakatime API Key"
description: 'Your Wakatime API Key'
required: true
# meta tweaks
API_BASE_URL:
description: "Alternative API base URL when using a third-party WakaTime-ish backend"
default: "https://wakatime.com/api"
required: false
REPOSITORY:
description: "Your GitHub repository"
default: ${{ github.repository }}
required: false
USERNAME:
description: 'Your GitHub username'
default: ${{ github.repository_owner }}
# content tweaks
SHOW_TITLE:
description: "Displays the week number and days in Readme as title"
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"
required: false
SHOW_TOTAL:
description: "Displays total coding time"
default: "false"
required: false
SHOW_MASKED_TIME:
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
IGNORED_LANGUAGES:
description: "Ignore space separated, listed languages"
default: ""
required: false
# commit tweaks
COMMIT_MESSAGE:
description: "Add a commit message of your choice"
default: "Updated waka-readme graph with new metrics"
required: false
TARGET_BRANCH:
description: "Target branch"
default: "NOT_SET"
required: false
TARGET_PATH:
description: "Target file path"
default: "NOT_SET"
required: false
COMMITTER_NAME:
description: "Committer name"
default: "NOT_SET"
required: false
COMMITTER_EMAIL:
description: "Committer email"
default: "NOT_SET"
required: false
AUTHOR_NAME:
description: "Author name"
default: "NOT_SET"
required: false
AUTHOR_EMAIL:
description: "Author email"
default: "NOT_SET"
required: false
runs:
using: "docker"
image: "dockerfile"
using: 'docker'
image: 'Dockerfile'
branding:
icon: "info"
color: "blue"
icon: 'info'
color: 'blue'

View File

@ -1,10 +0,0 @@
# for CI testing
services:
waka-readme:
env_file:
- .env.template
build:
context: .
dockerfile: containerfile
image: waka-readme:testing
container_name: WakaReadmeTesting

View File

@ -1,51 +0,0 @@
FROM docker.io/python:3-slim
ENV INPUT_GITEA_TOKEN \
INPUT_WAKATIME_API_KEY \
# meta
INPUT_API_BASE_URL \
INPUT_REPOSITORY \
# content
INPUT_SHOW_TITLE \
INPUT_SECTION_NAME \
INPUT_BLOCKS \
INPUT_CODE_LANG \
INPUT_TIME_RANGE \
INPUT_LANG_COUNT \
INPUT_SHOW_TIME \
INPUT_SHOW_TOTAL \
INPUT_SHOW_MASKED_TIME \
INPUT_STOP_AT_OTHER \
INPUT_IGNORED_LANGUAGES \
# commit
INPUT_COMMIT_MESSAGE \
INPUT_TARGET_BRANCH \
INPUT_TARGET_PATH \
INPUT_COMMITTER_NAME \
INPUT_COMMITTER_EMAIL \
INPUT_AUTHOR_NAME \
INPUT_AUTHOR_EMAIL
ENV PATH="${PATH}:/root/.local/bin" \
# python
PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
# pip
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DEFAULT_TIMEOUT=100
# copy project files
COPY --chown=root:root pyproject.toml main.py /app/
# install dependencies
RUN python -m pip install /app/ -i https://pypi.tuna.tsinghua.edu.cn/simple
# copy tests
COPY --chown=root:root tests /app/tests/
# run tests
CMD python -m unittest discover /app/

View File

@ -1,9 +0,0 @@
services:
waka-readme:
env_file:
- .env
build:
context: .
dockerfile: dockerfile
image: waka-readme:dev
container_name: WakaReadmeDev

View File

@ -1,48 +0,0 @@
FROM docker.io/python:3-slim
ENV INPUT_GH_TOKEN \
INPUT_WAKATIME_API_KEY \
# meta
INPUT_API_BASE_URL \
INPUT_REPOSITORY \
# content
INPUT_SHOW_TITLE \
INPUT_SECTION_NAME \
INPUT_BLOCKS \
INPUT_CODE_LANG \
INPUT_TIME_RANGE \
INPUT_LANG_COUNT \
INPUT_SHOW_TIME \
INPUT_SHOW_TOTAL \
INPUT_SHOW_MASKED_TIME \
INPUT_STOP_AT_OTHER \
INPUT_IGNORED_LANGUAGES \
# commit
INPUT_COMMIT_MESSAGE \
INPUT_TARGET_BRANCH \
INPUT_TARGET_PATH \
INPUT_COMMITTER_NAME \
INPUT_COMMITTER_EMAIL \
INPUT_AUTHOR_NAME \
INPUT_AUTHOR_EMAIL
ENV PATH="${PATH}:/root/.local/bin" \
# python
PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
# pip
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DEFAULT_TIMEOUT=100
# copy project files
COPY --chown=root:root pyproject.toml main.py /app/
# install dependencies
RUN python -m pip install /app/ -i https://pypi.tuna.tsinghua.edu.cn/simple
# execute program
CMD python /app/main.py

614
main.py
View File

@ -1,558 +1,58 @@
"""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 %
YAML 2 hrs 14 mins 06.33 %
Markdown 1 hr 54 mins 05.39 %
TOML 1 hr 48 mins 05.11 %
Other 35 mins 01.68 %
```
Contents := Title + Byline + Body
"""
# standard
from base64 import b64encode, b64decode
from dataclasses import dataclass
from datetime import datetime
from functools import partial
import logging as logger
import os
from random import SystemRandom
import base64
import requests
import re
import sys
from time import sleep
from typing import Any
# external
from faker import Faker
# from github import ContentFile, Github, GithubException, InputGitAuthor, Repository
import gitea
from requests import get as rq_get
from requests.exceptions import RequestException
################### setup ###################
print()
# hush existing loggers
for lgr_name in logger.root.manager.loggerDict:
# to disable log propagation completely set '.propagate = False'
logger.getLogger(lgr_name).setLevel(logger.WARNING)
# somehow github.Requester gets missed out from loggerDict
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,
)
try:
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
logger.debug("loguru loaded")
# load from .env before class def gets parsed
load_dotenv()
logger.debug("dotenv loaded")
waka_key: str | None = os.getenv("INPUT_WAKATIME_API_KEY")
except ImportError as im_err:
logger.warning(im_err)
################### 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
gitea_token: str | None = os.getenv("INPUT_GITEA_TOKEN")
gitea_url: str | None = os.getenv("GITHUB_SERVER_URL", "https://gitea.com")
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", ".profile")
# # 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
ignored_languages: str = os.getenv("INPUT_IGNORED_LANGUAGES", "")
# # optional meta
target_branch: str = os.getenv("INPUT_TARGET_BRANCH", "NOT_SET")
target_path: str = os.getenv("INPUT_TARGET_PATH", "NOT_SET")
committer_name: str = os.getenv("INPUT_COMMITTER_NAME", "NOT_SET")
committer_email: str = os.getenv("INPUT_COMMITTER_EMAIL", "NOT_SET")
author_name: str = os.getenv("INPUT_AUTHOR_NAME", "NOT_SET")
author_email: str = os.getenv("INPUT_AUTHOR_EMAIL", "NOT_SET")
def validate_input(self):
"""Validate Input Env Variables."""
logger.debug("Validating input variables")
if (
not self.gitea_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
for option in (
"target_branch",
"target_path",
"committer_name",
"committer_email",
"author_name",
"author_email",
):
if not getattr(self, option):
logger.warning(f"Improper '{option}' configuration")
logger.debug(f"Using default '{option}'")
setattr(self, option, "NOT_SET")
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], /):
"""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 do not for get to set `pad_len` to say 13 :)
)
language_count, stop_at_other = int(wk_i.language_count), bool(wk_i.stop_at_other)
if language_count == 0 and not wk_i.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")
ignored_languages = set[str](igl.lower() for igl in wk_i.ignored_languages.strip().split())
for idx, lang in enumerate(lang_info):
lang_name = str(lang["name"])
if ignored_languages and lang_name.lower() in ignored_languages:
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"Cannot 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)
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,
)
logger.debug(new_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 or testing waka-readme
return None
return None if new_readme == old_readme else new_readme
# def qualify_target(gitea_repo: Repository.Repository):
def qualify_target(gitea_repo: gitea.Repository, gitea_connect: gitea.Gitea):
"""Qualify target repository defaults."""
@dataclass
class TargetRepository:
# this: ContentFile.ContentFile
this: gitea.Content
path: str
commit_message: str
sha: str
branch: str
# committer: InputGitAuthor | None
# author: InputGitAuthor | None
committer: None
author: None
gitea_branch = gitea_repo.get_branches()[0].name
# if wk_i.target_branch != "NOT_SET":
# gitea_branch = gitea_repo.get_branch(wk_i.target_branch)
readme_content = gitea.Content(gitea_connect)
readme_content.path = "README.md"
readme_content.type = gitea.Content.FILE
target = gitea_repo.get_file_content(readme_content) # base64 encoded
# target = gitea_repo.get_readme()
# if wk_i.target_path != "NOT_SET":
# target = gitea_repo.get_contents(
# path=wk_i.target_path,
# ref=gitea_branch if isinstance(gitea_branch, str) else gitea_branch.commit.sha,
# )
if isinstance(target, list):
raise RuntimeError("Cannot handle multiple files.")
committer, author = None, None
# if wk_i.committer_name != "NOT_SET" and wk_i.committer_email != "NOT_SET":
# committer = InputGitAuthor(name=wk_i.committer_name, email=wk_i.committer_email)
# if wk_i.author_name != "NOT_SET" and wk_i.author_email != "NOT_SET":
# author = InputGitAuthor(name=wk_i.author_name, email=wk_i.author_email)
sha = ""
return TargetRepository(
this=target,
path="README.md",
commit_message=wk_i.commit_message,
sha=sha,
branch=gitea_branch if isinstance(gitea_branch, str) else gitea_branch.name,
committer=committer,
author=author,
)
def genesis():
"""Run Program."""
logger.debug("Connecting to Gitea")
gitea_connect = gitea.Gitea(wk_i.gitea_url, wk_i.gitea_token)
logger.debug("logged into gitea\n")
# since a validator is being used earlier, casting
# `wk_i.ENV_VARIABLE` to a string here, is okay
# gitea_repo = gitea_connect.get_repo(str(wk_i.repository))
owner = "sangge"
repo_name = ".profile"
gitea_repo = gitea.Repository.request(gitea_connect, owner, repo_name)
if len(sys.argv) == 2 and sys.argv[1] == "--dev":
logger.debug("Detected run in `dev` mode.,fetched repo \n")
target = qualify_target(gitea_repo, gitea_connect)
logger.debug("Decoding readme contents\n")
readme_contents = str(b64decode(target.this), encoding="utf-8")
if not (new_content := churn(readme_contents)):
logger.info("WakaReadme was not updated")
return
logger.debug("WakaReadme stats has changed")
# update_metric = partial(
# gitea_repo.update_file,
# path=target.path,
# message=target.commit_message,
# content=new_content,
# sha=target.sha,
# branch=target.branch,
# )
# if target.committer:
# update_metric = partial(update_metric, committer=target.committer)
# if target.author:
# update_metric = partial(update_metric, author=target.author)
# update_metric()
b64_new_content = b64encode(bytes(new_content, "utf-8"))
repo_content = gitea_repo.get_git_content()
readmes = [c for c in repo_content if c.name == "README.md"]
str_new_content = b64_new_content.decode("utf-8")
gitea_repo.change_file(readmes[0].name, readmes[0].sha, str_new_content)
logger.info("Stats updated successfully")
return
################### driver ###################
if __name__ == "__main__":
# faker data preparation
fake = Faker()
Faker.seed(0)
cryptogenic = SystemRandom()
# initial waka-readme setup
logger.debug("Initialize WakaReadme")
wk_i = WakaInput()
if not wk_i.validate_input():
logger.error("Environment variables are misconfigured\n")
sys.exit(1)
# run
try:
genesis()
except KeyboardInterrupt:
print("\r", end=" ")
logger.error("Interrupt signal received\n")
sys.exit(1)
except RuntimeError as err:
logger.error(f"{type(err).__name__}: {err}\n")
sys.exit(1)
# except (GithubException, RequestException) as rq_exp:
# logger.critical(f"{rq_exp}\n")
# sys.exit(1)
print("\nThanks for using WakaReadme!\n")
import os
from github import Github
START_COMMENT = '<!--START_SECTION:waka-->'
END_COMMENT = '<!--END_SECTION:waka-->'
listReg = f'{START_COMMENT}[\\s\\S]+{END_COMMENT}'
user = os.getenv("INPUT_USERNAME")
waka_key = os.getenv("INPUT_WAKATIME_API_KEY")
ghtoken = os.getenv("INPUT_GH_TOKEN")
def makeGraph(percent: float):
done_block = ""
empty_block = ""
pc_rnd = round(percent)
return (f'{done_block*int(pc_rnd/4)}{empty_block*int( 25-int(pc_rnd/4))}')
def getStats():
data = requests.get(
f"https://wakatime.com/api/v1/users/current/stats/last_7_days?api_key={waka_key}").json()
lang_data = data['data']['languages']
data_list = []
for l in lang_data[:5]:
ln = len(l['name'])
ln_text = len(l['text'])
op = f"{l['name']}{' '*(12-ln)}{l['text']}{' '*(20-ln_text)}{makeGraph(l['percent'])} {l['percent']}"
data_list.append(op)
data = " \n".join(data_list)
return ("```text\n"+data+"\n```")
def decodeReadme(data: str):
decodedBytes = base64.b64decode(data)
decodedStr = str(decodedBytes, "utf-8")
return decodedStr
def generatenewReadme(stats: str, readme: str):
statsinReadme = f"{START_COMMENT}\n{stats}\n{END_COMMENT}"
newReadme = re.sub(listReg, statsinReadme, readme)
return newReadme
if __name__ == '__main__':
g = Github(ghtoken)
repo = g.get_repo(f"{user}/{user}")
contents = repo.get_readme()
stats = getStats()
rdmd = decodeReadme(contents.content)
newreadme = generatenewReadme(stats=stats, readme=rdmd)
if newreadme != rdmd:
repo.update_file(path=contents.path, message="Updated with Dev Metrics",
content=newreadme, sha=contents.sha, branch="master")

View File

@ -1,86 +0,0 @@
####################
# Metadata #
####################
[project]
name = "waka-readme"
version = "0.3.0"
description = "Wakatime Weekly Metrics on your Profile Readme."
authors = [{ name = "Athul Cyriac Ajay", email = "athul8720@gmail.com" }]
license = { text = "MIT" }
readme = "README.md"
keywords = ["readme", "profile-page", "wakatime"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python",
"Typing :: Typed",
]
requires-python = ">=3.12"
dependencies = ["faker>=21.0.0", "pygithub>=2.1.1", "requests>=2.31.0", "py-gitea>=0.2.6"]
[project.urls]
Homepage = "https://github.com/athul/waka-readme"
Documentation = "https://github.com/athul/waka-readme#readme"
Repository = "https://github.com/athul/waka-readme"
Changelog = "https://github.com/athul/waka-readme/commits/master"
#############################
# Optional Dependencies #
#############################
[project.optional-dependencies]
extra = ["loguru>=0.7.2", "python-dotenv>=1.0.0"]
#############################
# Development Dependencies #
#############################
[tool.pdm.dev-dependencies]
tooling = ["bandit>=1.7.6", "black>=23.12.1", "ruff>=0.1.9", "pyright>=1.1.342"]
####################
# Configurations #
####################
[tool.bandit]
exclude_dirs = [".github", "tests", ".venv", ".vscode"]
[tool.black]
line-length = 100
target-version = ["py312"]
[tool.pyright]
exclude = ["**/__pycache__", ".venv/"]
pythonVersion = "3.12"
pythonPlatform = "All"
typeCheckingMode = "strict"
[tool.ruff]
select = [
# Pyflakes
"F",
# pycodestyle
"W",
"E",
# mccabe
# C90
# isort
"I",
# pep8-naming
"N",
# pydocstyle
"D",
]
line-length = 100
target-version = "py312"
extend-exclude = ["**/__pycache__"]
[tool.ruff.isort]
# case-sensitive = true
combine-as-imports = true
force-sort-within-sections = true
force-wrap-aliases = true
relative-imports-order = "closest-to-furthest"
[tool.ruff.pydocstyle]
convention = "google"

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
certifi==2020.6.20
chardet==3.0.4
Deprecated==1.2.10
idna==2.10
PyGithub==1.51
PyJWT==1.7.1
requests==2.24.0
urllib3==1.25.9
wrapt==1.12.1

View File

@ -1,7 +0,0 @@
"""Initialize test module."""
# standard
import logging
# comment to enable logging w/ tests
logging.disable(logging.CRITICAL)

File diff suppressed because it is too large Load Diff

View File

@ -1,148 +0,0 @@
"""Unit Tests."""
# standard
from dataclasses import dataclass # , field
from importlib import import_module
from itertools import product
import os
import sys
import unittest
# from pathlib import Path
# from inspect import cleandoc
# from typing import Any
# from json import load
try:
prime = import_module("main")
# works when running as
# python -m unittest discover
except ImportError as err:
print(err)
# sys.exit(1)
@dataclass
class TestData:
"""Test Data."""
# for future tests
# 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."""
# for future tests
# 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.graph_blocks = ("░▒▓█", "⚪⚫", "⓪①②③④⑤⑥⑦⑧⑨⑩")
self.waka_graphs = (
[
"░░░░░░░░░░░░░░░░░░░░░░░░░",
"█████████████████████████",
"████████████▒░░░░░░░░░░░░",
"████████████▓░░░░░░░░░░░░",
"██████▒░░░░░░░░░░░░░░░░░░",
"██████████████████▓░░░░░░",
"▓░░░░░░░░░░░░░░░░░░░░░░░░",
"██▒░░░░░░░░░░░░░░░░░░░░░░",
"██████████████████████░░░",
"█████████████████████▓░░░",
"█░░░░░░░░░░░░░░░░░░░░░░░░",
"█▒░░░░░░░░░░░░░░░░░░░░░░░",
],
[
"⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫",
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
"⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪⚪",
"⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
"⚫⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪",
"⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚫⚪⚪⚪",
"⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
"⚫⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪⚪",
],
[
"⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩",
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑤⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑤⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
"⑩⑩⑩⑩⑩⑩③⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑧⓪⓪⓪⓪⓪⓪",
"⑧⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
"⑩⑩⑤⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑧⓪⓪⓪",
"⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑩⑧⓪⓪⓪",
"⑩②⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
"⑩②⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪⓪",
],
)
# self.dummy_readme = cleandoc("""
# My Test Readme Start
# <!--START_SECTION:waka-->
# <!--END_SECTION:waka-->
# My Test Readme End
# """)
class TestMain(unittest.TestCase):
"""Testing Main Module."""
def test_make_graph(self) -> None:
"""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])
def test_make_title(self) -> None:
"""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}",
)
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__":
try:
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:
print(im_er)
sys.exit(1)
unittest.main()