Git Product home page Git Product logo

pyo3-polars's Introduction

1. Shared library plugins for Polars

Documentation for this functionality may also be found in the Polars User Guide. This is new functionality and should be preferred over 2. as this will circumvent the GIL and will be the way we want to support extending polars.

Parallelism and optimizations are managed by the default polars runtime. That runtime will call into the plugin function. The plugin functions are compiled separately.

We can therefore keep polars more lean and maybe add support for a polars-distance, polars-geo, polars-ml, etc. Those can then have specialized expressions and don't have to worry as much for code bloat as they can be optionally installed.

The idea is that you define an expression in another Rust crate with a proc_macro polars_expr.

The macro may have one of the following attributes:

  • output_type -> to define the output type of that expression
  • output_type_func -> to define a function that computes the output type based on input types.
  • output_type_func_with_kwargs -> to define a function that computes the output type based on input types and keyword args.

Here is an example of a String conversion expression that converts any string to pig latin:

fn pig_latin_str(value: &str, capitalize: bool, output: &mut String) {
    if let Some(first_char) = value.chars().next() {
        if capitalize {
            for c in value.chars().skip(1).map(|char| char.to_uppercase()) {
                write!(output, "{c}").unwrap()
            }
            write!(output, "AY").unwrap()
        } else {
            let offset = first_char.len_utf8();
            write!(output, "{}{}ay", &value[offset..], first_char).unwrap()
        }
    }
}

#[derive(Deserialize)]
struct PigLatinKwargs {
    capitalize: bool,
}

#[polars_expr(output_type=String)]
fn pig_latinnify(inputs: &[Series], kwargs: PigLatinKwargs) -> PolarsResult<Series> {
    let ca = inputs[0].str()?;
    let out: StringChunked =
        ca.apply_to_buffer(|value, output| pig_latin_str(value, kwargs.capitalize, output));
    Ok(out.into_series())
}

This can then be exposed on the Python side:

import polars as pl
from polars.type_aliases import IntoExpr
from polars.utils.udfs import _get_shared_lib_location

from expression_lib.utils import parse_into_expr

lib = _get_shared_lib_location(__file__)


def pig_latinnify(expr: IntoExpr, capitalize: bool = False) -> pl.Expr:
    expr = parse_into_expr(expr)
    return expr.register_plugin(
        lib=lib,
        symbol="pig_latinnify",
        is_elementwise=True,
        kwargs={"capitalize": capitalize},
    )

Compile/ship and then it is ready to use:

import polars as pl
from expression_lib import language

df = pl.DataFrame({
    "names": ["Richard", "Alice", "Bob"],
})


out = df.with_columns(
   pig_latin = language.pig_latinnify("names")
)

Alternatively, you can register a custom namespace, which enables you to write:

out = df.with_columns(
   pig_latin = pl.col("names").language.pig_latinnify()
)

See the full example in [example/derive_expression]: https://github.com/pola-rs/pyo3-polars/tree/main/example/derive_expression

2. Pyo3 extensions for Polars

See the example directory for a concrete example. Here we send a polars DataFrame to rust and then compute a jaccard similarity in parallel using rayon and rust hash sets.

Run example

$ cd example && make install $ venv/bin/python run.py

This will output:

shape: (2, 2)
┌───────────┬───────────────┐
│ list_a    ┆ list_b        │
│ ---       ┆ ---           │
│ list[i64] ┆ list[i64]     │
╞═══════════╪═══════════════╡
│ [1, 2, 3] ┆ [1, 2, ... 8] │
│ [5, 5]    ┆ [5, 1, 1]     │
└───────────┴───────────────┘
shape: (2, 1)
┌─────────┐
│ jaccard │
│ ---     │
│ f64     │
╞═════════╡
│ 0.75    │
│ 0.5     │
└─────────┘

Compile for release

$ make install-release

What to expect

This crate offers a PySeries and a PyDataFrame which are simple wrapper around Series and DataFrame. The advantage of these wrappers is that they can be converted to and from python as they implement FromPyObject and IntoPy.

pyo3-polars's People

Contributors

andysham avatar cmdlineluser avatar dalcde avatar itamarst avatar marcogorelli avatar mariushelf avatar messense avatar ritchie46 avatar shoeboxam avatar simong85 avatar stinodego avatar wukan1986 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

pyo3-polars's Issues

` #[polars_expr(type_func=...)]` always fails

I haven't been able to get this feature to work locally, based on the example - note that the haversine expression isn't used in the test run.py file.

When I attempt to use the haversine expression, I get the following error

