Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 48 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,55 @@ permissions:

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
- name: Run lint checks
run: make check
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]
python-version: ["3.10", "3.11", "3.12", "3.13"]
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 Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-in-project: true
- name: Cache Poetry virtualenv
uses: actions/cache@v4
with:
path: .venv
key: poetry-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
poetry-${{ runner.os }}-py${{ matrix.python-version }}-
- name: Install dependencies
run: poetry install
- name: Build with maturin
run: make build
- 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: make test
run: pytest tests/ -v

build:
name: Build wheels
name: Build wheels (${{ matrix.os }}-${{ matrix.target }})
needs: test
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ${{ matrix.os }}
Expand All @@ -79,21 +82,41 @@ jobs:
- 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 -i python3.10 python3.11 python3.12 python3.13
args: --release --out dist
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}-${{ matrix.target }}
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
needs: [build, sdist]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: pypi
Expand Down
30 changes: 13 additions & 17 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
repos:
- repo: https://github.com/rust-lang/rust-clippy
rev: v1.77.0
hooks:
- id: clippy
name: clippy
entry: cargo clippy -- -D warnings
language: system
types: [rust]
pass_filenames: false
stages: [commit]

- repo: local
hooks:
- id: cargo-fmt
name: cargo fmt
entry: cargo fmt
entry: cargo fmt --check
language: system
types: [rust]
pass_filenames: false
stages: [commit]

- id: cargo-check
name: cargo check
entry: cargo check
- id: clippy
name: clippy
entry: cargo clippy --all-targets -- -D warnings
language: system
types: [rust]
pass_filenames: false
stages: [commit]

- 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/
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ormar_rust_utils"
version = "0.1.1"
version = "0.2.0"
edition = "2021"
description = "Rust-accelerated utility functions for the ormar ORM"
license = "MIT"
Expand All @@ -10,7 +10,10 @@ name = "ormar_rust_utils"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.23.5", features = ["extension-module"] }
# 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"
16 changes: 13 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
.PHONY: fmt check test build clean
.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 -- -D warnings
cargo clippy --all-targets -- -D warnings

lint: fmt-check check
poetry run ruff check tests

test:
poetry run pytest tests/ -v
Expand All @@ -20,8 +28,10 @@ clean:

help:
@echo "Available targets:"
@echo " fmt - Format code with rustfmt"
@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"
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ormar-utils

Rust-accelerated utility functions for the [ormar](https://github.com/collerek/ormar) async ORM.
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.

Expand All @@ -21,6 +21,10 @@ pip install ormar[rust]
- 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:
Expand All @@ -40,9 +44,8 @@ All functions are exposed from the `ormar_rust_utils` module:
### Collections
- `UniqueList(initial=None)` - A list that prevents duplicates using hash-based O(1) lookups

### Row Processing
- `extract_prefixed_columns(column_mappings, selected_columns, row, column_prefix, item)` - Extract prefixed columns from a database row
- `prepare_model_to_save(new_kwargs, aliases_map, fields_to_keep)` - Consolidate column alias translation and field filtering
### 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
Expand Down
14 changes: 10 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "ormar-utils"
version = "0.1.1"
version = "0.2.0"
description = "Rust-accelerated utility functions for the ormar ORM"
readme = "README.md"
license = { text = "MIT" }
Expand All @@ -22,20 +22,22 @@ classifiers = [
"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/collerek/ormar-utils"
Repository = "https://github.com/collerek/ormar-utils"
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.1.1"
version = "0.2.0"
description = "Rust-accelerated utility functions for the ormar ORM"
authors = ["Radosław Drążkiewicz <collerek@gmail.com>"]
package-mode = false
Expand All @@ -47,6 +49,10 @@ python = "^3.10"
pytest = "^7.0"
maturin = "^1.0"
pre-commit = "^3.0"
ruff = "^0.15"

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
target-version = "py310"
50 changes: 0 additions & 50 deletions src/alias_utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use pyo3::prelude::*;
use pyo3::types::PyDict;
use std::collections::HashMap;

/// 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),
Expand All @@ -25,52 +24,3 @@ pub fn build_reverse_alias_map<'py>(
}
Ok(result)
}

/// Translate dict keys from field names to their database aliases.
/// Takes the dict to translate and a field_name->alias mapping.
/// Returns a new dict with aliased keys.
#[pyfunction]
pub fn translate_columns_to_aliases<'py>(
py: Python<'py>,
new_kwargs: &Bound<'py, PyDict>,
field_to_alias: &Bound<'py, PyDict>,
) -> PyResult<Bound<'py, PyDict>> {
let result = PyDict::new(py);
// Pre-extract the mapping into a Rust HashMap for fast lookup
let mut alias_map: HashMap<String, String> = HashMap::new();
for (k, v) in field_to_alias.iter() {
let key: String = k.extract()?;
let val: String = v.extract()?;
alias_map.insert(key, val);
}

for (key, value) in new_kwargs.iter() {
let field_name: String = key.extract()?;
if let Some(alias) = alias_map.get(&field_name) {
result.set_item(alias, value)?;
} else {
result.set_item(key, value)?;
}
}
Ok(result)
}

/// Translate dict keys from database aliases to field names.
/// Takes the dict to translate and an alias->field_name mapping.
/// Returns a new dict with field name keys.
#[pyfunction]
pub fn translate_aliases_to_columns<'py>(
py: Python<'py>,
new_kwargs: &Bound<'py, PyDict>,
alias_to_field: &Bound<'py, PyDict>,
) -> PyResult<Bound<'py, PyDict>> {
let result = PyDict::new(py);
for (key, value) in new_kwargs.iter() {
if let Some(field_name) = alias_to_field.get_item(&key)? {
result.set_item(field_name, value)?;
} else {
result.set_item(key, value)?;
}
}
Ok(result)
}
27 changes: 0 additions & 27 deletions src/extract_columns.rs

This file was deleted.

Loading
Loading