graphty 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphty-0.3.0/PKG-INFO +116 -0
- graphty-0.3.0/README.md +105 -0
- {graphty-0.2.0 → graphty-0.3.0}/pyproject.toml +1 -1
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/0.14.6/1516144176499143322 +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/0.14.6/16552783075482950175 +0 -0
- graphty-0.3.0/src/graphty/.ruff_cache/0.15.17/18262235589301934996 +0 -0
- graphty-0.3.0/src/graphty/.ruff_cache/0.15.17/8059422986951603277 +0 -0
- graphty-0.3.0/src/graphty/__init__.py +7 -0
- graphty-0.3.0/src/graphty/materializer.py +38 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/planner.py +70 -69
- graphty-0.3.0/src/graphty/utils/.ruff_cache/0.15.17/3781096723789676511 +0 -0
- graphty-0.3.0/src/graphty/utils/.ruff_cache/0.15.17/6602623227124377539 +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/alias_map.py +4 -5
- graphty-0.3.0/src/graphty/utils/exceptions.py +18 -0
- graphty-0.3.0/src/graphty/utils/structlog.py +17 -0
- graphty-0.3.0/src/graphty/utils/types.py +36 -0
- graphty-0.2.0/PKG-INFO +0 -31
- graphty-0.2.0/README.md +0 -20
- graphty-0.2.0/src/graphty/__init__.py +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/.gitignore +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/0.6.9/5647188018562798146 +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/0.6.9/9121026693023336299 +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/CACHEDIR.TAG +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/py.typed +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/.ruff_cache/.gitignore +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/.ruff_cache/0.14.6/15861910901900155226 +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/.ruff_cache/0.14.6/7883837630368280160 +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/.ruff_cache/CACHEDIR.TAG +0 -0
- {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/type_utils.py +0 -0
graphty-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: graphty
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Lukas Plank
|
|
6
|
+
Author-email: Lukas Plank <lupl@tuta.io>
|
|
7
|
+
Requires-Dist: polars>=1.39.3
|
|
8
|
+
Requires-Dist: pydantic>=2.12.5
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
<img src="https://raw.githubusercontent.com/lu-pl/graphty/refs/heads/main/graphty.svg" width="50%" height="50%" />
|
|
13
|
+
|
|
14
|
+
#
|
|
15
|
+

|
|
16
|
+
[](https://coveralls.io/github/lu-pl/graphty?branch=lupl/actions)
|
|
17
|
+
[](https://github.com/astral-sh/ruff)
|
|
18
|
+
[](https://github.com/astral-sh/uv)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
`graphty` is a Python library for materializing [Pydantic](https://github.com/pydantic/pydantic) object graphs from relational data.
|
|
22
|
+
|
|
23
|
+
> WARNING: This project is in an early stage of development and should be used with caution.
|
|
24
|
+
|
|
25
|
+
## Introduction
|
|
26
|
+
|
|
27
|
+
The `graphty` library addresses the *structural* [impedance mismatch](https://en.wikipedia.org/wiki/Object%E2%80%93relational_impedance_mismatch) between flat relational data representations and hierarchical object models. It extends Pydantic with a small declarative DSL for expressing grouping, aggregation, and deduplication operations. These transformations are compiled into [Polars](https://github.com/pola-rs/polars) expressions, yielding records that are subsequently validated and materialized as Pydantic model objects.
|
|
28
|
+
|
|
29
|
+
Although originally developed for implementing typed REST APIs over SPARQL endpoints, the model materializer can be applied to any tabular data representation, including SQL query results, CSV files, dataframes, etc.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
`graphty` is a [PEP 621](https://peps.python.org/pep-0621/)-compliant package and available on [PyPI](https://pypi.org/project/graphty/).
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
As mentioned, `graphty` uses Pydantic model definitions as declarative data transformation instructions, extending Pydantic with a small DSL for grouping and aggregation.
|
|
39
|
+
|
|
40
|
+
Nested models are resolved recursively, `list` types are interpreted as aggregation targets and require a `group_by` definition in `ConfigDict`.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Basic Example
|
|
44
|
+
|
|
45
|
+
Given simple relational Author/Work data
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
data = [
|
|
49
|
+
{"name": "Tolkien", "title": "The Hobbit", "year": 1937},
|
|
50
|
+
{"name": "Tolkien", "title": "The Lord of the Rings", "year": 1954},
|
|
51
|
+
{"name": "Tolkien", "title": "The Silmarillion", "year": 1977},
|
|
52
|
+
{"name": "Orwell", "title": "Animal Farm", "year": 1945},
|
|
53
|
+
{"name": "Orwell", "title": "1984", "year": 1949},
|
|
54
|
+
]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
one can define and materialize a Pydantic model like so:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from collections.abc import Iterator
|
|
61
|
+
from pydantic import BaseModel
|
|
62
|
+
from graphty import ConfigDict, ModelMaterializer
|
|
63
|
+
|
|
64
|
+
class Work(BaseModel):
|
|
65
|
+
title: str
|
|
66
|
+
year: int
|
|
67
|
+
|
|
68
|
+
class Author(BaseModel):
|
|
69
|
+
model_config = ConfigDict(group_by="name")
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
works: list[Work]
|
|
73
|
+
|
|
74
|
+
models: Iterator[Author] = ModelMaterializer(model=Author, data=data).generate_models()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Here, the `Author` model defines a model aggregation target for the `Author.works` field; the `graphty` planner will therefore partition the underlying data according to the "name" key and aggregate `Work` objects into a list.
|
|
78
|
+
|
|
79
|
+
> Note that `graphty` is recursive on all code paths and ergo enables materialization of arbitrarily nested and aggregated object graphs.
|
|
80
|
+
|
|
81
|
+
The above validates against the `Author` model and serializes to the following JSON representation:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
[
|
|
85
|
+
{
|
|
86
|
+
"name": "Tolkien",
|
|
87
|
+
"works": [
|
|
88
|
+
{
|
|
89
|
+
"title": "The Hobbit",
|
|
90
|
+
"year": 1937
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"title": "The Lord of the Rings",
|
|
94
|
+
"year": 1954
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"title": "The Silmarillion",
|
|
98
|
+
"year": 1977
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "Orwell",
|
|
104
|
+
"works": [
|
|
105
|
+
{
|
|
106
|
+
"title": "Animal Farm",
|
|
107
|
+
"year": 1945
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"title": "1984",
|
|
111
|
+
"year": 1949
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
```
|
graphty-0.3.0/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/lu-pl/graphty/refs/heads/main/graphty.svg" width="50%" height="50%" />
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+

|
|
5
|
+
[](https://coveralls.io/github/lu-pl/graphty?branch=lupl/actions)
|
|
6
|
+
[](https://github.com/astral-sh/ruff)
|
|
7
|
+
[](https://github.com/astral-sh/uv)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
`graphty` is a Python library for materializing [Pydantic](https://github.com/pydantic/pydantic) object graphs from relational data.
|
|
11
|
+
|
|
12
|
+
> WARNING: This project is in an early stage of development and should be used with caution.
|
|
13
|
+
|
|
14
|
+
## Introduction
|
|
15
|
+
|
|
16
|
+
The `graphty` library addresses the *structural* [impedance mismatch](https://en.wikipedia.org/wiki/Object%E2%80%93relational_impedance_mismatch) between flat relational data representations and hierarchical object models. It extends Pydantic with a small declarative DSL for expressing grouping, aggregation, and deduplication operations. These transformations are compiled into [Polars](https://github.com/pola-rs/polars) expressions, yielding records that are subsequently validated and materialized as Pydantic model objects.
|
|
17
|
+
|
|
18
|
+
Although originally developed for implementing typed REST APIs over SPARQL endpoints, the model materializer can be applied to any tabular data representation, including SQL query results, CSV files, dataframes, etc.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
`graphty` is a [PEP 621](https://peps.python.org/pep-0621/)-compliant package and available on [PyPI](https://pypi.org/project/graphty/).
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
As mentioned, `graphty` uses Pydantic model definitions as declarative data transformation instructions, extending Pydantic with a small DSL for grouping and aggregation.
|
|
28
|
+
|
|
29
|
+
Nested models are resolved recursively, `list` types are interpreted as aggregation targets and require a `group_by` definition in `ConfigDict`.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Basic Example
|
|
33
|
+
|
|
34
|
+
Given simple relational Author/Work data
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
data = [
|
|
38
|
+
{"name": "Tolkien", "title": "The Hobbit", "year": 1937},
|
|
39
|
+
{"name": "Tolkien", "title": "The Lord of the Rings", "year": 1954},
|
|
40
|
+
{"name": "Tolkien", "title": "The Silmarillion", "year": 1977},
|
|
41
|
+
{"name": "Orwell", "title": "Animal Farm", "year": 1945},
|
|
42
|
+
{"name": "Orwell", "title": "1984", "year": 1949},
|
|
43
|
+
]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
one can define and materialize a Pydantic model like so:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from collections.abc import Iterator
|
|
50
|
+
from pydantic import BaseModel
|
|
51
|
+
from graphty import ConfigDict, ModelMaterializer
|
|
52
|
+
|
|
53
|
+
class Work(BaseModel):
|
|
54
|
+
title: str
|
|
55
|
+
year: int
|
|
56
|
+
|
|
57
|
+
class Author(BaseModel):
|
|
58
|
+
model_config = ConfigDict(group_by="name")
|
|
59
|
+
|
|
60
|
+
name: str
|
|
61
|
+
works: list[Work]
|
|
62
|
+
|
|
63
|
+
models: Iterator[Author] = ModelMaterializer(model=Author, data=data).generate_models()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Here, the `Author` model defines a model aggregation target for the `Author.works` field; the `graphty` planner will therefore partition the underlying data according to the "name" key and aggregate `Work` objects into a list.
|
|
67
|
+
|
|
68
|
+
> Note that `graphty` is recursive on all code paths and ergo enables materialization of arbitrarily nested and aggregated object graphs.
|
|
69
|
+
|
|
70
|
+
The above validates against the `Author` model and serializes to the following JSON representation:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
[
|
|
74
|
+
{
|
|
75
|
+
"name": "Tolkien",
|
|
76
|
+
"works": [
|
|
77
|
+
{
|
|
78
|
+
"title": "The Hobbit",
|
|
79
|
+
"year": 1937
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"title": "The Lord of the Rings",
|
|
83
|
+
"year": 1954
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"title": "The Silmarillion",
|
|
87
|
+
"year": 1977
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "Orwell",
|
|
93
|
+
"works": [
|
|
94
|
+
{
|
|
95
|
+
"title": "Animal Farm",
|
|
96
|
+
"year": 1945
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"title": "1984",
|
|
100
|
+
"year": 1949
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from graphty.materializer import ModelMaterializer as ModelMaterializer
|
|
2
|
+
from graphty.utils.exceptions import (
|
|
3
|
+
MissingDiscriminatorError as MissingDiscriminatorError,
|
|
4
|
+
)
|
|
5
|
+
from graphty.utils.exceptions import MissingGroupByError as MissingGroupByError
|
|
6
|
+
from graphty.utils.types import Agg as Agg
|
|
7
|
+
from graphty.utils.types import ConfigDict as ConfigDict
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from graphty.planner import LazyFramePlanner
|
|
9
|
+
from graphty.utils.structlog import StructuredMessage
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelMaterializer[TModel: BaseModel]:
|
|
15
|
+
def __init__(
|
|
16
|
+
self, model: type[TModel], data: pl._typing.FrameInitTypes | pl.LazyFrame
|
|
17
|
+
) -> None:
|
|
18
|
+
self._model = model
|
|
19
|
+
self._planner = LazyFramePlanner(model=model, data=data)
|
|
20
|
+
|
|
21
|
+
@cached_property
|
|
22
|
+
def df(self) -> pl.DataFrame:
|
|
23
|
+
lazy_frame: pl.LazyFrame = self._planner.run()
|
|
24
|
+
return lazy_frame.collect()
|
|
25
|
+
|
|
26
|
+
def generate_bindings(self) -> Iterator[dict[str, object]]:
|
|
27
|
+
return self.df.iter_rows(named=True)
|
|
28
|
+
|
|
29
|
+
def generate_models(self) -> Iterator[TModel]:
|
|
30
|
+
for binding in self.generate_bindings():
|
|
31
|
+
logger.debug(
|
|
32
|
+
StructuredMessage(
|
|
33
|
+
message="Instantiating model.",
|
|
34
|
+
model=self._model,
|
|
35
|
+
binding=binding,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
yield self._model.model_validate(binding)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
from collections.abc import Callable, Iterable, Iterator
|
|
2
|
-
from dataclasses import dataclass
|
|
3
2
|
from functools import cached_property, reduce
|
|
4
3
|
from itertools import chain
|
|
5
4
|
from types import UnionType
|
|
6
|
-
from typing import Annotated, cast, get_args, get_origin
|
|
5
|
+
from typing import Annotated, Literal, cast, get_args, get_origin, overload
|
|
7
6
|
|
|
8
7
|
import polars as pl
|
|
9
8
|
from pydantic import BaseModel, Discriminator, Tag
|
|
10
|
-
from pydantic import ConfigDict as PydanticConfigDict
|
|
11
9
|
from pydantic.fields import FieldInfo
|
|
12
10
|
from typing_extensions import TypeForm, get_annotations
|
|
13
11
|
|
|
12
|
+
from graphty.utils.alias_map import AliasMap
|
|
13
|
+
from graphty.utils.exceptions import MissingDiscriminatorError, MissingGroupByError
|
|
14
14
|
from graphty.utils.type_utils import (
|
|
15
15
|
de_annotate,
|
|
16
16
|
is_parametrized_list_static_type,
|
|
@@ -18,44 +18,53 @@ from graphty.utils.type_utils import (
|
|
|
18
18
|
is_pydantic_model_union_static_type,
|
|
19
19
|
is_structured_field_static_type,
|
|
20
20
|
)
|
|
21
|
+
from graphty.utils.types import Agg
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def get_model_projection(model: type[BaseModel]) -> set[str]:
|
|
24
|
+
def get_model_projection(model: type[BaseModel], base_cols: set[str]) -> set[str]:
|
|
25
|
+
alias_map = AliasMap(model=model, projection=base_cols)
|
|
26
|
+
|
|
24
27
|
return {
|
|
25
|
-
field_name
|
|
28
|
+
alias_map[field_name]
|
|
26
29
|
for field_name, field_info in model.model_fields.items()
|
|
27
30
|
if not is_structured_field_static_type(field_info.annotation)
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
def build_model_struct(model: type[BaseModel], base_cols: set[str]) -> pl.Struct:
|
|
35
|
+
model_projection: set[str] = get_model_projection(model=model, base_cols=base_cols)
|
|
36
|
+
exprs: chain[pl.Expr] = chain(
|
|
37
|
+
[pl.col(member) for member in model_projection],
|
|
38
|
+
Exprs(model=model, base_cols=base_cols),
|
|
39
|
+
)
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
class Agg:
|
|
37
|
-
"""Configuration class for aggregation fields.
|
|
41
|
+
return pl.struct(*exprs)
|
|
38
42
|
|
|
39
|
-
The class allows to configure aggregation behavior for
|
|
40
|
-
model fields with aggretation targets:
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
@overload
|
|
45
|
+
def get_group_by_value(
|
|
46
|
+
model: type[BaseModel], base_cols: set[str], strict: Literal[True] = True
|
|
47
|
+
) -> str: ...
|
|
44
48
|
|
|
45
|
-
Note: This is a draft and likely subject of breaking API changes.
|
|
46
|
-
"""
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
@overload
|
|
51
|
+
def get_group_by_value(
|
|
52
|
+
model: type[BaseModel], base_cols: set[str], strict: Literal[False]
|
|
53
|
+
) -> str | None: ...
|
|
50
54
|
|
|
51
|
-
def __iter__(self) -> Iterator[Callable[[pl.Expr], pl.Expr]]:
|
|
52
|
-
if self.unique:
|
|
53
|
-
yield lambda expr: expr.unique()
|
|
54
|
-
if self.drop_nulls:
|
|
55
|
-
yield lambda expr: expr.drop_nulls()
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
def get_group_by_value(
|
|
57
|
+
model: type[BaseModel], base_cols: set[str], strict: bool = True
|
|
58
|
+
) -> str | None:
|
|
59
|
+
try:
|
|
60
|
+
group_by_value = model.model_config["group_by"]
|
|
61
|
+
except KeyError:
|
|
62
|
+
if strict:
|
|
63
|
+
raise MissingGroupByError(model=model)
|
|
64
|
+
return None
|
|
65
|
+
else:
|
|
66
|
+
alias_map = AliasMap(model=model, projection=base_cols)
|
|
67
|
+
return alias_map[group_by_value]
|
|
59
68
|
|
|
60
69
|
|
|
61
70
|
class ModelUnionDispatch:
|
|
@@ -83,11 +92,7 @@ class ModelUnionDispatch:
|
|
|
83
92
|
def compute_model_expr(self) -> pl.Expr:
|
|
84
93
|
match self.model_members, self.model_union_members:
|
|
85
94
|
case [model], []:
|
|
86
|
-
|
|
87
|
-
model_projection: set[str] = get_model_projection(model=model)
|
|
88
|
-
return pl.struct(model_projection).struct.with_fields(
|
|
89
|
-
*Exprs(model=model, base_cols=self.base_cols),
|
|
90
|
-
)
|
|
95
|
+
return build_model_struct(model=model, base_cols=self.base_cols)
|
|
91
96
|
|
|
92
97
|
whens = self._compute_whens()
|
|
93
98
|
when, *rest_whens = whens
|
|
@@ -110,7 +115,10 @@ class ModelUnionDispatch:
|
|
|
110
115
|
|
|
111
116
|
union_projection: set[str] = reduce(
|
|
112
117
|
set.union,
|
|
113
|
-
[
|
|
118
|
+
[
|
|
119
|
+
get_model_projection(model=member, base_cols=self.base_cols)
|
|
120
|
+
for member in self.model_members
|
|
121
|
+
],
|
|
114
122
|
)
|
|
115
123
|
|
|
116
124
|
discriminator: Discriminator = self._resolve_discriminator()
|
|
@@ -123,8 +131,14 @@ class ModelUnionDispatch:
|
|
|
123
131
|
for model in self.model_members
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
# Resolve the discriminator field name to its column alias.
|
|
135
|
+
# Pydantic requires all union members to share the same alias for the discriminator field,
|
|
136
|
+
# so the first (or any) union member is sufficient for instantiating AliasMap.
|
|
137
|
+
_model, *_ = self.model_members
|
|
138
|
+
alias_map = AliasMap(model=_model, projection=union_projection)
|
|
139
|
+
|
|
126
140
|
return [
|
|
127
|
-
pl.when(pl.col(discriminator_value).is_in(list(k))).then(
|
|
141
|
+
pl.when(pl.col(alias_map[discriminator_value]).is_in(list(k))).then(
|
|
128
142
|
pl.struct(union_projection).struct.with_fields(
|
|
129
143
|
*Exprs(model=v, base_cols=self.base_cols),
|
|
130
144
|
)
|
|
@@ -149,7 +163,7 @@ class ModelUnionDispatch:
|
|
|
149
163
|
for k, v in tag_mapping.items()
|
|
150
164
|
]
|
|
151
165
|
|
|
152
|
-
case _:
|
|
166
|
+
case _: # pragma: no cover
|
|
153
167
|
assert False, "Expected discriminator to be of type str | Callable."
|
|
154
168
|
|
|
155
169
|
def _compute_model_union_whens(self) -> list["pl.When"]:
|
|
@@ -187,11 +201,7 @@ class ModelUnionDispatch:
|
|
|
187
201
|
case Discriminator():
|
|
188
202
|
return arg
|
|
189
203
|
|
|
190
|
-
|
|
191
|
-
"Multi-Model unions must be discriminated unions. "
|
|
192
|
-
f"Unable to extract discriminator for type form '{self.type_form}'."
|
|
193
|
-
)
|
|
194
|
-
raise ValueError(msg)
|
|
204
|
+
raise MissingDiscriminatorError(type_form=self.type_form)
|
|
195
205
|
|
|
196
206
|
# TODO: this needs to be more defensive and raise a clear exception in case Tags cannot be retrieved
|
|
197
207
|
def _get_tag_mapping(self):
|
|
@@ -220,7 +230,10 @@ class Exprs(Iterable[pl.Expr]):
|
|
|
220
230
|
annotation = cast(TypeForm, field_info.annotation)
|
|
221
231
|
|
|
222
232
|
if is_pydantic_model_static_type(annotation):
|
|
223
|
-
expr: pl.Expr =
|
|
233
|
+
expr: pl.Expr = build_model_struct(
|
|
234
|
+
model=annotation, base_cols=self.base_cols
|
|
235
|
+
).alias(field_name)
|
|
236
|
+
|
|
224
237
|
yield (expr.first() if self.group_context else expr)
|
|
225
238
|
|
|
226
239
|
elif is_pydantic_model_union_static_type(annotation):
|
|
@@ -240,13 +253,10 @@ class Exprs(Iterable[pl.Expr]):
|
|
|
240
253
|
elif is_parametrized_list_static_type(annotation):
|
|
241
254
|
(item_annotation,) = get_args(annotation)
|
|
242
255
|
|
|
243
|
-
partition_value: str = self._get_partition_value(model=self.model)
|
|
244
|
-
agg: Agg = self._get_agg(field_info)
|
|
245
|
-
|
|
246
256
|
if is_pydantic_model_static_type(item_annotation):
|
|
247
|
-
inner: pl.Expr =
|
|
248
|
-
|
|
249
|
-
)
|
|
257
|
+
inner: pl.Expr = build_model_struct(
|
|
258
|
+
model=item_annotation, base_cols=self.base_cols
|
|
259
|
+
).alias(field_name)
|
|
250
260
|
elif is_pydantic_model_union_static_type(item_annotation):
|
|
251
261
|
inner = (
|
|
252
262
|
ModelUnionDispatch(
|
|
@@ -258,35 +268,22 @@ class Exprs(Iterable[pl.Expr]):
|
|
|
258
268
|
.alias(field_name)
|
|
259
269
|
)
|
|
260
270
|
else:
|
|
261
|
-
|
|
271
|
+
alias_map = AliasMap(model=self.model, projection=self.base_cols)
|
|
272
|
+
inner: pl.Expr = pl.col(alias_map[field_name])
|
|
262
273
|
|
|
274
|
+
agg: Agg = self._get_agg(field_info)
|
|
263
275
|
expr: pl.Expr = agg.apply_to(inner)
|
|
276
|
+
|
|
277
|
+
partition_value: str = get_group_by_value(
|
|
278
|
+
model=self.model, base_cols=self.base_cols
|
|
279
|
+
)
|
|
280
|
+
|
|
264
281
|
yield (
|
|
265
282
|
expr
|
|
266
283
|
if self.group_context
|
|
267
284
|
else expr.implode().over(partition_by=partition_value)
|
|
268
285
|
)
|
|
269
286
|
|
|
270
|
-
def _struct_expr(self, model: type[BaseModel]) -> pl.Expr:
|
|
271
|
-
model_projection: set[str] = get_model_projection(model=model)
|
|
272
|
-
exprs: chain[pl.Expr] = chain(
|
|
273
|
-
[pl.col(member) for member in model_projection],
|
|
274
|
-
Exprs(model=model, base_cols=self.base_cols),
|
|
275
|
-
)
|
|
276
|
-
return pl.struct(*exprs)
|
|
277
|
-
|
|
278
|
-
@staticmethod
|
|
279
|
-
def _get_partition_value(model: type[BaseModel]) -> str:
|
|
280
|
-
try:
|
|
281
|
-
partition_value = model.model_config["group_by"] # type: ignore
|
|
282
|
-
except KeyError:
|
|
283
|
-
raise Exception(
|
|
284
|
-
f"Model '{model.__name__}' with aggregation target "
|
|
285
|
-
"does not specify ConfigDict.group_by."
|
|
286
|
-
)
|
|
287
|
-
else:
|
|
288
|
-
return partition_value
|
|
289
|
-
|
|
290
287
|
@staticmethod
|
|
291
288
|
def _get_agg(field_info: FieldInfo) -> Agg:
|
|
292
289
|
agg: Agg | None = next(
|
|
@@ -305,8 +302,12 @@ class LazyFramePlanner:
|
|
|
305
302
|
)
|
|
306
303
|
|
|
307
304
|
def run(self) -> pl.LazyFrame:
|
|
308
|
-
group_by: str | None =
|
|
309
|
-
|
|
305
|
+
group_by: str | None = get_group_by_value(
|
|
306
|
+
model=self.model, base_cols=self._base_cols, strict=False
|
|
307
|
+
)
|
|
308
|
+
model_projection: set[str] = get_model_projection(
|
|
309
|
+
model=self.model, base_cols=self._base_cols
|
|
310
|
+
)
|
|
310
311
|
|
|
311
312
|
if group_by is None:
|
|
312
313
|
return self.lazy_frame.with_columns(
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from collections import UserDict
|
|
2
2
|
from collections.abc import Callable, Iterator
|
|
3
|
-
from types import resolve_bases
|
|
4
3
|
|
|
5
4
|
from pydantic import AliasChoices, BaseModel
|
|
6
5
|
from pydantic.fields import FieldInfo
|
|
@@ -88,13 +87,13 @@ class AliasMap(UserDict):
|
|
|
88
87
|
)
|
|
89
88
|
|
|
90
89
|
match validate_by_name, validate_by_alias:
|
|
91
|
-
case True, False:
|
|
90
|
+
case True, False:
|
|
92
91
|
return lambda field_name, _: [field_name]
|
|
93
92
|
case False, True:
|
|
94
93
|
return lambda _, field_info: self._compute_alias_candidates(
|
|
95
94
|
field_info=field_info
|
|
96
95
|
)
|
|
97
|
-
case True, True:
|
|
96
|
+
case True, True:
|
|
98
97
|
return lambda field_name, field_info: [
|
|
99
98
|
field_name,
|
|
100
99
|
*self._compute_alias_candidates(field_info=field_info),
|
|
@@ -109,10 +108,10 @@ class AliasMap(UserDict):
|
|
|
109
108
|
assert False, "This should never happen."
|
|
110
109
|
|
|
111
110
|
def _compute_alias_candidates(self, field_info: FieldInfo) -> list[str]:
|
|
112
|
-
if
|
|
111
|
+
if validation_alias := field_info.validation_alias:
|
|
113
112
|
return self._resolve_alias(validation_alias)
|
|
114
113
|
return []
|
|
115
|
-
|
|
114
|
+
|
|
116
115
|
@staticmethod
|
|
117
116
|
def _resolve_alias(alias: str | AliasChoices) -> list[str]:
|
|
118
117
|
"""Helper for resolving alias/validation_alias values in FieldInfo objects.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing_extensions import TypeForm
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MissingGroupByError(Exception):
|
|
6
|
+
def __init__(self, model: type[BaseModel]) -> None:
|
|
7
|
+
super().__init__(
|
|
8
|
+
f"Model '{model.__name__}' with aggregation target "
|
|
9
|
+
"does not specify ConfigDict.group_by."
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MissingDiscriminatorError(Exception):
|
|
14
|
+
def __init__(self, type_form: TypeForm) -> None:
|
|
15
|
+
super().__init__(
|
|
16
|
+
"Multi-Model unions must be discriminated unions. "
|
|
17
|
+
f"Unable to extract discriminator for union type '{type_form}'."
|
|
18
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StructuredMessage:
|
|
5
|
+
"""Structured log message class.
|
|
6
|
+
|
|
7
|
+
This is taken from the Python logging cookbook:
|
|
8
|
+
https://docs.python.org/3/howto/logging-cookbook.html#implementing-structured-logging;
|
|
9
|
+
jsond.dumps.default is set to str for logging of non-json-serializable objects.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str, **kwargs) -> None:
|
|
13
|
+
self.message = message
|
|
14
|
+
self.kwargs = kwargs
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return "%s >>> %s" % (self.message, json.dumps(self.kwargs, default=str))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections.abc import Callable, Iterator
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from functools import reduce
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
from pydantic import ConfigDict as PydanticConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigDict(PydanticConfigDict):
|
|
10
|
+
group_by: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Agg:
|
|
15
|
+
"""Configuration class for aggregation fields.
|
|
16
|
+
|
|
17
|
+
The class allows to configure aggregation behavior for
|
|
18
|
+
model fields with aggretation targets:
|
|
19
|
+
|
|
20
|
+
E.g. `field: typing.Annotated[list[int], Agg(unique=False)]`
|
|
21
|
+
will aggregate non-unique values of a given partition into `field`.
|
|
22
|
+
|
|
23
|
+
Note: This is a draft and likely subject of breaking API changes.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
unique: bool = True
|
|
27
|
+
drop_nulls: bool = True
|
|
28
|
+
|
|
29
|
+
def __iter__(self) -> Iterator[Callable[[pl.Expr], pl.Expr]]:
|
|
30
|
+
if self.unique:
|
|
31
|
+
yield lambda expr: expr.unique()
|
|
32
|
+
if self.drop_nulls:
|
|
33
|
+
yield lambda expr: expr.drop_nulls()
|
|
34
|
+
|
|
35
|
+
def apply_to(self, expr: pl.Expr) -> pl.Expr:
|
|
36
|
+
return reduce(lambda x, y: y(x), self, expr)
|
graphty-0.2.0/PKG-INFO
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: graphty
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: Add your description here
|
|
5
|
-
Author: Lukas Plank
|
|
6
|
-
Author-email: Lukas Plank <lupl@tuta.io>
|
|
7
|
-
Requires-Dist: polars>=1.39.3
|
|
8
|
-
Requires-Dist: pydantic>=2.12.5
|
|
9
|
-
Requires-Python: >=3.12
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
|
|
12
|
-
<img src="graphty.svg" width="50%" height="50%" />
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-

|
|
16
|
-
[](https://coveralls.io/github/lu-pl/graphty?branch=lupl/actions)
|
|
17
|
-
[](https://github.com/astral-sh/ruff)
|
|
18
|
-
[](https://github.com/astral-sh/uv)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Typed object graph materializer over relational binding sets.
|
|
22
|
-
|
|
23
|
-
> WARNING: This project is in an early stage of development and should be used with caution.
|
|
24
|
-
|
|
25
|
-
## Introduction
|
|
26
|
-
|
|
27
|
-
[Intro]
|
|
28
|
-
|
|
29
|
-
## Installation
|
|
30
|
-
|
|
31
|
-
[Installation]
|
graphty-0.2.0/README.md
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
<img src="graphty.svg" width="50%" height="50%" />
|
|
2
|
-
|
|
3
|
-
#
|
|
4
|
-

|
|
5
|
-
[](https://coveralls.io/github/lu-pl/graphty?branch=lupl/actions)
|
|
6
|
-
[](https://github.com/astral-sh/ruff)
|
|
7
|
-
[](https://github.com/astral-sh/uv)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Typed object graph materializer over relational binding sets.
|
|
11
|
-
|
|
12
|
-
> WARNING: This project is in an early stage of development and should be used with caution.
|
|
13
|
-
|
|
14
|
-
## Introduction
|
|
15
|
-
|
|
16
|
-
[Intro]
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
[Installation]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|