Traceback (most recent call last):
  File "/home/andy/git/pyo3-polars/example/derive_expression/run.py", line 13, in <module>
    out = df.with_columns(
  File "/home/andy/git/pyo3-polars/example/derive_expression/venv/lib/python3.10/site-packages/polars/dataframe/frame.py", line 7903, in with_columns
    return self.lazy().with_columns(*exprs, **named_exprs).collect(eager=True)
  File "/home/andy/git/pyo3-polars/example/derive_expression/venv/lib/python3.10/site-packages/polars/lazyframe/frame.py", line 3809, in with_columns
    return self._from_pyldf(self._ldf.with_columns(pyexprs))
pyo3_runtime.PanicException: called `Result::unwrap()` on an `Err` value: DlSym { desc: "/home/andy/git/pyo3-polars/example/derive_expression/expression_lib/expression_lib/expression_lib.cpython-310-x86_64-linux-gnu.so: undefined symbol: __polars_field_haversine" }
make: *** [Makefile:22: run] Error 1

Minimal reproduction

This modification to ./example/derive_expression/run.py returns the above error, everything else unchanged.

import polars as pl
from expression_lib import Language, Distance

df = pl.DataFrame({
    "names": ["Richard", "Alice", "Bob"],
    "moons": ["full", "half", "red"],
    "dist_a": [[12, 32, 1], [], [1, -2]],
    "dist_b": [[-12, 1], [43], [876, -45, 9]],
    "floats": [5.6, -1245.8, 242.224]
})


out = df.with_columns(
   pig_latin = pl.col("names").language.pig_latinnify(),
).with_columns(
    hamming_dist = pl.col("names").dist.hamming_distance("pig_latin"),
    jaccard_sim = pl.col("dist_a").dist.jaccard_similarity("dist_b"),
    haversine = pl.col("floats").dist.haversine("floats", "floats", "floats", "floats"),
)

print(out)

Potential fix

It seems that whenever the polars-plan crate resolves the type information of an FFI call, it looks for the symbol __polars_field_{}, where {} is the name of the Rust function under #[polars_expr(...)], see this LoC.

The function that is actually exposed for this field resolution is defined here for expressions of the pattern #[polars_expr(type_func=...)], and here for expressions of the pattern #[polars_expr(output_type=...)].

In the latter case, as fn_name is the function name itself (i.e. haversine in this case), get_field_name returns the correct symbol - in the former, fn_name is instead the name of the type resolution function haversine_output, meaning the symbol exposed in the FFI is instead __polars_field_haversine_output, which is unknown to polars-plan.

We can verify this by inspecting the .so file created by pyo3:

$ nm -D ./expression_lib/expression_lib/expression_lib.cpython-310-x86_64-linux-gnu.so | grep " T"
00000000001d0eb0 T __polars_field_hamming_distance
00000000001d14f0 T __polars_field_haversine_output
00000000001d0890 T __polars_field_jaccard_similarity
00000000001d0340 T __polars_field_pig_latinnify
00000000001d1110 T hamming_distance
00000000001d1710 T haversine
00000000001d0af0 T jaccard_similarity
00000000001d05a0 T pig_latinnify

If the fix is simply changing the symbol here, I have implemented this in this PR.

Expose expressions API

This crate exposes Series and DataFrame, but sometimes people write a bunch of functions that return Exprs (e.g. tidypolars).

It would be nice to be able to write these kinds of functions once in Rust and reuse them in Python.

Not sure if my favourite PyO3 issue makes this impossible for now

Support `pyo3` v0.18.0

I was going to use this with pyo3-asyncio, but it requires 0.18.0.

Any chance to add support for this any time soon?

Support for Using Polars Extension with `over` Syntax

Description

I am currently working on a Polars extension and am interested in leveraging the over syntax in conjunction with my extension. However, it seems that there might be limitations or challenges in using both simultaneously.

Expected Behavior

I would like to request support or guidance on how to seamlessly integrate my Polars extension with the over syntax. It would be beneficial for the extension to work seamlessly with existing over functionality.

I have explored the documentation and searched for relevant examples, but I couldn't find a clear solution. Any assistance or insights into making the Polars extension compatible with the over syntax would be greatly appreciated. If there are specific considerations or workarounds, please provide them.

Minimal plugin may segfault

I've posted the smallest reproducible example I could come up with here: https://github.com/MarcoGorelli/mcve-segfaulty-plugin

It doesn't always segfault, just sometimes (I tried running pytest now, and it failed 6 times out of 10)

If I remove the argument other from def sub, then pytest always passes

I'll continue trying to narrow this down, but reporting in the meantime

Allow for custom datasources as plugins

The current system makes it pretty easy to add new transformations (expr)'s as plugins, but there is currently no good way for users to provide custom datasources.


Ideally, custom datasources should be as easy as implementing a trait or macro. There is already the AnonymousScan trait that mostly works for this use case, but doesn't work via pyo3-polars due to (de)serialization issues (see #67). Maybe we can have an FFI equivalent instead of the in memory AnonymousScan?

If we loosely base it off of datafusion's TableProvider it may look something like this

pub struct DummyDatasource {}

impl PolarsDatasource for DummyDatasource {
  fn schema(&self) -> SchemaRef {
    Arc::new(Schema::empty())
  }
  fn scan(&self, projection: &Option<Vec<usize>>, filters: &[Expr], limit: Option<usize>) -> Result<Box<dyn Executor>> {
    Ok(Box::new(DummyExec::new()))}
  }
}

pub struct DummyExec {}

impl DummyExec {
    pub fn new() -> Self {
        DummyExec {}
    }
}

impl Executor for DummyExec {
    fn execute(&mut self, cache: &mut ExecutionState) -> PolarsResult<DataFrame> {
        Ok(DataFrame::empty())
    };
}

Related issues

#67

Adding support of PyLazyFrame

Moving pola-rs/polars#7933 here

I am trying to implement an anonymous scan method (with the AnonymousScan trait) reading data from custom sources. I am also hoping to make the resulting LazyFrame available in python.

Using the pyo3-polars crate, I am able to convert between DataFrame in Python and Rust. However, for LazyFrame, it seems that we don't have an easy option to pass it to Python.

Maintain input series names, in rust, when a plugin is called within .over() context

Hi - thanks a lot for making it easy to write nice polars plugins!

In my plugin extension I produce a polars series struct output with field names based on the names of the passed &[inputs] (context is naming least squares coefficients and returning a struct series after doing some manipulation to inputs).

This seemingly works well when called in a normal context, but when the plugin extension expression is chained with .over() the input series appear to have empty names ("").

Here is a simplified dummy example:

fn output_struct_dtype(input_fields: &[Field]) -> PolarsResult<Field> {
    for field in input_fields {
        println!("field_name={:?}", field.name());
    }
    Ok(Field::new(
        "coefficients",
        DataType::Struct(input_fields.to_vec()),
    ))
}

#[polars_expr(output_type_func=output_struct_dtype)]
fn inputs_to_struct(inputs: &[Series]) -> PolarsResult<Series> {
    for input in inputs {
        println!("series_name={:?}", input.name());
    }
    // Create DataFrame from the vector of Series
    let df = DataFrame::new(inputs.to_vec()).unwrap();
    // Convert DataFrame to a Series of struct dtype
    Ok(df.into_struct("coefficients").into_series().with_name("coefficients"))
}

