├── .dockerignore
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .pypi_chart
└── artifact
│ ├── boto_badge.svg
│ ├── django_badge.svg
│ ├── fastapi_badge.svg
│ ├── pydantic_badge.svg
│ └── requests_badge.svg
├── Dockerfile
├── LICENSE
├── README.md
├── action.yml
├── chart
└── __init__.py
├── main.py
├── pypi
└── __init__.py
├── pyproject.toml
└── tests
├── __init__.py
└── sample_test.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Ignore all files and folders that start with a dot.
2 | .*
3 |
4 | # Ignore all virtual envs'
5 | venv/
6 |
7 | # Ignore all Python bytecode files.
8 | __pycache__/
9 |
10 | # Ignore all temporary files.
11 | *.tmp
12 | *.swp
13 |
14 | # Ignore all build artifacts.
15 | build/
16 | dist/
17 |
18 | # Ignore all pyaction-related files.
19 | README.md
20 | CONTRIBUTING.md
21 | CHANGELOG.md
22 | LICENSE
23 | Dockerfile
24 | Makefile
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Testing the action
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * *" # <= runs every 24 hours
6 | workflow_dispatch:
7 |
8 | jobs:
9 | updating-badge-files:
10 | runs-on: ubuntu-latest
11 | strategy:
12 | matrix:
13 | include:
14 | - package: fastapi
15 | color: '#039284'
16 | - package: requests
17 | color: yellow
18 | - package: django
19 | color: '#0A4A33'
20 | - package: pydantic
21 | color: '#E51EE8'
22 |
23 | steps:
24 | - name: Checkout
25 | uses: actions/checkout@v4
26 |
27 | - name: Run action
28 | uses: ./
29 | with:
30 | package_name: ${{ matrix.package }}
31 | file_name: ${{ matrix.package }}_badge.svg
32 | badge_color: ${{ matrix.color }}
33 |
34 | - name: Upload badges as artifact
35 | uses: actions/upload-artifact@v3
36 | with:
37 | path: .pypi_chart/${{ matrix.package }}_badge.svg
38 |
39 | commit-badges:
40 | needs: updating-badge-files
41 | runs-on: ubuntu-latest
42 | steps:
43 | - name: Checkout
44 | uses: actions/checkout@v4
45 |
46 | - name: Download badges artifact
47 | uses: actions/download-artifact@v3
48 | with:
49 | path: .pypi_chart/
50 | merge-multiple: true
51 |
52 | - name: Commiting
53 | uses: EndBug/add-and-commit@v9
54 | with:
55 | default_author: github_actions
56 | message: 'chart badge updated'
57 |
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
7 | __pypackages__/
8 |
9 | # Environments
10 | .env
11 | .venv
12 | env/
13 | venv/
14 | ENV/
15 | env.bak/
16 | venv.bak/
--------------------------------------------------------------------------------
/.pypi_chart/artifact/boto_badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.pypi_chart/artifact/django_badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.pypi_chart/artifact/fastapi_badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.pypi_chart/artifact/pydantic_badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.pypi_chart/artifact/requests_badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Setting the base-image
2 | FROM python:3.12-slim
3 |
4 | # Copy only the necessary binaries from uv
5 | COPY --from=ghcr.io/astral-sh/uv:0.5.1 /uv /uvx /bin/
6 |
7 | # Set the working directory to /action
8 | WORKDIR /action
9 |
10 | # Import the action
11 | COPY . .
12 |
13 | # Run the pre-script.sh
14 | RUN [ -f pre-script.sh ] && sh pre-script.sh || true
15 |
16 | # Install action dependencies
17 | RUN uv pip install . --system
18 |
19 | # running the post-script.sh
20 | RUN [ -f post-script.sh ] && sh post-script.sh || true
21 |
22 | # Specify the command to run main.py with uv
23 | CMD [ "python", "/action/main.py" ]
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sadra Yahyapour
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## PyPI Chart Badge [](https://pyaction.imsadra.me/)
2 |
3 | This action allows you to create and put fancy-looking chart badges indicating the recent download rate of your Python packages in the README.
4 |
5 | > [!NOTE]
6 | > Read [this article](https://blog.imsadra.me/display-your-package-download-rate-on-github) to learn how this action works and how to use it in your repositories.
7 |
8 | ### Examples
9 | This chart depicts the download rate of some popular Python packages over the last 15 days. (It dynamically updates every 24 hours)
10 |
11 | | fastapi | django | requests | pydantic |
12 | | ------- | ------ | -------- | ---- |
13 | |  |  |  |  |
14 |
15 |
16 | ### Basic Usage
17 | ```yml
18 | name: Update the PyPI chart badge
19 |
20 | on:
21 | schedule:
22 | - cron: "0 0 1 * *" # <= runs every month
23 |
24 | jobs:
25 | update-chart-badge:
26 | name: Updating the pypi chart badge
27 | runs-on: ubuntu-latest
28 |
29 | steps:
30 | - name: Checkout
31 | uses: actions/checkout@v4
32 |
33 | - name: Updating the badge
34 | uses: lnxpy/pypi-chart-badge@v1.4
35 | with:
36 | package_name: ''
37 |
38 | - uses: EndBug/add-and-commit@v9
39 | with:
40 | default_author: github_actions
41 | message: 'chart badge updated'
42 |
43 | ```
44 |
45 | After each run, you'll have your badge stored in `.pypi_chart/badge.svg` of your repository.
46 |
47 | > [!IMPORTANT]
48 | > You have to give the "Write Access" to your workflow so that changes can be committed back into the repo.
49 |
50 | ### Options
51 |
52 | | Option | Default value | Description | Required |
53 | | :-----------------: | :------------: |-------------------------------------------------------------------------------------------------|:--------:|
54 | | `package_name` | - | The Package name | Yes |
55 | | `badge_width` | `60` | Badge width size in pixels | No |
56 | | `badge_height` | `20` | Badge height size in pixels | No |
57 | | `badge_color` | `'#4492F9'` | Badge plot color (HEX or CSS color names) | No |
58 | | `badge_dark_color` | `'#4492F9'` | Badge plot dark color (HEX or CSS color names) | No |
59 | | `days_limit` | `15` | The amount of selected days | No |
60 | | `output_path` | `.pypi_chart/` | Badge file path directory | No |
61 | | `file_name` | `badge.svg` | Badge file name and extension (`.png`, `.jpg`, `.jpeg`, `.webp`, and `.pdf` are also supported) | No |
62 |
63 | ### License
64 | MIT license terms.
65 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: pypi chart badge
2 | description: PyPI package chart badge generator action
3 | author: Sadra Yahyapour
4 |
5 | branding:
6 | icon: "activity"
7 | color: "white"
8 |
9 | runs:
10 | using: docker
11 | image: Dockerfile
12 |
13 | inputs:
14 | github_token:
15 | description: The GitHub auth token
16 | default: ${{ github.token }}
17 | required: true
18 |
19 | repository:
20 | description: The repository name in the form of "/"
21 | default: ${{ github.repository }}
22 | required: true
23 |
24 | package_name:
25 | description: The package name
26 | required: true
27 |
28 | badge_width:
29 | description: Badge width size
30 | default: 60
31 |
32 | badge_height:
33 | description: Badge height size
34 | default: 20
35 |
36 | badge_color:
37 | description: The plot line color (HEX or CSS-valid names)
38 | default: "#4492F9"
39 |
40 | badge_dark_color:
41 | description: The plot line dark color (HEX or CSS-valid names)
42 | default: "#4492F9"
43 |
44 | days_limit:
45 | description: The amount of selected days
46 | default: 15
47 |
48 | output_path:
49 | description: The badge file path directory
50 | default: .pypi_chart/
51 |
52 | file_name:
53 | description: The badge file name
54 | default: badge.svg
55 |
--------------------------------------------------------------------------------
/chart/__init__.py:
--------------------------------------------------------------------------------
1 | import plotly.express as px
2 | from pandas import DataFrame
3 | from plotly.graph_objs._figure import Figure
4 |
5 |
6 | class Badge:
7 | def __init__(self, df: DataFrame) -> None:
8 | """badge class for creating plotly badge-sized charts
9 |
10 | Args:
11 | df (DataFrame): a vector-like data
12 | """
13 |
14 | self.df = df
15 |
16 | def create(self, badge_height: int, badge_width: int, badge_color: str) -> Figure:
17 | """creates the badge
18 |
19 | Args:
20 | badge_height (int): badge height size (in pixels)
21 | badge_width (int): badge width size (in pixels)
22 | badge_color (str): badge color
23 |
24 | Returns:
25 | Figure: badge figure
26 | """
27 |
28 | # creating the figure with the linear plot graphed
29 | fig = (
30 | px.line(
31 | self.df,
32 | height=badge_height,
33 | width=badge_width,
34 | line_shape="spline",
35 | )
36 | .update_traces(line_color=badge_color) # coloring the plot
37 | .update_xaxes(visible=False) # hiding the x axes
38 | .update_yaxes(visible=False) # hiding the y axes
39 | )
40 |
41 | # strip down the rest of the plot
42 | fig.update_layout(
43 | showlegend=False,
44 | paper_bgcolor="rgba(0,0,0,0)",
45 | plot_bgcolor="rgba(0,0,0,0)",
46 | margin=dict(t=0, l=0, b=0, r=0),
47 | )
48 |
49 | return fig
50 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 |
4 | from pyaction import PyAction
5 | from pyaction.workflow import annotations as A
6 |
7 | from chart import Badge
8 | from pypi import PyPI
9 |
10 | workflow = PyAction()
11 |
12 |
13 | def get_or_create_path(path: str) -> str:
14 | """gets or creates the path then returns it
15 |
16 | Args:
17 | path (str): path
18 |
19 | Returns:
20 | str: path
21 | """
22 |
23 | if not os.path.exists(path):
24 | A.warning(f"Couldn't find `{path}` path in the repo. Creating it!")
25 | os.makedirs(path)
26 |
27 | return path
28 |
29 |
30 | @workflow.action
31 | def action(
32 | package_name: str,
33 | badge_width: int,
34 | badge_height: int,
35 | badge_color: str,
36 | badge_dark_color: str,
37 | days_limit: int,
38 | output_path: str,
39 | file_name: str,
40 | ) -> None:
41 | package = PyPI(package_name)
42 | rates_df = package.get_rates(days_limit)
43 |
44 | badge = Badge(rates_df).create(badge_height, badge_width, badge_color)
45 | dark_badge = Badge(rates_df).create(badge_height, badge_width, badge_dark_color)
46 |
47 | badge_path = Path(get_or_create_path(output_path)).joinpath(file_name)
48 | dark_badge_path = Path(get_or_create_path(output_path)).joinpath(
49 | f"dark_{file_name}"
50 | )
51 |
52 | badge.write_image(badge_path)
53 | dark_badge.write_image(dark_badge_path)
54 |
--------------------------------------------------------------------------------
/pypi/__init__.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from pandas import DataFrame
3 | from pyaction.workflow import annotations
4 |
5 |
6 | class PyPI:
7 | def __init__(self, package_name: str) -> None:
8 | """pypi interface
9 |
10 | Args:
11 | package_name (str): package name
12 | """
13 |
14 | self.package_name = package_name
15 |
16 | def get_rates(self, limit: int) -> DataFrame:
17 | """get the download rates
18 |
19 | Args:
20 | limit (int): days limit
21 |
22 | Returns:
23 | DataFrame: pandas vector data frame
24 | """
25 |
26 | endpoint = f"https://pypistats.org/api/packages/{self.package_name}/overall"
27 |
28 | r = requests.get(endpoint, params={"mirrors": True})
29 |
30 | if r.status_code != 200:
31 | annotations.error(
32 | f"There was an issue with fetching the package data: {r.reason}"
33 | )
34 | raise SystemExit
35 |
36 | return DataFrame([i["downloads"] for i in r.json()["data"][-limit:]])
37 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "pypi-chart-badge"
3 | version = "1.4.0"
4 | description = "PyPI Chart Badge Generator"
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = [
8 | "kaleido==0.2.1",
9 | "pandas==2.2.1",
10 | "plotly==5.24.1",
11 | "pyaction==0.8.1",
12 | "requests==2.32.3",
13 | ]
14 |
15 | [tool.setuptools.packages.find]
16 | exclude = ["test*"]
17 |
18 | [project.optional-dependencies]
19 | cli = ["pyaction[cli]==0.8.1"]
20 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lnxpy/pypi-chart-badge/5fe924704db83b3a6c05a9fdd0bd9608cb893be1/tests/__init__.py
--------------------------------------------------------------------------------
/tests/sample_test.py:
--------------------------------------------------------------------------------
1 | def test_sample():
2 | assert True
3 |
--------------------------------------------------------------------------------