24 Commits

Author SHA1 Message Date
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
13 changed files with 1389 additions and 814 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

5
.gitignore vendored
View File

@@ -112,7 +112,7 @@ celerybeat.pid
*.sage.py
# Environments
.env
*.env
.venv
env/
venv/
@@ -153,3 +153,6 @@ cython_debug/
# VSCode
.vscode/
# asdf
.tool-versions

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,67 @@ 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 |
| `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 |
# Example
@@ -97,7 +101,7 @@ on:
workflow_dispatch:
schedule:
# Runs at 12am UTC
- cron: '0 0 * * *'
- cron: "0 0 * * *"
jobs:
update-readme:
@@ -112,6 +116,7 @@ jobs:
TIME_RANGE: all_time
SHOW_TIME: true
SHOW_MASKED_TIME: true
LANG_COUNT: 10
```
**`README.md`**
@@ -131,17 +136,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,6 +34,11 @@ 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: "░▒▓█"
@@ -44,6 +49,11 @@ inputs:
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"
@@ -61,7 +71,7 @@ inputs:
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" ]

453
main.py
View File

@@ -34,6 +34,7 @@ Other 35 mins ⣦⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀
# standard
from dataclasses import dataclass
from random import SystemRandom
from datetime import datetime
from base64 import b64encode
import logging as logger
@@ -44,123 +45,68 @@ 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
# # github
from github import GithubException, Github
# # faker
from faker import Faker
# pylint: disable=logging-fstring-interpolation
################### data ###################
@dataclass(frozen=True, slots=True)
class WakaConstants:
"""
WakaConstants
-------------
"""
prefix_length: int = 16
graph_length: int = 25
start_comment: str = '<!--START_SECTION:waka-->'
end_comment: str = '<!--END_SECTION:waka-->'
waka_block_pattern: str = f'{start_comment}[\\s\\S]+{end_comment}'
# pylint: disable = logging-fstring-interpolation
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
################### setup ###################
def strtobool(val: str) -> bool:
print()
# hush existing loggers
# pylint: disable = no-member # see: https://stackoverflow.com/q/20965287
for lgr_name in logger.root.manager.loggerDict:
# to disable log propagation completely set '.propagate = False'
logger.getLogger(lgr_name).setLevel(logger.WARNING)
# pylint: enable = no-member
# somehow github.Requester gets missed out from loggerDict
logger.getLogger('github.Requester').setLevel(logger.WARNING)
# 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
# load from .env before class def gets parsed
load_dotenv()
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
PEP 632 https://www.python.org/dev/peps/pep-0632/ is depreciating distutils.
This is from the official source code with slight modifications.
Following code is somewhat shamelessly copied from the original source.
Converts a string representation of truth to True or False.
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'`
- 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.
"""
if isinstance(val, bool):
return val
val = val.lower()
if val in {'y', 'yes', 't', 'true', 'on', '1'}:
@@ -172,9 +118,103 @@ def strtobool(val: str) -> bool:
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'
)
_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
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)
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'
if not str(self.language_count).isnumeric():
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, dusk: str, /) -> str:
def make_title(dawn: str | None, dusk: str | None, /):
"""
WakaReadme Title
----------------
@@ -182,24 +222,22 @@ def make_title(dawn: str, dusk: str, /) -> str:
Makes title for WakaReadme.
"""
logger.debug('Making title')
if not (dawn or dusk):
logger.error('Cannot find start/end date')
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(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: str, /,
*, lg_nm: str = ''
) -> str:
def make_graph(block_style: str, percent: float, gr_len: int, lg_nm: str = '', /):
"""
WakaReadme Graph
----------------
@@ -207,33 +245,29 @@ def make_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(
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')
logger.debug(f'"{lg_nm or "..."}" graph generated')
return graph_bar
def prep_content(stats: dict | None, /) -> str:
def prep_content(stats: dict[str, Any], language_count: int = 5, /):
"""
WakaReadme Prepare Markdown
---------------------------
Prepared markdown content from the fetched statistics
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
logger.debug('Making contents')
contents = ''
# make title
if wk_i.show_title:
@@ -250,100 +284,117 @@ def prep_content(stats: dict | None, /) -> str:
):
contents += f'Total Time: {total_time}\n\n'
# make content
logger.debug('Making contents')
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
# make lang content
pad_len = len(
# comment if it feels way computationally expensive
max((str(l.get('name')) for l in lang_info), key=len)
max((str(lng['name']) for lng 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')
lang_name = str(lang['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
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 idx >= 5 or lang_name == 'Other':
if idx >= language_count or lang_name == 'Other':
break
logger.debug('Contents were made\n')
return contents.rstrip('\n')
def fetch_stats() -> Any:
def fetch_stats():
"""
WakaReadme Fetch Stats
----------------------
Returns statistics as JSON string
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],
attempts = 4
statistic: dict[str, dict[str, Any]] = {}
encoded_key = str(
b64encode(bytes(str(wk_i.waka_key), 'utf-8')), 'utf-8'
)
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
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 * (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}{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(err)
logger.error(f'{err}\n')
sys.exit(1)
print()
return statistic.get('data')
def churn(old_readme: str, /) -> str | None:
def churn(old_readme: str, /):
"""
WakaReadme Churn
----------------
Composes WakaTime stats within markdown code snippet
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 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)
if not (waka_stats := fetch_stats()):
logger.error('Unable to fetch data, please rerun workflow\n')
sys.exit(1)
# processing content
generated_content = prep_content(waka_stats)
try:
generated_content = prep_content(waka_stats, int(wk_i.language_count))
except (AttributeError, KeyError, ValueError) as err:
logger.error(f'Unable to read API data | {err}\n')
sys.exit(1)
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}',
pattern=wk_i.waka_block_pattern,
repl=f'{wk_i.start_comment}\n\n```text\n{generated_content}\n```\n\n{wk_i.end_comment}',
string=old_readme
)
if len(sys.argv) == 2 and sys.argv[1] == '--dev':
@@ -351,14 +402,19 @@ def churn(old_readme: str, /) -> str | None:
# 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')
def genesis():
"""
Run Program
-----------
"""
logger.debug('Connecting to GitHub')
gh_connect = Github(wk_i.gh_token)
gh_repo = gh_connect.get_repo(wk_i.repository)
# 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')
@@ -372,47 +428,34 @@ def genesis() -> None:
)
logger.info('Stats updated successfully')
return
logger.info('WakaReadme was not updated')
################### driver ###################
print()
# 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
)
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
load_dotenv()
except ImportError as im_err:
logger.warning(im_err)
if __name__ == '__main__':
# initial setup
wk_c = WakaConstants()
wk_i = WakaInput()
# 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')
logger.error('Environment variables are misconfigured\n')
sys.exit(1)
logger.debug('Input validation complete')
# run
try:
genesis()
except KeyboardInterrupt:
print()
logger.error('Interrupt signal received')
logger.error('Interrupt signal received\n')
sys.exit(1)
except GithubException 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')

1284
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,24 @@
[tool.poetry]
name = "waka-readme"
version = "0.1.6"
version = "0.1.9"
description = "Wakatime Weekly Metrics on your Profile Readme."
authors = ["Athul Cyriac Ajay <athul8720@gmail.com>"]
license = "MIT"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.28.1"
PyGithub = "^1.55"
python = "^3.11"
faker = "^18.3.1"
pygithub = "^1.58.1"
requests = "^2.28.2"
[tool.poetry.dev-dependencies]
autopep8 = "^1.6.0"
pylint = "^2.14.5"
python-dotenv = "^0.20.0"
[tool.poetry.group.dev.dependencies]
autopep8 = "^2.0.2"
bandit = "^1.7.5"
loguru = "^0.6.0"
pylint = "^2.17.1"
python-dotenv = "^1.0.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,12 +1,15 @@
'''
"""
Tests for the main.py
'''
"""
# standard
from importlib import import_module
from dataclasses import dataclass
from dataclasses import dataclass # , field
from itertools import product
# from pathlib import Path
# from inspect import cleandoc
# from json import loads
# from typing import Any
# from json import load
import unittest
import sys
import os
@@ -24,17 +27,23 @@ except ImportError as err:
class TestData:
"""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
# 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='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
@@ -98,6 +107,9 @@ class TestMain(unittest.TestCase):
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)
):
@@ -113,11 +125,14 @@ class TestMain(unittest.TestCase):
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()