Now on python side let's say we have:

def convert_series_to_struct(*inputs: pl.Expr) -> pl.Expr:
    return register_plugin_function(
        plugin_path=Path(__file__).parent,
        function_name="inputs_to_struct",
        args=inputs,
    )
  1. First we try select on the expression without an .over() context:
from test_project import convert_series_to_struct

df = pl.DataFrame({"y": [1.16, -2.16, -1.57, 0.21, 0.22, 1.6, -2.11, -2.92, -0.86, 0.47],
                   "x1": [0.72, -2.43, -0.63, 0.05, -0.07, 0.65, -0.02, -1.64, -0.92, -0.27],
                   "x2": [0.24, 0.18, -0.95, 0.23, 0.44, 1.01, -2.08, -1.36, 0.01, 0.75],
                   "group": [1, 1, 1, 1, 1, 2, 2, 2, 2, 2],
                   "weights": [0.34, 0.97, 0.39, 0.8, 0.57, 0.41, 0.19, 0.87, 0.06, 0.34],
                   })

>> df.select(convert_series_to_struct(pl.col("x1"), pl.col("x2"), pl.col("x3"))      
image
  1. Next, let's try select on expression chained with .over() with "POLARS_VERBOSE" set:
df.select(convert_series_to_struct(pl.col("x1"), pl.col("x2")).over("group"))
image

Notice that the input series names are lost (but the input fields which is used for the output type annotation don't) -- which causes a duplicate error.

So far I've side-stepped this by naming the interrim dataframe, in rust, with some arbitrary column names ("1", "2", ..., "n") and then calling something like .struct.rename_fields([f.meta.output_name() for f in features]) but this is blocking using input_wildcard_expansion=True and is probably not clean.

Any idea if it is easy to propagate series names like you do for fields? Or any settings etc. that I may be missing?

Thanks a lot!

LazyFrame::anonymous_scan can't be send to python

Hello I'm trying to create a module with maturin and LazyFrame::anonymous_scan
Compilation works just fine but unfortunately it crashes at runtime
The goal is to implement projection_pushdown and predicate_pushdown once it's working

src/lib.rs

use polars::prelude::*;
use pyo3::prelude::*;
use pyo3_polars::PyLazyFrame;
use std::any::Any;

#[pymodule]
fn bigtable2(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(get, m)?)?;
    Ok(())
}

#[pyfunction(name = "test")]
pub fn get() -> PyLazyFrame {
    LazyFrame::anonymous_scan(
        Arc::new(BigtableScan {}),
        ScanArgsAnonymous {
            schema: Some(Arc::new(Schema::from_iter([Field::new(
                "a",
                DataType::UInt32,
            )]))),
            ..Default::default()
        },
    )
    .map(PyLazyFrame)
    // .inspect(|lf| println!("{:?}", lf.0.logical_plan))
    .expect("anonymous_scan error")
}

pub struct BigtableScan {}
impl AnonymousScan for BigtableScan {
    fn as_any(&self) -> &dyn Any {
        unimplemented!()
    }

    fn scan(&self, _scan_opts: AnonymousScanArgs) -> PolarsResult<DataFrame> {
        df!("a" => [1u32, 2u32])
    }
}

Cargo.toml

[package]
name = "bigtable2"
version = "0.0.1"
edition = "2021"

[lib]
name = "bigtable2"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.20.2", features = ["extension-module"] }
pyo3-polars = { version = "0.11.3", features = ["lazy"] }
polars = { version = "0.37.0", features = ["lazy"] }

rust-toolchain.toml

[toolchain]
channel = "nightly-2024-01-24"
profile = "minimal"

Terminal

> maturin develop # works fine
> python3 -c "import bigtable2; print(bigtable2.test())"
thread '<unnamed>' panicked at /Users/<USER>/.cargo/registry/src/index.crates.io-6f17d22bba15001f/pyo3-polars-0.11.3/src/lib.rs:244:71:
called `Result::unwrap()` on an `Err` value: Value("the enum variant FileScan::Anonymous cannot be serialized")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Traceback (most recent call last):
  File "<string>", line 1, in <module>
pyo3_runtime.PanicException: called `Result::unwrap()` on an `Err` value: Value("the enum variant FileScan::Anonymous cannot be serialized")

How to pass kwarg to func output type

We added a function in polars-xdt which changes the timezone of the datetime, this know at hand since we pass the time-zone through the kwargs, see some example code here to mimic the behaviour:

#[derive(Deserialize)]
pub struct OutputKwargs{
    to_tz: std::string::String,
}

pub fn output(input_fields: &[Field]) -> PolarsResult<Field> {
    let field = input_fields[0].clone();
    let dtype = match field.dtype {
        DataType::Datetime(unit, _) => DataType::Datetime(unit, "SET TZ HERE from KWARGS"),
        _ => polars_bail!(InvalidOperation:
            "dtype '{}' not supported", field.dtype
        ),
    };
    Ok(Field::new(&field.name, dtype))
}
#[polars_expr(output_type_func=output)]
fn function(inputs: &[Series], kwargs: OutputKwargs) -> PolarsResult<Series> {
    let s1 = &inputs[0];
    let ca = s1.datetime().unwrap();
    let converted = ca.clone().convert_time_zone(kwargs.to_tz)?;
    
    Ok(converted.clone().into_series())
}

I would like to pass the kargs.to_tz to the from_local_datetime_output, is this possible? If not how should I handle the func output type in that case.

Do you have any ideas @ritchie46?

Please add examples of passing dataframes between python and rust

this simple task seems hard. there are 2 examples and none for the simple task of sending a dataframe from python to rust. Maybe i'm just missing something but, basics before brilliance, I could really use help for this, could you please add super basic examples of how to send a pandas dataframe to rust using this crate? If we need to convert pandas to polars or pandas to arrow in python, fine, but I'm just hitting a wall of compile errors and it's really annoying for such a simple thing to be difficult.

`ArrowInvalid` when return a `PyDataFrame` to python

After update polars to 0.37 and pyo3-polars to 0.11.1, I find there would be an error when return PyResult<PyDataFrame> to Python:

thread '<unnamed>' panicked at /Users/sun/.cargo/registry/src/index.crates.io-6f17d22bba15001f/pyo3-polars-0.11.1/src/lib.rs:169:49:
called `Result::unwrap()` on an `Err` value: PyErr { type: <class 'pyarrow.lib.ArrowInvalid'>, value: ArrowInvalid("Invalid or unsupported format string: 'vu'"), traceback: Some(<traceback object at 0x1032dfcc0>) }

My code works fine with polars 0.36 and pyo3-polars 0.10.0, and I have run cargo clean and delete cargo.lock. I print the DataFrame in rust side which is below:
WX20240201-174527@2x

I find no "vu" in the df.

Add recommended packaging practices

From my naive reading of this repo, it looks like this builds a locally installed pyo3-polars extension module. How should this be packaged up into a wheel or conda package?

Could you add those steps/scripts to the repo so users start following best practices in building pyo3 polars extensions?

If this is already documented somewhere else (pyo3 core)? could you link to that documentation and provide context? I stumbled onto this repo b ecause I was reading about the new expression-plugins in 0.19.9, but this is the first time I have seen pyo3 code.

`LazyFrame` example is broken

EDIT:

My bad, was not using current version of python-polars. Problem solved with update.

And I think the problem may be the LazyFrame handler is not working, rather than the example.

Reproducible error:

lf = (
    pl.DataFrame({
        "list_a": [[1, 2, 3], [5, 5]],
        "list_b": [[1, 2, 3, 8], [5, 1, 1]]
    })
    .lazy()
)
lazy_parallel_jaccard(lf, "list_a", "list_b")

output:

PanicException: called `Result::unwrap()` on an `Err` value: PyErr { type: <class 'RuntimeError'>, value: RuntimeError('BindingsError: "Semantic(None, \\"unknown field `bit_settings`, expected one of `name`, `datatype`, `values`\\")"'), traceback: Some(<traceback object at 0x7f5febcf6540>) }

Kernel panics when passing list(categorical) as input

Passing a list(categorical) to a registered plugin expr will instantly kill the kernel:

Assume this simple expression

#[polars_expr(output_type=Float64)]
fn test(inputs: &[Series]) -> PolarsResult<Series> {
    Ok(Series::from_vec("TEST", vec![0.0]))
}

Then this plugin code:

@pl.api.register_expr_namespace("testing")
class TestSpace:
    def __init__(self, expr: pl.Expr):
        self._expr = expr

    def test(self) -> pl.Expr:
        """test"""
        return self._expr.register_plugin(
            lib=lib,
            symbol="test",
            is_elementwise=True,
        )

Now exucting this expression on list(categorical) will faill.

df = pl.DataFrame(
    {"x": [["1"]],},
    schema={"x": pl.List(pl.Categorical)},
)

df.with_columns(
    pl.col("x").testing.test(),
)

image

However this runs fine:

df = pl.DataFrame(
    {"x": [["1"]]},
    schema={"x": pl.List(pl.Utf8)},
)

df.with_columns(
    pl.col("x").dist_list.test(),
)
shape: (1, 1)
┌─────┐
│ x   │
│ --- │
│ f64 │
╞═════╡
│ 0.0 │
└─────┘

expression plugin function with multiple columns as input

I've looked as the example but the haversine one is a bit confusing.
It looks like the python example gives 5 parameters to the Rust function so the last one is ignored as the functions only takes 4 parameters.

What is the best way to implement a function that takes multiple columns as parameters and still use all the parallelism and optimization from Polars?

Allow to set expression output type based on kwargs

I'm currently experimenting with the expression plugins and, in particular, I'd like to implement a function

to_dummies(categories: list[str])

that turns a single column into a struct with the fields provided in categories.

Unfortunately, one cannot currently define the output_type of a polars_expr based on the kwargs passed to the function but only based on the input dtypes.

Rust macros are not my strong suite, unfortunately, so I can't really judge if this is feasible at all 👀

Upgrade to PyO3 0.21.1

I'm receiving the following error when trying to install pyo3-polars after upgrading to pyo3 version 0.21.1:

error: failed to select a version for `pyo3-ffi`.
    ... required by package `pyo3 v0.20.0`
    ... which satisfies dependency `pyo3 = "^0.20.0"` of package `pyo3-polars v0.12.0`
    ... which satisfies dependency `pyo3-polars = "^0.12.0"` of package <my-package>`
versions that meet the requirements `=0.20.0` are: 0.20.0

the package `pyo3-ffi` links to the native library `python`, but it conflicts with a previous package which links to `python` as well:
package `pyo3-ffi v0.21.1`
    ... which satisfies dependency `pyo3-ffi = "^0.21.1"` of package `<my-package>`
Only one package in the dependency graph may specify the same links value. This helps ensure that only one copy of a native library is linked in the final binary. Try to adjust your dependencies so that only one package uses the `links = "python"` value. For more information, see https://doc.rust-lang.org/cargo/reference/resolver.html#links.

failed to select a version for `pyo3-ffi` which could resolve this conflict

Basically this issue: #38.

Would it be possible to upgrade to pyo3 version 0.21.1? (Happy to create the PR myself.)

Forced rechunking

This took me a while to figure out (since this was the last place I'd expect a forced rechunk to happen) - while passing huge frames from Python to Rust and back, noticed that they end up arriving in one chunk even if they were multi-chunked originally.

Is there any reason to not leave rechunking to the end-user? (since in some cases it may end up being very detrimental)

let ob = ob.call_method0("rechunk")?;

... and also this:

let s = self.0.rechunk();

Polars 0.26 rc1: plugins panic when passing String input

To reproduce:

Cargo.toml:

[package]
name = "minimal_plugin"
version = "0.1.0"
edition = "2021"

[lib]
name = "minimal_plugin"
crate-type= ["cdylib"]

[dependencies]
pyo3 = { version = "0.20.0", features = ["extension-module"] }
pyo3-polars = { version = "0.10.0", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
polars = { version = "0.36.2", default-features = false }

[target.'cfg(target_os = "linux")'.dependencies]
jemallocator = { version = "0.5", features = ["disable_initial_exec_tls"] }

pyproject.toml:

[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"

[project]
name = "minimal_plugin"  # Should match the folder with your code!
requires-python = ">=3.8"
classifiers = [
  "Programming Language :: Rust",
  "Programming Language :: Python :: Implementation :: CPython",
  "Programming Language :: Python :: Implementation :: PyPy",
]

minimal_plugin/__init__.py:

import polars as pl
from polars.utils.udfs import _get_shared_lib_location
from polars.type_aliases import IntoExpr

lib = _get_shared_lib_location(__file__)


def noop(expr: pl.Expr) -> pl.Expr:
    return expr.register_plugin(
        lib=lib,
        symbol="noop",
        is_elementwise=True,
    )

src/expressions.rs

#![allow(clippy::unused_unit)]
use polars::prelude::arity::binary_elementwise;
use polars::prelude::*;
use pyo3_polars::derive::polars_expr;

fn same_output_type(input_fields: &[Field]) -> PolarsResult<Field> {
    let field = &input_fields[0];
    Ok(field.clone())
}

#[polars_expr(output_type_func=same_output_type)]
fn noop(inputs: &[Series]) -> PolarsResult<Series> {
    let s = &inputs[0];
    Ok(s.clone())
}

src/lib.rs:

mod expressions;

#[cfg(target_os = "linux")]
use jemallocator::Jemalloc;

#[global_allocator]
#[cfg(target_os = "linux")]
static ALLOC: Jemalloc = Jemalloc;

run.py

import polars as pl
import minimal_plugin as mp

df = pl.DataFrame({'a': ['bob', 'billy']})
print(df.with_columns(mp.noop(pl.col('a'))))

This gives:

$ POLARS_VERBOSE=1  python run.py 
panicked at src/expressions.rs:17:1:
called `Result::unwrap()` on an `Err` value: ComputeError(ErrString("The datatype \"vu\" is still not supported in Rust implementation"))
Traceback (most recent call last):
  File "/home/marcogorelli/polars-plugins-minimal-examples/run.py", line 5, in <module>
    print(df.with_columns(mp.noop('a')))
  File "/home/marcogorelli/polars-plugins-minimal-examples/venv/lib/python3.10/site-packages/polars/dataframe/frame.py", line 8281, in with_columns
    return self.lazy().with_columns(*exprs, **named_exprs).collect(_eager=True)
  File "/home/marcogorelli/polars-plugins-minimal-examples/venv/lib/python3.10/site-packages/polars/lazyframe/frame.py", line 1730, in collect
    return wrap_df(ldf.collect())
polars.exceptions.ComputeError: the plugin panicked

The message is suppressed. Set POLARS_VERBOSE=1 to send the panic message to stderr.

Error originated just after this operation:
DF ["a"]; PROJECT */1 COLUMNS; SELECTION: "None"

plugins' names not respected?

Here's a reproducible example:

Cargo.toml

[package]
name = "minimal_plugin"
version = "0.1.0"
edition = "2021"

[lib]
name = "minimal_plugin"
crate-type= ["cdylib"]

[dependencies]
pyo3 = { version = "0.20.0", features = ["extension-module"] }
pyo3-polars = { version = "0.10.0", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
polars = { version = "0.36.2", default-features = false }

[target.'cfg(target_os = "linux")'.dependencies]
jemallocator = { version = "0.5", features = ["disable_initial_exec_tls"] }

pyproject.toml

[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"

[project]
name = "minimal_plugin"
requires-python = ">=3.8"
classifiers = [
  "Programming Language :: Rust",
  "Programming Language :: Python :: Implementation :: CPython",
  "Programming Language :: Python :: Implementation :: PyPy",
]

minimal_plugin/__init__.py:

import polars as pl
from polars.utils.udfs import _get_shared_lib_location
from polars.type_aliases import IntoExpr

lib = _get_shared_lib_location(__file__)


@pl.api.register_expr_namespace("mp")
class MinimalExamples:
    def __init__(self, expr: pl.Expr):
        self._expr = expr

    def rename(self) -> pl.Expr:
        return self._expr.register_plugin(
            lib=lib,
            symbol="rename",
            is_elementwise=True,
        )

src/lib.rs:

mod expressions;

#[cfg(target_os = "linux")]
use jemallocator::Jemalloc;

#[global_allocator]
#[cfg(target_os = "linux")]
static ALLOC: Jemalloc = Jemalloc;

src/expressions.rs:

#![allow(clippy::unused_unit)]
use polars::prelude::*;
use pyo3_polars::derive::polars_expr;

fn same_output_type(input_fields: &[Field]) -> PolarsResult<Field> {
    let field = &input_fields[0];
    Ok(field.clone())
}

#[polars_expr(output_type_func=same_output_type)]
fn rename(inputs: &[Series]) -> PolarsResult<Series> {
    let mut s = inputs[0].clone();
    s.rename("foo");
    Ok(s)
}

run.py

import polars as pl
import minimal_plugin  # noqa: F401

df = pl.DataFrame({'a': [1,2,3], 'b': [4,5,6]})
print(df.with_columns(pl.col('a').mp.rename()))

This outputs:

shape: (3, 2)
┌─────┬─────┐
│ a   ┆ b   │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1   ┆ 4   │
│ 2   ┆ 5   │
│ 3   ┆ 6   │
└─────┴─────┘

Expected:

shape: (3, 3)
┌─────┬─────┬─────┐
│ abfoo │
│ --------- │
│ i64i64i64 │
╞═════╪═════╪═════╡
│ 141   │
│ 252   │
│ 363   │
└─────┴─────┴─────┘

Upgrade version

Should we update the version of this package in order for it to work with polars 0.35?

Sorted flag not preserved when returning the PyDataFrame to Rust

After sorting DataFrame in Python, the sorted flag is not preserved and is set to IsSorted::Not value in Rust.

Cargo.toml:

[package]
name = "polars_sorted_flag"
version = "0.1.0"
edition = "2021"

[dependencies]
pyo3-polars = "0.9.0"
polars = "0.35.4"
pyo3 = "0.20.0"

main.rs:

use pyo3::{Py, PyAny, Python};
use pyo3::types::PyModule;
use pyo3_polars::PyDataFrame;

fn main() {
    pyo3::prepare_freethreaded_python();

    Python::with_gil(|py| {
        let code = "
import polars as pl
def get_sorted_df():
    df = pl.DataFrame(
        {
            'a': [9, 2, 0],
            'b': [6.0, 5.0, 4.0],
            'c': ['a', 'c', 'b'],
        }
    )
    return df.sort('a')

def get_is_sorted():
    df = get_sorted_df()
    return str(df['a'].is_sorted())
";

        let py_module = PyModule::from_code(py, code, "", "").unwrap();

        let get_sorted_df: Py<PyAny> = py_module.getattr("get_sorted_df").unwrap().into();
        let df = get_sorted_df.call0(py).unwrap().extract::<PyDataFrame>(py).unwrap().0;
        println!("df: {:?}", df);

        let is_sorted_flag = df.column("a").unwrap().is_sorted_flag();
        println!("Rust is_sorted_flag(): {:?}", is_sorted_flag);

        let get_is_sorted: Py<PyAny> = py_module.getattr("get_is_sorted").unwrap().into();
        let py_is_sorted = get_is_sorted.call0(py).unwrap().extract::<String>(py).unwrap();
        println!("Python is_sorted(): {:?}", py_is_sorted);
    })
}

Output:

df: shape: (3, 3)
┌─────┬─────┬─────┐
│ a   ┆ b   ┆ c   │
│ --- ┆ --- ┆ --- │
│ i64 ┆ f64 ┆ str │
╞═════╪═════╪═════╡
│ 0   ┆ 4.0 ┆ b   │
│ 2   ┆ 5.0 ┆ c   │
│ 9   ┆ 6.0 ┆ a   │
└─────┴─────┴─────┘
Rust is_sorted_flag(): Not
Python is_sorted(): "True"

BTW, how to properly format the Python script while avoiding Unexpected indent?

The first call to pyfunction returning PyDataFrame is slow

I found that the very first call to a pyfunction which returns PyDataFrame has a 100ms lag.

Here is a minimal reproducible example:

extension

use pyo3_polars::PyDataFrame;
use pyo3::prelude::*;

#[pyfunction]
fn dup(pydf: PyDataFrame) -> PyDataFrame {
    let df = pydf.0;
    let new_df = df.vstack(&df.clone()).unwrap();
    PyDataFrame(new_df)
    // Python::with_gil(|py| into_py(&PyDataFrame(new_df), py))
}

#[pymodule]
#[pyo3(name = "test")]
fn py(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(dup, m)?)?;
    Ok(())
}

ipython

In [1]: import test
   ...: import polars as pl
   ...: df = pl.DataFrame({'a': [1], 'b': [2]})

In [2]:

In [2]: %time py.dup(df)
Wall time: 110 ms
Out[2]:
shape: (2, 2)
┌─────┬─────┐
│ a   ┆ b   │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1   ┆ 2   │
│ 1   ┆ 2   │
└─────┴─────┘

In [3]: %time test.dup(df)
Wall time: 0 ns
Out[3]:
shape: (2, 2)
┌─────┬─────┐
│ a   ┆ b   │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1   ┆ 2   │
│ 1   ┆ 2   │
└─────┴─────┘

Is there anything I can do to eliminate this delay?

New polars version broke this wrapper

With the release 0.27 of polars this crate has been broken.

In particular, the function into() that allows to transform a PyDataFrame into a polars DataFrame is broken.

The compiler says:
"the trait bound polars::prelude::DataFrame: From<PyDataFrame> is not satisfied
the trait From<&polars::prelude::Schema> is implemented for polars::prelude::DataFrame
required for PyDataFrame to implement Into<polars::prelude::DataFrame>"

With 0.26 version of polars it works perfectly.

I have checked and also the example is broken.

0.7.0 in crates.io is not using pyo3==0.20

There seems to be a conflict on 0.7.0 with pyo3=0.20.

error: failed to select a version for `pyo3-ffi`.
    ... required by package `pyo3 v0.19.0`
    ... which satisfies dependency `pyo3 = "^0.19.0"` of package `pyo3-polars v0.7.0`
    ... which satisfies dependency `pyo3-polars = "^0.7.0"` of package `valar v0.37.4 (D:\SUN\valar)`
versions that meet the requirements `=0.19.0` are: 0.19.0

the package `pyo3-ffi` links to the native library `python`, but it conflicts with a previous package which links to `python` as well:
package `pyo3-ffi v0.20.0`
    ... which satisfies dependency `pyo3-ffi = "=0.20.0"` of package `pyo3 v0.20.0`
    ... which satisfies dependency `pyo3 = "^0.20"` of package `valar v0.37.4 (D:\SUN\valar)`
Only one package in the dependency graph may specify the same links value. This helps ensure that only one copy of a native library is linked in the final binary. Try to adjust your dependencies so that only one package uses the links ='pyo3-ffi' value. For more information, see https://doc.rust-lang.org/cargo/reference/resolver.html#links.

failed to select a version for `pyo3-ffi` which could resolve this conflict

Namespace API for DataFrame/LazyFrame

Currently, Polars supports defining custom functions on the DataFrame namespace on the python side with pl.api.register_dataframe_namespace/pl.api.register_lazyframe_namespace. Would it be possible for pyo3-polars to support creating a custom DataFrame namespace with this same functionality, with the defined transformations living in rust?

For a somewhat abstract example, I have a function written in rust that takes a pl.DataFrame as an argument, loads a xgboost Booster, calculates a column, then returns the DataFrame. I want to expose this exact same functionality in Python by registering some custom namespace.

Sharing reference in pyclass

Thanks for the great work with polars as well as the pyo3 exposition of dataframe/series.

I have the following problem (disclaimer still a couple of hours away form hello rust).

I want a rust struct containing several dataframes accessible in python with polars (and eventually zero copy to pandas with arrow).

I don't quite understand how to pass a shared reference in that case.

Here is a minimal example.

  • Cargo.toml
[dependencies]
pyo3 = "0.18.1"
polars = { version = "0.27", default_features = false }
polars-core = { version = "0.27", default_features = false }
thiserror = "1"
arrow2 = "0.16"
pyo3-polars = "0.2.0"
  • src/lib.rs
use pyo3::prelude::*;
use pyo3_polars::PyDataFrame;

#[pyclass]
struct Container {
    #[pyo3(get)]
    df: PyDataFrame,
}

#[pymethods]
impl Container {
    #[new]
    fn new(somedf: PyDataFrame) -> Self {
        let df = somedf.into();
        Self { df }
    }
}

#[pymodule]
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_class::<Container>()?;
    Ok(())
}

compilation is ok, now in python

import mylib                 # dumb module name  
import polars as pl

df = pl.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
mycontainer = mylib.Container(df)

print(mycontainer.df)           # print df

a = mycontainer.df
a[0, 'A'] = 5

print(a)                                  # the first row of col 'A' of a is now 5
print(mycontainer.df)         # is sill df

I thought that everything would be zero-copy or just passing of references so that any manipulation on the rust or python side would be on the same reference object.

I might be a bit naive (there is something about using some Py<..> for classes in pyo3 but I don't understand how it fits within pyo3-polars.

Many thanks and sorry for a beginner kind of question.

panic when passing Date column

I'm trying to make sense of pyo3-polars in order to write a plugin

I've tried writing the simplest function:

#[polars_expr(output_type=Date)]
fn add_bday(inputs: &[Series]) -> PolarsResult<Series> {
    let ca = inputs[0].date()?;
    Ok(ca.clone().into_series())
}

but I get

thread '<unnamed>' panicked at 'assertion failed: `(left == right)`
  left: `Date32`,
 right: `Int32`', /home/marcogorelli/.cargo/registry/src/index.crates.io-6f17d22bba15001f/polars-core-0.33.2/src/chunked_array/from.rs:166:17

Looking like

let inputs = polars_ffi::import_series_buffer(e, len).unwrap();

might be panicking?

Here's my complete repo, for a reproducible example: https://github.com/MarcoGorelli/my-wip-polars-extension

Pluggin list

Since this is an initial project, I propose to add an "*.md" file with a list of known extensions produced with this project, to avoid duplication of developments, something like:

PlugginDescriptionUrlGithub Score
polars-piglatinConverts any string to pig latinpyo3-polars ⭐ 1.8k stars

`LazyFrame` support

Is it possible to support the conversion of LazyFrame from Python to Rust, so that I can apply my custom logic on LazyFrame coming from Python side to Rust?

trait bound `polars::prelude::DataFrame: From<PyDataFrame>` is not satisfied

Trying to select a column based on regex as in the example of polars.

but I seem to not be able to convert the PyDataFrame to a Dataframe, or lazyframe

Not sure if I'm doing it wrong, or if the library is not complet.

Error on pydf.into();

the trait bound `polars::prelude::DataFrame: From<PyDataFrame>` is not satisfied
required for `PyDataFrame` to implement `Into<polars::prelude::DataFrame>`
// Might have some unused imports
use ndarray::{Array2, Zip, Array1};
use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2};
use polars::prelude::*;
use pyo3::{PyResult, Python, pyfunction};
use pyo3_polars::PyDataFrame;

#[pyfunction]
pub fn apply<'py>(_py: Python<'py>, pydf: PyDataFrame) -> PyResult<()> {

    let df: DataFrame = pydf.into();

    let df2 = df.clone().lazy().select([col("^col_*")]).collect();
    println!("{:?}", df2);
    Ok(())
}

I tried without the clone and lazy, but it still the same issue

    let df: DataFrame = pydf.into();
                         ^^^^ ---- required by a bound introduced by this call
                         |
                         the trait `From<PyDataFrame>` is not implemented for `polars::prelude::DataFrame`
    let df2 = df.select([col(&"^col_*")]).unwrap();
                 ^^^^^^ the trait `AsRef<str>` is not implemented for `Expr`

maybe it's simply a regex issue....

cannot find -lpython3.10: No such file or directory

derive_expression can not install

kan@DESKTOP-7DFCADR:~/test/pyo3-polars/example/derive_expression$ make install
python3 -m venv venv
venv/bin/pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
Writing to /home/kan/.config/pip/pip.conf
venv/bin/pip install -r requirements.txt
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Collecting maturin
  Using cached https://pypi.tuna.tsinghua.edu.cn/packages/88/c0/d4502bc09d630fb6813cfd83007875d29eb17fe92c505fe954c2fc45a6e1/maturin-1.4.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl (10.6 MB)
Collecting polars
  Using cached https://pypi.tuna.tsinghua.edu.cn/packages/ef/91/53eeb28756a25b6ccad7855f3f29eb9d54e202d0077981b39ce2f8221299/polars-0.19.19-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (28.5 MB)
Collecting tomli>=1.1.0
  Using cached https://pypi.tuna.tsinghua.edu.cn/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl (12 kB)
Installing collected packages: tomli, polars, maturin
Successfully installed maturin-1.4.0 polars-0.19.19 tomli-2.0.1
unset CONDA_PREFIX && \
source venv/bin/activate && maturin develop -m expression_lib/Cargo.toml
    Updating crates.io index
remote: Enumerating objects: 38034, done.
remote: Counting objects: 100% (23903/23903), done.
remote: Compressing objects: 100% (1066/1066), done.
remote: Total 38034 (delta 22876), reused 23836 (delta 22809), pack-reused 14131
Receiving objects: 100% (38034/38034), 28.43 MiB | 10.20 MiB/s, done.
Resolving deltas: 100% (28824/28824), completed with 1524 local objects.
From https://github.com/rust-lang/crates.io-index
   fe38024d35..29db9c3160             -> origin/HEAD
  Downloaded crossbeam-utils v0.8.17
  Downloaded crossbeam-deque v0.8.4
  Downloaded crossbeam-epoch v0.9.16
  Downloaded ryu v1.0.16
  Downloaded zerocopy v0.7.31
  Downloaded syn v2.0.41
  Downloaded zerocopy-derive v0.7.31
  Downloaded libc v0.2.151
  Downloaded 8 crates (1.8 MB) in 1m 18s
🍹 Building a mixed python/rust project
🔗 Found pyo3 bindings
🐍 Found CPython 3.10 at /home/kan/test/pyo3-polars/example/derive_expression/venv/bin/python
   Compiling autocfg v1.1.0
   Compiling libc v0.2.151
   Compiling version_check v0.9.4

......

   Compiling pyo3-polars-derive v0.3.0 (/home/kan/test/pyo3-polars/pyo3-polars-derive)
   Compiling pyo3-polars v0.9.0 (/home/kan/test/pyo3-polars/pyo3-polars)
   Compiling jemallocator v0.5.4
   Compiling expression_lib v0.1.0 (/home/kan/test/pyo3-polars/example/derive_expression/expression_lib)
error: linking with `cc` failed: exit status: 1
  |
  = note: LC_ALL="C" PATH="/home/kan/.rustup/toolchains/nightly-2023-10-12-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin:/home/kan/test/pyo3-polars/example/derive_expression/venv/bin:/home/kan/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:

......

/rustlib/x86_64-unknown-linux-gnu/lib" "-o" "/home/kan/test/pyo3-polars/target/debug/deps/libexpression_lib.so" "-Wl,--gc-sections" "-shared" "-Wl,-z,relro,-z,now" "-nodefaultlibs"
  = note: /usr/bin/ld: cannot find -lpython3.10: No such file or directory
          collect2: error: ld returned 1 exit status


error: could not compile `expression_lib` (lib) due to previous error
💥 maturin failed
  Caused by: Failed to build a native library through cargo
  Caused by: Cargo build finished with "exit status: 101": `env -u CARGO PYO3_ENVIRONMENT_SIGNATURE="cpython-3.10-64bit" PYO3_PYTHON="/home/kan/test/pyo3-polars/example/derive_expression/venv/bin/python" PYTHON_SYS_EXECUTABLE="/home/kan/test/pyo3-polars/example/derive_expression/venv/bin/python" "cargo" "rustc" "--message-format" "json-render-diagnostics" "--manifest-path" "/home/kan/test/pyo3-polars/example/derive_expression/expression_lib/Cargo.toml" "--lib"`
make: *** [Makefile:10: install] Error 1

Installed versions:

(venv) kan@DESKTOP-7DFCADR:~/test/pyo3-polars/example/derive_expression$ pip list
Package    Version
---------- -------
maturin    1.4.0
pip        22.0.2
polars     0.19.19
setuptools 59.6.0
tomli      2.0.1
(venv) kan@DESKTOP-7DFCADR:~/test/pyo3-polars/example/derive_expression$ python --version
Python 3.10.6
(venv) kan@DESKTOP-7DFCADR:~/test/pyo3-polars/example/derive_expression/venv/bin$ ls -al
total 31764
drwxr-xr-x 2 kan kan     4096 Dec 15 13:40 .
drwxr-xr-x 5 kan kan     4096 Dec 15 13:40 ..
-rw-r--r-- 1 kan kan     9033 Dec 15 13:40 Activate.ps1
-rw-r--r-- 1 kan kan     2024 Dec 15 13:40 activate
-rw-r--r-- 1 kan kan      950 Dec 15 13:40 activate.csh
-rw-r--r-- 1 kan kan     2092 Dec 15 13:40 activate.fish
-rwxr-xr-x 1 kan kan 32480392 Dec 15 13:40 maturin
-rwxr-xr-x 1 kan kan      274 Dec 15 13:40 pip
-rwxr-xr-x 1 kan kan      274 Dec 15 13:40 pip3
-rwxr-xr-x 1 kan kan      274 Dec 15 13:40 pip3.10
lrwxrwxrwx 1 kan kan        7 Dec 15 13:40 python -> python3
lrwxrwxrwx 1 kan kan       16 Dec 15 13:40 python3 -> /usr/bin/python3
lrwxrwxrwx 1 kan kan        7 Dec 15 13:40 python3.10 -> python3
(venv) kan@DESKTOP-7DFCADR:~/test/pyo3-polars/example/derive_expression/venv/lib$ ls -al
total 12
drwxr-xr-x 3 kan kan 4096 Dec 15 13:40 .
drwxr-xr-x 5 kan kan 4096 Dec 15 13:40 ..
drwxr-xr-x 3 kan kan 4096 Dec 15 13:40 python3.10

Cannot create polars series from UInt16 type

It seems that u16 is not supported. I tried to create a data frame where the dtype of my columns are set to u16, and when I try to pass this data frame to my rust program, I get

Cannot create polars series from UInt16 type

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.