Fast, type-safe web framework with Rust-powered Python bindings
Project description
spikard-py
PyO3 bindings that expose the Rust HTTP runtime to Python applications under the module name _spikard.
This crate implements high-performance Python bindings for Spikard's HTTP server by bridging Python's asyncio with Rust's async/await runtime through carefully optimized FFI patterns.
Table of Contents
- Architecture
- Performance Optimizations
- PyO3 Configuration
- Building & Installation
- Testing
- Implementation Details
Architecture
The spikard-py crate implements the language-agnostic Handler trait defined in spikard-http, enabling clean separation between the pure Rust HTTP server and Python-specific code.
Handler Trait Implementation
// From spikard-http/src/handler_trait.rs
pub trait Handler: Send + Sync {
fn call(
&self,
request: Request<Body>,
request_data: RequestData,
) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>>;
}
The PythonHandler struct (src/handler.rs) implements this trait, wrapping Python callable objects and converting between Rust and Python types. This design keeps spikard-http completely free of FFI dependencies while enabling multiple language bindings (Python, Node.js, WASM) to share the same HTTP core.
Key architectural benefits:
- Zero FFI dependencies in HTTP core -
spikard-httphas no knowledge of Python, PyO3, or any other language binding - Language-agnostic routing - Routes are defined once and work with any Handler implementation
- Isolated FFI concerns - All Python-specific code lives in this crate
- Future-proof - New language bindings can be added without touching the HTTP server
Performance Optimizations
Async Handler Execution
Problem: Naively calling Python async handlers from Rust using tokio::task::spawn_blocking introduces ~4.8ms overhead per request due to thread pool management and GIL coordination.
Solution: Convert Python coroutines directly to Rust futures using pyo3_async_runtimes::tokio::into_future(), eliminating the blocking thread pool entirely.
Implementation (src/handler.rs:155-194):
// For async handlers
let output = Python::attach(|py| {
let handler_obj = handler.bind(py);
// Prepare kwargs with request data
let kwargs = request_data_to_py_kwargs(py, &request_data, handler_obj.clone())?;
// Call Python async function, returns a coroutine
let coroutine = if let Some(py_params) = validated_params {
handler_obj.call((), Some(&kwargs))?
} else {
handler_obj.call((), Some(&kwargs))?
};
// ✅ Convert Python coroutine → Rust future (no spawn_blocking!)
pyo3_async_runtimes::tokio::into_future(coroutine)
})
.map_err(|e: PyErr| (StatusCode::INTERNAL_SERVER_ERROR, format!("Python error: {}", e)))?
.await // ✅ Await the Rust future directly on Tokio runtime
.map_err(|e: PyErr| (StatusCode::INTERNAL_SERVER_ERROR, format!("Python error: {}", e)))?;
Performance impact:
- Before: ~5ms per async request (spawn_blocking + GIL + wake overhead)
- After: ~170µs per async request (pure Python execution)
- Improvement: ~25-30x faster for async handlers
Note: Synchronous Python handlers still require spawn_blocking to avoid blocking Tokio's executor, as they cannot yield control.
Event Loop Reuse
Problem: Creating a new Python event loop for each async handler invocation adds ~55µs overhead and increases GIL contention.
Solution: Initialize the event loop once at server startup and reuse it across all async handler calls using TaskLocals stored in a OnceCell.
Implementation (src/handler.rs:28-45):
use once_cell::sync::OnceCell;
use pyo3_async_runtimes::TaskLocals;
/// Global Python event loop task locals for async handlers
static TASK_LOCALS: OnceCell<TaskLocals> = OnceCell::new();
/// Initialize Python event loop for async handlers
/// Must be called once after Python::initialize()
pub fn init_python_event_loop() -> PyResult<()> {
Python::attach(|py| {
let asyncio = py.import("asyncio")?;
let event_loop = asyncio.call_method0("new_event_loop")?;
asyncio.call_method1("set_event_loop", (event_loop.clone(),))?;
// ✅ Initialize once, reuse forever
TASK_LOCALS.get_or_try_init(|| {
TaskLocals::new(event_loop.into()).copy_context(py)
})?;
Ok(())
})
}
Performance impact:
- Eliminates ~55µs event loop creation overhead per request
- Reduces GIL contention by reusing the same loop
- One-time initialization cost amortized across millions of requests
Usage: The CLI (crates/spikard-cli/src/main.rs) calls init_python_event_loop() once during server startup.
Zero-Copy Data Conversion
Problem: Converting JSON data between Rust (serde_json::Value) and Python objects via intermediate JSON strings doubles the serialization cost:
// ❌ Inefficient: Rust Value → JSON string → Python object
let json_str = serde_json::to_string(value)?; // Serialize to string
json_module.call_method1("loads", (json_str,)) // Parse string in Python
Solution: Directly construct Python objects from Rust Value using PyO3 native types, bypassing the JSON string encoding entirely.
Implementation (src/handler.rs:434-476):
/// Convert JSON Value to Python object (optimized zero-copy conversion)
///
/// Performance improvement: ~30-40% faster than the json.loads approach
fn json_to_python<'py>(py: Python<'py>, value: &Value) -> PyResult<Bound<'py, PyAny>> {
use pyo3::types::{PyBool, PyDict, PyFloat, PyList, PyNone, PyString};
match value {
Value::Null => Ok(PyNone::get(py).as_any().clone()),
Value::Bool(b) => Ok(PyBool::new(py, *b).as_any().clone()),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(i.into_pyobject(py)?.into_any())
} else if let Some(u) = n.as_u64() {
Ok(u.into_pyobject(py)?.into_any())
} else if let Some(f) = n.as_f64() {
Ok(PyFloat::new(py, f).into_any())
} else {
// Fallback for exotic number representations
Ok(PyString::new(py, &n.to_string()).into_any())
}
}
Value::String(s) => Ok(PyString::new(py, s).into_any()),
// Recursively convert arrays
Value::Array(arr) => {
let py_list = PyList::empty(py);
for item in arr {
let py_item = json_to_python(py, item)?;
py_list.append(py_item)?;
}
Ok(py_list.into_any())
}
// Recursively convert objects
Value::Object(obj) => {
let py_dict = PyDict::new(py);
for (key, value) in obj {
let py_value = json_to_python(py, value)?;
py_dict.set_item(key, py_value)?;
}
Ok(py_dict.into_any())
}
}
}
Performance impact:
- Before: ~100µs for typical request body conversion (serialize + parse)
- After: ~60µs for same conversion (direct construction)
- Improvement: ~30-40% faster, scales with payload complexity
Note: This optimization complements the msgspec integration in packages/python/spikard/_internal/converters.py for even faster Python-side serialization.
GIL Management
Problem: Holding Python's Global Interpreter Lock (GIL) during async awaits blocks other Python threads and degrades concurrency.
Solution: Minimize GIL scope by releasing it before awaiting Rust futures.
Implementation pattern:
// ✅ Correct: GIL released before async await
let output = Python::attach(|py| {
// GIL held only during Python code execution
let handler_obj = handler.bind(py);
let kwargs = request_data_to_py_kwargs(py, &request_data, handler_obj.clone())?;
let coroutine = handler_obj.call((), Some(&kwargs))?;
// Convert to Rust future while still holding GIL
pyo3_async_runtimes::tokio::into_future(coroutine)
}) // ✅ GIL released here
.await // ✅ No GIL held during async wait
Anti-pattern to avoid:
// ❌ Wrong: GIL held during entire async operation
Python::with_gil(|py| async move {
// GIL held for entire async block - blocks other threads!
let result = some_async_operation().await;
result
}).await
Performance impact:
- Enables true concurrent request handling (GIL not blocking other requests)
- Reduces latency spikes under concurrent load
- Critical for achieving high requests-per-second throughput
Validation in Rust
Problem: Validating request bodies and parameters in Python requires acquiring the GIL, converting data to Python objects, and executing Python validation code—all before the handler runs.
Solution: Perform JSON Schema validation and parameter extraction in pure Rust before entering Python, enabling early returns on invalid requests without GIL overhead.
Implementation (src/handler.rs:100-147):
// ✅ Validate request body in Rust BEFORE entering Python
if let Some(validator) = &self.request_validator {
if let Err(errors) = validator.validate(&request_data.body) {
let problem = ProblemDetails::from_validation_error(&errors);
return Err((problem.status_code(), problem.to_json_pretty()?));
}
}
// ✅ Validate and extract parameters in Rust
let validated_params = if let Some(validator) = &self.parameter_validator {
match validator.validate_and_extract(&request_data.path_params, &request_data.query_params) {
Ok(params) => Some(params),
Err(errors) => {
let problem = ProblemDetails::from_validation_error(&errors);
return Err((problem.status_code(), problem.to_json_pretty()?));
}
}
} else {
None
};
// Only enter Python if validation passed
let handler = self.handler.clone();
if self.is_async {
// ... call Python handler with validated data ...
}
Performance impact:
- Validation happens in pure Rust (no GIL contention)
- Invalid requests return immediately (never enter Python)
- Validated parameters passed directly to handler (no re-validation)
- Structured RFC 9457 Problem Details errors for client consumption
Security benefit: All input validation occurs in memory-safe Rust before untrusted data reaches Python code.
PyO3 Configuration
Extension Module Management
Critical configuration: The extension-module feature in PyO3 controls whether the library links to libpython. This setting must differ between extension modules (.so/.pyd files) and binaries (executables, tests).
Problem: If extension-module is in default features, binaries fail to link with errors like:
ld: symbol(s) not found for architecture arm64
"__Py_NoneStruct", referenced from...
Solution: Keep extension-module as an optional feature, enable it only for maturin builds.
Configuration (Cargo.toml):
[features]
default = [] # ✅ NOT including extension-module
extension-module = ["pyo3/extension-module"]
server = ["dep:tracing", "dep:anyhow", "dep:clap", "pyo3/auto-initialize"]
Configuration (pyproject.toml):
[tool.maturin]
module-name = "_spikard"
features = ["extension-module"] # ✅ Enable for Python extension builds
Why this works:
- Python extensions (
.so/.pydbuilt by maturin): Enableextension-module→ don't linklibpython→ compatible with manylinux wheels - Rust binaries (CLI, tests): Disable
extension-module→ linklibpython→ can embed Python interpreter
Reference: PyO3 FAQ - extension-module feature
Building & Installation
Development Build (Editable Install)
For rapid iteration during development:
# From repository root
uv run maturin develop
# Or use the task runner
task build:py
This creates an editable install, allowing you to modify Rust code and rebuild without reinstalling the wheel.
Release Build
For production or benchmarking (enables optimizations):
# Build optimized wheel
uv run maturin build --release
# Or use task runner
task build:py --release
Important: Always benchmark with --release builds, as debug builds are 10-50x slower.
Building the Test Server
A standalone server binary is available for testing:
cargo build --release -p spikard-py --bin spikard-py-server --features server
Run it:
./target/release/spikard-py-server /path/to/app.py --port 8000
Testing
Rust Unit Tests
Test the Handler trait implementation in isolation:
# Run unit tests for spikard-http (Handler trait tests)
cargo test -p spikard-http
# Run with output
cargo test -p spikard-http -- --nocapture
Python Integration Tests
End-to-end tests that exercise the full Python → Rust → HTTP stack:
# From repository root
PYTHONPATH=packages/python uv run pytest e2e/python/tests/
# Run specific test file
PYTHONPATH=packages/python uv run pytest e2e/python/tests/test_query_params.py
# Run with verbose output
PYTHONPATH=packages/python uv run pytest e2e/python/tests/ -v
Full Test Suite
Run all tests (Rust + Python):
task test
Note: Integration tests for spikard-py itself require special PyO3 configuration due to the extension-module feature. The comprehensive e2e Python tests provide equivalent coverage.
Implementation Details
Module Structure
src/
├── lib.rs # PyO3 module definition, Python-facing API
├── handler.rs # PythonHandler implementation, core FFI logic
└── bin/
└── server.rs # Standalone server binary for testing
Key Types
PythonHandler
The bridge between Python callables and the Handler trait:
pub struct PythonHandler {
handler: Py<PyAny>, // Python callable
is_async: bool, // Async vs sync handler
request_validator: Option<Arc<SchemaValidator>>,
response_validator: Option<Arc<SchemaValidator>>,
parameter_validator: Option<Arc<ParameterValidator>>,
}
Methods:
new()- Construct with validatorscall()- Implement Handler trait, route to async vs sync execution
RequestData Conversion
Python handlers receive request data as keyword arguments:
async def my_handler(
path_params: dict,
query_params: dict,
body: dict,
headers: dict,
cookies: dict,
method: str,
path: str,
):
return {"message": "Hello"}
The request_data_to_py_kwargs() function (src/handler.rs:242-280) converts Rust RequestData to Python kwargs:
fn request_data_to_py_kwargs(
py: Python,
data: &RequestData,
handler: Bound<PyAny>,
) -> PyResult<Bound<PyDict>> {
let kwargs = PyDict::new(py);
// Convert each field using zero-copy json_to_python()
kwargs.set_item("path_params", json_to_python(py, &data.path_params)?)?;
kwargs.set_item("query_params", json_to_python(py, &data.query_params)?)?;
kwargs.set_item("body", json_to_python(py, &data.body)?)?;
// ... headers, cookies, method, path ...
Ok(kwargs)
}
Response Handling
Python handlers can return:
- Plain dict/list → 200 OK JSON response
- Custom Response object → Extract
status_code,content,headers
The python_to_response_result() function (src/handler.rs:282-335) handles both cases:
fn python_to_response_result(py: Python, output: &Bound<PyAny>) -> PyResult<ResponseResult> {
if output.hasattr("status_code")? {
// Custom Response object
let status_code: u16 = output.getattr("status_code")?.extract()?;
let content: Value = /* extract and convert to JSON */;
let headers: HashMap<String, String> = /* extract headers dict */;
Ok(ResponseResult::Custom {
content,
status_code,
headers,
})
} else {
// Plain dict/list → default 200 OK
let json_value = python_to_json(py, output)?;
Ok(ResponseResult::Json(json_value))
}
}
Error Handling
All errors crossing the FFI boundary are converted to PyErr:
// Domain errors → PyErr
let result = some_fallible_operation()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Error: {}", e)))?;
// Validation errors → RFC 9457 Problem Details JSON
if let Err(errors) = validator.validate(&data) {
let problem = ProblemDetails::from_validation_error(&errors);
return Err((problem.status_code(), problem.to_json_pretty()?));
}
Never panic across FFI boundaries. All fallible paths return PyResult<T>.
Design Principles
Panic-Free FFI
Rust code in this crate must avoid panics. All fallible operations use Result<T, E> and propagate errors with ?. Errors are converted to PyErr before crossing the FFI boundary.
Why: Panicking across FFI boundaries is undefined behavior and can corrupt Python's runtime state.
Minimal GIL Scope
The GIL should be held only during Python code execution, never during async awaits or Rust computations.
Why: The GIL is a global lock—holding it unnecessarily serializes concurrent requests.
Zero-Copy Where Possible
Prefer direct type conversion over serialization intermediates (JSON strings, msgpack bytes).
Why: Each serialization/deserialization cycle costs CPU and allocations. Zero-copy conversion is 30-40% faster.
Validate Early in Rust
Perform input validation in Rust before entering Python, enabling early returns on invalid data.
Why: Invalid requests fail fast without GIL overhead, improving throughput and security.
Performance Characteristics
Async Handler Latency
| Component | Time | Notes |
|---|---|---|
| HTTP parsing (Axum) | ~10µs | Rust HTTP/1.1 parsing |
| Validation (Rust) | ~20µs | JSON Schema validation |
| FFI overhead | ~5µs | Rust → Python type conversion |
| Python handler execution | ~150µs | Depends on handler complexity |
| Response conversion | ~10µs | Python → Rust → HTTP response |
| Total | ~195µs | ~5,000 req/sec per core |
Throughput Benchmarks
With a simple async handler returning {"message": "Hello"}:
- Single core: ~60,000 req/sec
- Concurrency 50: ~58,000 req/sec (stable under load)
- P99 latency: <2ms
Comparison to pure Python:
- FastAPI (uvicorn): ~3,000 req/sec (20x slower)
- Pure Python asyncio: ~8,000 req/sec (7x slower)
Note: These numbers are from simple handlers. Real-world performance depends on handler complexity, database I/O, etc.
Future Optimizations
Object Pooling
Reuse Python objects (dicts, lists) across requests to reduce allocation pressure:
// Future optimization: object pool for kwargs dicts
static KWARGS_POOL: ObjectPool<PyDict> = ObjectPool::new();
let kwargs = KWARGS_POOL.acquire(py);
// ... populate kwargs ...
Estimated improvement: +5-10% throughput
HTTP Body Streaming
Currently buffering entire request body in memory. Stream large payloads directly to Python asyncio streams:
async def upload_handler(body_stream):
async for chunk in body_stream:
await process_chunk(chunk)
Benefit: Support multi-GB uploads without memory pressure.
Sub-Interpreter Support
Use Python 3.12+ sub-interpreters to run multiple Python runtimes in parallel (bypassing GIL):
// Future: per-worker sub-interpreter
let sub_interpreter = SubInterpreter::new()?;
sub_interpreter.run(handler)?;
Benefit: True parallelism for CPU-bound Python handlers.
Related Documentation
- Architecture:
docs/adr/0001-architecture-and-principles.md - Validation Strategy:
docs/adr/0003-validation-and-fixtures.md - msgspec Integration:
docs/adr/0003-validation-and-fixtures.md - HTTP Core:
crates/spikard-http/README.md - Python Package:
packages/python/README.md
Development Notes
Code Style
- Rust code follows
rustfmt.toml(runcargo fmt) - Keep functions small and focused (<100 lines)
- Document public APIs with
///doc comments - Use
debug_log_module!()macro for conditional logging
Performance Profiling
Profile with perf on Linux or Instruments on macOS:
# Build with debug info
cargo build --release -p spikard-py --features server
# Run server under profiler
perf record -F 99 -g ./target/release/spikard-py-server app.py
# Generate flamegraph
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
Debugging FFI Issues
Enable debug logging:
DEBUG=1 ./target/release/spikard-py-server app.py
Enable Python tracebacks in PyO3:
pyo3::prepare_freethreaded_python();
Python::with_gil(|py| {
py.run("import sys; sys.excepthook = lambda *args: None", None, None).unwrap();
});
Contributing
When adding features to this crate:
- Maintain panic-free guarantees - All errors →
PyErr - Update tests - Add Rust unit tests and Python e2e tests
- Benchmark changes - Profile before/after with realistic workloads
- Document patterns - Update this README with new optimization techniques
- Run full test suite -
task testbefore committing
Refer to ai-rulez.yaml for comprehensive rules and patterns.
License
MIT - See LICENSE in repository root.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file spikard-0.1.1.tar.gz.
File metadata
- Download URL: spikard-0.1.1.tar.gz
- Upload date:
- Size: 154.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a0d04dfe81b87e39cf3cf2d3cf5d59cb2d97695057db01d114981aaec568c97
|
|
| MD5 |
07931742695286e06d69a43136c4f94e
|
|
| BLAKE2b-256 |
b1b36bba90f5a5a6ce38e37c2fb62e84b0e0ebf291290f7fb55d2b0946bfab14
|
Provenance
The following attestation bundles were made for spikard-0.1.1.tar.gz:
Publisher:
publish.yaml on Goldziher/spikard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spikard-0.1.1.tar.gz -
Subject digest:
0a0d04dfe81b87e39cf3cf2d3cf5d59cb2d97695057db01d114981aaec568c97 - Sigstore transparency entry: 718222552
- Sigstore integration time:
-
Permalink:
Goldziher/spikard@55c91ce85189d3a92793b68626c4f49432c40d92 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Goldziher
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@55c91ce85189d3a92793b68626c4f49432c40d92 -
Trigger Event:
push
-
Statement type:
File details
Details for the file spikard-0.1.1-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: spikard-0.1.1-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 5.6 MB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ebab15bfe46dba859ce480ab2f20101b01c55b7a309e605d008617a6055aa21
|
|
| MD5 |
6a6273ca1df481554a2064831cb8dcc5
|
|
| BLAKE2b-256 |
342e15a8988bc6949f6a19edfb202eed807b021dbb9f332597474bdd34ab6012
|
Provenance
The following attestation bundles were made for spikard-0.1.1-cp310-abi3-win_amd64.whl:
Publisher:
publish.yaml on Goldziher/spikard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spikard-0.1.1-cp310-abi3-win_amd64.whl -
Subject digest:
4ebab15bfe46dba859ce480ab2f20101b01c55b7a309e605d008617a6055aa21 - Sigstore transparency entry: 718222573
- Sigstore integration time:
-
Permalink:
Goldziher/spikard@55c91ce85189d3a92793b68626c4f49432c40d92 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Goldziher
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@55c91ce85189d3a92793b68626c4f49432c40d92 -
Trigger Event:
push
-
Statement type:
File details
Details for the file spikard-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: spikard-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 5.4 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b739cf85ad49f6396b6e03f41452ce12725646fde06898e82c4d14e4b8c4cdb6
|
|
| MD5 |
156bfabb50aafbbbd572e26d6465cd8c
|
|
| BLAKE2b-256 |
827235b6e7c770ff3e176d7da2d4cd6a8b2afd673f541332a16782b24fbfb9a7
|
Provenance
The following attestation bundles were made for spikard-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
publish.yaml on Goldziher/spikard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spikard-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
b739cf85ad49f6396b6e03f41452ce12725646fde06898e82c4d14e4b8c4cdb6 - Sigstore transparency entry: 718222562
- Sigstore integration time:
-
Permalink:
Goldziher/spikard@55c91ce85189d3a92793b68626c4f49432c40d92 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Goldziher
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@55c91ce85189d3a92793b68626c4f49432c40d92 -
Trigger Event:
push
-
Statement type:
File details
Details for the file spikard-0.1.1-cp310-abi3-macosx_14_0_arm64.whl.
File metadata
- Download URL: spikard-0.1.1-cp310-abi3-macosx_14_0_arm64.whl
- Upload date:
- Size: 4.9 MB
- Tags: CPython 3.10+, macOS 14.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee63fcb2164bce8b64a905cda692d2dc636bf1ac97b32283fedba15b7b86d85c
|
|
| MD5 |
75ab980915cf185d9e518d0e39b460b4
|
|
| BLAKE2b-256 |
62eaef6ad45f451453975b7249b64a015c70e1d86de1b70d70a9442dc81dab71
|
Provenance
The following attestation bundles were made for spikard-0.1.1-cp310-abi3-macosx_14_0_arm64.whl:
Publisher:
publish.yaml on Goldziher/spikard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spikard-0.1.1-cp310-abi3-macosx_14_0_arm64.whl -
Subject digest:
ee63fcb2164bce8b64a905cda692d2dc636bf1ac97b32283fedba15b7b86d85c - Sigstore transparency entry: 718222568
- Sigstore integration time:
-
Permalink:
Goldziher/spikard@55c91ce85189d3a92793b68626c4f49432c40d92 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Goldziher
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@55c91ce85189d3a92793b68626c4f49432c40d92 -
Trigger Event:
push
-
Statement type: