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
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,57 @@ jobs:
- name: cargo test --all-targets
run: cargo test --all-targets --features "tcp,uds,stdio,websocket,http,quic,webtransport,mmap,csv,json,parquet,zstd,snappy,protocol,msgpack,protobuf,tls"

test-native-platforms:
name: Native Rust and Python tests (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux-x86_64
runner: ubuntu-latest
unix-features: ",uds,mmap"
- platform: macos-x86_64
runner: macos-15-intel
unix-features: ",uds,mmap"
- platform: macos-aarch64
runner: macos-14
unix-features: ",uds,mmap"
- platform: windows-x86_64
runner: windows-latest
unix-features: ""
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
rust
python
- name: Test native Rust library
working-directory: rust
env:
FEATURES: tcp,tls,http,websocket,quic,webtransport,stdio,csv,json,parquet,zstd,snappy,protocol,msgpack,protobuf,datetime,decimal,extended_categorical,extended_numeric_types${{ matrix.unix-features }}
run: cargo test --lib --features "$FEATURES"
- name: Install Python build and test dependencies
run: python -m pip install maturin pytest pyarrow polars duckdb
- name: Build Python wheel
run: python -m maturin build -m python/Cargo.toml --out dist --interpreter python
- name: Install Python wheel
run: python -m pip install dist/*.whl
- name: Test Python API (Unix)
if: runner.os != 'Windows'
run: python -m pytest python/tests/ -v --basetemp=/tmp/ls-py-tests
- name: Test Python API (Windows)
if: runner.os == 'Windows'
run: python -m pytest python/tests/ -v

doc:
name: cargo doc
runs-on: ubuntu-latest
Expand Down
80 changes: 73 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ name: Release
# workflow_dispatch runs the same build and audit, then offers a crate dry-run
# and a TestPyPI upload for exercising the pipeline without a real release.
#
# Wheels cover Linux (x86_64, aarch64) and macOS (x86_64, aarch64). Windows is
# not built: the uds and mmap paths are POSIX-only, so a Windows port is tracked
# separately.
# Wheels cover Linux and macOS (x86_64, aarch64), plus Windows x86_64.
# Each wheel is tested natively before publication. Windows excludes mmap
# and Unix-domain sockets; Linux and macOS retain both.

on:
push:
Expand Down Expand Up @@ -64,10 +64,14 @@ jobs:

wheels-macos:
name: Wheels (macos ${{ matrix.target }})
runs-on: macos-14
runs-on: ${{ matrix.runner }}
strategy:
matrix:
target: [x86_64, aarch64]
include:
- target: x86_64
runner: macos-15-intel
- target: aarch64
runner: macos-14
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
Expand All @@ -80,6 +84,24 @@ jobs:
name: wheels-macos-${{ matrix.target }}
path: dist

wheels-windows:
name: Wheels (windows x86_64)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: PyO3/maturin-action@v1
with:
target: x86_64
rust-toolchain: nightly
args: --release --out dist ${{ env.MANIFEST }}
- uses: actions/upload-artifact@v4
with:
name: wheels-windows-x86_64
path: dist

sdist:
name: Source distribution
runs-on: ubuntu-latest
Expand Down Expand Up @@ -117,6 +139,50 @@ jobs:
fi
echo "binary audit passed"

# Install every release wheel on its native OS and architecture.
test-python:
name: Python tests (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
needs: [wheels-linux, wheels-macos, wheels-windows]
strategy:
fail-fast: false
matrix:
include:
- platform: linux-x86_64
runner: ubuntu-latest
- platform: linux-aarch64
runner: ubuntu-24.04-arm
- platform: macos-x86_64
runner: macos-15-intel
- platform: macos-aarch64
runner: macos-14
- platform: windows-x86_64
runner: windows-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/download-artifact@v4
with:
name: wheels-${{ matrix.platform }}
path: dist
- name: Install wheel and test dependencies
run: |
set -euo pipefail
python -m pip install dist/*.whl
python -m pip install pytest pyarrow polars duckdb
- name: Run Python tests (Unix)
if: runner.os != 'Windows'
# Keep socket paths below macOS's Unix socket path-length limit.
run: python -m pytest python/tests/ -v --basetemp=/tmp/ls-py-tests
- name: Run Python tests (Windows)
if: runner.os == 'Windows'
run: python -m pytest python/tests/ -v

# Tag push -> crates.io, gated by the `crates-io` environment approval.
publish-crate:
name: Publish crate to crates.io
Expand Down Expand Up @@ -147,7 +213,7 @@ jobs:
name: Publish lightstream-io to PyPI
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs: [wheels-linux, wheels-macos, sdist, audit]
needs: [wheels-linux, wheels-macos, wheels-windows, sdist, audit, test-python]
environment:
name: pypi
url: https://pypi.org/project/lightstream-io/
Expand Down Expand Up @@ -181,7 +247,7 @@ jobs:
name: Publish to TestPyPI
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
needs: [wheels-linux, wheels-macos, sdist, audit]
needs: [wheels-linux, wheels-macos, wheels-windows, sdist, audit, test-python]
environment:
name: testpypi
url: https://test.pypi.org/project/lightstream-io/
Expand Down
6 changes: 5 additions & 1 deletion python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ name = "lightstream_py"
crate-type = ["cdylib", "rlib"]

[dependencies]
lightstream = { version = "0.7", path = "../rust", features = ["csv", "datetime", "decimal", "extended_categorical", "extended_numeric_types", "json", "mmap", "http", "parquet", "protocol", "quic", "snappy", "stdio", "tcp", "tls", "uds", "webtransport", "websocket", "zstd"] }
lightstream = { version = "0.7", path = "../rust", features = ["csv", "datetime", "decimal", "extended_categorical", "extended_numeric_types", "json", "http", "parquet", "protocol", "quic", "snappy", "stdio", "tcp", "tls", "webtransport", "websocket", "zstd"] }
# The categorical and numeric feature set mirrors the minarrow-py build.
# minarrow-pyo3's dictionary-index conversion needs the extended features,
# and they flow through lightstream's flags so its match arms gate in step
Expand All @@ -38,6 +38,10 @@ tokio = { version = "1", features = ["fs", "io-std", "net", "rt-multi-thread", "
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
wtransport = "0.7"

# Unix-only transports stay out of Windows wheels.
[target.'cfg(unix)'.dependencies]
lightstream = { version = "0.7", path = "../rust", features = ["mmap", "uds"] }

[features]
default = []
extension-module = ["pyo3/extension-module"]
Expand Down
3 changes: 3 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Move Arrow tables between processes, services and storage from Python without ad
pip install lightstream-io
```

Unix-domain sockets and memory-mapped reads are available on Linux and macOS.
Windows uses buffered file reads; `uds://` and explicit `mmap=True` requests are unsupported.

## Usage

Everything is `read` or `write`.
Expand Down
6 changes: 6 additions & 0 deletions python/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use lightstream::models::readers::chunked::csv::ChunkedCsvReader;
use lightstream::models::readers::chunked::parquet::ChunkedParquetReader;
use lightstream::models::readers::csv::CsvReader;
use lightstream::models::readers::ipc::file_table::FileTableReader;
#[cfg(unix)]
use lightstream::models::readers::ipc::mmap_table::MmapTableReader;
use lightstream::models::readers::http::HttpTableReader;
use lightstream::models::readers::parallel::tcp::TcpParallelTableReader;
Expand All @@ -35,6 +36,7 @@ use lightstream::models::readers::parquet::load_parquet_table;
use lightstream::models::readers::quic::QuicTableReader;
use lightstream::models::readers::stdio::StdinTableReader;
use lightstream::models::readers::tcp::TcpTableReader;
#[cfg(unix)]
use lightstream::models::readers::uds::UdsTableReader;
use lightstream::models::readers::websocket::WebSocketTableReader;
use lightstream::models::readers::webtransport::WebTransportTableReader;
Expand Down Expand Up @@ -79,6 +81,7 @@ pub enum FileIO {
reader: FileTableReader,
cursor: usize,
},
#[cfg(unix)]
IpcMmap {
reader: MmapTableReader,
cursor: usize,
Expand Down Expand Up @@ -118,6 +121,7 @@ pub enum ArrowIO {
TcpParallel(TcpParallelTableReader),
Ws(WebSocketTableReader),
Http(HttpTableReader),
#[cfg(unix)]
Uds(UdsTableReader),
Quic(QuicTableReader),
Wt(WebTransportTableReader),
Expand All @@ -139,6 +143,7 @@ impl ArrowIO {
}
ArrowIO::Ws(reader) => runtime().block_on(reader.read_next()),
ArrowIO::Http(reader) => runtime().block_on(reader.read_next()),
#[cfg(unix)]
ArrowIO::Uds(reader) => runtime().block_on(reader.read_next()),
ArrowIO::Quic(reader) => runtime().block_on(reader.read_next()),
ArrowIO::Wt(reader) => runtime().block_on(reader.read_next()),
Expand Down Expand Up @@ -227,6 +232,7 @@ impl FileIO {
*cursor += 1;
Ok(Some(table))
}
#[cfg(unix)]
FileIO::IpcMmap { reader, cursor } => {
if *cursor >= reader.num_batches() {
return Ok(None);
Expand Down
12 changes: 10 additions & 2 deletions python/src/listeners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@
//! and later accepting calls on the endpoint reuse that identity.

use std::collections::HashMap;
#[cfg(unix)]
use std::fs;
use std::io;
use std::net;
use std::net::ToSocketAddrs;
use std::path::{Path, PathBuf};
use std::path::Path;
#[cfg(unix)]
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};

use pyo3::PyResult;
use tokio::net::{TcpListener, UnixListener};
use tokio::net::TcpListener;
#[cfg(unix)]
use tokio::net::UnixListener;
use wtransport::endpoint::endpoint_side::Server;

use crate::errors::{TransportError, to_py_err};
Expand All @@ -43,13 +48,15 @@ enum ListenerKey {
Wss(String),
Http(String),
Https(String),
#[cfg(unix)]
Uds(PathBuf),
Quic(String),
Wt(String),
}

enum BoundListener {
Tcp(Arc<TcpListener>),
#[cfg(unix)]
Uds(Arc<UnixListener>),
Quic(Arc<quinn::Endpoint>),
Wt(Arc<wtransport::Endpoint<Server>>),
Expand Down Expand Up @@ -100,6 +107,7 @@ pub fn https(url: &str) -> PyResult<Arc<TcpListener>> {
/// it on the first call. A stale socket file left by an earlier
/// process is removed before the bind. Call within the runtime
/// context.
#[cfg(unix)]
pub fn uds(path: &Path) -> PyResult<Arc<UnixListener>> {
let mut map = registry().lock().expect("listener registry lock");
if let Some(BoundListener::Uds(listener)) = map.get(&ListenerKey::Uds(path.to_path_buf())) {
Expand Down
5 changes: 5 additions & 0 deletions python/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use lightstream::models::writers::parquet::write_parquet_table;
use lightstream::models::writers::http::HttpTableWriter;
use lightstream::models::writers::stdio::StdoutTableWriter;
use lightstream::models::writers::tcp::TcpTableWriter;
#[cfg(unix)]
use lightstream::models::writers::uds::UdsTableWriter;
use lightstream::models::writers::websocket::WebSocketTableWriter;
use lightstream::traits::parallel_transport_writer::ParallelTransportWriter;
Expand All @@ -48,6 +49,7 @@ use pyo3::exceptions::PyValueError;
use tokio::io::{AsyncWrite, ReadHalf, WriteHalf};
use tokio::net::TcpStream;
use tokio::net::tcp::OwnedWriteHalf as TcpOwnedWriteHalf;
#[cfg(unix)]
use tokio::net::unix::OwnedWriteHalf as UdsOwnedWriteHalf;
use tokio_rustls::server::TlsStream as ServerTlsStream;

Expand Down Expand Up @@ -188,6 +190,7 @@ pub enum ArrowIO {
compression: Option<Compression>,
writer: Option<HttpTableWriter>,
},
#[cfg(unix)]
Uds {
link: Link<PathBuf, UdsOwnedWriteHalf>,
compression: Option<Compression>,
Expand Down Expand Up @@ -402,6 +405,7 @@ impl ArrowIO {
.write_table(table.clone())
.await
}
#[cfg(unix)]
ArrowIO::Uds {
link,
compression,
Expand Down Expand Up @@ -533,6 +537,7 @@ impl ArrowIO {
Some(writer) => writer.finish().await,
None => Ok(()),
},
#[cfg(unix)]
ArrowIO::Uds { writer, .. } => match writer.as_mut() {
Some(writer) => writer.finish().await,
None => Ok(()),
Expand Down
Loading
Loading