From dfb66b38f1d4afc41c942985af4d0c2c99497ef7 Mon Sep 17 00:00:00 2001 From: Brady Fomegne Date: Thu, 23 Oct 2025 23:05:04 +0100 Subject: [PATCH 1/8] patch: add github ci workflows for lint and build tests This commit introduces the following changes: - updated the tests - fixed linting - added github ci workflows for linting and build tests - configured the uv tools for a easy project build As additional changes: - removed integration tests - included a LICENCE file - added @pythonbrad as a project maintainor - updated dependencies --- .github/workflows/python.yml | 20 +++ .github/workflows/rust.yml | 42 +++++++ Cargo.toml | 6 +- LICENSE | 21 ++++ pyproject.toml | 29 +++++ src/preprocessor.rs | 7 +- src/translator.rs | 8 +- tests/conftest.py | 67 ---------- tests/test_preprocessor.py | 71 +++++------ tests/test_translator.py | 108 +++++++--------- uv.lock | 236 +++++++++++++++++++++++++++++++++++ 11 files changed, 439 insertions(+), 176 deletions(-) create mode 100644 .github/workflows/python.yml create mode 100644 .github/workflows/rust.yml create mode 100644 LICENSE create mode 100644 pyproject.toml delete mode 100644 tests/conftest.py create mode 100644 uv.lock diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..eb628a9 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,20 @@ +name: Python CI +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + # Update output format to enable automatic inline annotations. + - name: Run Ruff + run: | + ruff format --check tests + ruff check --output-format=github tests diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..1a72409 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,42 @@ +name: Rust CI + +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + rust-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 -- -D warnings + + - 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/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/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..f221a3b --- /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.0.1" +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" }, +] From 3436b9fd14886c510a13f29c50d7efaae75fc454 Mon Sep 17 00:00:00 2001 From: Brady Fomegne Date: Fri, 24 Oct 2025 00:25:36 +0100 Subject: [PATCH 2/8] udpate documentation and add changelog --- CHANGELOG.md | 12 ++++++++ README.md | 80 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 CHANGELOG.md 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/README.md b/README.md index 8d25bc7..dadc288 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,12 @@ Python Rust License + Changelog +

- Repository + Repository

