From 41440502f7ab63af3a7e91ae8154f5beb1fc8799 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 16 Mar 2026 18:19:43 -0700 Subject: [PATCH 1/2] Update PyO3 from 0.24 to 0.28.2 for Python 3.14 support PyO3 0.24 does not support Python 3.14 (max 3.13). Update to 0.28.2 which has full Python 3.14 support. Migrate all deprecated APIs: - Python::with_gil -> Python::attach - .downcast() -> .cast(), .downcast_bound() -> .cast_bound() - Add #[pyclass(from_py_object)] on Clone types - IntoPyObject -> PyCallArgs trait bound Also declare Python 3.11-3.14 support in pyproject.toml classifiers. Co-Authored-By: Claude Opus 4.6 --- sentry_streams/Cargo.lock | 62 +++++++------------ sentry_streams/Cargo.toml | 2 +- sentry_streams/pyproject.toml | 6 ++ .../rust_transforms/Cargo.toml | 2 +- sentry_streams/src/callers.rs | 4 +- sentry_streams/src/committable.rs | 2 +- sentry_streams/src/consumer.rs | 4 +- sentry_streams/src/gcs_writer.rs | 2 +- sentry_streams/src/kafka_config.rs | 8 +-- sentry_streams/src/messages.rs | 14 ++--- sentry_streams/src/metrics_config.rs | 2 +- sentry_streams/src/python_operator.rs | 4 +- sentry_streams/src/routes.rs | 2 +- sentry_streams/src/sinks.rs | 2 +- sentry_streams/src/testutils.rs | 2 +- sentry_streams/src/utils.rs | 2 +- .../tests/rust_test_functions/Cargo.toml | 2 +- sentry_streams/uv.lock | 2 +- sentry_streams_k8s/pyproject.toml | 6 ++ 19 files changed, 63 insertions(+), 67 deletions(-) diff --git a/sentry_streams/Cargo.lock b/sentry_streams/Cargo.lock index 63c00976..21696ca4 100644 --- a/sentry_streams/Cargo.lock +++ b/sentry_streams/Cargo.lock @@ -863,12 +863,6 @@ dependencies = [ "hashbrown 0.15.2", ] -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - [[package]] name = "ipnet" version = "2.11.0" @@ -955,15 +949,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "metrics" version = "0.24.3" @@ -1307,20 +1292,16 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.24.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1c6c3591120564d64db2261bec5f910ae454f01def849b9c22835a84695e86" +checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" dependencies = [ - "cfg-if", - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", - "pyo3-build-config", + "pyo3-build-config 0.28.2", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] @@ -1333,21 +1314,30 @@ dependencies = [ "target-lexicon", ] +[[package]] +name = "pyo3-build-config" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" +dependencies = [ + "target-lexicon", +] + [[package]] name = "pyo3-ffi" -version = "0.24.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5507651906a46432cdda02cd02dd0319f6064f1374c9147c45b978621d2c3a9c" +checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" dependencies = [ "libc", - "pyo3-build-config", + "pyo3-build-config 0.28.2", ] [[package]] name = "pyo3-macros" -version = "0.24.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d394b5b4fd8d97d48336bb0dd2aebabad39f1d294edd6bcd2cccf2eefe6f42" +checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1357,13 +1347,13 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd72da09cfa943b1080f621f024d2ef7e2773df7badd51aa30a2be1f8caa7c8e" +checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", + "pyo3-build-config 0.28.2", "quote", "syn", ] @@ -1599,7 +1589,7 @@ dependencies = [ "metrics-exporter-dogstatsd", "parking_lot", "pyo3", - "pyo3-build-config", + "pyo3-build-config 0.24.0", "rand 0.8.5", "rdkafka", "reqwest", @@ -1977,9 +1967,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" @@ -2270,12 +2260,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/sentry_streams/Cargo.toml b/sentry_streams/Cargo.toml index 0b098e69..4f48273f 100644 --- a/sentry_streams/Cargo.toml +++ b/sentry_streams/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -pyo3 = { version = "0.24.0" } +pyo3 = { version = "0.28.2" } serde = { version = "1.0", features = ["derive"] } sentry_arroyo = { version = "2.38.2", features = ["ssl"] } chrono = "0.4.40" diff --git a/sentry_streams/pyproject.toml b/sentry_streams/pyproject.toml index 95081b41..3452c256 100644 --- a/sentry_streams/pyproject.toml +++ b/sentry_streams/pyproject.toml @@ -9,6 +9,12 @@ description = "The python Sentry Streaming API" readme = "README.md" requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] dependencies = [ "requests>=2.32.3", diff --git a/sentry_streams/sentry_streams/examples/rust_simple_map_filter/rust_transforms/Cargo.toml b/sentry_streams/sentry_streams/examples/rust_simple_map_filter/rust_transforms/Cargo.toml index d7adf896..47fd06b5 100644 --- a/sentry_streams/sentry_streams/examples/rust_simple_map_filter/rust_transforms/Cargo.toml +++ b/sentry_streams/sentry_streams/examples/rust_simple_map_filter/rust_transforms/Cargo.toml @@ -12,7 +12,7 @@ name = "runner" path = "src/main.rs" [dependencies] -pyo3 = { version = "0.24" } +pyo3 = { version = "0.28" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/sentry_streams/src/callers.rs b/sentry_streams/src/callers.rs index 867b4305..63ee9289 100644 --- a/sentry_streams/src/callers.rs +++ b/sentry_streams/src/callers.rs @@ -1,4 +1,4 @@ -use pyo3::{import_exception, prelude::*, types::PyTuple}; +use pyo3::{call::PyCallArgs, import_exception, prelude::*}; import_exception!(sentry_streams.pipeline.exception, InvalidMessageError); @@ -16,7 +16,7 @@ pub fn try_apply_py<'py, N>( args: N, ) -> ApplyResult> where - N: IntoPyObject<'py, Target = PyTuple>, + N: PyCallArgs<'py>, { callable.call1(py, args).map_err(|py_err| { py_err.print(py); diff --git a/sentry_streams/src/committable.rs b/sentry_streams/src/committable.rs index 86b1c9d0..4ee8200a 100644 --- a/sentry_streams/src/committable.rs +++ b/sentry_streams/src/committable.rs @@ -40,7 +40,7 @@ pub fn convert_py_committable( let mut committable = BTreeMap::new(); let dict = py_committable.bind(py); for (key, value) in dict.iter() { - let partition = key.downcast::()?; + let partition = key.cast::()?; let topic: String = partition.get_item(0)?.extract()?; let index: u16 = partition.get_item(1)?.extract()?; let offset: u64 = value.extract()?; diff --git a/sentry_streams/src/consumer.rs b/sentry_streams/src/consumer.rs index afc8e520..3b77dee7 100644 --- a/sentry_streams/src/consumer.rs +++ b/sentry_streams/src/consumer.rs @@ -329,7 +329,7 @@ mod tests { if let PyStreamingMessage::RawMessage { ref content } = py_payload { let payload = content.getattr(py, "payload").unwrap(); - let down: &Bound = payload.bind(py).downcast().unwrap(); + let down: &Bound = payload.bind(py).cast().unwrap(); let payload_bytes: &[u8] = down.as_bytes(); assert_eq!(payload_bytes, payload_data); } else { @@ -355,7 +355,7 @@ mod tests { .getattr(py, "payload") .unwrap() .bind(py) - .downcast::() + .cast::() .unwrap() .as_bytes() .to_vec(); diff --git a/sentry_streams/src/gcs_writer.rs b/sentry_streams/src/gcs_writer.rs index f4df1eb8..9e825232 100644 --- a/sentry_streams/src/gcs_writer.rs +++ b/sentry_streams/src/gcs_writer.rs @@ -36,7 +36,7 @@ fn pybytes_to_bytes(message: &PyStreamingMessage, py: Python<'_>) -> PyResult { let payload_content = content.bind(py).getattr("payload").unwrap(); - let py_bytes: &Bound = payload_content.downcast().unwrap(); + let py_bytes: &Bound = payload_content.cast().unwrap(); Ok(py_bytes.as_bytes().to_vec()) } } diff --git a/sentry_streams/src/kafka_config.rs b/sentry_streams/src/kafka_config.rs index 89beeddd..d9bf9559 100644 --- a/sentry_streams/src/kafka_config.rs +++ b/sentry_streams/src/kafka_config.rs @@ -10,7 +10,7 @@ use sentry_arroyo::backends::kafka::config::KafkaConfig; use sentry_arroyo::backends::kafka::InitialOffset as KafkaInitialOffset; use std::collections::HashMap; -#[pyclass] +#[pyclass(from_py_object)] #[derive(Debug, Clone, Copy, Default)] pub enum InitialOffset { #[default] @@ -34,7 +34,7 @@ impl From for KafkaInitialOffset { } } -#[pyclass] +#[pyclass(from_py_object)] #[derive(Debug, Clone)] pub struct OffsetResetConfig { #[pyo3(get, set)] @@ -55,7 +55,7 @@ impl OffsetResetConfig { } // Python version of the Kafka consumer configuration -#[pyclass] +#[pyclass(from_py_object)] #[derive(Debug, Clone)] pub struct PyKafkaConsumerConfig { bootstrap_servers: Vec, @@ -101,7 +101,7 @@ impl From for KafkaConfig { } } -#[pyclass] +#[pyclass(from_py_object)] #[derive(Debug, Clone)] pub struct PyKafkaProducerConfig { bootstrap_servers: Vec, diff --git a/sentry_streams/src/messages.rs b/sentry_streams/src/messages.rs index e3a6492a..86ec8799 100644 --- a/sentry_streams/src/messages.rs +++ b/sentry_streams/src/messages.rs @@ -54,7 +54,7 @@ pub fn headers_to_vec(py: Python<'_>, headers: Py) -> PyResult PyResult<(String, Vec)> { let tuple_i = item?; - let tuple = tuple_i.downcast::()?; + let tuple = tuple_i.cast::()?; let key = tuple.get_item(0)?.unbind().extract(py)?; let value: Vec = tuple.get_item(1)?.unbind().extract(py)?; Ok((key, value)) @@ -333,7 +333,7 @@ pub fn into_pyraw(py: Python<'_>, message: RawMessage) -> PyResult }, RawMessage { content: Py }, @@ -454,12 +454,12 @@ impl From> for PyStreamingMessage { traced_with_gil!(|py| { let bound = value.clone_ref(py).into_bound(py); if bound.is_instance_of::() { - let content = bound.downcast::()?; + let content = bound.cast::()?; Ok(PyStreamingMessage::PyAnyMessage { content: content.clone().unbind(), }) } else if bound.is_instance_of::() { - let content = bound.downcast::()?; + let content = bound.cast::()?; Ok(PyStreamingMessage::RawMessage { content: content.clone().unbind(), }) @@ -481,16 +481,16 @@ impl TryFrom> for WatermarkMessage { traced_with_gil!(|py| { let bound = value.clone_ref(py).into_bound(py); if bound.is_instance_of::() { - let py_watermark = bound.downcast::()?; + let py_watermark = bound.cast::()?; let py_committable = py_watermark .getattr("committable")? - .downcast::()? + .cast::()? .clone() .unbind(); let committable = convert_py_committable(py, py_committable).unwrap(); let timestamp = match py_watermark .getattr("timestamp")? - .downcast::()? + .cast::()? .extract::() { Ok(time) => time, diff --git a/sentry_streams/src/metrics_config.rs b/sentry_streams/src/metrics_config.rs index ad7e64d2..a45e3653 100644 --- a/sentry_streams/src/metrics_config.rs +++ b/sentry_streams/src/metrics_config.rs @@ -7,7 +7,7 @@ use pyo3::prelude::*; use std::collections::HashMap; -#[pyclass] +#[pyclass(from_py_object)] #[derive(Debug, Clone)] pub struct PyMetricConfig { host: String, diff --git a/sentry_streams/src/python_operator.rs b/sentry_streams/src/python_operator.rs index 0dc6ff54..1a62a620 100644 --- a/sentry_streams/src/python_operator.rs +++ b/sentry_streams/src/python_operator.rs @@ -65,11 +65,11 @@ impl PythonAdapter { /// the Python delegate. fn handle_py_return_value(&mut self, py: Python<'_>, payloads: Vec>) { for py_payload in payloads { - let entry = py_payload.downcast_bound::(py).unwrap(); + let entry = py_payload.cast_bound::(py).unwrap(); let payload: Py = entry.get_item(0).unwrap().unbind(); let committable: Py = entry.get_item(1).unwrap().unbind(); let committable_dict = committable - .downcast_bound::(py) + .cast_bound::(py) .unwrap() .as_unbound() .clone_ref(py); diff --git a/sentry_streams/src/routes.rs b/sentry_streams/src/routes.rs index a3193a2d..09b749a6 100644 --- a/sentry_streams/src/routes.rs +++ b/sentry_streams/src/routes.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; /// /// The waypoints sequence contains the branches taken by the message /// in order following the pipeline. -#[pyclass] +#[pyclass(from_py_object)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct Route { /// The name of the streaming source this route starts from. diff --git a/sentry_streams/src/sinks.rs b/sentry_streams/src/sinks.rs index c9df8587..dd539087 100644 --- a/sentry_streams/src/sinks.rs +++ b/sentry_streams/src/sinks.rs @@ -104,7 +104,7 @@ fn to_kafka_payload(message: Message) -> Message { RoutedValuePayload::PyStreamingMessage(PyStreamingMessage::RawMessage { ref content }) => { traced_with_gil!(|py| { let payload_content = content.bind(py).getattr("payload").unwrap(); - let py_bytes: &Bound = payload_content.downcast().unwrap(); + let py_bytes: &Bound = payload_content.cast().unwrap(); let raw_bytes = py_bytes.as_bytes(); let mut headers = Headers::new(); headers = headers.insert("is_watermark", Some(vec![0])); diff --git a/sentry_streams/src/testutils.rs b/sentry_streams/src/testutils.rs index e3aadc4e..abf13379 100644 --- a/sentry_streams/src/testutils.rs +++ b/sentry_streams/src/testutils.rs @@ -142,7 +142,7 @@ pub fn initialize_python() { let python_path = std::env::var("STREAMS_TEST_PYTHONPATH").unwrap(); let python_path: Vec<_> = python_path.split(':').map(String::from).collect(); - Python::with_gil(|py| -> PyResult<()> { + Python::attach(|py| -> PyResult<()> { PyModule::import(py, "sys")?.setattr("executable", python_executable)?; PyModule::import(py, "sys")?.setattr("path", python_path)?; Ok(()) diff --git a/sentry_streams/src/utils.rs b/sentry_streams/src/utils.rs index 25e49683..1a034a39 100644 --- a/sentry_streams/src/utils.rs +++ b/sentry_streams/src/utils.rs @@ -49,7 +49,7 @@ where let thread_id = thread::current().id(); let start_time = Instant::now(); - Python::with_gil(|py| { + Python::attach(|py| { let acquire_time = Instant::now().duration_since(start_time); if acquire_time > warn_threshold { diff --git a/sentry_streams/tests/rust_test_functions/Cargo.toml b/sentry_streams/tests/rust_test_functions/Cargo.toml index 1beca45c..fe5a0335 100644 --- a/sentry_streams/tests/rust_test_functions/Cargo.toml +++ b/sentry_streams/tests/rust_test_functions/Cargo.toml @@ -8,7 +8,7 @@ name = "rust_test_functions" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.24" } +pyo3 = { version = "0.28" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" paste = "1.0" diff --git a/sentry_streams/uv.lock b/sentry_streams/uv.lock index fab98f60..3a25fc96 100644 --- a/sentry_streams/uv.lock +++ b/sentry_streams/uv.lock @@ -893,7 +893,7 @@ wheels = [ [[package]] name = "sentry-streams" -version = "0.0.38" +version = "0.0.39" source = { editable = "." } dependencies = [ { name = "click" }, diff --git a/sentry_streams_k8s/pyproject.toml b/sentry_streams_k8s/pyproject.toml index 653237fe..0ac51218 100644 --- a/sentry_streams_k8s/pyproject.toml +++ b/sentry_streams_k8s/pyproject.toml @@ -8,6 +8,12 @@ version = "0.0.4" description = "Kubernetes integration for Sentry Streams" readme = "README.md" requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] dependencies = [ "jsonschema>=4.23.0", "requests>=2.32.3", From b73ea2e5831db46cb8db7f24fb75a95413fd76c2 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 16 Mar 2026 18:27:57 -0700 Subject: [PATCH 2/2] Fix allow_threads -> detach rename and remove unused build dep - py.allow_threads() was renamed to py.detach() in PyO3 0.28 - Remove unused pyo3-build-config build dependency (no build.rs exists) Co-Authored-By: Claude Opus 4.6 --- sentry_streams/Cargo.lock | 17 +++-------------- sentry_streams/Cargo.toml | 3 --- sentry_streams/src/ffi.rs | 2 +- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/sentry_streams/Cargo.lock b/sentry_streams/Cargo.lock index 21696ca4..57072b92 100644 --- a/sentry_streams/Cargo.lock +++ b/sentry_streams/Cargo.lock @@ -1299,21 +1299,11 @@ dependencies = [ "libc", "once_cell", "portable-atomic", - "pyo3-build-config 0.28.2", + "pyo3-build-config", "pyo3-ffi", "pyo3-macros", ] -[[package]] -name = "pyo3-build-config" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b6c2b34cf71427ea37c7001aefbaeb85886a074795e35f161f5aecc7620a7a" -dependencies = [ - "once_cell", - "target-lexicon", -] - [[package]] name = "pyo3-build-config" version = "0.28.2" @@ -1330,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" dependencies = [ "libc", - "pyo3-build-config 0.28.2", + "pyo3-build-config", ] [[package]] @@ -1353,7 +1343,7 @@ checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config 0.28.2", + "pyo3-build-config", "quote", "syn", ] @@ -1589,7 +1579,6 @@ dependencies = [ "metrics-exporter-dogstatsd", "parking_lot", "pyo3", - "pyo3-build-config 0.24.0", "rand 0.8.5", "rdkafka", "reqwest", diff --git a/sentry_streams/Cargo.toml b/sentry_streams/Cargo.toml index 4f48273f..98b94e22 100644 --- a/sentry_streams/Cargo.toml +++ b/sentry_streams/Cargo.toml @@ -39,9 +39,6 @@ cli = ["dep:clap", "pyo3/auto-initialize"] parking_lot = "0.12.1" pyo3 = { version = "*", features = ["auto-initialize"] } -[build-dependencies] -pyo3-build-config = "*" - # For now (while we are still rolling out streams to SBC), let's enable maximal # debug information. There should only be overhead in space and when many # stacktraces are generated (thousands/sec) diff --git a/sentry_streams/src/ffi.rs b/sentry_streams/src/ffi.rs index b6420dbe..57e0cfa5 100644 --- a/sentry_streams/src/ffi.rs +++ b/sentry_streams/src/ffi.rs @@ -158,7 +158,7 @@ macro_rules! rust_function { let rust_msg = $crate::ffi::convert_py_message_to_rust::<$input_type>(py, &py_msg)?; // Release GIL and call Rust function - let result_msg = py.allow_threads(|| { + let result_msg = py.detach(|| { // clone metadata, but try very hard to avoid cloning the payload let (payload, metadata) = rust_msg.take(); let metadata_clone = metadata.clone();