From d1548b6e8a32156bac4764bf0ff0185cdcf943bb Mon Sep 17 00:00:00 2001 From: Paillat Date: Sat, 3 May 2025 23:49:48 +0200 Subject: [PATCH] First commit --- .copywrite.hcl | 17 ++ .github/workflows/CI.yaml | 25 +++ .github/workflows/publish.yaml | 31 +++ .github/workflows/quality.yaml | 87 +++++++++ .gitignore | 175 +++++++++++++++++ .gitmodules | 3 + .pre-commit-config.yaml | 34 ++++ LICENSE | 21 ++ README.md | 343 +++++++++++++++++++++++++++++++++ pyproject.toml | 158 +++++++++++++++ renovate.json | 25 +++ src/dismoji/__init__.py | 37 ++++ src/dismoji/py.typed | 0 src/dismoji/raw | 1 + tests/__init__.py | 2 + tests/emoji_test.py | 43 +++++ uv.lock | 151 +++++++++++++++ 17 files changed, 1153 insertions(+) create mode 100644 .copywrite.hcl create mode 100644 .github/workflows/CI.yaml create mode 100644 .github/workflows/publish.yaml create mode 100644 .github/workflows/quality.yaml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 renovate.json create mode 100644 src/dismoji/__init__.py create mode 100644 src/dismoji/py.typed create mode 160000 src/dismoji/raw create mode 100644 tests/__init__.py create mode 100644 tests/emoji_test.py create mode 100644 uv.lock diff --git a/.copywrite.hcl b/.copywrite.hcl new file mode 100644 index 0000000..68d009a --- /dev/null +++ b/.copywrite.hcl @@ -0,0 +1,17 @@ +schema_version = 1 + +project { + license = "MIT" + copyright_year = 2025 + copyright_holder = "Paillat-dev" + header_ignore = [ + ".venv/**", + "logs/**", + ".idea/**", + ".git/**", + ".vscode/**", + "__pycache__/**", + "*.pyc", + "src/dismoji/raw/**", + ] +} diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml new file mode 100644 index 0000000..547e9fd --- /dev/null +++ b/.github/workflows/CI.yaml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [ "master", "dev" ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + pull_request: + branches: ["master", "dev"] + release: + types: [created] + +jobs: + quality: + uses: ./.github/workflows/quality.yaml + permissions: + contents: read + + publish: + needs: quality + if: github.event_name == 'release' + uses: ./.github/workflows/publish.yaml + permissions: + id-token: write + contents: read \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..79d6ae2 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,31 @@ +name: Quality Checks + +on: + workflow_call: + +jobs: + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + environment: pypi + steps: + - uses: actions/checkout@v4 + + - name: "Install uv" + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: "Set up Python" + uses: actions/setup-python@v5 + with: + python-version-file: "pyproject.toml" + + - name: Install dependencies + run: uv sync + + - name: Build + run: uv build + + - name: Publish + run: uv publish \ No newline at end of file diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml new file mode 100644 index 0000000..f52df19 --- /dev/null +++ b/.github/workflows/quality.yaml @@ -0,0 +1,87 @@ +name: Quality Checks + +on: + workflow_call: + +jobs: + check-license-header: + name: License Header Check + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Copywrite + uses: hashicorp/setup-copywrite@5e3e8a26d7b9f8a508848ad0a069dfd2f7aa5339 + - name: Check Header Compliance + run: copywrite headers --plan --config .copywrite.hcl + + tests: + name: Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [3.9, 3.10, 3.11, 3.12, 3.13] + include: + - python-version: 3.9 + name: "Python 3.9" + - python-version: 3.10 + name: "Python 3.10" + - python-version: 3.11 + name: "Python 3.11" + - python-version: 3.12 + name: "Python 3.12" + - python-version: 3.13 + name: "Python 3.13" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: "Set up Python" + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --no-managed-python --no-python-downloads + + - name: Run tests + run: uv run pytest ./tests + + quality: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + check: [format, lint, basedpyright] + include: + - check: format + name: "Format Check" + command: "uv run ruff format --check ." + - check: lint + name: "Lint Check" + command: "uv run ruff check ." + - check: basedpyright + name: "Type Check" + command: "uv run basedpyright ." + + name: ${{ matrix.name }} + + steps: + - uses: actions/checkout@v4 + + - name: "Install uv" + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: "Set up Python" + uses: actions/setup-python@v5 + with: + python-version-file: "pyproject.toml" + + - name: Install dependencies + run: uv sync + + - name: ${{ matrix.name }} + run: ${{ matrix.command }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9313e30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +./build/ # so hatch includes discord-emojis' build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# PyPI configuration file +.pypirc + +_version.py + +node_modules/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e4dca35 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/dismoji/raw"] + path = src/dismoji/raw + url = git@github.com:Paillat-dev/discord-emojis.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..66f3e29 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +ci: + autoupdate_commit_msg: ":construction_worker: pre-commit autoupdate" + autofix_commit_msg: ":art: auto fixes from pre-commit.com hooks" + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + exclude: \.(po|pot|yml|yaml)$ + - id: end-of-file-fixer + exclude: \.(po|pot|yml|yaml)$ + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + args: [--prose-wrap=always, --print-width=88] + exclude: \.(po|pot|yml|yaml)$ + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.9.10 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format + - repo: https://github.com/bhundven/copywrite # waiting for https://github.com/hashicorp/copywrite/pull/120 to be merged + rev: 937f17f09c46992447dfa8977bb96eda512588c4 + hooks: + - id: add-headers \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1af0286 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Paillat-dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..be54d2b --- /dev/null +++ b/README.md @@ -0,0 +1,343 @@ +
+

Discord Progress Bar

+ + + +![PyPI - Version](https://img.shields.io/pypi/v/dismoji) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/dismoji) +![PyPI - Types](https://img.shields.io/pypi/types/dismoji) +![PyPI - License](https://img.shields.io/pypi/l/dismoji) +![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Paillat-dev/dismoji/CI.yaml) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Paillat-dev/dismoji/main.svg)](https://results.pre-commit.ci/latest/github/Paillat-dev/dismoji/main) + + + +A Python library for creating customizable progress bars with Discord emojis in your +Discord bot messages. + +
+ +## Table of Contents + +- [Overview](#overview) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Core Concepts](#core-concepts) + - [How It Works](#how-it-works) + - [Key Components](#key-components) +- [Features](#features) + - [Type Safety](#type-safety) +- [Usage Examples](#usage-examples) + - [Basic Progress Bar](#basic-progress-bar) + - [Different Progress Bar States](#different-progress-bar-states) + - [Custom Progress Bar Length](#custom-progress-bar-length) +- [Configuration](#configuration) + - [Bot Configuration](#bot-configuration) + - [Progress Bar Manager Initialization](#progress-bar-manager-initialization) + - [Custom Progress Bar Styles](#custom-progress-bar-styles) +- [Limitations](#limitations) +- [Getting Help](#getting-help) +- [Development](#development) + - [Local Testing](#local-testing) + - [Contributing](#contributing) +- [License](#license) + +## Overview + +Discord Progress Bar is a Python library that allows you to create visually appealing +progress bars in your Discord bot messages using custom emojis. It provides a simple API +to generate progress bars of different styles and lengths, making it easy to display +progress, loading states, or completion percentages in your Discord applications. + +Built on: + +- [py-cord](https://github.com/Pycord-Development/pycord) - A modern, easy-to-use, + feature-rich, and async-ready API wrapper for Discord +- Custom Discord emojis - Used to create visually consistent progress bar segments + +## Installation + +```bash +pip install discord-progress-bar --pre +``` + + +> [!NOTE] +> The package is currently in pre-release. + +## Quick Start + + + +> [!TIP] +> Create beautiful progress bars in your Discord bot with just a few lines of code! + +```python +import discord +from discord_progress_bar import ProgressBarManager + +# Create a Discord bot with emoji caching enabled +bot = discord.Bot(cache_app_emojis=True) + +progress_bar_manager = None +progress_bar = None + +@bot.event +async def on_ready(): + """Initialize the ProgressBarManager when the bot is ready.""" + global progress_bar_manager, progress_bar + + # Initialize the progress bar manager + progress_bar_manager = await ProgressBarManager(bot) + # Get a progress bar with the "green" style + progress_bar = await progress_bar_manager.progress_bar("green") + +@bot.slash_command() +async def show_progress(ctx, percent: float = 0.5): + """Display a progress bar with the specified percentage.""" + + await ctx.respond(f"Progress: {progress_bar.partial(percent)}") + +# Run your bot +bot.run("YOUR_TOKEN") +``` + + +> [!NOTE] +> For a complete example, check the [examples directory](/examples). + +## Core Concepts + +### How It Works + +Discord Progress Bar works by using custom Discord emojis to create visually appealing +progress bars in your bot messages. Here's how it works: + +1. **Progress Bar Structure**: Each progress bar consists of multiple emoji segments: + +- Left edge (filled or empty) +- Middle sections (filled or empty) +- Right edge (filled or empty) + +2. **Emoji Management**: The `ProgressBarManager` class handles: + +- Loading existing emojis from your bot +- Creating new emojis from URLs or files if needed +- Providing the appropriate emojis to the `ProgressBar` class + +3. **Progress Bar Rendering**: The `ProgressBar` class handles: + +- Rendering full progress bars (100%) +- Rendering empty progress bars (0%) +- Rendering partial progress bars (any percentage) + +4. **Default Styles**: The library comes with a default "green" style, but you can + create custom styles by providing your own emoji images. + +### Key Components + +- **ProgressBarManager**: Initializes with your Discord bot and manages emoji resources +- **ProgressBar**: Renders progress bars with different percentages +- **ProgressBarPart**: Enum defining the different parts of a progress bar (LEFT_EMPTY, + LEFT_FILLED, etc.) +- **Default Bars**: Pre-configured progress bar styles that can be used out of the box + +## Features + +- **Easy Integration**: Seamlessly integrates with Discord bots built using py-cord +- **Customizable Progress Bars**: Create progress bars with different styles and lengths +- **Default Styles**: Comes with a pre-configured "green" style ready to use +- **Custom Styles**: Create your own progress bar styles using custom emoji images +- **Flexible Rendering**: Render progress bars at any percentage (0-100%) +- **Async Support**: Fully supports asynchronous operations for Discord bots +- **Emoji Management**: Automatically handles emoji creation and management + +### Type Safety + +Discord Progress Bar is fully type-annotated and type-safe. It uses `basedpyright` for +type checking. + + +> [!NOTE] +> While Discord Progress Bar itself is fully typed, the underlying py-cord library has limited type annotations, which may affect type checking in some areas. + +## Usage Examples + +### Basic Progress Bar + +```python +@discord.slash_command() +async def show_progress(self, ctx: discord.ApplicationContext, percent: float = 0.5) -> None: + """Display a progress bar with the specified percentage.""" + # Get a progress bar with the default "green" style + progress_bar = await self.progress_bar_manager.progress_bar("green", length=10) + # Render the progress bar at the specified percentage + await ctx.respond(f"Progress: {progress_bar.partial(percent)}") +``` + +### Different Progress Bar States + +```python +@discord.slash_command() +async def show_progress_states(self, ctx: discord.ApplicationContext) -> None: + """Display different progress bar states.""" + progress_bar = await self.progress_bar_manager.progress_bar("green", length=10) + + # Empty progress bar (0%) + empty = progress_bar.empty() + + # Partial progress bar (50%) + half = progress_bar.partial(0.5) + + # Full progress bar (100%) + full = progress_bar.full() + + await ctx.respond(f"Empty: {empty}\nHalf: {half}\nFull: {full}") +``` + +### Custom Progress Bar Length + +```python +@discord.slash_command() +async def show_different_lengths(self, ctx: discord.ApplicationContext) -> None: + """Display progress bars with different lengths.""" + # Short progress bar (5 segments) + short_bar = await self.progress_bar_manager.progress_bar("green", length=5) + + # Medium progress bar (10 segments) + medium_bar = await self.progress_bar_manager.progress_bar("green", length=10) + + # Long progress bar (20 segments) + long_bar = await self.progress_bar_manager.progress_bar("green", length=20) + + await ctx.respond( + f"Short (5): {short_bar.partial(0.7)}\n" + f"Medium (10): {medium_bar.partial(0.7)}\n" + f"Long (20): {long_bar.partial(0.7)}" + ) +``` + +For more examples, check the [examples directory](/examples) in the repository. + +## Configuration + +### Bot Configuration + +When creating your Discord bot, make sure to enable emoji caching: + +```python +bot = discord.Bot(cache_app_emojis=True) +``` + +This is required for the `ProgressBarManager` to properly load and manage emojis. + +### Progress Bar Manager Initialization + +Initialize the `ProgressBarManager` after your bot is ready: + +```python +@discord.Cog.listener() +async def on_ready(self) -> None: + self.progress_bar_manager = await ProgressBarManager(self.bot) +``` + +### Custom Progress Bar Styles + +You can create custom progress bar styles by providing your own emoji images: + +#### From URLs + +```python +from discord_progress_bar import ProgressBarPart + +# Define URLs for each part of the progress bar +custom_style = { + ProgressBarPart.LEFT_EMPTY: "https://example.com/left_empty.png", + ProgressBarPart.LEFT_FILLED: "https://example.com/left_filled.png", + ProgressBarPart.MIDDLE_EMPTY: "https://example.com/middle_empty.png", + ProgressBarPart.MIDDLE_FILLED: "https://example.com/middle_filled.png", + ProgressBarPart.RIGHT_EMPTY: "https://example.com/right_empty.png", + ProgressBarPart.RIGHT_FILLED: "https://example.com/right_filled.png", +} + +# Create emojis from URLs +await self.progress_bar_manager.create_emojis_from_urls("custom_style", custom_style) +``` + +#### From Files + +```python +import pathlib +from discord_progress_bar import ProgressBarPart + +# Define file paths for each part of the progress bar +custom_style = { + ProgressBarPart.LEFT_EMPTY: pathlib.Path("path/to/left_empty.png"), + ProgressBarPart.LEFT_FILLED: pathlib.Path("path/to/left_filled.png"), + ProgressBarPart.MIDDLE_EMPTY: pathlib.Path("path/to/middle_empty.png"), + ProgressBarPart.MIDDLE_FILLED: pathlib.Path("path/to/middle_filled.png"), + ProgressBarPart.RIGHT_EMPTY: pathlib.Path("path/to/right_empty.png"), + ProgressBarPart.RIGHT_FILLED: pathlib.Path("path/to/right_filled.png"), +} + +# Create emojis from files +await self.progress_bar_manager.create_emojis_from_files("custom_style", custom_style) +``` + +## Limitations + + +> [!WARNING] +> Please be aware of the following limitations: +> +> - **Python Version**: Supports Python 3.12 only +> - **Discord Bot Framework**: Currently only supports py-cord, not discord.py or other Discord API wrappers +> - **Emoji Limits**: Subject to Discord's app emoji limits (2'000 emojis per app - should be plenty for most use cases) +> - **Pre-release Status**: This package is currently in alpha stage and may have unexpected behaviors or breaking changes in future versions +> - **Custom Styles**: Creating custom styles requires providing all six emoji parts (LEFT_EMPTY, LEFT_FILLED, MIDDLE_EMPTY, MIDDLE_FILLED, RIGHT_EMPTY, RIGHT_FILLED) + +## Getting Help + +If you encounter issues or have questions about discord-progress-bar: + +- **GitHub Issues**: + [Submit a bug report or feature request](https://github.com/Paillat-dev/discord-progress-bar/issues) +- **Discord Support**: + - For py-cord related questions: Join the + [Pycord Official Server](https://discord.gg/pycord) + - For discord-progress-bar specific help: Join the + [Pycord Official Server](https://discord.gg/pycord) and mention `@paillat` + + +> [!TIP] +> Before asking for help, check if your question is already answered in the [examples directory](/examples) or existing GitHub issues. + +## Development + +### Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run linter, formatter and type checker: `ruff check .`,`ruff format .`, + `basedpyright .` +5. Submit a pull request + +**Development Tools**: + +- **uv**: For dependency management +- **Ruff**: For linting and formatting +- **HashiCorp Copywrite**: For managing license headers +- **basedpyright**: For type checking + + +> [!CAUTION] +> This is an early-stage project and may have unexpected behaviors or bugs. Please report any issues you encounter. + +## License + +MIT License - Copyright (c) 2025 Paillat-dev + +--- + +Made with ❤ by Paillat-dev diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c2cf477 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,158 @@ +[build-system] +requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "dismoji" +dynamic = ["version", "urls", "readme"] +description = "***" +authors = [ + { name = "Paillat-dev", email = "me@paillat.dev" } +] +license = "MIT" +requires-python = "==3.12.*" +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.9", + "Typing :: Typed", + "Operating System :: OS Independent", +] +keywords = ["discord", "bot", "emojis", "emoji"] +dependencies = [ + "json5>=0.12.0", +] + +[dependency-groups] +dev = [ + "basedpyright>=1.28.1", + "pytest>=8.3.5", + "python-dotenv>=1.0.1", + "ruff>=0.9.9", +] + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "src/dismoji/_version.py" + +[tool.hatch.metadata.hooks.vcs.urls] +Homepage = "https://github.com/Paillat-dev/dismoji" +source_archive = "https://github.com/Paillat-dev/dismoji/archive/{commit_hash}.zip" + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" +start-after = "\n" +end-before = "\n" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = "\n\n---\n" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" +start-after = "## Overview\n" +end-before = "\n## Installation" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" +start-after = "" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' +replacement = '[\1](https://github.com/Paillat-dev/dismoji/tree/main\g<2>)' + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +pattern = '\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]' +replacement = '**\1**:' + +[tool.hatchling] +name = "dismoji" + +[tool.hatch.build] +include = [ + "src/dismoji/", + "src/dismoji/raw/build/emojis.json", +] +exclude = [ + ".copywrite.hcl", + ".github", + ".python-version", + "uv.lock", + "src/dismoji/raw/*.*", + "src/dismoji/raw/.*/", + "src/dismoji/raw/src/" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/dismoji"] + +[tool.pyright] +pythonVersion = "3.9" +typeCheckingMode = "all" +reportUnusedCallResult = false +reportAny = false +executionEnvironments = [ + { root = "src/dismoji/_version.py", reportDeprecated = false }, + { root = "examples", reportExplicitAny = false, reportUnknownMemberType = false, reportUnusedParameter = false, reportImplicitOverride = false } +] + +[tool.ruff] +target-version = "py39" +line-length = 120 +indent-width = 4 + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = false +docstring-code-line-length = "dynamic" +exclude = [ + "src/dismoji/_version.py" +] + +[tool.ruff.lint] +select = ["ALL"] +per-file-ignores = { "examples/**/*" = ["INP001", "ARG002", "T201"], "tests/**/*" = ["S101"], "src/dismoji/_version.py" = ["I001", "Q000", "UP005", "UP006", "UP035"] } +extend-ignore = [ + "N999", + "D104", + "D100", + "D103", + "D102", + "D101", + "D107", + "D105", + "D106", + "ANN401", + "TRY003", + "EM101", + "EM102", + "G004", + "PTH", + "D211", + "D213", + "COM812", + "ISC001", + "D203", + "FBT001", + "FBT002", + "PLR2004", + "PLR0913", + "C901", + "ISC003" # conflicts with basedpyright reportImplicitStringConcatenation +] + +[tool.uv.sources] +py-cord = { git = "https://github.com/Pycord-Development/pycord", rev = "c0c0b7c58f7b489983a159f5e0eea2c0dab0b0c8" } diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7611ba7 --- /dev/null +++ b/renovate.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "baseBranches": ["master"], + "labels": ["deps"], + "ignorePaths": ["requirements.txt"], + "commitMessagePrefix": "⬆️", + "commitMessageAction": "Upgrade", + "packageRules": [ + { + "updateTypes": ["pin"], + "commitMessagePrefix": "📌", + "commitMessageAction": "Pin" + }, + { + "updateTypes": ["rollback"], + "commitMessagePrefix": "⬇️", + "commitMessageAction": "Downgrade" + }, + { + "matchDatasources": ["pypi"], + "addLabels": ["pypi"] + } + ] +} diff --git a/src/dismoji/__init__.py b/src/dismoji/__init__.py new file mode 100644 index 0000000..260c105 --- /dev/null +++ b/src/dismoji/__init__.py @@ -0,0 +1,37 @@ +# Copyright (c) Paillat-dev +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import json +import re +from pathlib import Path + +EMOJIS_PATH = Path(__file__).parent / "raw" / "build" / "emojis.json" + +with EMOJIS_PATH.open("r", encoding="utf-8") as f: + EMOJIS = json.load(f) + +EMOJI_MAPPING: dict[str, str] = {k: EMOJIS["emojis"][v]["surrogates"] for k, v in EMOJIS["nameToEmoji"].items()} + +EMOJI_PATTERN = re.compile(r"(? str: + """Convert a string with emoji names to a string with emoji characters. + + Args: + s (str): The input string containing emoji names. + + Returns: + str: The input string with emoji names replaced by emoji characters. + + """ + + def replace(match: re.Match[str]) -> str: + emoji_name = match.group(1) + if emoji_name in EMOJI_MAPPING: + return EMOJI_MAPPING[emoji_name] + return match.group(0) + + return EMOJI_PATTERN.sub(replace, s) diff --git a/src/dismoji/py.typed b/src/dismoji/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/dismoji/raw b/src/dismoji/raw new file mode 160000 index 0000000..f4c3abf --- /dev/null +++ b/src/dismoji/raw @@ -0,0 +1 @@ +Subproject commit f4c3abf8442751135ebb31666a48fbf6007f176a diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..aaaf00d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Paillat-dev +# SPDX-License-Identifier: MIT diff --git a/tests/emoji_test.py b/tests/emoji_test.py new file mode 100644 index 0000000..46f4470 --- /dev/null +++ b/tests/emoji_test.py @@ -0,0 +1,43 @@ +# Copyright (c) Paillat-dev +# SPDX-License-Identifier: MIT + +from dismoji import emojize + + +def test_basic() -> None: + """Test basic functionality of emojize function.""" + assert emojize("Hello :smile:") == "Hello 😄" + + +def test_no_match() -> None: + """Test emojize function with no matches.""" + assert emojize("Hello world") == "Hello world" + + +def test_not_emoji() -> None: + """Test emojize function with non-emoji input.""" + assert emojize("Hello :not_an_emoji:") == "Hello :not_an_emoji:" + + +def test_surrogate() -> None: + """Test emojize function with surrogate pairs.""" + surrogate_pairs = [ + (":handshake_light_skin_tone_dark_skin_tone:", "🫱🏻‍🫲🏿"), + (":handshake_dark_skin_tone_light_skin_tone:", "🫱🏿‍🫲🏻"), + (":handshake_medium_skin_tone_light_skin_tone:", "🫱🏽‍🫲🏻"), + (":handshake_medium_light_skin_tone_dark_skin_tone:", "🫱🏼‍🫲🏿"), + (":handshake_medium_dark_skin_tone_light_skin_tone:", "🫱🏾‍🫲🏻"), + ] + + for emoji_name, surrogate in surrogate_pairs: + assert emojize(emoji_name) == surrogate + + +def test_multiple_emojis() -> None: + """Test emojize function with multiple emojis.""" + assert emojize(":smile: :wave:") == "😄 👋" + + +def test_complex_sentence() -> None: + """Test emojize function with a complex sentence.""" + assert emojize("Hello :wave:, what's up? :smile: :white_check_mark: :smile:") == "Hello 👋, what's up? 😄 ✅ 😄" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..dacb14f --- /dev/null +++ b/uv.lock @@ -0,0 +1,151 @@ +version = 1 +revision = 1 +requires-python = "==3.12.*" + +[[package]] +name = "basedpyright" +version = "1.29.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/18/f5e488eac4960ad9a2e71b95f0d91cf93a982c7f68aa90e4e0554f0bc37e/basedpyright-1.29.1.tar.gz", hash = "sha256:06bbe6c3b50ab4af20f80e154049477a50d8b81d2522eadbc9f472f2f92cd44b", size = 21773469 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/1b/1bb837bbb7e259928f33d3c105dfef4f5349ef08b3ef45576801256e3234/basedpyright-1.29.1-py3-none-any.whl", hash = "sha256:b7eb65b9d4aaeeea29a349ac494252032a75a364942d0ac466d7f07ddeacc786", size = 11397959 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "dismoji" +source = { editable = "." } +dependencies = [ + { name = "json5" }, +] + +[package.dev-dependencies] +dev = [ + { name = "basedpyright" }, + { name = "pytest" }, + { name = "python-dotenv" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "json5", specifier = ">=0.12.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "basedpyright", specifier = ">=1.28.1" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "ruff", specifier = ">=0.9.9" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "json5" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079 }, +] + +[[package]] +name = "nodejs-wheel-binaries" +version = "22.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/c7/4fd3871d2b7fd5122216245e273201ab98eda92bbd6fe9ad04846b758c56/nodejs_wheel_binaries-22.14.0.tar.gz", hash = "sha256:c1dc43713598c7310d53795c764beead861b8c5021fe4b1366cb912ce1a4c8bf", size = 8055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/b6/66ef4ef75ea7389ea788f2d5505bf9a8e5c3806d56c7a90cf46a6942f1cf/nodejs_wheel_binaries-22.14.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d8ab8690516a3e98458041286e3f0d6458de176d15c14f205c3ea2972131420d", size = 50326597 }, + { url = "https://files.pythonhosted.org/packages/7d/78/023d91a293ba73572a643bc89d11620d189f35f205a309dd8296aa45e69a/nodejs_wheel_binaries-22.14.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:b2f200f23b3610bdbee01cf136279e005ffdf8ee74557aa46c0940a7867956f6", size = 51158258 }, + { url = "https://files.pythonhosted.org/packages/af/86/324f6342c79e5034a13319b02ba9ed1f4ac8813af567d223c9a9e56cd338/nodejs_wheel_binaries-22.14.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0877832abd7a9c75c8c5caafa37f986c9341ee025043c2771213d70c4c1defa", size = 57180264 }, + { url = "https://files.pythonhosted.org/packages/6d/9f/42bdaab26137e31732bff00147b9aca2185d475b5752b57a443e6c7ba93f/nodejs_wheel_binaries-22.14.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fded5a70a8a55c2135e67bd580d8b7f2e94fcbafcc679b6a2d5b92f88373d69", size = 57693251 }, + { url = "https://files.pythonhosted.org/packages/ab/d7/94f8f269aa86cf35f9ed2b70d09aca48dc971fb5656fdc4a3b69364b189f/nodejs_wheel_binaries-22.14.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c1ade6f3ece458b40c02e89c91d5103792a9f18aaad5026da533eb0dcb87090e", size = 58841717 }, + { url = "https://files.pythonhosted.org/packages/2d/a0/43b7316eaf22b4ee9bfb897ee36c724efceac7b89d7d1bedca28057b7be1/nodejs_wheel_binaries-22.14.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34fa5ed4cf3f65cbfbe9b45c407ffc2fc7d97a06cd8993e6162191ff81f29f48", size = 59808791 }, + { url = "https://files.pythonhosted.org/packages/10/0a/814491f751a25136e37de68a2728c9a9e3c1d20494aba5ff3c230d5f9c2d/nodejs_wheel_binaries-22.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:ca7023276327455988b81390fa6bbfa5191c1da7fc45bc57c7abc281ba9967e9", size = 40478921 }, + { url = "https://files.pythonhosted.org/packages/f4/5c/cab444afaa387dceac8debb817b52fd00596efcd2d54506c27311c6fe6a8/nodejs_wheel_binaries-22.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:fd59c8e9a202221e316febe1624a1ae3b42775b7fb27737bf12ec79565983eaf", size = 36206637 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, +] + +[[package]] +name = "ruff" +version = "0.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/11/bcef6784c7e5d200b8a1f5c2ddf53e5da0efec37e6e5a44d163fb97e04ba/ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79", size = 4010053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/1f/8848b625100ebcc8740c8bac5b5dd8ba97dd4ee210970e98832092c1635b/ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1", size = 10248105 }, + { url = "https://files.pythonhosted.org/packages/e0/47/c44036e70c6cc11e6ee24399c2a1e1f1e99be5152bd7dff0190e4b325b76/ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de", size = 11001494 }, + { url = "https://files.pythonhosted.org/packages/ed/5b/170444061650202d84d316e8f112de02d092bff71fafe060d3542f5bc5df/ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a", size = 10352151 }, + { url = "https://files.pythonhosted.org/packages/ff/91/f02839fb3787c678e112c8865f2c3e87cfe1744dcc96ff9fc56cfb97dda2/ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193", size = 10541951 }, + { url = "https://files.pythonhosted.org/packages/9e/f3/c09933306096ff7a08abede3cc2534d6fcf5529ccd26504c16bf363989b5/ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e", size = 10079195 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/a87f8933fccbc0d8c653cfbf44bedda69c9582ba09210a309c066794e2ee/ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308", size = 11698918 }, + { url = "https://files.pythonhosted.org/packages/52/7d/8eac0bd083ea8a0b55b7e4628428203441ca68cd55e0b67c135a4bc6e309/ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55", size = 12319426 }, + { url = "https://files.pythonhosted.org/packages/c2/dc/d0c17d875662d0c86fadcf4ca014ab2001f867621b793d5d7eef01b9dcce/ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc", size = 11791012 }, + { url = "https://files.pythonhosted.org/packages/f9/f3/81a1aea17f1065449a72509fc7ccc3659cf93148b136ff2a8291c4bc3ef1/ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2", size = 13949947 }, + { url = "https://files.pythonhosted.org/packages/61/9f/a3e34de425a668284e7024ee6fd41f452f6fa9d817f1f3495b46e5e3a407/ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6", size = 11471753 }, + { url = "https://files.pythonhosted.org/packages/df/c5/4a57a86d12542c0f6e2744f262257b2aa5a3783098ec14e40f3e4b3a354a/ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2", size = 10417121 }, + { url = "https://files.pythonhosted.org/packages/58/3f/a3b4346dff07ef5b862e2ba06d98fcbf71f66f04cf01d375e871382b5e4b/ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03", size = 10073829 }, + { url = "https://files.pythonhosted.org/packages/93/cc/7ed02e0b86a649216b845b3ac66ed55d8aa86f5898c5f1691797f408fcb9/ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b", size = 11076108 }, + { url = "https://files.pythonhosted.org/packages/39/5e/5b09840fef0eff1a6fa1dea6296c07d09c17cb6fb94ed5593aa591b50460/ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9", size = 11512366 }, + { url = "https://files.pythonhosted.org/packages/6f/4c/1cd5a84a412d3626335ae69f5f9de2bb554eea0faf46deb1f0cb48534042/ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287", size = 10485900 }, + { url = "https://files.pythonhosted.org/packages/42/46/8997872bc44d43df986491c18d4418f1caff03bc47b7f381261d62c23442/ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e", size = 11558592 }, + { url = "https://files.pythonhosted.org/packages/d7/6a/65fecd51a9ca19e1477c3879a7fda24f8904174d1275b419422ac00f6eee/ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79", size = 10682766 }, +]