35 Commits

Author SHA1 Message Date
Jovial Joe Jayarson
c34fb893a6 Merge pull request #131 from joe733/workshop
maint: monthly updates for June '23
2023-06-14 17:30:31 +05:30
Jovial Joe Jayarson
1fc26a4121 feat: update dependencies; bump version 2023-06-14 14:13:38 +05:30
Jovial Joe Jayarson
82c9408d6d maint: follows google pydocstyle 2023-06-13 19:40:52 +05:30
Jovial Joe Jayarson
9bee9ba11b maint: migrate to black formatting 2023-06-13 19:17:26 +05:30
Jovial Joe Jayarson
b42a071671 Merge pull request #129 from athul/dependabot/pip/cryptography-41.0.0
chore(deps): bump cryptography from 40.0.2 to 41.0.0
2023-06-03 05:39:40 +05:30
dependabot[bot]
e8541dbe4e chore(deps): bump cryptography from 40.0.2 to 41.0.0
Bumps [cryptography](https://github.com/pyca/cryptography) from 40.0.2 to 41.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/40.0.2...41.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-02 20:31:09 +00:00
Jisan
da0f4f1847 feat: Add language syntax support for color code (#128)
- feat: text highlight
- fix: wrong env, typo
2023-06-02 11:17:41 +05:30
Jovial Joe Jayarson
d26ed33e7a Merge pull request #124 from athul/dependabot/pip/requests-2.31.0
chore(deps): bump requests from 2.29.0 to 2.31.0
2023-05-23 12:31:51 +05:30
dependabot[bot]
a50019231d chore(deps): bump requests from 2.29.0 to 2.31.0
Bumps [requests](https://github.com/psf/requests) from 2.29.0 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.29.0...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-23 06:51:25 +00:00
Jovial Joe Jayarson
080a8c9b63 Merge pull request #122 from joe733/workshop
fix: adds granularity to show more languages
2023-05-05 16:07:45 +05:30
Jovial Joe Jayarson
fbc9196645 fix: adds granularity to show more languages
- adds option `STOP_AT_OTHER` to stop retrieval at lang marked `Other`
- updates dependencies
- makes dev dependencies optional
- bumps project version
- updates readme

**Related Items**

*Issues*

- Closes #121
2023-05-04 17:15:14 +05:30
Jovial Joe Jayarson
59f35b046b Merge pull request #120 from joe733/workshop
feat: bumped version to 0.1.9
2023-03-25 12:52:13 +05:30
Jovial Joe Jayarson
60fa45f3f0 feat: bumped version to 0.1.9
- updates dependencies
- prefer type inference with function return
- fix typos / minor improvements
- ignore all ``*.env` files
- bumped package version to 0.1.9
2023-03-25 10:05:49 +05:30
Jovial Joe Jayarson
60fe3b9f48 Merge pull request #119 from joe733/workshop
fix: adds check to find placeholder in old readme
2023-02-28 12:36:09 +05:30
Jovial Joe Jayarson
29dba6dd79 fix: adds check to find placeholder in old readme 2023-02-27 12:36:25 +05:30
Jovial Joe Jayarson
ac0bb21462 feat: bump version, update deps 2023-02-09 07:25:07 +05:30
Jovial Joe Jayarson
413150be53 Merge pull request #116 from joe733/workshop
fix: adds lang count option properly
2023-02-09 00:29:20 +05:30
Jovial Joe Jayarson
b2db3c3280 fix: adds lang count option properly
- adds language count, thanks @novialriptide
- validates language count input
- adds documentation for lang count
- adds lang count in action.yml
- stricter type checks & changes were made
- formatting changes
- updates dependencies
2023-02-04 17:31:44 +05:30
Jovial Joe Jayarson
6e66f34e5a Merge pull request #115 from novialriptide/add-language-count
Add language_count
2023-02-02 16:52:42 +05:30
Andrew Hong
de673c4749 Add language_count 2023-02-02 01:18:18 -05:00
dependabot[bot]
ce472c9c93 Merge pull request #113 from athul/dependabot/pip/certifi-2022.12.7 2022-12-09 12:49:54 +00:00
dependabot[bot]
8514942821 chore(deps): bump certifi from 2022.9.24 to 2022.12.7
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.9.24 to 2022.12.7.
- [Release notes](https://github.com/certifi/python-certifi/releases)
- [Commits](https://github.com/certifi/python-certifi/compare/2022.09.24...2022.12.07)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-09 09:30:09 +00:00
Jovial Joe Jayarson
d2c91885c3 Merge pull request #111 from joe733/workshop
maint: misc. refactorings, fixes, updates
2022-12-03 07:46:30 +05:30
Jovial Joe Jayarson
bd7707fc5a maint: misc. refactorings, fixes, updates
- puts tests in the correct location of github workflow
- fixes `.env` values not loading via `load_dotenv` - required early loading
- corrects many static type linting errors
- combines all inputs into a single class with validation
- formats markdown & python files, as well as output
- slightly improved log messages, caught potential attribute error
- updates dependencies
2022-12-01 07:41:38 +05:30
Jovial Joe Jayarson
72af24c8af Merge pull request #110 from guilyx/master
Custom section name in user readme
2022-11-28 17:15:18 +05:30
Erwin Lejeune
8ffb95d479 Customizable Section Name 2022-11-28 15:30:28 +04:00
Jovial Joe Jayarson
a80f7247c2 Merge pull request #108 from joe733/workshop
feat: release v0.1.7 🚀
2022-11-24 07:34:37 +05:30
Jovial Joe Jayarson
f106d3b9cc feat: release v0.1.7 🚀
- migrated to python 3.11 & poetry 1.2+
- updated project dependencies & readme
- DockerfileDev is now containerfile
- improve contribution docs
- it includes development with podman
2022-11-22 16:01:02 +05:30
Jovial Joe Jayarson
c2075190e1 Merge pull request #101 from joe733/workshop
feat: updated dockerfile, deps, guide & fix src
2022-09-21 09:30:15 +05:30
Jovial Joe Jayarson
44f2fac0d4 feat: updated dockerfile, deps, guide & fix src
- fix logical error, better non 200 resp message in main.py
- updated dependencies
- major updates to contributing guide
- quicker prod builds with reduced docker layers
- adds dockerfile for development
- updates github action for unit-tests
2022-09-21 09:21:22 +05:30
Jovial Joe Jayarson
3f32dda864 Merge pull request #99 from reekystive/patch-1
Fix API_BASE_URL default value in README
2022-09-13 23:00:18 +05:30
ReekyStive
8ba2186686 Fix API_BASE_URL default value in README 2022-09-13 17:57:17 +08:00
Jovial Joe Jayarson
6a37da6353 Merge pull request #92 from joe733/workshop
maint: attempt to fix repeated HTTP 202
2022-08-15 08:18:18 +05:30
Jovial Joe Jayarson
6c57b99980 maint: attempt to fix repeated HTTP 202
- removes http:// & https:// mount from request session
- removes request session (corrupt cookies might cause trouble)
- uses fake user-agent for each fresh requsts (new depns)
- updt. deps, adds SAST dev deps - bandit
2022-08-12 21:06:41 +05:30
Jovial Joe Jayarson
4451606530 Merge pull request #89 from joe733/workshop
feat: bumped versions, upd. deps
2022-08-07 21:40:37 +05:30
14 changed files with 1582 additions and 1043 deletions

28
.github/testing.yml vendored
View File

@@ -1,28 +0,0 @@
name: WakaReadme
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Install dependencies
run: |
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - --version 1.1.13
echo "##vso[task.setvariable variable=PATH]${PATH}:$HOME/.poetry/bin"
source $HOME/.poetry/env
poetry install
- name: Run unit tests
run: |
poetry run python -m unittest discover

26
.github/workflows/testing.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: UnitTests
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
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}"
poetry install
- name: Run unit tests
run: |
poetry run python -m unittest discover

9
.gitignore vendored
View File

@@ -112,7 +112,8 @@ celerybeat.pid
*.sage.py
# Environments
.env
*.env
env.sh
.venv
env/
venv/
@@ -153,3 +154,9 @@ cython_debug/
# VSCode
.vscode/
# asdf
.tool-versions
# ruff
.ruff_cache

View File

@@ -1,19 +1,142 @@
# Contributing
![python_ver](https://img.shields.io/badge/python-%5E3.10-blue.svg)
![python_ver](https://img.shields.io/badge/Python-%5E3.11-blue.svg)
First off, thank you! Please follow along.
> First off, thank you! Please follow along.
1. Fork this repository and clone your fork into a local machine.
2. Install poetry with: `curl -sSL https://install.python-poetry.org | python -`
3. Open a terminal in the cloned folder and create a virtual environment using: `poetry shell` and install dependencies with `poetry install`
4. Put environment variables in a local `.env` file
5. Test the program `python -m unittest discover`.
6. Read [main.py:L389](main.py#L389) before step 7.
7. Finally run it in development mode with `python -m main --dev`.
**You need to _fork_ this repository and _clone_ it, onto your system.**
## Resources
## Using Docker/Podman (recommended)
- [All about git](https://stackoverflow.com/q/315911)
- [Poetry](https://python-poetry.org/)
- [Unit testing](https://docs.python.org/3/library/unittest.html)
> Assumes you've already installed & configured latest version of [docker](https://www.docker.com/) or [podman](https://podman.io/).
>
> Replace `docker` with `podman` everywhere, if you're using the latter.
1. **Inside the cloned folder**, run:
```console
$ git archive -o 'waka-readme.tar.gz' HEAD
$ docker build . -f containerfile -t 'waka-readme:dev'
```
to build an image. (Image is identified as `<name>:<tag>`)
2. Then create containers and use them as dev environments.
- Temporary:
```console
$ docker run --rm -it --name 'WakaReadmeDev' 'waka-readme:dev' bash
```
- or Persistent
```console
$ docker run --detach --name 'WakaReadmeDev' 'waka-readme:dev'
```
where `WakaReadmeDev` is the docker container name. Then execute `bash` in the container:
```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:
```console
// inside container, create a file `.env`
# micro .env
```
paste (`Ctrl+Shift+V`) the following contents:
```env
INPUT_GH_TOKEN='<GITHUB TOKEN>'
INPUT_WAKATIME_API_KEY='<WAKATIME API KEY>'
INPUT_API_BASE_URL='https://wakatime.com/api'
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'
INPUT_TIME_RANGE='last_7_days'
INPUT_SHOW_MASKED_TIME='True'
```
and execute program with:
```console
# poetry shell
# set -a && . ./.env && set +a # optional
(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:
```console
// exit container
# exit
$ docker container stop 'WakaReadmeDev'
$ docker container rm 'WakaReadmeDev'
```
---
> **NOTE** With VSCode on Windows
>
> Add these to `.vscode/settings.json`
>
> ```json
> {
> "terminal.integrated.commandsToSkipShell": [
> "-workbench.action.quickOpenView"
> ]
> }
> ```
>
> To quit the `micro` editor from the vscode terminal.
---
## Manual
> Assumes you've already installed & configured latest version of [python](https://www.python.org/) and [poetry](https://python-poetry.org/).
1. Inside the cloned folder run:
```console
$ poetry shell
(waka-readme-py3_11)$ poetry install
```
to create and activate a virtual environnement and install dependencies.
2. Put environment variables in a `.env` file
```env
INPUT_GH_TOKEN='<GITHUB TOKEN>'
INPUT_WAKATIME_API_KEY='<WAKATIME API KEY>'
INPUT_API_BASE_URL='https://wakatime.com/api'
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'
INPUT_TIME_RANGE='last_7_days'
INPUT_SHOW_MASKED_TIME='True'
```
3. Execute program in development mode with:
```console
(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
```

View File

@@ -1,29 +0,0 @@
FROM python:3.10.2-slim-bullseye
ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
# pip:
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
# poetry:
POETRY_VERSION=1.1.14 \
POETRY_NO_INTERACTION=1 \
POETRY_CACHE_DIR='/var/cache/pypoetry' \
PATH="$PATH:/root/.local/bin"
# install poetry
# RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
RUN pip install pipx
RUN pipx install "poetry==$POETRY_VERSION"
RUN pipx ensurepath
# install dependencies
COPY pyproject.toml poetry.lock /
RUN poetry install --no-dev --no-root --no-interaction --no-ansi
# copy and run program
ADD main.py /main.py
CMD [ "poetry", "run", "python", "/main.py" ]

View File

@@ -4,7 +4,7 @@
</center>
# Dev Metrics in Readme [![Build Status](https://travis-ci.com/athul/waka-readme.svg?branch=master)](https://travis-ci.com/athul/waka-readme)
# Dev Metrics in Readme [![Unit Tests](https://github.com/athul/waka-readme/actions/workflows/testing.yml/badge.svg?branch=master)](https://github.com/athul/waka-readme/actions/workflows/testing.yml) ![Python Version](https://img.shields.io/badge/Python-^3.11-blue)
[WakaTime](https://wakatime.com) weekly metrics on your profile readme.
@@ -28,63 +28,69 @@ 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
There are many flags that you can tweak to suit your taste!
| Flag | Default | Options | Meaning |
| ------------------ | -------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `API_BASE_URL` | `https://wakapi.dev/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 |
| `SHOW_TITLE` | `false` | `false`, `true` | Add title to waka-readme stats blob |
| `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`) |
| Flag | Default | Options | Meaning |
| ------------------ | -------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `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 |
| `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 |
| `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,17 +138,15 @@ 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
[workflow_dispatch]: https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/
[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

@@ -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,10 +73,15 @@ 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"
image: "Dockerfile"
image: "dockerfile"
branding:
icon: "info"

28
containerfile Normal file
View File

@@ -0,0 +1,28 @@
FROM python:slim-bullseye
WORKDIR /root/waka-readme/
ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
# pip:
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
# poetry:
# POETRY_VERSION= \
POETRY_NO_INTERACTION=1 \
POETRY_CACHE_DIR=/var/cache/pypoetry \
PATH=${PATH}:/root/.local/bin
# import project
ADD waka-readme.tar.gz .
# install poetry & dependencies
RUN apt-get update && apt-get install --no-install-recommends -y curl git micro \
&& curl -sSL https://install.python-poetry.org | python - \
&& poetry install --no-root --no-ansi
# copy and run program
CMD [ "sleep", "infinity" ]

26
dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM python:slim-bullseye
ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
# pip:
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
# poetry:
# POETRY_VERSION= \
POETRY_NO_INTERACTION=1 \
POETRY_CACHE_DIR=/var/cache/pypoetry \
PATH=${PATH}:/root/.local/bin
# copy project files
COPY pyproject.toml poetry.lock main.py /
# install poetry & dependencies
RUN apt-get update && apt-get install --no-install-recommends -y curl \
&& curl -sSL https://install.python-poetry.org | python - \
&& poetry install --no-root --no-ansi --only main
# copy and run program
CMD [ "poetry", "run", "python", "/main.py" ]

733
main.py
View File

@@ -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,11 +24,12 @@ TOML 1 hr 48 mins ⣿⣤⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀
Other 35 mins ⣦⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀ 01.68 %
```
#### Contents = Title + Byline + Body
Contents := Title + Byline + Body
"""
# standard
from dataclasses import dataclass
from random import SystemRandom
from datetime import datetime
from base64 import b64encode
import logging as logger
@@ -44,375 +40,408 @@ import re
import os
# external
# # github
from github import GithubException, Github
# # requests
from requests.exceptions import RequestException
from requests.adapters import HTTPAdapter, Retry
from requests.sessions import Session
from requests import get as rq_get
from github import GithubException, Github
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 or self.waka_key or self.api_base_url or self.repository):
logger.error('Invalid required input(s)')
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, retries = 2, {}, Retry( # for auto retry
total=5,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504],
)
encoded_key: str = str(b64encode(bytes(wk_i.waka_key, 'utf-8')), 'utf-8')
# making a request
with Session() as rqs:
rqs.mount('http://', HTTPAdapter(max_retries=retries))
rqs.mount('https://', HTTPAdapter(max_retries=retries))
while attempts > 0:
logger.debug('Fetching WakaTime statistics')
resp = rqs.get(
url=f'{wk_i.api_base_url.rstrip("/")}/v1/users/current/stats/{wk_i.time_range}',
headers={'Authorization': f'Basic {encoded_key}'},
)
logger.debug(
f'API response @ trial #{3 - attempts}: {resp.status_code}{resp.reason}'
)
if resp.status_code == 200 and (statistic := resp.json()):
logger.debug('Fetched WakaTime statistics')
break
logger.debug('Retrying in 3s ...')
sleep(3)
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
try:
if not (waka_stats := fetch_stats()):
logger.error('Unable to fetch data, please rerun workflow')
sys.exit(1)
except RequestException as rq_exp:
logger.critical(rq_exp)
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('Conneting 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
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.getLogger('urllib3').setLevel(logger.WARNING)
logger.getLogger('github.Requester').setLevel(logger.WARNING)
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__':
# initial setup
wk_c = WakaConstants()
wk_i = WakaInput()
logger.debug('Initialize WakaReadme')
if not wk_i.validate_input():
logger.error('Environment variables are misconfigured')
################### 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)
logger.debug('Input validation complete')
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_i = WakaInput()
if not wk_i.validate_input():
logger.error("Environment variables are misconfigured\n")
sys.exit(1)
# run
try:
genesis()
except KeyboardInterrupt:
print()
logger.error('Interrupt signal received')
logger.error("Interrupt signal received\n")
sys.exit(1)
except GithubException as gh_exp:
logger.critical(gh_exp)
except (GithubException, RequestException) as rq_exp:
logger.critical(f"{rq_exp}\n")
sys.exit(1)
print('\nThanks for using WakaReadme!')
print("\nThanks for using WakaReadme!\n")

1240
poetry.lock generated

File diff suppressed because it is too large Load Diff

4
poetry.toml Normal file
View File

@@ -0,0 +1,4 @@
[virtualenvs]
prefer-active-python = true
in-project = true
path = ".venv"

View File

@@ -1,21 +1,68 @@
####################
# Metadata #
####################
[tool.poetry]
name = "waka-readme"
version = "0.1.6"
version = "0.2.1"
description = "Wakatime Weekly Metrics on your Profile Readme."
authors = ["Athul Cyriac Ajay <athul8720@gmail.com>"]
license = "MIT"
readme = "README.md"
####################
# Dependencies #
####################
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.28.1"
PyGithub = "^1.55"
python = "^3.11"
faker = "^18.10.1"
pygithub = "^1.58.2"
requests = "^2.31.0"
[tool.poetry.dev-dependencies]
autopep8 = "^1.6.0"
pylint = "^2.14.5"
python-dotenv = "^0.20.0"
loguru = "^0.6.0"
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
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>=1.0.0"]
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"

View File

@@ -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: