polars_python/conversion/
mod.rs

1pub(crate) mod any_value;
2pub(crate) mod chunked_array;
3mod datetime;
4
5use std::convert::Infallible;
6use std::fmt::{Display, Formatter};
7use std::fs::File;
8use std::hash::{Hash, Hasher};
9use std::path::PathBuf;
10
11#[cfg(feature = "object")]
12use polars::chunked_array::object::PolarsObjectSafe;
13use polars::frame::row::Row;
14#[cfg(feature = "avro")]
15use polars::io::avro::AvroCompression;
16#[cfg(feature = "cloud")]
17use polars::io::cloud::CloudOptions;
18use polars::series::ops::NullBehavior;
19use polars_core::utils::arrow::array::Array;
20use polars_core::utils::arrow::types::NativeType;
21use polars_core::utils::materialize_dyn_int;
22use polars_lazy::prelude::*;
23#[cfg(feature = "parquet")]
24use polars_parquet::write::StatisticsOptions;
25use polars_plan::plans::ScanSources;
26use polars_utils::mmap::MemSlice;
27use polars_utils::pl_str::PlSmallStr;
28use polars_utils::total_ord::{TotalEq, TotalHash};
29use pyo3::basic::CompareOp;
30use pyo3::exceptions::{PyTypeError, PyValueError};
31use pyo3::intern;
32use pyo3::prelude::*;
33use pyo3::pybacked::PyBackedStr;
34use pyo3::types::{PyDict, PyList, PySequence, PyString};
35
36use crate::error::PyPolarsErr;
37use crate::file::{get_python_scan_source_input, PythonScanSourceInput};
38#[cfg(feature = "object")]
39use crate::object::OBJECT_NAME;
40use crate::prelude::*;
41use crate::py_modules::{pl_series, polars};
42use crate::series::PySeries;
43use crate::{PyDataFrame, PyLazyFrame};
44
45/// # Safety
46/// Should only be implemented for transparent types
47pub(crate) unsafe trait Transparent {
48    type Target;
49}
50
51unsafe impl Transparent for PySeries {
52    type Target = Series;
53}
54
55unsafe impl<T> Transparent for Wrap<T> {
56    type Target = T;
57}
58
59unsafe impl<T: Transparent> Transparent for Option<T> {
60    type Target = Option<T::Target>;
61}
62
63pub(crate) fn reinterpret_vec<T: Transparent>(input: Vec<T>) -> Vec<T::Target> {
64    assert_eq!(size_of::<T>(), size_of::<T::Target>());
65    assert_eq!(align_of::<T>(), align_of::<T::Target>());
66    let len = input.len();
67    let cap = input.capacity();
68    let mut manual_drop_vec = std::mem::ManuallyDrop::new(input);
69    let vec_ptr: *mut T = manual_drop_vec.as_mut_ptr();
70    let ptr: *mut T::Target = vec_ptr as *mut T::Target;
71    unsafe { Vec::from_raw_parts(ptr, len, cap) }
72}
73
74pub(crate) fn vec_extract_wrapped<T>(buf: Vec<Wrap<T>>) -> Vec<T> {
75    reinterpret_vec(buf)
76}
77
78#[repr(transparent)]
79pub struct Wrap<T>(pub T);
80
81impl<T> Clone for Wrap<T>
82where
83    T: Clone,
84{
85    fn clone(&self) -> Self {
86        Wrap(self.0.clone())
87    }
88}
89impl<T> From<T> for Wrap<T> {
90    fn from(t: T) -> Self {
91        Wrap(t)
92    }
93}
94
95// extract a Rust DataFrame from a python DataFrame, that is DataFrame<PyDataFrame<RustDataFrame>>
96pub(crate) fn get_df(obj: &Bound<'_, PyAny>) -> PyResult<DataFrame> {
97    let pydf = obj.getattr(intern!(obj.py(), "_df"))?;
98    Ok(pydf.extract::<PyDataFrame>()?.df)
99}
100
101pub(crate) fn get_lf(obj: &Bound<'_, PyAny>) -> PyResult<LazyFrame> {
102    let pydf = obj.getattr(intern!(obj.py(), "_ldf"))?;
103    Ok(pydf.extract::<PyLazyFrame>()?.ldf)
104}
105
106pub(crate) fn get_series(obj: &Bound<'_, PyAny>) -> PyResult<Series> {
107    let s = obj.getattr(intern!(obj.py(), "_s"))?;
108    Ok(s.extract::<PySeries>()?.series)
109}
110
111pub(crate) fn to_series(py: Python, s: PySeries) -> PyResult<Bound<PyAny>> {
112    let series = pl_series(py).bind(py);
113    let constructor = series.getattr(intern!(py, "_from_pyseries"))?;
114    constructor.call1((s,))
115}
116
117impl<'a> FromPyObject<'a> for Wrap<PlSmallStr> {
118    fn extract_bound(ob: &Bound<'a, PyAny>) -> PyResult<Self> {
119        Ok(Wrap((&*ob.extract::<PyBackedStr>()?).into()))
120    }
121}
122
123#[cfg(feature = "csv")]
124impl<'a> FromPyObject<'a> for Wrap<NullValues> {
125    fn extract_bound(ob: &Bound<'a, PyAny>) -> PyResult<Self> {
126        if let Ok(s) = ob.extract::<PyBackedStr>() {
127            Ok(Wrap(NullValues::AllColumnsSingle((&*s).into())))
128        } else if let Ok(s) = ob.extract::<Vec<PyBackedStr>>() {
129            Ok(Wrap(NullValues::AllColumns(
130                s.into_iter().map(|x| (&*x).into()).collect(),
131            )))
132        } else if let Ok(s) = ob.extract::<Vec<(PyBackedStr, PyBackedStr)>>() {
133            Ok(Wrap(NullValues::Named(
134                s.into_iter()
135                    .map(|(a, b)| ((&*a).into(), (&*b).into()))
136                    .collect(),
137            )))
138        } else {
139            Err(
140                PyPolarsErr::Other("could not extract value from null_values argument".into())
141                    .into(),
142            )
143        }
144    }
145}
146
147fn struct_dict<'py, 'a>(
148    py: Python<'py>,
149    vals: impl Iterator<Item = AnyValue<'a>>,
150    flds: &[Field],
151) -> PyResult<Bound<'py, PyDict>> {
152    let dict = PyDict::new(py);
153    flds.iter().zip(vals).try_for_each(|(fld, val)| {
154        dict.set_item(fld.name().as_str(), Wrap(val).into_pyobject(py)?)
155    })?;
156    Ok(dict)
157}
158
159// accept u128 array to ensure alignment is correct
160fn decimal_to_digits(v: i128, buf: &mut [u128; 3]) -> usize {
161    const ZEROS: i128 = 0x3030_3030_3030_3030_3030_3030_3030_3030;
162    // SAFETY: transmute is safe as there are 48 bytes in 3 128bit ints
163    // and the minimal alignment of u8 fits u16
164    let buf = unsafe { std::mem::transmute::<&mut [u128; 3], &mut [u8; 48]>(buf) };
165    let mut buffer = itoa::Buffer::new();
166    let value = buffer.format(v);
167    let len = value.len();
168    for (dst, src) in buf.iter_mut().zip(value.as_bytes().iter()) {
169        *dst = *src
170    }
171
172    let ptr = buf.as_mut_ptr() as *mut i128;
173    unsafe {
174        // this is safe because we know that the buffer is exactly 48 bytes long
175        *ptr -= ZEROS;
176        *ptr.add(1) -= ZEROS;
177        *ptr.add(2) -= ZEROS;
178    }
179    len
180}
181
182impl<'py> IntoPyObject<'py> for &Wrap<DataType> {
183    type Target = PyAny;
184    type Output = Bound<'py, Self::Target>;
185    type Error = PyErr;
186
187    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
188        let pl = polars(py).bind(py);
189
190        match &self.0 {
191            DataType::Int8 => {
192                let class = pl.getattr(intern!(py, "Int8"))?;
193                class.call0()
194            },
195            DataType::Int16 => {
196                let class = pl.getattr(intern!(py, "Int16"))?;
197                class.call0()
198            },
199            DataType::Int32 => {
200                let class = pl.getattr(intern!(py, "Int32"))?;
201                class.call0()
202            },
203            DataType::Int64 => {
204                let class = pl.getattr(intern!(py, "Int64"))?;
205                class.call0()
206            },
207            DataType::UInt8 => {
208                let class = pl.getattr(intern!(py, "UInt8"))?;
209                class.call0()
210            },
211            DataType::UInt16 => {
212                let class = pl.getattr(intern!(py, "UInt16"))?;
213                class.call0()
214            },
215            DataType::UInt32 => {
216                let class = pl.getattr(intern!(py, "UInt32"))?;
217                class.call0()
218            },
219            DataType::UInt64 => {
220                let class = pl.getattr(intern!(py, "UInt64"))?;
221                class.call0()
222            },
223            DataType::Int128 => {
224                let class = pl.getattr(intern!(py, "Int128"))?;
225                class.call0()
226            },
227            DataType::Float32 => {
228                let class = pl.getattr(intern!(py, "Float32"))?;
229                class.call0()
230            },
231            DataType::Float64 | DataType::Unknown(UnknownKind::Float) => {
232                let class = pl.getattr(intern!(py, "Float64"))?;
233                class.call0()
234            },
235            DataType::Decimal(precision, scale) => {
236                let class = pl.getattr(intern!(py, "Decimal"))?;
237                let args = (*precision, *scale);
238                class.call1(args)
239            },
240            DataType::Boolean => {
241                let class = pl.getattr(intern!(py, "Boolean"))?;
242                class.call0()
243            },
244            DataType::String | DataType::Unknown(UnknownKind::Str) => {
245                let class = pl.getattr(intern!(py, "String"))?;
246                class.call0()
247            },
248            DataType::Binary => {
249                let class = pl.getattr(intern!(py, "Binary"))?;
250                class.call0()
251            },
252            DataType::Array(inner, size) => {
253                let class = pl.getattr(intern!(py, "Array"))?;
254                let inner = Wrap(*inner.clone());
255                let args = (&inner, *size);
256                class.call1(args)
257            },
258            DataType::List(inner) => {
259                let class = pl.getattr(intern!(py, "List"))?;
260                let inner = Wrap(*inner.clone());
261                class.call1((&inner,))
262            },
263            DataType::Date => {
264                let class = pl.getattr(intern!(py, "Date"))?;
265                class.call0()
266            },
267            DataType::Datetime(tu, tz) => {
268                let datetime_class = pl.getattr(intern!(py, "Datetime"))?;
269                datetime_class.call1((tu.to_ascii(), tz.as_deref()))
270            },
271            DataType::Duration(tu) => {
272                let duration_class = pl.getattr(intern!(py, "Duration"))?;
273                duration_class.call1((tu.to_ascii(),))
274            },
275            #[cfg(feature = "object")]
276            DataType::Object(_, _) => {
277                let class = pl.getattr(intern!(py, "Object"))?;
278                class.call0()
279            },
280            DataType::Categorical(_, ordering) => {
281                let class = pl.getattr(intern!(py, "Categorical"))?;
282                class.call1((Wrap(*ordering),))
283            },
284            DataType::Enum(rev_map, _) => {
285                // we should always have an initialized rev_map coming from rust
286                let categories = rev_map.as_ref().unwrap().get_categories();
287                let class = pl.getattr(intern!(py, "Enum"))?;
288                let s =
289                    Series::from_arrow(PlSmallStr::from_static("category"), categories.to_boxed())
290                        .map_err(PyPolarsErr::from)?;
291                let series = to_series(py, s.into())?;
292                class.call1((series,))
293            },
294            DataType::Time => pl.getattr(intern!(py, "Time")),
295            DataType::Struct(fields) => {
296                let field_class = pl.getattr(intern!(py, "Field"))?;
297                let iter = fields.iter().map(|fld| {
298                    let name = fld.name().as_str();
299                    let dtype = Wrap(fld.dtype().clone());
300                    field_class.call1((name, &dtype)).unwrap()
301                });
302                let fields = PyList::new(py, iter)?;
303                let struct_class = pl.getattr(intern!(py, "Struct"))?;
304                struct_class.call1((fields,))
305            },
306            DataType::Null => {
307                let class = pl.getattr(intern!(py, "Null"))?;
308                class.call0()
309            },
310            DataType::Unknown(UnknownKind::Int(v)) => {
311                Wrap(materialize_dyn_int(*v).dtype()).into_pyobject(py)
312            },
313            DataType::Unknown(_) => {
314                let class = pl.getattr(intern!(py, "Unknown"))?;
315                class.call0()
316            },
317            DataType::BinaryOffset => {
318                unimplemented!()
319            },
320        }
321    }
322}
323
324impl<'py> FromPyObject<'py> for Wrap<Field> {
325    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
326        let py = ob.py();
327        let name = ob
328            .getattr(intern!(py, "name"))?
329            .str()?
330            .extract::<PyBackedStr>()?;
331        let dtype = ob
332            .getattr(intern!(py, "dtype"))?
333            .extract::<Wrap<DataType>>()?;
334        Ok(Wrap(Field::new((&*name).into(), dtype.0)))
335    }
336}
337
338impl<'py> FromPyObject<'py> for Wrap<DataType> {
339    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
340        let py = ob.py();
341        let type_name = ob.get_type().qualname()?.to_string();
342
343        let dtype = match &*type_name {
344            "DataTypeClass" => {
345                // just the class, not an object
346                let name = ob
347                    .getattr(intern!(py, "__name__"))?
348                    .str()?
349                    .extract::<PyBackedStr>()?;
350                match &*name {
351                    "Int8" => DataType::Int8,
352                    "Int16" => DataType::Int16,
353                    "Int32" => DataType::Int32,
354                    "Int64" => DataType::Int64,
355                    "Int128" => DataType::Int128,
356                    "UInt8" => DataType::UInt8,
357                    "UInt16" => DataType::UInt16,
358                    "UInt32" => DataType::UInt32,
359                    "UInt64" => DataType::UInt64,
360                    "Float32" => DataType::Float32,
361                    "Float64" => DataType::Float64,
362                    "Boolean" => DataType::Boolean,
363                    "String" => DataType::String,
364                    "Binary" => DataType::Binary,
365                    "Categorical" => DataType::Categorical(None, Default::default()),
366                    "Enum" => DataType::Enum(None, Default::default()),
367                    "Date" => DataType::Date,
368                    "Time" => DataType::Time,
369                    "Datetime" => DataType::Datetime(TimeUnit::Microseconds, None),
370                    "Duration" => DataType::Duration(TimeUnit::Microseconds),
371                    "Decimal" => DataType::Decimal(None, None), // "none" scale => "infer"
372                    "List" => DataType::List(Box::new(DataType::Null)),
373                    "Array" => DataType::Array(Box::new(DataType::Null), 0),
374                    "Struct" => DataType::Struct(vec![]),
375                    "Null" => DataType::Null,
376                    #[cfg(feature = "object")]
377                    "Object" => DataType::Object(OBJECT_NAME, None),
378                    "Unknown" => DataType::Unknown(Default::default()),
379                    dt => {
380                        return Err(PyTypeError::new_err(format!(
381                            "'{dt}' is not a Polars data type",
382                        )))
383                    },
384                }
385            },
386            "Int8" => DataType::Int8,
387            "Int16" => DataType::Int16,
388            "Int32" => DataType::Int32,
389            "Int64" => DataType::Int64,
390            "Int128" => DataType::Int128,
391            "UInt8" => DataType::UInt8,
392            "UInt16" => DataType::UInt16,
393            "UInt32" => DataType::UInt32,
394            "UInt64" => DataType::UInt64,
395            "Float32" => DataType::Float32,
396            "Float64" => DataType::Float64,
397            "Boolean" => DataType::Boolean,
398            "String" => DataType::String,
399            "Binary" => DataType::Binary,
400            "Categorical" => {
401                let ordering = ob.getattr(intern!(py, "ordering")).unwrap();
402                let ordering = ordering.extract::<Wrap<CategoricalOrdering>>()?.0;
403                DataType::Categorical(None, ordering)
404            },
405            "Enum" => {
406                let categories = ob.getattr(intern!(py, "categories")).unwrap();
407                let s = get_series(&categories.as_borrowed())?;
408                let ca = s.str().map_err(PyPolarsErr::from)?;
409                let categories = ca.downcast_iter().next().unwrap().clone();
410                create_enum_dtype(categories)
411            },
412            "Date" => DataType::Date,
413            "Time" => DataType::Time,
414            "Datetime" => {
415                let time_unit = ob.getattr(intern!(py, "time_unit")).unwrap();
416                let time_unit = time_unit.extract::<Wrap<TimeUnit>>()?.0;
417                let time_zone = ob.getattr(intern!(py, "time_zone")).unwrap();
418                let time_zone = time_zone.extract::<Option<PyBackedStr>>()?;
419                DataType::Datetime(time_unit, time_zone.as_deref().map(|x| x.into()))
420            },
421            "Duration" => {
422                let time_unit = ob.getattr(intern!(py, "time_unit")).unwrap();
423                let time_unit = time_unit.extract::<Wrap<TimeUnit>>()?.0;
424                DataType::Duration(time_unit)
425            },
426            "Decimal" => {
427                let precision = ob.getattr(intern!(py, "precision"))?.extract()?;
428                let scale = ob.getattr(intern!(py, "scale"))?.extract()?;
429                DataType::Decimal(precision, Some(scale))
430            },
431            "List" => {
432                let inner = ob.getattr(intern!(py, "inner")).unwrap();
433                let inner = inner.extract::<Wrap<DataType>>()?;
434                DataType::List(Box::new(inner.0))
435            },
436            "Array" => {
437                let inner = ob.getattr(intern!(py, "inner")).unwrap();
438                let size = ob.getattr(intern!(py, "size")).unwrap();
439                let inner = inner.extract::<Wrap<DataType>>()?;
440                let size = size.extract::<usize>()?;
441                DataType::Array(Box::new(inner.0), size)
442            },
443            "Struct" => {
444                let fields = ob.getattr(intern!(py, "fields"))?;
445                let fields = fields
446                    .extract::<Vec<Wrap<Field>>>()?
447                    .into_iter()
448                    .map(|f| f.0)
449                    .collect::<Vec<Field>>();
450                DataType::Struct(fields)
451            },
452            "Null" => DataType::Null,
453            #[cfg(feature = "object")]
454            "Object" => DataType::Object(OBJECT_NAME, None),
455            "Unknown" => DataType::Unknown(Default::default()),
456            dt => {
457                return Err(PyTypeError::new_err(format!(
458                    "'{dt}' is not a Polars data type",
459                )))
460            },
461        };
462        Ok(Wrap(dtype))
463    }
464}
465
466impl<'py> IntoPyObject<'py> for Wrap<CategoricalOrdering> {
467    type Target = PyString;
468    type Output = Bound<'py, Self::Target>;
469    type Error = Infallible;
470
471    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
472        match self.0 {
473            CategoricalOrdering::Physical => "physical",
474            CategoricalOrdering::Lexical => "lexical",
475        }
476        .into_pyobject(py)
477    }
478}
479
480impl<'py> IntoPyObject<'py> for Wrap<TimeUnit> {
481    type Target = PyString;
482    type Output = Bound<'py, Self::Target>;
483    type Error = Infallible;
484
485    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
486        self.0.to_ascii().into_pyobject(py)
487    }
488}
489
490#[cfg(feature = "parquet")]
491impl<'s> FromPyObject<'s> for Wrap<StatisticsOptions> {
492    fn extract_bound(ob: &Bound<'s, PyAny>) -> PyResult<Self> {
493        let mut statistics = StatisticsOptions::empty();
494
495        let dict = ob.downcast::<PyDict>()?;
496        for (key, val) in dict {
497            let key = key.extract::<PyBackedStr>()?;
498            let val = val.extract::<bool>()?;
499
500            match key.as_ref() {
501                "min" => statistics.min_value = val,
502                "max" => statistics.max_value = val,
503                "distinct_count" => statistics.distinct_count = val,
504                "null_count" => statistics.null_count = val,
505                _ => {
506                    return Err(PyTypeError::new_err(format!(
507                        "'{key}' is not a valid statistic option",
508                    )))
509                },
510            }
511        }
512
513        Ok(Wrap(statistics))
514    }
515}
516
517impl<'s> FromPyObject<'s> for Wrap<Row<'s>> {
518    fn extract_bound(ob: &Bound<'s, PyAny>) -> PyResult<Self> {
519        let vals = ob.extract::<Vec<Wrap<AnyValue<'s>>>>()?;
520        let vals = reinterpret_vec(vals);
521        Ok(Wrap(Row(vals)))
522    }
523}
524
525impl<'py> FromPyObject<'py> for Wrap<Schema> {
526    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
527        let dict = ob.downcast::<PyDict>()?;
528
529        Ok(Wrap(
530            dict.iter()
531                .map(|(key, val)| {
532                    let key = key.extract::<PyBackedStr>()?;
533                    let val = val.extract::<Wrap<DataType>>()?;
534
535                    Ok(Field::new((&*key).into(), val.0))
536                })
537                .collect::<PyResult<Schema>>()?,
538        ))
539    }
540}
541
542impl<'py> FromPyObject<'py> for Wrap<ScanSources> {
543    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
544        let list = ob.downcast::<PyList>()?.to_owned();
545
546        if list.is_empty() {
547            return Ok(Wrap(ScanSources::default()));
548        }
549
550        enum MutableSources {
551            Paths(Vec<PathBuf>),
552            Files(Vec<File>),
553            Buffers(Vec<MemSlice>),
554        }
555
556        let num_items = list.len();
557        let mut iter = list
558            .into_iter()
559            .map(|val| get_python_scan_source_input(val.unbind(), false));
560
561        let Some(first) = iter.next() else {
562            return Ok(Wrap(ScanSources::default()));
563        };
564
565        let mut sources = match first? {
566            PythonScanSourceInput::Path(path) => {
567                let mut sources = Vec::with_capacity(num_items);
568                sources.push(path);
569                MutableSources::Paths(sources)
570            },
571            PythonScanSourceInput::File(file) => {
572                let mut sources = Vec::with_capacity(num_items);
573                sources.push(file);
574                MutableSources::Files(sources)
575            },
576            PythonScanSourceInput::Buffer(buffer) => {
577                let mut sources = Vec::with_capacity(num_items);
578                sources.push(buffer);
579                MutableSources::Buffers(sources)
580            },
581        };
582
583        for source in iter {
584            match (&mut sources, source?) {
585                (MutableSources::Paths(v), PythonScanSourceInput::Path(p)) => v.push(p),
586                (MutableSources::Files(v), PythonScanSourceInput::File(f)) => v.push(f),
587                (MutableSources::Buffers(v), PythonScanSourceInput::Buffer(f)) => v.push(f),
588                _ => {
589                    return Err(PyTypeError::new_err(
590                        "Cannot combine in-memory bytes, paths and files for scan sources",
591                    ))
592                },
593            }
594        }
595
596        Ok(Wrap(match sources {
597            MutableSources::Paths(i) => ScanSources::Paths(i.into()),
598            MutableSources::Files(i) => ScanSources::Files(i.into()),
599            MutableSources::Buffers(i) => ScanSources::Buffers(i.into()),
600        }))
601    }
602}
603
604impl<'py> IntoPyObject<'py> for Wrap<&Schema> {
605    type Target = PyDict;
606    type Output = Bound<'py, Self::Target>;
607    type Error = PyErr;
608
609    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
610        let dict = PyDict::new(py);
611        self.0
612            .iter()
613            .try_for_each(|(k, v)| dict.set_item(k.as_str(), &Wrap(v.clone())))?;
614        Ok(dict)
615    }
616}
617
618#[derive(Debug)]
619#[repr(transparent)]
620pub struct ObjectValue {
621    pub inner: PyObject,
622}
623
624impl Clone for ObjectValue {
625    fn clone(&self) -> Self {
626        Python::with_gil(|py| Self {
627            inner: self.inner.clone_ref(py),
628        })
629    }
630}
631
632impl Hash for ObjectValue {
633    fn hash<H: Hasher>(&self, state: &mut H) {
634        let h = Python::with_gil(|py| self.inner.bind(py).hash().expect("should be hashable"));
635        state.write_isize(h)
636    }
637}
638
639impl Eq for ObjectValue {}
640
641impl PartialEq for ObjectValue {
642    fn eq(&self, other: &Self) -> bool {
643        Python::with_gil(|py| {
644            match self
645                .inner
646                .bind(py)
647                .rich_compare(other.inner.bind(py), CompareOp::Eq)
648            {
649                Ok(result) => result.is_truthy().unwrap(),
650                Err(_) => false,
651            }
652        })
653    }
654}
655
656impl TotalEq for ObjectValue {
657    fn tot_eq(&self, other: &Self) -> bool {
658        self == other
659    }
660}
661
662impl TotalHash for ObjectValue {
663    fn tot_hash<H>(&self, state: &mut H)
664    where
665        H: Hasher,
666    {
667        self.hash(state);
668    }
669}
670
671impl Display for ObjectValue {
672    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
673        write!(f, "{}", self.inner)
674    }
675}
676
677#[cfg(feature = "object")]
678impl PolarsObject for ObjectValue {
679    fn type_name() -> &'static str {
680        "object"
681    }
682}
683
684impl From<PyObject> for ObjectValue {
685    fn from(p: PyObject) -> Self {
686        Self { inner: p }
687    }
688}
689
690impl<'a> FromPyObject<'a> for ObjectValue {
691    fn extract_bound(ob: &Bound<'a, PyAny>) -> PyResult<Self> {
692        Ok(ObjectValue {
693            inner: ob.to_owned().unbind(),
694        })
695    }
696}
697
698/// # Safety
699///
700/// The caller is responsible for checking that val is Object otherwise UB
701#[cfg(feature = "object")]
702impl From<&dyn PolarsObjectSafe> for &ObjectValue {
703    fn from(val: &dyn PolarsObjectSafe) -> Self {
704        unsafe { &*(val as *const dyn PolarsObjectSafe as *const ObjectValue) }
705    }
706}
707
708impl<'a, 'py> IntoPyObject<'py> for &'a ObjectValue {
709    type Target = PyAny;
710    type Output = Borrowed<'a, 'py, Self::Target>;
711    type Error = std::convert::Infallible;
712
713    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
714        Ok(self.inner.bind_borrowed(py))
715    }
716}
717
718impl Default for ObjectValue {
719    fn default() -> Self {
720        Python::with_gil(|py| ObjectValue { inner: py.None() })
721    }
722}
723
724impl<'a, T: NativeType + FromPyObject<'a>> FromPyObject<'a> for Wrap<Vec<T>> {
725    fn extract_bound(obj: &Bound<'a, PyAny>) -> PyResult<Self> {
726        let seq = obj.downcast::<PySequence>()?;
727        let mut v = Vec::with_capacity(seq.len().unwrap_or(0));
728        for item in seq.try_iter()? {
729            v.push(item?.extract::<T>()?);
730        }
731        Ok(Wrap(v))
732    }
733}
734
735#[cfg(feature = "asof_join")]
736impl<'py> FromPyObject<'py> for Wrap<AsofStrategy> {
737    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
738        let parsed = match &*(ob.extract::<PyBackedStr>()?) {
739            "backward" => AsofStrategy::Backward,
740            "forward" => AsofStrategy::Forward,
741            "nearest" => AsofStrategy::Nearest,
742            v => {
743                return Err(PyValueError::new_err(format!(
744                    "asof `strategy` must be one of {{'backward', 'forward', 'nearest'}}, got {v}",
745                )))
746            },
747        };
748        Ok(Wrap(parsed))
749    }
750}
751
752impl<'py> FromPyObject<'py> for Wrap<InterpolationMethod> {
753    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
754        let parsed = match &*(ob.extract::<PyBackedStr>()?) {
755            "linear" => InterpolationMethod::Linear,
756            "nearest" => InterpolationMethod::Nearest,
757            v => {
758                return Err(PyValueError::new_err(format!(
759                    "interpolation `method` must be one of {{'linear', 'nearest'}}, got {v}",
760                )))
761            },
762        };
763        Ok(Wrap(parsed))
764    }
765}
766
767#[cfg(feature = "avro")]
768impl<'py> FromPyObject<'py> for Wrap<Option<AvroCompression>> {
769    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
770        let parsed = match &*ob.extract::<PyBackedStr>()? {
771            "uncompressed" => None,
772            "snappy" => Some(AvroCompression::Snappy),
773            "deflate" => Some(AvroCompression::Deflate),
774            v => {
775                return Err(PyValueError::new_err(format!(
776                "avro `compression` must be one of {{'uncompressed', 'snappy', 'deflate'}}, got {v}",
777            )))
778            },
779        };
780        Ok(Wrap(parsed))
781    }
782}
783
784impl<'py> FromPyObject<'py> for Wrap<CategoricalOrdering> {
785    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
786        let parsed = match &*ob.extract::<PyBackedStr>()? {
787            "physical" => CategoricalOrdering::Physical,
788            "lexical" => CategoricalOrdering::Lexical,
789            v => {
790                return Err(PyValueError::new_err(format!(
791                    "categorical `ordering` must be one of {{'physical', 'lexical'}}, got {v}",
792                )))
793            },
794        };
795        Ok(Wrap(parsed))
796    }
797}
798
799impl<'py> FromPyObject<'py> for Wrap<StartBy> {
800    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
801        let parsed = match &*ob.extract::<PyBackedStr>()? {
802            "window" => StartBy::WindowBound,
803            "datapoint" => StartBy::DataPoint,
804            "monday" => StartBy::Monday,
805            "tuesday" => StartBy::Tuesday,
806            "wednesday" => StartBy::Wednesday,
807            "thursday" => StartBy::Thursday,
808            "friday" => StartBy::Friday,
809            "saturday" => StartBy::Saturday,
810            "sunday" => StartBy::Sunday,
811            v => {
812                return Err(PyValueError::new_err(format!(
813                    "`start_by` must be one of {{'window', 'datapoint', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'}}, got {v}",
814                )))
815            }
816        };
817        Ok(Wrap(parsed))
818    }
819}
820
821impl<'py> FromPyObject<'py> for Wrap<ClosedWindow> {
822    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
823        let parsed = match &*ob.extract::<PyBackedStr>()? {
824            "left" => ClosedWindow::Left,
825            "right" => ClosedWindow::Right,
826            "both" => ClosedWindow::Both,
827            "none" => ClosedWindow::None,
828            v => {
829                return Err(PyValueError::new_err(format!(
830                    "`closed` must be one of {{'left', 'right', 'both', 'none'}}, got {v}",
831                )))
832            },
833        };
834        Ok(Wrap(parsed))
835    }
836}
837
838#[cfg(feature = "csv")]
839impl<'py> FromPyObject<'py> for Wrap<CsvEncoding> {
840    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
841        let parsed = match &*ob.extract::<PyBackedStr>()? {
842            "utf8" => CsvEncoding::Utf8,
843            "utf8-lossy" => CsvEncoding::LossyUtf8,
844            v => {
845                return Err(PyValueError::new_err(format!(
846                    "csv `encoding` must be one of {{'utf8', 'utf8-lossy'}}, got {v}",
847                )))
848            },
849        };
850        Ok(Wrap(parsed))
851    }
852}
853
854#[cfg(feature = "ipc")]
855impl<'py> FromPyObject<'py> for Wrap<Option<IpcCompression>> {
856    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
857        let parsed = match &*ob.extract::<PyBackedStr>()? {
858            "uncompressed" => None,
859            "lz4" => Some(IpcCompression::LZ4),
860            "zstd" => Some(IpcCompression::ZSTD),
861            v => {
862                return Err(PyValueError::new_err(format!(
863                    "ipc `compression` must be one of {{'uncompressed', 'lz4', 'zstd'}}, got {v}",
864                )))
865            },
866        };
867        Ok(Wrap(parsed))
868    }
869}
870
871impl<'py> FromPyObject<'py> for Wrap<JoinType> {
872    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
873        let parsed = match &*ob.extract::<PyBackedStr>()? {
874            "inner" => JoinType::Inner,
875            "left" => JoinType::Left,
876            "right" => JoinType::Right,
877            "full" => JoinType::Full,
878            "semi" => JoinType::Semi,
879            "anti" => JoinType::Anti,
880            #[cfg(feature = "cross_join")]
881            "cross" => JoinType::Cross,
882            v => {
883                return Err(PyValueError::new_err(format!(
884                "`how` must be one of {{'inner', 'left', 'full', 'semi', 'anti', 'cross'}}, got {v}",
885            )))
886            },
887        };
888        Ok(Wrap(parsed))
889    }
890}
891
892impl<'py> FromPyObject<'py> for Wrap<Label> {
893    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
894        let parsed = match &*ob.extract::<PyBackedStr>()? {
895            "left" => Label::Left,
896            "right" => Label::Right,
897            "datapoint" => Label::DataPoint,
898            v => {
899                return Err(PyValueError::new_err(format!(
900                    "`label` must be one of {{'left', 'right', 'datapoint'}}, got {v}",
901                )))
902            },
903        };
904        Ok(Wrap(parsed))
905    }
906}
907
908impl<'py> FromPyObject<'py> for Wrap<ListToStructWidthStrategy> {
909    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
910        let parsed = match &*ob.extract::<PyBackedStr>()? {
911            "first_non_null" => ListToStructWidthStrategy::FirstNonNull,
912            "max_width" => ListToStructWidthStrategy::MaxWidth,
913            v => {
914                return Err(PyValueError::new_err(format!(
915                    "`n_field_strategy` must be one of {{'first_non_null', 'max_width'}}, got {v}",
916                )))
917            },
918        };
919        Ok(Wrap(parsed))
920    }
921}
922
923impl<'py> FromPyObject<'py> for Wrap<NonExistent> {
924    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
925        let parsed = match &*ob.extract::<PyBackedStr>()? {
926            "null" => NonExistent::Null,
927            "raise" => NonExistent::Raise,
928            v => {
929                return Err(PyValueError::new_err(format!(
930                    "`non_existent` must be one of {{'null', 'raise'}}, got {v}",
931                )))
932            },
933        };
934        Ok(Wrap(parsed))
935    }
936}
937
938impl<'py> FromPyObject<'py> for Wrap<NullBehavior> {
939    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
940        let parsed = match &*ob.extract::<PyBackedStr>()? {
941            "drop" => NullBehavior::Drop,
942            "ignore" => NullBehavior::Ignore,
943            v => {
944                return Err(PyValueError::new_err(format!(
945                    "`null_behavior` must be one of {{'drop', 'ignore'}}, got {v}",
946                )))
947            },
948        };
949        Ok(Wrap(parsed))
950    }
951}
952
953impl<'py> FromPyObject<'py> for Wrap<NullStrategy> {
954    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
955        let parsed = match &*ob.extract::<PyBackedStr>()? {
956            "ignore" => NullStrategy::Ignore,
957            "propagate" => NullStrategy::Propagate,
958            v => {
959                return Err(PyValueError::new_err(format!(
960                    "`null_strategy` must be one of {{'ignore', 'propagate'}}, got {v}",
961                )))
962            },
963        };
964        Ok(Wrap(parsed))
965    }
966}
967
968#[cfg(feature = "parquet")]
969impl<'py> FromPyObject<'py> for Wrap<ParallelStrategy> {
970    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
971        let parsed = match &*ob.extract::<PyBackedStr>()? {
972            "auto" => ParallelStrategy::Auto,
973            "columns" => ParallelStrategy::Columns,
974            "row_groups" => ParallelStrategy::RowGroups,
975            "prefiltered" => ParallelStrategy::Prefiltered,
976            "none" => ParallelStrategy::None,
977            v => {
978                return Err(PyValueError::new_err(format!(
979                "`parallel` must be one of {{'auto', 'columns', 'row_groups', 'prefiltered', 'none'}}, got {v}",
980            )))
981            },
982        };
983        Ok(Wrap(parsed))
984    }
985}
986
987impl<'py> FromPyObject<'py> for Wrap<IndexOrder> {
988    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
989        let parsed = match &*ob.extract::<PyBackedStr>()? {
990            "fortran" => IndexOrder::Fortran,
991            "c" => IndexOrder::C,
992            v => {
993                return Err(PyValueError::new_err(format!(
994                    "`order` must be one of {{'fortran', 'c'}}, got {v}",
995                )))
996            },
997        };
998        Ok(Wrap(parsed))
999    }
1000}
1001
1002impl<'py> FromPyObject<'py> for Wrap<QuantileMethod> {
1003    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1004        let parsed = match &*ob.extract::<PyBackedStr>()? {
1005            "lower" => QuantileMethod::Lower,
1006            "higher" => QuantileMethod::Higher,
1007            "nearest" => QuantileMethod::Nearest,
1008            "linear" => QuantileMethod::Linear,
1009            "midpoint" => QuantileMethod::Midpoint,
1010            "equiprobable" => QuantileMethod::Equiprobable,
1011            v => {
1012                return Err(PyValueError::new_err(format!(
1013                    "`interpolation` must be one of {{'lower', 'higher', 'nearest', 'linear', 'midpoint', 'equiprobable'}}, got {v}",
1014                )))
1015            }
1016        };
1017        Ok(Wrap(parsed))
1018    }
1019}
1020
1021impl<'py> FromPyObject<'py> for Wrap<RankMethod> {
1022    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1023        let parsed = match &*ob.extract::<PyBackedStr>()? {
1024            "min" => RankMethod::Min,
1025            "max" => RankMethod::Max,
1026            "average" => RankMethod::Average,
1027            "dense" => RankMethod::Dense,
1028            "ordinal" => RankMethod::Ordinal,
1029            "random" => RankMethod::Random,
1030            v => {
1031                return Err(PyValueError::new_err(format!(
1032                    "rank `method` must be one of {{'min', 'max', 'average', 'dense', 'ordinal', 'random'}}, got {v}",
1033                )))
1034            }
1035        };
1036        Ok(Wrap(parsed))
1037    }
1038}
1039
1040impl<'py> FromPyObject<'py> for Wrap<Roll> {
1041    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1042        let parsed = match &*ob.extract::<PyBackedStr>()? {
1043            "raise" => Roll::Raise,
1044            "forward" => Roll::Forward,
1045            "backward" => Roll::Backward,
1046            v => {
1047                return Err(PyValueError::new_err(format!(
1048                    "`roll` must be one of {{'raise', 'forward', 'backward'}}, got {v}",
1049                )))
1050            },
1051        };
1052        Ok(Wrap(parsed))
1053    }
1054}
1055
1056impl<'py> FromPyObject<'py> for Wrap<TimeUnit> {
1057    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1058        let parsed = match &*ob.extract::<PyBackedStr>()? {
1059            "ns" => TimeUnit::Nanoseconds,
1060            "us" => TimeUnit::Microseconds,
1061            "ms" => TimeUnit::Milliseconds,
1062            v => {
1063                return Err(PyValueError::new_err(format!(
1064                    "`time_unit` must be one of {{'ns', 'us', 'ms'}}, got {v}",
1065                )))
1066            },
1067        };
1068        Ok(Wrap(parsed))
1069    }
1070}
1071
1072impl<'py> FromPyObject<'py> for Wrap<UniqueKeepStrategy> {
1073    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1074        let parsed = match &*ob.extract::<PyBackedStr>()? {
1075            "first" => UniqueKeepStrategy::First,
1076            "last" => UniqueKeepStrategy::Last,
1077            "none" => UniqueKeepStrategy::None,
1078            "any" => UniqueKeepStrategy::Any,
1079            v => {
1080                return Err(PyValueError::new_err(format!(
1081                    "`keep` must be one of {{'first', 'last', 'any', 'none'}}, got {v}",
1082                )))
1083            },
1084        };
1085        Ok(Wrap(parsed))
1086    }
1087}
1088
1089#[cfg(feature = "ipc")]
1090impl<'py> FromPyObject<'py> for Wrap<IpcCompression> {
1091    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1092        let parsed = match &*ob.extract::<PyBackedStr>()? {
1093            "zstd" => IpcCompression::ZSTD,
1094            "lz4" => IpcCompression::LZ4,
1095            v => {
1096                return Err(PyValueError::new_err(format!(
1097                    "ipc `compression` must be one of {{'zstd', 'lz4'}}, got {v}",
1098                )))
1099            },
1100        };
1101        Ok(Wrap(parsed))
1102    }
1103}
1104
1105#[cfg(feature = "search_sorted")]
1106impl<'py> FromPyObject<'py> for Wrap<SearchSortedSide> {
1107    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1108        let parsed = match &*ob.extract::<PyBackedStr>()? {
1109            "any" => SearchSortedSide::Any,
1110            "left" => SearchSortedSide::Left,
1111            "right" => SearchSortedSide::Right,
1112            v => {
1113                return Err(PyValueError::new_err(format!(
1114                    "sorted `side` must be one of {{'any', 'left', 'right'}}, got {v}",
1115                )))
1116            },
1117        };
1118        Ok(Wrap(parsed))
1119    }
1120}
1121
1122impl<'py> FromPyObject<'py> for Wrap<ClosedInterval> {
1123    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1124        let parsed = match &*ob.extract::<PyBackedStr>()? {
1125            "both" => ClosedInterval::Both,
1126            "left" => ClosedInterval::Left,
1127            "right" => ClosedInterval::Right,
1128            "none" => ClosedInterval::None,
1129            v => {
1130                return Err(PyValueError::new_err(format!(
1131                    "`closed` must be one of {{'both', 'left', 'right', 'none'}}, got {v}",
1132                )))
1133            },
1134        };
1135        Ok(Wrap(parsed))
1136    }
1137}
1138
1139impl<'py> FromPyObject<'py> for Wrap<WindowMapping> {
1140    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1141        let parsed = match &*ob.extract::<PyBackedStr>()? {
1142            "group_to_rows" => WindowMapping::GroupsToRows,
1143            "join" => WindowMapping::Join,
1144            "explode" => WindowMapping::Explode,
1145            v => {
1146                return Err(PyValueError::new_err(format!(
1147                "`mapping_strategy` must be one of {{'group_to_rows', 'join', 'explode'}}, got {v}",
1148            )))
1149            },
1150        };
1151        Ok(Wrap(parsed))
1152    }
1153}
1154
1155impl<'py> FromPyObject<'py> for Wrap<JoinValidation> {
1156    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1157        let parsed = match &*ob.extract::<PyBackedStr>()? {
1158            "1:1" => JoinValidation::OneToOne,
1159            "1:m" => JoinValidation::OneToMany,
1160            "m:m" => JoinValidation::ManyToMany,
1161            "m:1" => JoinValidation::ManyToOne,
1162            v => {
1163                return Err(PyValueError::new_err(format!(
1164                    "`validate` must be one of {{'m:m', 'm:1', '1:m', '1:1'}}, got {v}",
1165                )))
1166            },
1167        };
1168        Ok(Wrap(parsed))
1169    }
1170}
1171
1172impl<'py> FromPyObject<'py> for Wrap<MaintainOrderJoin> {
1173    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1174        let parsed = match &*ob.extract::<PyBackedStr>()? {
1175            "none" => MaintainOrderJoin::None,
1176            "left" => MaintainOrderJoin::Left,
1177            "right" => MaintainOrderJoin::Right,
1178            "left_right" => MaintainOrderJoin::LeftRight,
1179            "right_left" => MaintainOrderJoin::RightLeft,
1180            v => {
1181                return Err(PyValueError::new_err(format!(
1182                    "`maintain_order` must be one of {{'none', 'left', 'right', 'left_right', 'right_left'}}, got {v}",
1183                )))
1184            },
1185        };
1186        Ok(Wrap(parsed))
1187    }
1188}
1189
1190#[cfg(feature = "csv")]
1191impl<'py> FromPyObject<'py> for Wrap<QuoteStyle> {
1192    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1193        let parsed = match &*ob.extract::<PyBackedStr>()? {
1194            "always" => QuoteStyle::Always,
1195            "necessary" => QuoteStyle::Necessary,
1196            "non_numeric" => QuoteStyle::NonNumeric,
1197            "never" => QuoteStyle::Never,
1198            v => {
1199                return Err(PyValueError::new_err(format!(
1200                    "`quote_style` must be one of {{'always', 'necessary', 'non_numeric', 'never'}}, got {v}",
1201                )))
1202            },
1203        };
1204        Ok(Wrap(parsed))
1205    }
1206}
1207
1208#[cfg(feature = "cloud")]
1209pub(crate) fn parse_cloud_options(uri: &str, kv: Vec<(String, String)>) -> PyResult<CloudOptions> {
1210    let out = CloudOptions::from_untyped_config(uri, kv).map_err(PyPolarsErr::from)?;
1211    Ok(out)
1212}
1213
1214#[cfg(feature = "list_sets")]
1215impl<'py> FromPyObject<'py> for Wrap<SetOperation> {
1216    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1217        let parsed = match &*ob.extract::<PyBackedStr>()? {
1218            "union" => SetOperation::Union,
1219            "difference" => SetOperation::Difference,
1220            "intersection" => SetOperation::Intersection,
1221            "symmetric_difference" => SetOperation::SymmetricDifference,
1222            v => {
1223                return Err(PyValueError::new_err(format!(
1224                    "set operation must be one of {{'union', 'difference', 'intersection', 'symmetric_difference'}}, got {v}",
1225                )))
1226            }
1227        };
1228        Ok(Wrap(parsed))
1229    }
1230}
1231
1232pub(crate) fn parse_fill_null_strategy(
1233    strategy: &str,
1234    limit: FillNullLimit,
1235) -> PyResult<FillNullStrategy> {
1236    let parsed = match strategy {
1237        "forward" => FillNullStrategy::Forward(limit),
1238        "backward" => FillNullStrategy::Backward(limit),
1239        "min" => FillNullStrategy::Min,
1240        "max" => FillNullStrategy::Max,
1241        "mean" => FillNullStrategy::Mean,
1242        "zero" => FillNullStrategy::Zero,
1243        "one" => FillNullStrategy::One,
1244        e => {
1245            return Err(PyValueError::new_err(format!(
1246                "`strategy` must be one of {{'forward', 'backward', 'min', 'max', 'mean', 'zero', 'one'}}, got {e}",
1247            )))
1248        }
1249    };
1250    Ok(parsed)
1251}
1252
1253#[cfg(feature = "parquet")]
1254pub(crate) fn parse_parquet_compression(
1255    compression: &str,
1256    compression_level: Option<i32>,
1257) -> PyResult<ParquetCompression> {
1258    let parsed = match compression {
1259        "uncompressed" => ParquetCompression::Uncompressed,
1260        "snappy" => ParquetCompression::Snappy,
1261        "gzip" => ParquetCompression::Gzip(
1262            compression_level
1263                .map(|lvl| {
1264                    GzipLevel::try_new(lvl as u8)
1265                        .map_err(|e| PyValueError::new_err(format!("{e:?}")))
1266                })
1267                .transpose()?,
1268        ),
1269        "lzo" => ParquetCompression::Lzo,
1270        "brotli" => ParquetCompression::Brotli(
1271            compression_level
1272                .map(|lvl| {
1273                    BrotliLevel::try_new(lvl as u32)
1274                        .map_err(|e| PyValueError::new_err(format!("{e:?}")))
1275                })
1276                .transpose()?,
1277        ),
1278        "lz4" => ParquetCompression::Lz4Raw,
1279        "zstd" => ParquetCompression::Zstd(
1280            compression_level
1281                .map(|lvl| {
1282                    ZstdLevel::try_new(lvl)
1283                        .map_err(|e| PyValueError::new_err(format!("{e:?}")))
1284                })
1285                .transpose()?,
1286        ),
1287        e => {
1288            return Err(PyValueError::new_err(format!(
1289                "parquet `compression` must be one of {{'uncompressed', 'snappy', 'gzip', 'lzo', 'brotli', 'lz4', 'zstd'}}, got {e}",
1290            )))
1291        }
1292    };
1293    Ok(parsed)
1294}
1295
1296pub(crate) fn strings_to_pl_smallstr<I, S>(container: I) -> Vec<PlSmallStr>
1297where
1298    I: IntoIterator<Item = S>,
1299    S: AsRef<str>,
1300{
1301    container
1302        .into_iter()
1303        .map(|s| PlSmallStr::from_str(s.as_ref()))
1304        .collect()
1305}
1306
1307#[derive(Debug, Copy, Clone)]
1308pub struct PyCompatLevel(pub CompatLevel);
1309
1310impl<'a> FromPyObject<'a> for PyCompatLevel {
1311    fn extract_bound(ob: &Bound<'a, PyAny>) -> PyResult<Self> {
1312        Ok(PyCompatLevel(if let Ok(level) = ob.extract::<u16>() {
1313            if let Ok(compat_level) = CompatLevel::with_level(level) {
1314                compat_level
1315            } else {
1316                return Err(PyValueError::new_err("invalid compat level"));
1317            }
1318        } else if let Ok(future) = ob.extract::<bool>() {
1319            if future {
1320                CompatLevel::newest()
1321            } else {
1322                CompatLevel::oldest()
1323            }
1324        } else {
1325            return Err(PyTypeError::new_err(
1326                "'compat_level' argument accepts int or bool",
1327            ));
1328        }))
1329    }
1330}
1331
1332#[cfg(feature = "string_normalize")]
1333impl<'py> FromPyObject<'py> for Wrap<UnicodeForm> {
1334    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1335        let parsed = match &*ob.extract::<PyBackedStr>()? {
1336            "NFC" => UnicodeForm::NFC,
1337            "NFKC" => UnicodeForm::NFKC,
1338            "NFD" => UnicodeForm::NFD,
1339            "NFKD" => UnicodeForm::NFKD,
1340            v => {
1341                return Err(PyValueError::new_err(format!(
1342                    "`form` must be one of {{'NFC', 'NFKC', 'NFD', 'NFKD'}}, got {v}",
1343                )))
1344            },
1345        };
1346        Ok(Wrap(parsed))
1347    }
1348}