Replies: 4 comments
-
It would absolutely be reasonable to explore if that is of interest to you. There should be no thread-safety constraints, once the extractors are created they're immutable (and anyway pyo3 requires exposed objects to be I didn't bother with it because I didn't really have a way to test it which was not completely artificial, and I never found clear guidance on performances: AFAIK the python interpreter releases the gil every 5ms, Looking at the polars docs it specifically calls out that An other option you might want to consider is to run free-threaded, looking at their bug tracker polars hasn't uploaded wheels yet but apparently free threading is supported, so it should work if built from source. ua-parser-rs already provides free-threaded wheels (though by the time you've built polars from source I'd think ua-parser-rs is hardly an issue anyway). That should give you full utilisation with strategy=threaded. Although full disclosure I’m not aware of anyone having ever used the free-threaded wheels, I just added them because I could. |
Beta Was this translation helpful? Give feedback.
-
|
Unfortunately the python versions I would need to support range from 3.11 onwards, so I dont think free threading would be enough to solve this. I will do a bit of investigation and see what I can come up with, thanks for the pointers. Maybe there is a way to get some batch processing working nicely. One option would be to create a polars extension that creates structs mirroring the python dataclass structure. I think that this would solve some problems though, e.g. there would be no per-row Python string/object allocation and polars handles the calls into rust code. Something like this might work: use polars::prelude::*;
use pyo3_polars::derive::polars_expr;
fn ua_output_type(_: &[Field]) -> PolarsResult<Field> {
Ok(Field::new(
"ua".into(),
DataType::Struct(vec![
Field::new(
"user_agent".into(),
DataType::Struct(vec![
Field::new("family".into(), DataType::String),
Field::new("major".into(), DataType::String),
Field::new("minor".into(), DataType::String),
Field::new("patch".into(), DataType::String),
]),
),
Field::new(
"os".into(),
DataType::Struct(vec![
Field::new("family".into(), DataType::String),
Field::new("major".into(), DataType::String),
Field::new("minor".into(), DataType::String),
Field::new("patch".into(), DataType::String),
Field::new("patch_minor".into(), DataType::String),
]),
),
Field::new(
"device".into(),
DataType::Struct(vec![
Field::new("family".into(), DataType::String),
Field::new("brand".into(), DataType::String),
Field::new("model".into(), DataType::String),
]),
),
]),
))
}
#[polars_expr(output_type_func=ua_output_type)]
fn parse_user_agents(inputs: &[Series]) -> PolarsResult<Series> {
let input = inputs[0].str()?;
let mut browser_family = Vec::with_capacity(input.len());
let mut browser_major = Vec::with_capacity(input.len());
let mut browser_minor = Vec::with_capacity(input.len());
let mut browser_patch = Vec::with_capacity(input.len());
let mut os_family = Vec::with_capacity(input.len());
let mut os_major = Vec::with_capacity(input.len());
let mut os_minor = Vec::with_capacity(input.len());
let mut os_patch = Vec::with_capacity(input.len());
let mut os_patch_minor = Vec::with_capacity(input.len());
let mut device_family = Vec::with_capacity(input.len());
let mut device_brand = Vec::with_capacity(input.len());
let mut device_model = Vec::with_capacity(input.len());
for value in input {
match value {
None => {
browser_family.push(None);
browser_major.push(None);
browser_minor.push(None);
browser_patch.push(None);
os_family.push(None);
os_major.push(None);
os_minor.push(None);
os_patch.push(None);
os_patch_minor.push(None);
device_family.push(None);
device_brand.push(None);
device_model.push(None);
}
Some(value) => {
let parsed = parse_one(value);
browser_family.push(parsed.user_agent.family);
browser_major.push(parsed.user_agent.major);
browser_minor.push(parsed.user_agent.minor);
browser_patch.push(parsed.user_agent.patch);
os_family.push(parsed.os.family);
os_major.push(parsed.os.major);
os_minor.push(parsed.os.minor);
os_patch.push(parsed.os.patch);
os_patch_minor.push(parsed.os.patch_minor);
device_family.push(parsed.device.family);
device_brand.push(parsed.device.brand);
device_model.push(parsed.device.model);
}
}
}
let user_agent = StructChunked::from_series(
"user_agent".into(),
input.len(),
[
Series::new("family".into(), browser_family),
Series::new("major".into(), browser_major),
Series::new("minor".into(), browser_minor),
Series::new("patch".into(), browser_patch),
]
.iter(),
)?
.into_series();
let os = StructChunked::from_series(
"os".into(),
input.len(),
[
Series::new("family".into(), os_family),
Series::new("major".into(), os_major),
Series::new("minor".into(), os_minor),
Series::new("patch".into(), os_patch),
Series::new("patch_minor".into(), os_patch_minor),
]
.iter(),
)?
.into_series();
let device = StructChunked::from_series(
"device".into(),
input.len(),
[
Series::new("family".into(), device_family),
Series::new("brand".into(), device_brand),
Series::new("model".into(), device_model),
]
.iter(),
)?
.into_series();
Ok(
StructChunked::from_series(
"ua".into(),
input.len(),
[user_agent, os, device].iter(),
)?
.into_series(),
)
}and then for registering in python @pl.api.register_expr_namespace("ua")
class UserAgentNamespace:
def __init__(self, expr: pl.Expr) -> None:
self._expr = expr
def parse(self) -> pl.Expr:
return pl.plugins.register_plugin_function(
plugin_path=PLUGIN_PATH,
function_name="parse_user_agents",
args=[self._expr],
is_elementwise=True,
)and usage looking something like parsed = pl.col("user_agent").ua.parse()
lf.select(
parsed.struct.field("user_agent")
.struct.field("family")
.alias("browser_family"),
parsed.struct.field("os")
.struct.field("family")
.alias("os_family"),
parsed.struct.field("device")
.struct.field("brand")
.alias("device_brand"),
)Im not sure where something like this should live... new repo/crate and whether its something that might you might want to belong here? Maybe it could be shipped as an extra |
Beta Was this translation helpful? Give feedback.
-
I don't think having it as part of uap-python makes much sense, it's basically an unrelated interface to the one uap-python provides, and has no reason to build on uap-python (if I understand correctly the extension would build on uap-rust directly?). It'd probably make more sense as a separate project, either personal or under ua-parser. Although I don't know if @elsigh would be interested in having what is essentially multiple uap-python so that might be a non-starter. A "last resort" of sorts would be to just have it be collocated, as a separate project and wheel in the same repository. That would likely require the repository to formally become a workspace tho. |
Beta Was this translation helpful? Give feedback.
-
|
I don't have a horse in this race so if someone wanted to maintain a uap-python-2 (or something better named) I'm ok with it if there's actual interest in it beyond the OP |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I was wondering whether releasing the GIL in the Rust-backed regex resolver has been considered.
I’m trying to use
ua-parser[regex]from a Polars expression UDF to parse a large number of user-agent strings. Polars has a threaded strategy formap_elements, but in practice I’m only seeing around one core being used. Looking throughua-parser-rs/src/lib.rs, it seems like the extractor methods callself.0.extract(s)while still holdingPython<'_>, and I couldn’t see apy.detach(...)/allow_threadssection around the Rust-only extraction work.Is this intentional because of lifetime/thread-safety constraints in the parser, or would this be a reasonable thing to explore?
Beta Was this translation helpful? Give feedback.
All reactions