├── .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 [![pyaction](https://img.shields.io/badge/PyAction-black?style=flat&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MDAgNjAwIj4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmNscy0xIHsKICAgICAgICBmaWxsOiAjZmZmOwogICAgICAgIGZpbGwtcnVsZTogZXZlbm9kZDsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPGcgaWQ9IlNWR1JlcG9faWNvbkNhcnJpZXIiIGRhdGEtbmFtZT0iU1ZHUmVwbyBpY29uQ2FycmllciI+CiAgICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0zNjAuNywyMjAuNDNsMjMzLjUzLDI1Mi4zMkwzMzEuNTksMTguODksNS45Niw1ODEuM0gzNzUuMmwtMjI0LjMxLTY1LjUzYy0xMi42MS0zLjY5LTE3Ljc5LTE4Ljc0LTEwLjA3LTI5LjRMMzMxLjM5LDIyMi4xOGM2Ljk4LTkuNzIsMjEuMTgtMTAuNTcsMjkuMy0xLjc0WiIvPgogIDwvZz4KPC9zdmc+)](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 | | ![](.pypi_chart/artifact/fastapi_badge.svg) | ![](.pypi_chart/artifact/django_badge.svg) | ![](.pypi_chart/artifact/requests_badge.svg) | ![](.pypi_chart/artifact/pydantic_badge.svg) | 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 | --------------------------------------------------------------------------------