diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
new file mode 100644
index 0000000..47f2fb7
--- /dev/null
+++ b/.github/workflows/python.yml
@@ -0,0 +1,59 @@
+name: Python CI
+on: push
+jobs:
+ lint:
+ name: Check lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: "Set up Python"
+ uses: actions/setup-python@v5
+ with:
+ python-version-file: "pyproject.toml"
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ # Update output format to enable automatic inline annotations.
+ - name: Run Ruff
+ run: |
+ uv run ruff format --check tests
+ uv run ruff check --output-format=github tests
+
+ build:
+ name: Check Build
+ runs-on: ubuntu-latest
+ needs: lint
+ strategy:
+ matrix:
+ python-version:
+ - "3.8"
+ - "3.9"
+ - "3.10"
+ - "3.11"
+ - "3.12"
+
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Install uv and set the Python version
+ uses: astral-sh/setup-uv@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+ enable-cache: true
+
+ - name: Build
+ run: |
+ uv build
+
+ # Check that basic features work and we didn't miss to include crucial files
+ - name: Smoke test (wheel)
+ run: uv run --isolated --with dist/*.whl pytest
+ - name: Smoke test (source distribution)
+ run: uv run --isolated --with dist/*.tar.gz pytest
+
+
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
new file mode 100644
index 0000000..9720333
--- /dev/null
+++ b/.github/workflows/rust.yml
@@ -0,0 +1,43 @@
+name: Rust CI
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ rust-build:
+ name: Check lint and build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: Swatinem/rust-cache@v2
+ with:
+ # To only cache runs from `master`
+ save-if: ${{ github.ref == 'refs/heads/main' }}
+
+ - name: Add components
+ run: rustup component add clippy rustfmt
+
+ - name: Rustfmt
+ run: cargo fmt -- --check
+
+ - name: Clippy
+ run: cargo clippy --all-targets
+
+ - name: Build (minimal)
+ run: |
+ cargo build --no-default-features --verbose
+
+ - name: Build (normal)
+ run: |
+ cargo build --verbose
+
+ - name: Build (full)
+ run: |
+ cargo build --all-features --verbose
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..83b4d6e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- (lib) Implement the initial library
diff --git a/Cargo.toml b/Cargo.toml
index f6a3e1b..24b7d4e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,6 @@ pythonize = "0.20"
indexmap = "2.11.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-afrim-preprocessor = { version = "0.6.2", default-features = false, features = ["serde"], git = "https://github.com/pythonbrad/afrim", rev = "448c9a4" }
-afrim-translator = { version = "0.2.2", default-features = false, features = ["serde"], git = "https://github.com/pythonbrad/afrim", rev = "448c9a4" }
-afrim-config = { version = "0.4.6", default-features = false, git = "https://github.com/pythonbrad/afrim", rev = "448c9a4" }
+afrim-preprocessor = { version = "0.6.3", default-features = false, features = ["serde"], git = "https://github.com/fodydev/afrim", rev = "16ade83" }
+afrim-translator = { version = "0.2.3", default-features = false, features = ["serde"], git = "https://github.com/fodydev/afrim", rev = "16ade83" }
+afrim-config = { version = "0.4.7", default-features = false, git = "https://github.com/fodydev/afrim", rev = "16ade83" }
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b3fc018
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Esubalew Chekol
+
+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
index 8d25bc7..b02b453 100644
--- a/README.md
+++ b/README.md
@@ -8,31 +8,21 @@
+
+
Built with 🦀🐍 by @esubaalew
-
- Inspired by afrim and afrim-js
## About
`afrim-py` provides Python bindings for the powerful afrim input method engine, enabling developers to build sophisticated input method applications in Python. This project brings the capabilities of the Rust-based afrim engine to the Python ecosystem through PyO3 bindings.
-## 🛠️ Build with `maturin`
-
-```bash
-# Development build
-maturin develop
-
-# Production build
-maturin build --release
-```
-
## 🔋 Features Included
* **Preprocessor** - Advanced key sequence processing and input transformation
@@ -44,33 +34,18 @@ maturin build --release
## Installation
-### From Source
+`afrim-py` is available on pypi.
```bash
-# Clone the repository
-git clone https://github.com/esubaalew/afrim-py.git
-cd afrim-py
-
-# Create virtual environment
-python -m venv .venv
-source .venv/bin/activate # On Windows: .venv\Scripts\activate
-
-# Install in development mode
-maturin develop
+pip install afrim-py
```
-### Requirements
-
-- Python 3.8+
-- Rust 1.70+
-- Cargo and maturin
-
## Usage
### Basic Example
```python
-from afrim_py import Preprocessor, Translator, convert_toml_to_json
+from afrim_py import Preprocessor, Translator, Config
# Configure the preprocessor with key mappings
preprocessor_data = {
@@ -114,33 +89,36 @@ while True:
print(f"Command: {command}")
```
-### TOML Configuration
+### Configuration
```python
-from afrim_py import convert_toml_to_json
+from afrim_py import Config
import json
-# TOML configuration
-toml_config = '''
-[preprocessor]
+# Configuration file `config.toml`
+'''
+[core]
+buffer_size = 64
+auto_capitalize = false
+auto_commit = false
+page_size = 10
+
+[data]
a1 = "à"
-e1 = "é"
-hello = "hi"
+e2 = "é"
-[translator.greetings]
-values = ["hello", "hi", "hey"]
+[translators]
+datetime = { path = "./scripts/datetime.toml" }
-[translator.world]
-values = ["earth", "globe", "planet"]
+[translation]
+hi = 'hello'
'''
-
-# Convert TOML to JSON
-config_json = convert_toml_to_json(toml_config)
-config = json.loads(config_json)
+config = Config('config.toml')
# Use the configuration
-preprocessor = Preprocessor(config["preprocessor"], 64)
-translator_dict = {k: v["values"] for k, v in config["translator"].items()}
+preprocessor_data = config.extract_data()
+preprocessor = Preprocessor(preprocessor_data, 64)
+translator_dict = config.extract_translation()
translator = Translator(translator_dict, True)
```
@@ -148,12 +126,13 @@ translator = Translator(translator_dict, True)
```python
import asyncio
-from afrim_py import Preprocessor, Translator
+from afrim_py import Preprocessor, Translator, Config
class InputMethodEngine:
- def __init__(self, preprocessor_data, translator_dict):
- self.preprocessor = Preprocessor(preprocessor_data, 64)
- self.translator = Translator(translator_dict, True)
+ def __init__(self, config_file: str):
+ config = Config(config_file)
+ self.preprocessor = Preprocessor(config.extract_data(), 64)
+ self.translator = Translator(config.extract_translation(), True)
self.running = False
async def process_commands(self):
@@ -203,16 +182,14 @@ class InputMethodEngine:
# Usage
async def main():
ime = InputMethodEngine(
- preprocessor_data={"hello": "hi", "world": "earth"},
- translator_dict={"hi": ["hello", "greetings"], "earth": ["world", "planet"]}
+ preprocessor_data={"A": "ዕ", "Aa": "ዓ", "C": "ጭ"},
+ translator_dict={"Atarah": ["ዓጣራ"], "Adiel": ["ዓዲዔል"]}
)
# Simulate key events
- translations = ime.handle_key_event("h")
- translations = ime.handle_key_event("e")
- translations = ime.handle_key_event("l")
- translations = ime.handle_key_event("l")
- translations = ime.handle_key_event("o")
+ translations = ime.handle_key_event("A")
+ translations = ime.handle_key_event("a")
+ translations = ime.handle_key_event("C")
print("Translations:", translations)
@@ -224,52 +201,31 @@ async def main():
# asyncio.run(main())
```
-
-## Testing
-
-The project includes a comprehensive test suite with 49+ test cases covering all functionality:
-
-```bash
-# Run all tests
-python -m pytest tests/ -v
-
-# Run with coverage
-python -m pytest tests/ --cov=afrim_py --cov-report=html
-
-# Run the comprehensive test suite
-python run_tests.py
-```
-
## Development
-### Prerequisites
+### Build requirements
-- Python 3.8+
- Rust 1.70+
-- maturin (`pip install maturin`)
+- Cargo
-### Setup
+- Python 3.8+ and and [maturin](https://www.maturin.rs/installation.html)
+- [uv](https://docs.astral.sh/uv/getting-started/installation/) *(optional)*
-```bash
-# Clone and setup
-git clone https://github.com/esubalew/afrim-py.git
-cd afrim-py
-python -m venv .venv
-source .venv/bin/activate
+### Build from source
-# Install development dependencies
-pip install -r requirements-test.txt
+To simplify the development, we recommend to use `uv`.
-# Build in development mode
-maturin develop
+**Using maturin**
-# Run tests
-python run_tests.py
-```
+```bash
+# Clone the repository
+git clone https://github.com/fodydev/afrim-py.git
+cd afrim-py
-### Building
+# Create virtual environment
+python -m venv .venv
+source .venv/bin/activate # On Windows: .venv\Scripts\activate
-```bash
# Development build
maturin develop
@@ -280,33 +236,37 @@ maturin build --release
maturin build --interpreter python
```
-## Contributing
+**Using uv**
-Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
+```bash
+# Clone the repository
+git clone https://github.com/fodydev/afrim-py.git
+cd afrim-py
-### Development Guidelines
+# Prerelease build
+uv build --prerelease
-1. Run tests before submitting: `python run_tests.py`
-2. Follow Python PEP 8 style guidelines
-3. Add tests for new functionality
-4. Update documentation as needed
+# Release build
+uv build
+```
-## Acknowledgments
+### Testing
-- **[afrim](https://github.com/pythonbrad/afrim)** - The original input method engine
-- **[afrim-js](https://github.com/pythonbrad/afrim-js)** - Web bindings that inspired this project
-- **[@pythonbrad](https://github.com/pythonbrad)** - Creator of the original afrim project
+The project includes tests that represent a real user scenario:
-## License
+```bash
+# Run all tests
+python -m pytest tests/ -v
+```
-Licensed under MIT license ([LICENSE](LICENSE) or http://opensource.org/licenses/MIT).
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
-## Author
+## Acknowledgments
-**Esubalew Chekol** ([@esubalew](https://github.com/esubalew))
+- **[afrim-js](https://github.com/pythonbrad/afrim-js)** - Web bindings that inspired this project
----
+## License
-
- Built with ❤️ using Rust and Python
-
+Licensed under the [MIT LICENSE](LICENSE).
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..bc6f547
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,29 @@
+[project]
+name = "afrim-py"
+version = "0.1.0"
+description = "Python binding of the afrim."
+readme = "README.md"
+license = "MIT"
+keywords = ["afrim", "ime", "typing"]
+authors = [
+ { name = "Esubalew Chekol" }
+]
+maintainers = [
+ { name = "Brady Fomegne", email = "brady.fomegne@outlook.com" },
+]
+repository = "github.com/fodydev/afrim-py"
+requires-python = ">=3.8,<3.13"
+dependencies = []
+
+[build-system]
+requires = ["maturin>=1.0,<2.0"]
+build-backend = "maturin"
+
+[tool.maturin]
+features = ["pyo3/extension-module"]
+
+[dependency-groups]
+dev = [
+ "pytest>=8.3.5",
+ "ruff>=0.12.4",
+]
diff --git a/src/preprocessor.rs b/src/preprocessor.rs
index 3905e19..2477c41 100644
--- a/src/preprocessor.rs
+++ b/src/preprocessor.rs
@@ -1,11 +1,10 @@
#![deny(missing_docs)]
/// Python binding of the afrim preprocessor.
-
use afrim_preprocessor::Preprocessor as NativePreprocessor;
use pyo3::prelude::*;
+use pythonize::pythonize;
use serde_json::Value;
use std::collections::HashMap;
-use pythonize::pythonize;
/// Core structure of the preprocessor.
///
@@ -37,7 +36,7 @@ impl Preprocessor {
/// Process a keyboard event (key string, state "keydown"|"keyup")
fn process(&mut self, key: &str, state: &str) -> PyResult {
let event = crate::preprocessor::utils::deserialize_event(key, state)
- .map_err(|err| pyo3::exceptions::PyValueError::new_err(err))?;
+ .map_err(pyo3::exceptions::PyValueError::new_err)?;
let (changed, _) = self.engine.process(event);
Ok(changed)
}
@@ -87,4 +86,4 @@ pub mod utils {
};
Ok(event)
}
-}
\ No newline at end of file
+}
diff --git a/src/translator.rs b/src/translator.rs
index 47c5a71..dfcbce3 100644
--- a/src/translator.rs
+++ b/src/translator.rs
@@ -1,13 +1,13 @@
#![deny(missing_docs)]
-/// Python binding of the afrim translator.
+//! Python binding of the afrim translator.
#[cfg(feature = "rhai")]
use afrim_translator::Engine;
use afrim_translator::Translator as NativeTranslator;
use indexmap::IndexMap;
use pyo3::prelude::*;
-use std::collections::HashMap;
use pythonize::pythonize;
+use std::collections::HashMap;
/// Core structure of the translator
#[pyclass(unsendable)]
@@ -54,7 +54,7 @@ impl Translator {
/// Translate input — returns a serde_json::Value that maps nicely to Python types
fn translate(&self, py: Python, input: &str) -> PyResult {
- let value = serde_json::to_value(&self.engine.translate(input)).unwrap();
+ let value = serde_json::to_value(self.engine.translate(input)).unwrap();
pythonize(py, &value).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
}
-}
\ No newline at end of file
+}
diff --git a/tests/conftest.py b/tests/conftest.py
deleted file mode 100644
index 3570024..0000000
--- a/tests/conftest.py
+++ /dev/null
@@ -1,67 +0,0 @@
-"""Pytest configuration and fixtures for afrim-py tests."""
-
-import pytest
-
-
-@pytest.fixture
-def sample_preprocessor_data():
- """Sample data for Preprocessor tests."""
- return {
- "a1": "à",
- "ae": "æ",
- "oe": "œ",
- "hello": "hi",
- "test": "result"
- }
-
-
-@pytest.fixture
-def sample_translator_dict():
- """Sample dictionary for Translator tests."""
- return {
- "hello": ["hi", "hey", "greetings"],
- "world": ["earth", "globe", "planet"],
- "bye": ["goodbye", "farewell", "see you"],
- "test": ["result", "outcome"],
- "café": ["coffee", "☕"]
- }
-
-
-@pytest.fixture
-def sample_toml_config():
- """Sample TOML configuration for testing."""
- return '''
-[info]
-name = "afrim-test"
-version = "1.0.0"
-
-[preprocessor]
-a1 = "à"
-ae = "æ"
-hello = "hi"
-
-[translator.hello]
-values = ["hi", "hey"]
-
-[translator.world]
-values = ["earth", "globe"]
-'''
-
-
-@pytest.fixture
-def complex_unicode_data():
- """Complex Unicode data for testing."""
- return {
- "preprocessor": {
- "cafe": "café",
- "naive": "naïve",
- "emoji1": "🚀",
- "resume": "résumé"
- },
- "translator": {
- "café": ["coffee", "☕"],
- "🚀": ["rocket", "spaceship"],
- "naïve": ["innocent", "simple"],
- "résumé": ["CV", "summary"]
- }
- }
diff --git a/tests/test_preprocessor.py b/tests/test_preprocessor.py
index 1959e69..bbb1b28 100644
--- a/tests/test_preprocessor.py
+++ b/tests/test_preprocessor.py
@@ -11,7 +11,7 @@ def test_preprocessor_initialization(self):
"""Test basic Preprocessor initialization."""
data = {"a1": "à", "ae": "æ", "oe": "œ"}
buffer_size = 64
-
+
preprocessor = Preprocessor(data, buffer_size)
assert preprocessor is not None
@@ -24,11 +24,11 @@ def test_single_key_processing(self):
"""Test processing a single key."""
data = {"a": "α"}
preprocessor = Preprocessor(data, 64)
-
+
# Process key 'a' down
result = preprocessor.process("a", "keydown")
assert isinstance(result, bool)
-
+
# Get current input
input_text = preprocessor.get_input()
assert isinstance(input_text, str)
@@ -37,13 +37,13 @@ def test_key_sequence_processing(self):
"""Test processing a sequence of keys."""
data = {"a1": "à", "ae": "æ", "hello": "hi"}
preprocessor = Preprocessor(data, 64)
-
+
# Process 'a' key
result1 = preprocessor.process("a", "keydown")
assert isinstance(result1, bool)
current_input = preprocessor.get_input()
assert current_input == "a"
-
+
# Process '1' key to complete 'a1'
result2 = preprocessor.process("1", "keydown")
assert isinstance(result2, bool)
@@ -54,7 +54,7 @@ def test_keyup_processing(self):
"""Test processing key up events."""
data = {"test": "result"}
preprocessor = Preprocessor(data, 64)
-
+
# Test keyup event
result = preprocessor.process("t", "keyup")
assert isinstance(result, bool)
@@ -63,7 +63,7 @@ def test_invalid_key_state(self):
"""Test processing with invalid key state."""
data = {"a": "alpha"}
preprocessor = Preprocessor(data, 64)
-
+
# Invalid state should raise an error
with pytest.raises(ValueError):
preprocessor.process("a", "invalid_state")
@@ -72,30 +72,30 @@ def test_commit_functionality(self):
"""Test commit functionality."""
data = {"test": "result"}
preprocessor = Preprocessor(data, 64)
-
+
# Commit should not raise an error
preprocessor.commit("test_text")
-
+
# Input should be cleared after commit
- input_text = preprocessor.get_input()
+ preprocessor.get_input()
# The exact behavior depends on the underlying implementation
def test_queue_operations(self):
"""Test queue operations (pop_queue, clear_queue)."""
data = {"a1": "à", "test": "result"}
preprocessor = Preprocessor(data, 64)
-
+
# Process some keys to potentially generate queue items
preprocessor.process("a", "keydown")
preprocessor.process("1", "keydown")
-
+
# Pop from queue
queue_item = preprocessor.pop_queue()
assert queue_item is not None
-
+
# Clear queue
preprocessor.clear_queue()
-
+
# After clearing, queue should be empty or return default
queue_item = preprocessor.pop_queue()
assert queue_item is not None # Should return default "NOP" or similar
@@ -103,28 +103,23 @@ def test_queue_operations(self):
def test_buffer_size_limits(self):
"""Test different buffer sizes."""
data = {"longkey": "result"}
-
+
# Small buffer
small_preprocessor = Preprocessor(data, 4)
assert small_preprocessor is not None
-
+
# Large buffer
large_preprocessor = Preprocessor(data, 1024)
assert large_preprocessor is not None
def test_unicode_keys_in_data(self):
"""Test with Unicode characters in data."""
- data = {
- "café": "coffee",
- "naïve": "naive",
- "résumé": "resume",
- "🚀": "rocket"
- }
+ data = {"café": "coffee", "naïve": "naive", "résumé": "resume", "🚀": "rocket"}
preprocessor = Preprocessor(data, 64)
-
+
# Should initialize without error
assert preprocessor is not None
-
+
# Test processing regular keys
result = preprocessor.process("c", "keydown")
assert isinstance(result, bool)
@@ -133,21 +128,21 @@ def test_multiple_preprocessors(self):
"""Test creating multiple preprocessor instances."""
data1 = {"a1": "à", "e1": "é"}
data2 = {"u1": "ù", "o1": "ò"}
-
+
prep1 = Preprocessor(data1, 32)
prep2 = Preprocessor(data2, 64)
-
+
# Both should work independently
result1 = prep1.process("a", "keydown")
result2 = prep2.process("u", "keydown")
-
+
assert isinstance(result1, bool)
assert isinstance(result2, bool)
-
+
# Inputs should be independent
input1 = prep1.get_input()
input2 = prep2.get_input()
-
+
assert isinstance(input1, str)
assert isinstance(input2, str)
@@ -155,20 +150,20 @@ def test_complex_key_mapping(self):
"""Test complex key mappings."""
data = {
"aa": "ā",
- "aaa": "ǟ",
+ "aaa": "ǟ",
"hello": "hi",
"world": "🌍",
"123": "numbers",
- "abc": "alphabet"
+ "abc": "alphabet",
}
preprocessor = Preprocessor(data, 128)
-
+
# Test sequential processing
keys = ["h", "e", "l", "l", "o"]
for key in keys:
result = preprocessor.process(key, "keydown")
assert isinstance(result, bool)
-
+
current_input = preprocessor.get_input()
assert current_input == "hello"
@@ -176,11 +171,11 @@ def test_special_characters_processing(self):
"""Test processing special characters."""
data = {"space": " ", "tab": "\t", "newline": "\n"}
preprocessor = Preprocessor(data, 64)
-
+
# Test space key
result = preprocessor.process(" ", "keydown")
assert isinstance(result, bool)
-
+
# Test other printable characters
for char in "!@#$%^&*()":
result = preprocessor.process(char, "keydown")
@@ -191,14 +186,14 @@ def test_edge_cases(self):
# Empty string keys
data = {"": "empty", "a": "alpha"}
preprocessor = Preprocessor(data, 64)
-
+
# Very long key sequences
long_data = {"a" * 100: "long_key"}
long_preprocessor = Preprocessor(long_data, 256)
-
+
# Minimum buffer size
min_preprocessor = Preprocessor({"a": "b"}, 1)
-
+
assert preprocessor is not None
assert long_preprocessor is not None
assert min_preprocessor is not None
diff --git a/tests/test_translator.py b/tests/test_translator.py
index 49bd02a..3802889 100644
--- a/tests/test_translator.py
+++ b/tests/test_translator.py
@@ -1,7 +1,7 @@
"""Tests for Translator functionality."""
import pytest
-from afrim_py import Translator
+from afrim_py import Translator, is_rhai_feature_enabled
class TestTranslator:
@@ -11,7 +11,7 @@ def test_translator_initialization(self):
"""Test basic Translator initialization."""
dictionary = {"hello": ["hi", "hey"], "world": ["earth", "globe"]}
auto_commit = True
-
+
translator = Translator(dictionary, auto_commit)
assert translator is not None
@@ -30,11 +30,11 @@ def test_simple_translation(self):
"""Test basic translation functionality."""
dictionary = {"hello": ["hi", "hey"], "world": ["earth"]}
translator = Translator(dictionary, True)
-
+
result = translator.translate("hello")
assert isinstance(result, list)
assert len(result) > 0
-
+
# Check the structure of the result
translation = result[0]
assert isinstance(translation, dict)
@@ -45,11 +45,11 @@ def test_single_option_translation(self):
"""Test translation with single option."""
dictionary = {"world": ["earth"]}
translator = Translator(dictionary, True)
-
+
result = translator.translate("world")
assert isinstance(result, list)
assert len(result) > 0
-
+
translation = result[0]
assert translation["texts"] == ["earth"]
@@ -57,7 +57,7 @@ def test_non_existent_key_translation(self):
"""Test translation of non-existent key."""
dictionary = {"hello": ["hi"]}
translator = Translator(dictionary, True)
-
+
result = translator.translate("nonexistent")
assert isinstance(result, list)
# Should return empty list or some default behavior
@@ -66,7 +66,7 @@ def test_empty_string_translation(self):
"""Test translation of empty string."""
dictionary = {"hello": ["hi"]}
translator = Translator(dictionary, True)
-
+
result = translator.translate("")
assert isinstance(result, list)
@@ -74,16 +74,16 @@ def test_multiple_options_translation(self):
"""Test translation with multiple options."""
dictionary = {
"hello": ["hi", "hey", "hello there"],
- "bye": ["goodbye", "farewell", "see you"]
+ "bye": ["goodbye", "farewell", "see you"],
}
translator = Translator(dictionary, False)
-
+
# Test hello
result_hello = translator.translate("hello")
assert isinstance(result_hello, list)
if len(result_hello) > 0:
assert result_hello[0]["texts"] == ["hi", "hey", "hello there"]
-
+
# Test bye
result_bye = translator.translate("bye")
assert isinstance(result_bye, list)
@@ -95,14 +95,14 @@ def test_unicode_translation(self):
dictionary = {
"café": ["coffee", "☕"],
"naïve": ["naive"],
- "🚀": ["rocket", "spaceship"]
+ "🚀": ["rocket", "spaceship"],
}
translator = Translator(dictionary, True)
-
+
# Test Unicode key
result = translator.translate("café")
assert isinstance(result, list)
-
+
# Test emoji key
result_emoji = translator.translate("🚀")
assert isinstance(result_emoji, list)
@@ -117,10 +117,10 @@ def test_complex_dictionary(self):
"world": ["earth", "globe", "planet"],
"programming": ["coding", "development"],
"python": ["snake", "language"],
- "rust": ["metal", "language", "oxidation"]
+ "rust": ["metal", "language", "oxidation"],
}
translator = Translator(dictionary, True)
-
+
# Test various keys
test_keys = ["a", "hello", "programming", "rust"]
for key in test_keys:
@@ -129,18 +129,14 @@ def test_complex_dictionary(self):
def test_case_sensitivity(self):
"""Test case sensitivity in translation."""
- dictionary = {
- "hello": ["hi"],
- "Hello": ["Hi"],
- "HELLO": ["HI"]
- }
+ dictionary = {"hello": ["hi"], "Hello": ["Hi"], "HELLO": ["HI"]}
translator = Translator(dictionary, True)
-
+
# Test different cases
result_lower = translator.translate("hello")
result_title = translator.translate("Hello")
result_upper = translator.translate("HELLO")
-
+
assert isinstance(result_lower, list)
assert isinstance(result_title, list)
assert isinstance(result_upper, list)
@@ -153,10 +149,10 @@ def test_special_characters_in_keys(self):
"wow...": ["amazing"],
"test@domain": ["email"],
"key-value": ["pair"],
- "under_score": ["underscore"]
+ "under_score": ["underscore"],
}
translator = Translator(dictionary, False)
-
+
for key in dictionary.keys():
result = translator.translate(key)
assert isinstance(result, list)
@@ -167,13 +163,13 @@ def test_numeric_strings_in_dictionary(self):
"123": ["numbers"],
"42": ["answer"],
"3.14": ["pi"],
- "0": ["zero", "null"]
+ "0": ["zero", "null"],
}
translator = Translator(dictionary, True)
-
+
result = translator.translate("123")
assert isinstance(result, list)
-
+
result_pi = translator.translate("3.14")
assert isinstance(result_pi, list)
@@ -181,15 +177,12 @@ def test_long_keys_and_values(self):
"""Test translation with long keys and values."""
long_key = "a" * 100
long_value = "b" * 200
- dictionary = {
- long_key: [long_value, "short"],
- "short": [long_value]
- }
+ dictionary = {long_key: [long_value, "short"], "short": [long_value]}
translator = Translator(dictionary, True)
-
+
result = translator.translate(long_key)
assert isinstance(result, list)
-
+
result_short = translator.translate("short")
assert isinstance(result_short, list)
@@ -197,14 +190,14 @@ def test_multiple_translators(self):
"""Test creating multiple translator instances."""
dict1 = {"hello": ["hi"], "world": ["earth"]}
dict2 = {"bonjour": ["hello"], "monde": ["world"]}
-
+
translator1 = Translator(dict1, True)
translator2 = Translator(dict2, False)
-
+
# Both should work independently
result1 = translator1.translate("hello")
result2 = translator2.translate("bonjour")
-
+
assert isinstance(result1, list)
assert isinstance(result2, list)
@@ -212,19 +205,19 @@ def test_translation_result_structure(self):
"""Test the structure of translation results."""
dictionary = {"test": ["result1", "result2"]}
translator = Translator(dictionary, True)
-
+
result = translator.translate("test")
assert isinstance(result, list)
-
+
if len(result) > 0:
translation = result[0]
assert isinstance(translation, dict)
-
+
# Check expected keys in translation result
expected_keys = ["texts", "code"]
for key in expected_keys:
assert key in translation
-
+
# Check texts structure
assert isinstance(translation["texts"], list)
assert translation["texts"] == ["result1", "result2"]
@@ -232,34 +225,29 @@ def test_translation_result_structure(self):
def test_edge_cases(self):
"""Test edge cases and boundary conditions."""
# Empty values in dictionary
- dictionary = {
- "empty": [],
- "normal": ["value"],
- "single": ["one"]
- }
+ dictionary = {"empty": [], "normal": ["value"], "single": ["one"]}
translator = Translator(dictionary, True)
-
+
# Test empty value list
result_empty = translator.translate("empty")
assert isinstance(result_empty, list)
-
+
# Test normal case
result_normal = translator.translate("normal")
assert isinstance(result_normal, list)
- @pytest.mark.skipif(True, reason="Rhai feature may not be enabled")
- def test_register_unregister_functionality(self):
- """Test register and unregister functionality (if rhai feature is enabled)."""
+ @pytest.mark.skipif(
+ not is_rhai_feature_enabled(), reason="Rhai feature not be enabled"
+ )
+ def test_register_unregister_script(self):
+ """Test register and unregister script (if rhai feature is enabled)."""
+
dictionary = {"test": ["result"]}
translator = Translator(dictionary, True)
-
+
# Try to register a script (this may not work if rhai feature is disabled)
- try:
- translator.register("test_script", "fn main() { }")
- translator.unregister("test_script")
- except AttributeError:
- # Methods don't exist if rhai feature is not enabled
- pytest.skip("Rhai feature not enabled")
+ translator.register("test_script", "fn main(input) { [input] }")
+ translator.unregister("test_script")
def test_whitespace_handling(self):
"""Test translation with whitespace in keys and values."""
@@ -267,10 +255,10 @@ def test_whitespace_handling(self):
" hello ": ["hi"],
"hello world": ["hi earth"],
"\ttest\t": ["result"],
- "\n\r": ["newlines"]
+ "\n\r": ["newlines"],
}
translator = Translator(dictionary, True)
-
+
for key in dictionary.keys():
result = translator.translate(key)
assert isinstance(result, list)
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..2637fe7
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,236 @@
+version = 1
+revision = 2
+requires-python = ">=3.8, <3.13"
+resolution-markers = [
+ "python_full_version >= '3.10'",
+ "python_full_version == '3.9.*'",
+ "python_full_version < '3.9'",
+]
+
+[[package]]
+name = "afrim-py"
+version = "0.1.0"
+source = { editable = "." }
+
+[package.dev-dependencies]
+dev = [
+ { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "pytest", specifier = ">=8.3.5" },
+ { name = "ruff", specifier = ">=0.12.4" },
+]
+
+[[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, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
+ { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.9.*'",
+ "python_full_version < '3.9'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[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, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.9'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+ "python_full_version == '3.9.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.3.5"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.9'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.9'" },
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
+ { name = "packaging", marker = "python_full_version < '3.9'" },
+ { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
+ { name = "tomli", marker = "python_full_version < '3.9'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+ "python_full_version == '3.9.*'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
+ { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "packaging", marker = "python_full_version >= '3.9'" },
+ { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "pygments", marker = "python_full_version >= '3.9'" },
+ { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.12.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9b/ce/8d7dbedede481245b489b769d27e2934730791a9a82765cb94566c6e6abd/ruff-0.12.4.tar.gz", hash = "sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873", size = 5131435, upload-time = "2025-07-17T17:27:19.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/9f/517bc5f61bad205b7f36684ffa5415c013862dee02f55f38a217bdbe7aa4/ruff-0.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a", size = 10188824, upload-time = "2025-07-17T17:26:31.412Z" },
+ { url = "https://files.pythonhosted.org/packages/28/83/691baae5a11fbbde91df01c565c650fd17b0eabed259e8b7563de17c6529/ruff-0.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442", size = 10884521, upload-time = "2025-07-17T17:26:35.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/8d/756d780ff4076e6dd035d058fa220345f8c458391f7edfb1c10731eedc75/ruff-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e", size = 10277653, upload-time = "2025-07-17T17:26:37.897Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/97/8eeee0f48ece153206dce730fc9e0e0ca54fd7f261bb3d99c0a4343a1892/ruff-0.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586", size = 10485993, upload-time = "2025-07-17T17:26:40.68Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b8/22a43d23a1f68df9b88f952616c8508ea6ce4ed4f15353b8168c48b2d7e7/ruff-0.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb", size = 10022824, upload-time = "2025-07-17T17:26:43.564Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/70/37c234c220366993e8cffcbd6cadbf332bfc848cbd6f45b02bade17e0149/ruff-0.12.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c", size = 11524414, upload-time = "2025-07-17T17:26:46.219Z" },
+ { url = "https://files.pythonhosted.org/packages/14/77/c30f9964f481b5e0e29dd6a1fae1f769ac3fd468eb76fdd5661936edd262/ruff-0.12.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a", size = 12419216, upload-time = "2025-07-17T17:26:48.883Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/79/af7fe0a4202dce4ef62c5e33fecbed07f0178f5b4dd9c0d2fcff5ab4a47c/ruff-0.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3", size = 11976756, upload-time = "2025-07-17T17:26:51.754Z" },
+ { url = "https://files.pythonhosted.org/packages/09/d1/33fb1fc00e20a939c305dbe2f80df7c28ba9193f7a85470b982815a2dc6a/ruff-0.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045", size = 11020019, upload-time = "2025-07-17T17:26:54.265Z" },
+ { url = "https://files.pythonhosted.org/packages/64/f4/e3cd7f7bda646526f09693e2e02bd83d85fff8a8222c52cf9681c0d30843/ruff-0.12.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57", size = 11277890, upload-time = "2025-07-17T17:26:56.914Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/d0/69a85fb8b94501ff1a4f95b7591505e8983f38823da6941eb5b6badb1e3a/ruff-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184", size = 10348539, upload-time = "2025-07-17T17:26:59.381Z" },
+ { url = "https://files.pythonhosted.org/packages/16/a0/91372d1cb1678f7d42d4893b88c252b01ff1dffcad09ae0c51aa2542275f/ruff-0.12.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb", size = 10009579, upload-time = "2025-07-17T17:27:02.462Z" },
+ { url = "https://files.pythonhosted.org/packages/23/1b/c4a833e3114d2cc0f677e58f1df6c3b20f62328dbfa710b87a1636a5e8eb/ruff-0.12.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1", size = 10942982, upload-time = "2025-07-17T17:27:05.343Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/ce/ce85e445cf0a5dd8842f2f0c6f0018eedb164a92bdf3eda51984ffd4d989/ruff-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b", size = 11343331, upload-time = "2025-07-17T17:27:08.652Z" },
+ { url = "https://files.pythonhosted.org/packages/35/cf/441b7fc58368455233cfb5b77206c849b6dfb48b23de532adcc2e50ccc06/ruff-0.12.4-py3-none-win32.whl", hash = "sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93", size = 10267904, upload-time = "2025-07-17T17:27:11.814Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/7e/20af4a0df5e1299e7368d5ea4350412226afb03d95507faae94c80f00afd/ruff-0.12.4-py3-none-win_amd64.whl", hash = "sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a", size = 11209038, upload-time = "2025-07-17T17:27:14.417Z" },
+ { url = "https://files.pythonhosted.org/packages/11/02/8857d0dfb8f44ef299a5dfd898f673edefb71e3b533b3b9d2db4c832dd13/ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e", size = 10469336, upload-time = "2025-07-17T17:27:16.913Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" },
+ { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" },
+ { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" },
+ { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
+ { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
+ { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
+ { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
+ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.13.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.9'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+ "python_full_version == '3.9.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]