pax_global_header00006660000000000000000000000064152405565740014527gustar00rootroot0000000000000052 comment=31add32173372c266eb19fd6f7d8bfea78034f99 ormar-orm-ormar-utils-2fd6230/000077500000000000000000000000001524055657400162465ustar00rootroot00000000000000ormar-orm-ormar-utils-2fd6230/.github/000077500000000000000000000000001524055657400176065ustar00rootroot00000000000000ormar-orm-ormar-utils-2fd6230/.github/workflows/000077500000000000000000000000001524055657400216435ustar00rootroot00000000000000ormar-orm-ormar-utils-2fd6230/.github/workflows/ci.yml000066400000000000000000000130131524055657400227570ustar00rootroot00000000000000name: CI on: push: branches: [main] tags: ["v*"] pull_request: branches: [main] permissions: contents: read jobs: lint: name: Lint (rustfmt, clippy, ruff) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - name: Rust format check run: cargo fmt --check - name: Clippy run: cargo clippy --all-targets -- -D warnings - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install Ruff run: pip install ruff - name: Ruff lint run: ruff check tests - name: Ruff format check run: ruff format --check tests test: name: Test (py${{ matrix.python-version }} on ${{ matrix.os }}) needs: lint runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install build/test tooling run: pip install "maturin>=1.0,<2.0" pytest - name: Build abi3 wheel run: maturin build --release --out dist - name: Install wheel run: pip install --no-index --find-links dist ormar-utils - name: Run tests run: pytest tests/ -v test-musl: name: Test (musllinux wheel on Alpine) needs: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build musllinux wheel uses: PyO3/maturin-action@v1 with: target: x86_64 args: --release --out dist manylinux: musllinux_1_2 # Alpine marks its system Python as externally managed (PEP 668), so the # wheel is installed into a venv. --no-index is the real assertion here: # if the wheel is not tagged musllinux, pip finds no candidate at all and # the job fails, which is precisely the breakage being guarded against. - name: Install wheel and run tests on Alpine run: | docker run --rm -v "$PWD:/io" -w /io alpine:latest sh -euc ' apk add --no-cache python3 python3 -m venv /venv /venv/bin/pip install pytest /venv/bin/pip install --no-index --find-links dist ormar-utils /venv/bin/pytest tests/ -v ' build: name: Build wheels (${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'auto' }}) needs: [test, test-musl] if: startsWith(github.ref, 'refs/tags/v') runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest target: x86_64 - os: ubuntu-latest target: aarch64 # musl builds for Alpine and other musl-libc distros. glibc wheels are # not installable there, so without these pip falls back to the sdist # and requires a Rust toolchain on the target machine. - os: ubuntu-latest target: x86_64 manylinux: musllinux_1_2 - os: ubuntu-latest target: aarch64 manylinux: musllinux_1_2 - os: macos-latest target: x86_64 - os: macos-latest target: aarch64 - os: windows-latest target: x86_64 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" # abi3-py310 produces a single stable-ABI wheel per platform that works on # CPython 3.10+ (including future releases), so no per-interpreter matrix. - name: Build wheels uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} args: --release --out dist manylinux: ${{ matrix.manylinux || 'auto' }} # The manylinux value is part of the name because the glibc and musl # builds otherwise collide on os+target, and upload-artifact@v4 rejects # duplicate artifact names. - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'auto' }} path: dist sdist: name: Build sdist needs: test if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build sdist uses: PyO3/maturin-action@v1 with: command: sdist args: --out dist - name: Upload sdist uses: actions/upload-artifact@v4 with: name: wheels-sdist path: dist publish: name: Publish to PyPI needs: [build, sdist] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest environment: pypi permissions: id-token: write steps: - name: Download all wheels uses: actions/download-artifact@v4 with: pattern: wheels-* merge-multiple: true path: dist # skip-existing keeps a re-run or a partially-completed upload from # failing the whole job on files PyPI already has, since a release can # otherwise only ever be published once. - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: skip-existing: true ormar-orm-ormar-utils-2fd6230/.gitignore000066400000000000000000000002411524055657400202330ustar00rootroot00000000000000# Rust /target/ # Python __pycache__/ *.py[cod] *.so *.dylib *.pyd dist/ *.egg-info/ .eggs/ *.whl # IDE .idea/ .vscode/ *.swp # Environment .env .venv/ venv/ ormar-orm-ormar-utils-2fd6230/.pre-commit-config.yaml000066400000000000000000000010571524055657400225320ustar00rootroot00000000000000repos: - repo: local hooks: - id: cargo-fmt name: cargo fmt entry: cargo fmt --check language: system types: [rust] pass_filenames: false - id: clippy name: clippy entry: cargo clippy --all-targets -- -D warnings language: system types: [rust] pass_filenames: false - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.16 hooks: - id: ruff args: [--fix] files: ^tests/ - id: ruff-format files: ^tests/ ormar-orm-ormar-utils-2fd6230/Cargo.lock000066400000000000000000000156721524055657400201660ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "indoc" version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" dependencies = [ "rustversion", ] [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "libc" version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "ormar_rust_utils" version = "0.2.1" dependencies = [ "base64", "indexmap", "pyo3", "serde_json", ] [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" dependencies = [ "cfg-if", "indoc", "libc", "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", "unindent", ] [[package]] name = "pyo3-build-config" version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" dependencies = [ "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" dependencies = [ "libc", "pyo3-build-config", ] [[package]] name = "pyo3-macros" version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", "syn", ] [[package]] name = "pyo3-macros-backend" version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", "syn", ] [[package]] name = "quote" version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "target-lexicon" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unindent" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" ormar-orm-ormar-utils-2fd6230/Cargo.toml000066400000000000000000000011051524055657400201730ustar00rootroot00000000000000[package] name = "ormar_rust_utils" version = "0.2.1" edition = "2021" description = "Rust-accelerated utility functions for the ormar ORM" license = "MIT" [lib] name = "ormar_rust_utils" crate-type = ["cdylib"] [dependencies] # abi3-py310 builds a single stable-ABI wheel that works on CPython 3.10 and # every newer version (3.14, 3.15, ...) without a per-version rebuild, so a new # Python release never leaves ormar without an installable wheel. pyo3 = { version = "0.23.5", features = ["extension-module", "abi3-py310"] } base64 = "0.22" serde_json = "1.0" indexmap = "2.0" ormar-orm-ormar-utils-2fd6230/LICENSE000066400000000000000000000020701524055657400172520ustar00rootroot00000000000000MIT License Copyright (c) 2026 Radosław Drążkiewicz 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. ormar-orm-ormar-utils-2fd6230/Makefile000066400000000000000000000015221524055657400177060ustar00rootroot00000000000000.PHONY: fmt fmt-check check lint test build clean fmt: cargo fmt poetry run ruff format tests fmt-check: cargo fmt --check poetry run ruff format --check tests check: cargo clippy --all-targets -- -D warnings lint: fmt-check check poetry run ruff check tests test: poetry run pytest tests/ -v build: poetry run maturin develop clean: cargo clean rm -rf target/ .DEFAULT_GOAL := help help: @echo "Available targets:" @echo " fmt - Format Rust (rustfmt) and Python (ruff) code" @echo " fmt-check - Check formatting without modifying files" @echo " check - Run clippy linter with strict warnings" @echo " lint - Run all format checks, clippy and ruff lint" @echo " test - Run tests with pytest" @echo " build - Build the extension module with maturin" @echo " clean - Clean build artifacts" ormar-orm-ormar-utils-2fd6230/README.md000066400000000000000000000034331524055657400175300ustar00rootroot00000000000000# ormar-utils Rust-accelerated utility functions for the [ormar](https://github.com/ormar-orm/ormar) async ORM. This package provides optional Rust implementations of performance-critical operations used internally by ormar. When installed, ormar automatically uses these faster implementations. ## Installation ```bash pip install ormar-utils ``` Or install ormar with the `rust` extra: ```bash pip install ormar[rust] ``` ## Requirements - Python >= 3.10 - A Rust toolchain (for building from source) Wheels are built against the CPython stable ABI (`abi3`, Python 3.10+), so a single wheel per platform works on current and future Python releases without a per-version rebuild. ## API Reference All functions are exposed from the `ormar_rust_utils` module: ### Parsers - `encode_bytes(value, represent_as_string=False)` - Encode bytes to string (UTF-8 or base64) - `decode_bytes(value, represent_as_string=False)` - Decode string to bytes (UTF-8 or base64) - `encode_json(value)` - Encode a value to JSON string ### Hashing - `hash_item(item)` - Convert a dict/list into a hashable tuple for use as dict key ### List/Dict Utilities - `translate_list_to_dict(list_to_trans, default=None)` - Split `__`-separated strings into nested dict - `group_related_list(list_)` - Group related strings into nested dictionary ### Collections - `UniqueList(initial=None)` - A list that prevents duplicates using hash-based O(1) lookups ### Alias Utilities - `build_reverse_alias_map(field_alias_map)` - Build a cached alias -> field_name lookup (with identity entries) from a field_name -> alias mapping ### Merge Infrastructure - `group_by_pk(pks)` - Group items by PK hash, preserving insertion order - `plan_merge_items_lists(current_pks, other_pks)` - Create a merge plan for two lists by PK ## License MIT ormar-orm-ormar-utils-2fd6230/pyproject.toml000066400000000000000000000030531524055657400211630ustar00rootroot00000000000000[build-system] requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [project] name = "ormar-utils" version = "0.2.1" description = "Rust-accelerated utility functions for the ormar ORM" readme = "README.md" license = { text = "MIT" } requires-python = ">=3.10" authors = [ { name = "Radosław Drążkiewicz", email = "collerek@gmail.com" }, ] keywords = ["ormar", "orm", "rust", "performance"] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: MIT License", "Topic :: Database", ] [project.urls] Homepage = "https://github.com/ormar-orm/ormar-utils" Repository = "https://github.com/ormar-orm/ormar-utils" [tool.maturin] features = ["pyo3/extension-module"] [tool.poetry] name = "ormar-utils" version = "0.2.1" description = "Rust-accelerated utility functions for the ormar ORM" authors = ["Radosław Drążkiewicz "] package-mode = false [tool.poetry.dependencies] python = "^3.10" [tool.poetry.group.dev.dependencies] pytest = "^7.0" maturin = "^1.0" pre-commit = "^3.0" ruff = "^0.15" [tool.pytest.ini_options] testpaths = ["tests"] [tool.ruff] target-version = "py310" ormar-orm-ormar-utils-2fd6230/src/000077500000000000000000000000001524055657400170355ustar00rootroot00000000000000ormar-orm-ormar-utils-2fd6230/src/alias_utils.rs000066400000000000000000000015741524055657400217230ustar00rootroot00000000000000use pyo3::prelude::*; use pyo3::types::PyDict; /// Build a reverse mapping from alias -> field_name given a forward mapping /// of field_name -> alias. If a field has no alias (alias == field_name), /// it still gets an identity entry. /// /// This is called once per model class and cached. #[pyfunction] pub fn build_reverse_alias_map<'py>( py: Python<'py>, field_alias_map: &Bound<'py, PyDict>, ) -> PyResult> { let result = PyDict::new(py); for (key, value) in field_alias_map.iter() { let field_name: String = key.extract()?; let alias: String = value.extract()?; // Map alias -> field_name result.set_item(&alias, &field_name)?; // Also keep field_name -> field_name for identity lookups if alias != field_name { result.set_item(&field_name, &field_name)?; } } Ok(result) } ormar-orm-ormar-utils-2fd6230/src/group_related.rs000066400000000000000000000036531524055657400222460ustar00rootroot00000000000000use pyo3::prelude::*; use pyo3::types::{PyDict, PyList}; use std::collections::BTreeMap; /// Translates a list of related strings into a nested dictionary. #[pyfunction] pub fn group_related_list(py: Python<'_>, list_: Vec) -> PyResult { group_related_inner(py, &list_) } fn group_related_inner(py: Python<'_>, list_: &[String]) -> PyResult { let mut groups: BTreeMap> = BTreeMap::new(); let mut sorted_list: Vec<&String> = list_.iter().collect(); sorted_list.sort_by(|a, b| { let a_first = a.split("__").next().unwrap_or(""); let b_first = b.split("__").next().unwrap_or(""); a_first.cmp(b_first) }); for item in &sorted_list { let parts: Vec<&str> = item.splitn(2, "__").collect(); let key = parts[0].to_string(); let rest = if parts.len() > 1 { Some(parts[1].to_string()) } else { None }; let entry = groups.entry(key).or_default(); if let Some(r) = rest { entry.push(r); } } let mut result_items: Vec<(String, PyObject)> = Vec::new(); for (key, mut children) in groups { children.sort(); let has_nested = children.iter().any(|x| x.contains("__")); if has_nested { let nested = group_related_inner(py, &children)?; result_items.push((key, nested)); } else { let py_list = PyList::new(py, &children)?; result_items.push((key, py_list.unbind().into())); } } // Sort by length of values, then by key result_items.sort_by(|a, b| { let a_len = a.1.bind(py).len().unwrap_or(0); let b_len = b.1.bind(py).len().unwrap_or(0); a_len.cmp(&b_len).then_with(|| a.0.cmp(&b.0)) }); let dict = PyDict::new(py); for (key, value) in &result_items { dict.set_item(key, value)?; } Ok(dict.unbind().into()) } ormar-orm-ormar-utils-2fd6230/src/hash_item.rs000066400000000000000000000054401524055657400213470ustar00rootroot00000000000000use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PyTuple}; /// Convert a dict or list into a hashable tuple for use as dictionary key. #[pyfunction] pub fn hash_item(py: Python<'_>, item: &Bound<'_, PyAny>) -> PyResult { hash_item_inner(py, item) } fn hash_item_inner(py: Python<'_>, item: &Bound<'_, PyAny>) -> PyResult { if let Ok(dict) = item.downcast::() { let mut items: Vec<(PyObject, PyObject)> = Vec::with_capacity(dict.len()); for (key, value) in dict.iter() { items.push((key.unbind(), value.unbind())); } // Sort by key using Python's comparison items.sort_by(|a, b| { let a_key = a.0.bind(py); let b_key = b.0.bind(py); a_key .lt(b_key) .and_then(|lt| { if lt { Ok(std::cmp::Ordering::Less) } else { a_key.gt(b_key).map(|gt| { if gt { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Equal } }) } }) .unwrap_or(std::cmp::Ordering::Equal) }); let mut result: Vec = Vec::with_capacity(items.len()); for (key, value) in &items { let val_bound = value.bind(py); let processed_value = if val_bound.is_instance_of::() || val_bound.is_instance_of::() { hash_item_inner(py, val_bound)? } else { value.clone_ref(py) }; let pair = PyTuple::new(py, &[key.clone_ref(py), processed_value])?; result.push(pair.unbind().into()); } let tuple = PyTuple::new(py, &result)?; Ok(tuple.unbind().into()) } else if let Ok(list) = item.downcast::() { let mut result: Vec = Vec::with_capacity(list.len()); for (idx, value) in list.iter().enumerate() { let processed_value = if value.is_instance_of::() || value.is_instance_of::() { hash_item_inner(py, &value)? } else { value.unbind() }; let idx_obj: PyObject = idx.into_pyobject(py)?.into_any().unbind(); let pair = PyTuple::new(py, &[idx_obj, processed_value])?; result.push(pair.unbind().into()); } let tuple = PyTuple::new(py, &result)?; Ok(tuple.unbind().into()) } else { Err(pyo3::exceptions::PyTypeError::new_err( "hash_item expects a dict or list", )) } } ormar-orm-ormar-utils-2fd6230/src/lib.rs000066400000000000000000000017611524055657400201560ustar00rootroot00000000000000mod alias_utils; mod group_related; mod hash_item; mod merge_instances; mod merge_items; mod parsers; mod translate_list; mod unique_list; use pyo3::prelude::*; /// Rust-accelerated utility functions for ormar ORM. #[pymodule] fn ormar_rust_utils(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(parsers::encode_bytes, m)?)?; m.add_function(wrap_pyfunction!(parsers::decode_bytes, m)?)?; m.add_function(wrap_pyfunction!(parsers::encode_json, m)?)?; m.add_function(wrap_pyfunction!(hash_item::hash_item, m)?)?; m.add_function(wrap_pyfunction!(translate_list::translate_list_to_dict, m)?)?; m.add_function(wrap_pyfunction!(group_related::group_related_list, m)?)?; m.add_class::()?; m.add_function(wrap_pyfunction!(merge_instances::group_by_pk, m)?)?; m.add_function(wrap_pyfunction!(merge_items::plan_merge_items_lists, m)?)?; m.add_function(wrap_pyfunction!(alias_utils::build_reverse_alias_map, m)?)?; Ok(()) } ormar-orm-ormar-utils-2fd6230/src/merge_instances.rs000066400000000000000000000012441524055657400225520ustar00rootroot00000000000000use indexmap::IndexMap; use pyo3::prelude::*; use pyo3::types::PyList; /// Groups items by PK hash, preserving insertion order. /// Returns list of index groups. #[pyfunction] pub fn group_by_pk(py: Python<'_>, pks: Vec) -> PyResult { let mut groups: IndexMap> = IndexMap::new(); for (idx, pk) in pks.iter().enumerate() { let hash = pk.bind(py).hash()? as u64; groups.entry(hash).or_default().push(idx); } let result = PyList::empty(py); for (_hash, indices) in &groups { let py_indices = PyList::new(py, indices)?; result.append(&py_indices)?; } Ok(result.unbind().into()) } ormar-orm-ormar-utils-2fd6230/src/merge_items.rs000066400000000000000000000023661524055657400217120ustar00rootroot00000000000000use pyo3::prelude::*; use std::collections::HashMap; /// Creates a merge plan for two lists of items identified by PK hashes. /// /// Returns Vec<(current_idx, Option)> where: /// - current_idx is the index in current_pks /// - other_idx is Some(index) in other_pks if a matching PK was found, None otherwise /// /// This replaces Python's O(n^2) linear scan with O(n) hash lookups. /// /// Parameters: /// - current_pks: list of PK values from the current field list /// - other_pks: list of PK values from the other list #[pyfunction] pub fn plan_merge_items_lists( py: Python<'_>, current_pks: Vec, other_pks: Vec, ) -> PyResult)>> { // Build hash map from other_pks: hash -> index let mut other_map: HashMap = HashMap::with_capacity(other_pks.len()); for (idx, pk) in other_pks.iter().enumerate() { let hash = pk.bind(py).hash()? as u64; other_map.entry(hash).or_insert(idx); } let mut plan = Vec::with_capacity(current_pks.len()); for (idx, pk) in current_pks.iter().enumerate() { let hash = pk.bind(py).hash()? as u64; let other_idx = other_map.get(&hash).copied(); plan.push((idx, other_idx)); } Ok(plan) } ormar-orm-ormar-utils-2fd6230/src/parsers.rs000066400000000000000000000102451524055657400210640ustar00rootroot00000000000000use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use base64::Engine; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyString}; /// Encode bytes to string representation. /// If represent_as_string is true, uses base64 encoding. /// Otherwise, decodes bytes as UTF-8. #[pyfunction] #[pyo3(signature = (value, represent_as_string=false))] pub fn encode_bytes( py: Python<'_>, value: &Bound<'_, PyAny>, represent_as_string: bool, ) -> PyResult { if value.is_instance_of::() { return Ok(value.clone().unbind()); } let bytes_val: &[u8] = value.downcast::()?.as_bytes(); if represent_as_string { let encoded = BASE64_STANDARD.encode(bytes_val); Ok(PyString::new(py, &encoded).into()) } else { let s = std::str::from_utf8(bytes_val) .map_err(|e| pyo3::exceptions::PyUnicodeDecodeError::new_err(e.to_string()))?; Ok(PyString::new(py, s).into()) } } /// Decode string to bytes. /// If represent_as_string is true, uses base64 decoding. /// Otherwise, encodes string as UTF-8 bytes. #[pyfunction] #[pyo3(signature = (value, represent_as_string=false))] pub fn decode_bytes( py: Python<'_>, value: &Bound<'_, PyAny>, represent_as_string: bool, ) -> PyResult { if value.is_instance_of::() { return Ok(value.clone().unbind()); } let s: &str = value.extract()?; if represent_as_string { let decoded = BASE64_STANDARD .decode(s) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; Ok(PyBytes::new(py, &decoded).into()) } else { Ok(PyBytes::new(py, s.as_bytes()).into()) } } /// Encode a value to JSON string. /// Handles datetime objects by calling .isoformat() first. #[pyfunction] pub fn encode_json(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult { let datetime_mod = py.import("datetime")?; let date_type = datetime_mod.getattr("date")?; let datetime_type = datetime_mod.getattr("datetime")?; let time_type = datetime_mod.getattr("time")?; let mut val = value.clone().unbind(); // Convert datetime types to isoformat string if value.is_instance(&datetime_type)? || value.is_instance(&date_type)? || value.is_instance(&time_type)? { val = value.call_method0("isoformat")?.unbind(); } let val_bound = val.bind(py); // If already a string, try to re-dump it (load then dump for normalization) if val_bound.is_instance_of::() { let s: String = val_bound.extract()?; // Try to parse as JSON and re-serialize for normalization match serde_json::from_str::(&s) { Ok(parsed) => { let result = serde_json::to_string(&parsed) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; return Ok(PyString::new(py, &result).into()); } Err(_) => { // Not valid JSON, return as-is return Ok(val_bound.clone().unbind()); } } } // For other types, use Python's json.dumps let json_mod = py.import("orjson").or_else(|_| py.import("json"))?; // Check if using orjson (which is always compact) or standard json let is_orjson = json_mod.getattr("__name__")?.extract::()? == "orjson"; let dumped = if is_orjson { // orjson is always compact, no need for separators parameter json_mod.call_method1("dumps", (val_bound,))? } else { // Standard json: use separators to match orjson's compact format let kwargs = PyDict::new(py); kwargs.set_item("separators", (",", ":"))?; json_mod.call_method("dumps", (val_bound,), Some(&kwargs))? }; // orjson returns bytes, json returns str if dumped.is_instance_of::() { let bytes_val: &[u8] = dumped.downcast::()?.as_bytes(); let s = std::str::from_utf8(bytes_val) .map_err(|e| pyo3::exceptions::PyUnicodeDecodeError::new_err(e.to_string()))?; Ok(PyString::new(py, s).into()) } else { Ok(dumped.unbind()) } } ormar-orm-ormar-utils-2fd6230/src/translate_list.rs000066400000000000000000000035571524055657400224450ustar00rootroot00000000000000use pyo3::prelude::*; use pyo3::types::PyDict; /// Splits the list of '__'-separated strings and converts them to a nested dict. #[pyfunction] #[pyo3(signature = (list_to_trans, default=None))] pub fn translate_list_to_dict( py: Python<'_>, list_to_trans: Vec, default: Option, ) -> PyResult { let ellipsis = py.Ellipsis(); let default_obj = default.unwrap_or_else(|| ellipsis.clone_ref(py)); let is_ellipsis = default_obj.bind(py).is(&ellipsis); let new_dict = PyDict::new(py); for path in &list_to_trans { let parts: Vec<&str> = path.split("__").collect(); let mut current_level = new_dict.clone(); let def_val = if is_ellipsis { default_obj.clone_ref(py) } else { let copy_mod = py.import("copy")?; copy_mod.call_method1("deepcopy", (&default_obj,))?.unbind() }; for (ind, part) in parts.iter().enumerate() { let is_last = ind == parts.len() - 1; let has_key = current_level.contains(*part)?; if has_key { let existing = current_level.get_item(*part)?.unwrap(); if !is_last && existing.downcast::().is_err() { let empty = PyDict::new(py); current_level.set_item(*part, &empty)?; current_level = empty; } else if is_last { break; } else { current_level = existing.downcast_into::()?; } } else if is_last { current_level.set_item(*part, &def_val)?; break; } else { let empty = PyDict::new(py); current_level.set_item(*part, &empty)?; current_level = empty; } } } Ok(new_dict.into()) } ormar-orm-ormar-utils-2fd6230/src/unique_list.rs000066400000000000000000000042321524055657400217450ustar00rootroot00000000000000use pyo3::prelude::*; use pyo3::types::PyList; use std::collections::HashSet; /// A list that prevents duplicates using hash-based O(1) lookups. #[pyclass] pub struct UniqueList { items: Vec, seen: HashSet, } #[pymethods] impl UniqueList { #[new] #[pyo3(signature = (initial=None))] fn new(py: Python<'_>, initial: Option<&Bound<'_, PyList>>) -> PyResult { let mut list = UniqueList { items: Vec::new(), seen: HashSet::new(), }; if let Some(init) = initial { for item in init.iter() { list.append(py, item.unbind())?; } } Ok(list) } fn append(&mut self, py: Python<'_>, item: PyObject) -> PyResult<()> { let hash = item.bind(py).hash()? as u64; if self.seen.insert(hash) { self.items.push(item); } Ok(()) } fn to_list(&self, py: Python<'_>) -> PyResult { let list = PyList::new(py, self.items.iter().map(|x| x.bind(py)))?; Ok(list.unbind().into()) } fn __len__(&self) -> usize { self.items.len() } fn __getitem__(&self, py: Python<'_>, idx: isize) -> PyResult { let len = self.items.len() as isize; let actual_idx = if idx < 0 { len + idx } else { idx }; if actual_idx < 0 || actual_idx >= len { return Err(pyo3::exceptions::PyIndexError::new_err( "list index out of range", )); } Ok(self.items[actual_idx as usize].clone_ref(py)) } fn __contains__(&self, _py: Python<'_>, item: &Bound<'_, PyAny>) -> PyResult { let hash = item.hash()? as u64; Ok(self.seen.contains(&hash)) } fn __iter__(&self, py: Python<'_>) -> PyResult { let list = self.to_list(py)?; list.bind(py).call_method0("__iter__").map(|x| x.unbind()) } fn __repr__(&self, py: Python<'_>) -> PyResult { let list = self.to_list(py)?; let repr = list.bind(py).repr()?; Ok(format!("UniqueList({})", repr)) } fn __bool__(&self) -> bool { !self.items.is_empty() } } ormar-orm-ormar-utils-2fd6230/tests/000077500000000000000000000000001524055657400174105ustar00rootroot00000000000000ormar-orm-ormar-utils-2fd6230/tests/test_all.py000066400000000000000000000150761524055657400216020ustar00rootroot00000000000000"""Tests for all ormar_rust_utils exported functions.""" import copy import ormar_rust_utils def test_encode_bytes_utf8(): """Test encoding bytes as UTF-8 string.""" result = ormar_rust_utils.encode_bytes(b"hello") assert result == "hello" def test_encode_bytes_already_string(): """Test that strings pass through unchanged.""" result = ormar_rust_utils.encode_bytes("hello") assert result == "hello" def test_encode_bytes_base64(): """Test encoding bytes as base64 string.""" result = ormar_rust_utils.encode_bytes(b"hello", represent_as_string=True) assert result == "aGVsbG8=" def test_decode_bytes_utf8(): """Test decoding UTF-8 string to bytes.""" result = ormar_rust_utils.decode_bytes("hello") assert result == b"hello" def test_decode_bytes_already_bytes(): """Test that bytes pass through unchanged.""" result = ormar_rust_utils.decode_bytes(b"hello") assert result == b"hello" def test_decode_bytes_base64(): """Test decoding base64 string to bytes.""" result = ormar_rust_utils.decode_bytes("aGVsbG8=", represent_as_string=True) assert result == b"hello" def test_encode_json_string(): """Test encoding a JSON string value.""" result = ormar_rust_utils.encode_json('{"key": "value"}') assert result == '{"key":"value"}' def test_encode_json_dict(): """Test encoding a dict to JSON.""" result = ormar_rust_utils.encode_json({"key": "value"}) assert result == '{"key":"value"}' def test_encode_json_non_json_string(): """Test that non-JSON strings pass through.""" result = ormar_rust_utils.encode_json("not json") assert result == "not json" def test_hash_item_dict(): """Test hashing a simple dict.""" result = ormar_rust_utils.hash_item({"b": 2, "a": 1}) # Should be a sorted tuple of (key, value) pairs assert isinstance(result, tuple) assert result == (("a", 1), ("b", 2)) def test_hash_item_nested_dict(): """Test hashing a nested dict.""" result = ormar_rust_utils.hash_item({"a": {"x": 1}}) assert isinstance(result, tuple) def test_hash_item_list(): """Test hashing a list.""" result = ormar_rust_utils.hash_item([10, 20, 30]) assert isinstance(result, tuple) assert result == ((0, 10), (1, 20), (2, 30)) def test_translate_list_to_dict_simple(): """Test translating simple list to dict.""" result = ormar_rust_utils.translate_list_to_dict(["a", "b"]) assert "a" in result assert "b" in result def test_translate_list_to_dict_nested(): """Test translating nested list with __ separator.""" result = ormar_rust_utils.translate_list_to_dict(["a__b", "a__c"]) assert "a" in result assert isinstance(result["a"], dict) assert "b" in result["a"] assert "c" in result["a"] def test_translate_list_to_dict_with_default(): """Test translate_list_to_dict with custom default.""" default = {"key": "val"} result = ormar_rust_utils.translate_list_to_dict(["x"], copy.deepcopy(default)) assert result["x"] == default def test_group_related_list_simple(): """Test grouping simple related list.""" result = ormar_rust_utils.group_related_list(["people__houses", "people__cars"]) assert "people" in result assert set(result["people"]) == {"cars", "houses"} def test_group_related_list_nested(): """Test grouping with nested relations.""" result = ormar_rust_utils.group_related_list( ["people__cars__models", "people__cars__colors"] ) assert "people" in result assert isinstance(result["people"], dict) assert "cars" in result["people"] def test_unique_list_empty(): """Test creating an empty UniqueList.""" ul = ormar_rust_utils.UniqueList() assert len(ul) == 0 def test_unique_list_append(): """Test appending to UniqueList.""" ul = ormar_rust_utils.UniqueList() ul.append(1) ul.append(2) ul.append(1) # duplicate assert len(ul) == 2 def test_unique_list_contains(): """Test membership check in UniqueList.""" ul = ormar_rust_utils.UniqueList() ul.append(42) assert 42 in ul assert 99 not in ul def test_unique_list_getitem(): """Test indexing UniqueList.""" ul = ormar_rust_utils.UniqueList() ul.append("a") ul.append("b") assert ul[0] == "a" assert ul[1] == "b" assert ul[-1] == "b" def test_unique_list_iter(): """Test iterating over UniqueList.""" ul = ormar_rust_utils.UniqueList() ul.append(1) ul.append(2) assert list(ul) == [1, 2] def test_unique_list_bool(): """Test bool conversion of UniqueList.""" ul = ormar_rust_utils.UniqueList() assert not ul ul.append(1) assert ul def test_unique_list_initial(): """Test creating UniqueList with initial values.""" ul = ormar_rust_utils.UniqueList([1, 2, 3, 2]) assert len(ul) == 3 def test_group_by_pk(): """Test grouping by primary key.""" pks = [1, 2, 1, 3, 2] result = ormar_rust_utils.group_by_pk(pks) assert len(result) == 3 # First group should be indices of pk=1 assert result[0] == [0, 2] assert result[1] == [1, 4] assert result[2] == [3] def test_plan_merge_items_lists(): """Test creating a merge plan.""" current_pks = [1, 2, 3] other_pks = [2, 4] plan = ormar_rust_utils.plan_merge_items_lists(current_pks, other_pks) assert len(plan) == 3 # pk=1 not in other assert plan[0] == (0, None) # pk=2 is at index 0 in other assert plan[1] == (1, 0) # pk=3 not in other assert plan[2] == (2, None) def test_plan_merge_items_lists_empty(): """Test merge plan with empty lists.""" plan = ormar_rust_utils.plan_merge_items_lists([], []) assert plan == [] def test_build_reverse_alias_map_basic(): """Test building reverse alias map with aliases.""" field_alias_map = {"name": "user_name", "age": "user_age"} result = ormar_rust_utils.build_reverse_alias_map(field_alias_map) # alias -> field_name assert result["user_name"] == "name" assert result["user_age"] == "age" # identity entries for field names assert result["name"] == "name" assert result["age"] == "age" def test_build_reverse_alias_map_identity(): """Test reverse alias map when alias equals field name.""" field_alias_map = {"name": "name", "age": "age"} result = ormar_rust_utils.build_reverse_alias_map(field_alias_map) assert result["name"] == "name" assert result["age"] == "age" assert len(result) == 2 def test_build_reverse_alias_map_empty(): """Test reverse alias map with empty input.""" result = ormar_rust_utils.build_reverse_alias_map({}) assert len(result) == 0