Built with 🦀🐍 by @esubaalew @@ -33,6 +35,12 @@ maturin develop maturin build --release ``` +## Build with [`uv`](https://docs.astral.sh/uv/getting-started/installation/) + +```bash +uv build +``` + ## 🔋 Features Included * **Preprocessor** - Advanced key sequence processing and input transformation @@ -42,13 +50,21 @@ maturin build --release * **Rhai Scripting** - Dynamic translation scripts (when `rhai` feature is enabled) * **String Similarity** - Fuzzy matching with `strsim` feature -## Installation +### Build requirements -### From Source +- Rust 1.70+ +- Cargo + +- Python 3.8+ and and [maturin](https://www.maturin.rs/installation.html) +- [uv](https://docs.astral.sh/uv/getting-started/installation/) *(optional)* + +## Build from source + +**Using maturin** ```bash # Clone the repository -git clone https://github.com/esubaalew/afrim-py.git +git clone https://github.com/fodydev/afrim-py.git cd afrim-py # Create virtual environment @@ -59,18 +75,22 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate maturin develop ``` -### Requirements +**Using uv** -- Python 3.8+ -- Rust 1.70+ -- Cargo and maturin +```bash +# Clone the repository +git clone https://github.com/fodydev/afrim-py.git +cd afrim-py + +uv build +``` ## 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 = { @@ -117,30 +137,33 @@ while True: ### TOML 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) ``` @@ -151,9 +174,10 @@ import asyncio from afrim_py import Preprocessor, Translator 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): From 2b21688656f39619fe5f7a7b20674b6e2cbfb9fd Mon Sep 17 00:00:00 2001 From: Brady Fomegne Date: Fri, 24 Oct 2025 00:54:42 +0100 Subject: [PATCH 3/8] update ci --- .github/workflows/python.yml | 56 +++++++++++++++++++++++++++++------- .github/workflows/rust.yml | 2 +- uv.lock | 2 +- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index eb628a9..b9e97b8 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,20 +1,56 @@ name: Python CI on: push jobs: - build: + lint: + name: Check lint runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v4 - - name: Install Python + - uses: actions/checkout@v5 + + - name: "Set up Python" uses: actions/setup-python@v5 with: - python-version: "3.11" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install ruff + python-version-file: "pyproject.toml" + + - name: Install uv + uses: astral-sh/setup-uv@v6 + # Update output format to enable automatic inline annotations. - name: Run Ruff run: | - ruff format --check tests - ruff check --output-format=github tests + uv run ruff format --check tests + uv run ruff check --output-format=github tests + + build: + name: continuous-integration + 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 }} + + - 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 --no-project --with dist/*.whl pytest + - name: Smoke test (source distribution) + run: uv run --isolated --no-project --with dist/*.tar.gz pytest + + diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1a72409..722b66e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,7 +27,7 @@ jobs: run: cargo fmt -- --check - name: Clippy - run: cargo clippy --all-targets -- -D warnings + run: cargo clippy --all-targets - name: Build (minimal) run: | diff --git a/uv.lock b/uv.lock index f221a3b..2637fe7 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ [[package]] name = "afrim-py" -version = "0.0.1" +version = "0.1.0" source = { editable = "." } [package.dev-dependencies] From c9feb488f93e4e86902934bf528ed6b660e1474f Mon Sep 17 00:00:00 2001 From: Brady Fomegne Date: Fri, 24 Oct 2025 01:04:22 +0100 Subject: [PATCH 4/8] fix test build --- .github/workflows/python.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index b9e97b8..1453530 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -49,8 +49,8 @@ jobs: # Check that basic features work and we didn't miss to include crucial files - name: Smoke test (wheel) - run: uv run --isolated --no-project --with dist/*.whl pytest + run: uv run --isolated --with dist/*.whl pytest - name: Smoke test (source distribution) - run: uv run --isolated --no-project --with dist/*.tar.gz pytest + run: uv run --isolated --with dist/*.tar.gz pytest From 74f5adffa31756dd8fd725ae18bc8f58f351d7d8 Mon Sep 17 00:00:00 2001 From: Brady Fomegne Date: Fri, 24 Oct 2025 01:09:28 +0100 Subject: [PATCH 5/8] enable ci cache for uv --- .github/workflows/python.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 1453530..26cb7e1 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -15,6 +15,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v6 + with: + enable-cache: true # Update output format to enable automatic inline annotations. - name: Run Ruff From f384fc52191a429e27fa0608119d06a554f9a5be Mon Sep 17 00:00:00 2001 From: Brady Fomegne Date: Fri, 24 Oct 2025 01:50:55 +0100 Subject: [PATCH 6/8] simplify the readme to avoid duplications --- README.md | 158 ++++++++++++++++-------------------------------------- 1 file changed, 47 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index dadc288..b02b453 100644 --- a/README.md +++ b/README.md @@ -17,30 +17,12 @@ 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 -``` - -## Build with [`uv`](https://docs.astral.sh/uv/getting-started/installation/) - -```bash -uv build -``` - ## 🔋 Features Included * **Preprocessor** - Advanced key sequence processing and input transformation @@ -50,39 +32,12 @@ uv build * **Rhai Scripting** - Dynamic translation scripts (when `rhai` feature is enabled) * **String Similarity** - Fuzzy matching with `strsim` feature -### Build requirements - -- Rust 1.70+ -- Cargo - -- Python 3.8+ and and [maturin](https://www.maturin.rs/installation.html) -- [uv](https://docs.astral.sh/uv/getting-started/installation/) *(optional)* - -## Build from source - -**Using maturin** - -```bash -# Clone the repository -git clone https://github.com/fodydev/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 -``` +## Installation -**Using uv** +`afrim-py` is available on pypi. ```bash -# Clone the repository -git clone https://github.com/fodydev/afrim-py.git -cd afrim-py - -uv build +pip install afrim-py ``` ## Usage @@ -134,7 +89,7 @@ while True: print(f"Command: {command}") ``` -### TOML Configuration +### Configuration ```python from afrim_py import Config @@ -171,7 +126,7 @@ 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, config_file: str): @@ -227,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) @@ -248,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 @@ -304,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 -## Author +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. -**Esubalew Chekol** ([@esubalew](https://github.com/esubalew)) +## Acknowledgments ---- +- **[afrim-js](https://github.com/pythonbrad/afrim-js)** - Web bindings that inspired this project -
- Built with ❤️ using Rust and Python -
+## License + +Licensed under the [MIT LICENSE](LICENSE). From 416ba02f01a5a45a0fce2bfd24cc19661bd5a1f2 Mon Sep 17 00:00:00 2001 From: Brady Fomegne Date: Fri, 24 Oct 2025 02:01:51 +0100 Subject: [PATCH 7/8] fix missing caching in build ci --- .github/workflows/python.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 26cb7e1..accb842 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -44,6 +44,7 @@ jobs: uses: astral-sh/setup-uv@v6 with: python-version: ${{ matrix.python-version }} + enable-cache: true - name: Build run: | From e38b1d2f47ce46c2da4f4e9d41e9394e21ac256e Mon Sep 17 00:00:00 2001 From: Brady Fomegne Date: Fri, 24 Oct 2025 02:02:05 +0100 Subject: [PATCH 8/8] udpate ci name --- .github/workflows/python.yml | 2 +- .github/workflows/rust.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index accb842..47f2fb7 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -25,7 +25,7 @@ jobs: uv run ruff check --output-format=github tests build: - name: continuous-integration + name: Check Build runs-on: ubuntu-latest needs: lint strategy: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 722b66e..9720333 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -12,6 +12,7 @@ env: jobs: rust-build: + name: Check lint and build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4