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.
Files changed (29) hide show
  1. graphty-0.3.0/PKG-INFO +116 -0
  2. graphty-0.3.0/README.md +105 -0
  3. {graphty-0.2.0 → graphty-0.3.0}/pyproject.toml +1 -1
  4. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/0.14.6/1516144176499143322 +0 -0
  5. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/0.14.6/16552783075482950175 +0 -0
  6. graphty-0.3.0/src/graphty/.ruff_cache/0.15.17/18262235589301934996 +0 -0
  7. graphty-0.3.0/src/graphty/.ruff_cache/0.15.17/8059422986951603277 +0 -0
  8. graphty-0.3.0/src/graphty/__init__.py +7 -0
  9. graphty-0.3.0/src/graphty/materializer.py +38 -0
  10. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/planner.py +70 -69
  11. graphty-0.3.0/src/graphty/utils/.ruff_cache/0.15.17/3781096723789676511 +0 -0
  12. graphty-0.3.0/src/graphty/utils/.ruff_cache/0.15.17/6602623227124377539 +0 -0
  13. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/alias_map.py +4 -5
  14. graphty-0.3.0/src/graphty/utils/exceptions.py +18 -0
  15. graphty-0.3.0/src/graphty/utils/structlog.py +17 -0
  16. graphty-0.3.0/src/graphty/utils/types.py +36 -0
  17. graphty-0.2.0/PKG-INFO +0 -31
  18. graphty-0.2.0/README.md +0 -20
  19. graphty-0.2.0/src/graphty/__init__.py +0 -0
  20. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/.gitignore +0 -0
  21. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/0.6.9/5647188018562798146 +0 -0
  22. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/0.6.9/9121026693023336299 +0 -0
  23. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/.ruff_cache/CACHEDIR.TAG +0 -0
  24. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/py.typed +0 -0
  25. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/.ruff_cache/.gitignore +0 -0
  26. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/.ruff_cache/0.14.6/15861910901900155226 +0 -0
  27. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/.ruff_cache/0.14.6/7883837630368280160 +0 -0
  28. {graphty-0.2.0 → graphty-0.3.0}/src/graphty/utils/.ruff_cache/CACHEDIR.TAG +0 -0
  29. {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
+ ![tests](https://github.com/lu-pl/graphty/actions/workflows/tests.yaml/badge.svg)
16
+ [![coverage](https://coveralls.io/repos/github/lu-pl/graphty/badge.svg?branch=main&kill_cache=1)](https://coveralls.io/github/lu-pl/graphty?branch=lupl/actions)
17
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
18
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](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
+ ```
@@ -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
+ ![tests](https://github.com/lu-pl/graphty/actions/workflows/tests.yaml/badge.svg)
5
+ [![coverage](https://coveralls.io/repos/github/lu-pl/graphty/badge.svg?branch=main&kill_cache=1)](https://coveralls.io/github/lu-pl/graphty?branch=lupl/actions)
6
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
7
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](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
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "graphty"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
- class ConfigDict(PydanticConfigDict):
32
- group_by: str
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
- @dataclass
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
- E.g. `field: typing.Annotated[list[int], Agg(unique=False)]`
43
- will aggregate non-unique values of a given partition into `field`.
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
- unique: bool = True
49
- drop_nulls: bool = True
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
- def apply_to(self, expr: pl.Expr) -> pl.Expr:
58
- return reduce(lambda x, y: y(x), self, expr)
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
- # TODO: this duplicates Exprs._struct_expr and should be abstracted
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
- [get_model_projection(member) for member in self.model_members],
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
- msg = (
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 = self._struct_expr(model=annotation).alias(field_name)
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 = self._struct_expr(model=item_annotation).alias(
248
- field_name
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
- inner: pl.Expr = pl.col(field_name)
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 = self.model.model_config.get("group_by")
309
- model_projection: set[str] = get_model_projection(self.model)
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(
@@ -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: # TODO: trigger in tests
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: # TODO: trigger in tests
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 (validation_alias := field_info.validation_alias):
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
- ![tests](https://github.com/lu-pl/graphty/actions/workflows/tests.yaml/badge.svg)
16
- [![coverage](https://coveralls.io/repos/github/lu-pl/graphty/badge.svg?branch=lupl/actions)](https://coveralls.io/github/lu-pl/graphty?branch=lupl/actions)
17
- [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
18
- [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](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
- ![tests](https://github.com/lu-pl/graphty/actions/workflows/tests.yaml/badge.svg)
5
- [![coverage](https://coveralls.io/repos/github/lu-pl/graphty/badge.svg?branch=lupl/actions)](https://coveralls.io/github/lu-pl/graphty?branch=lupl/actions)
6
- [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
7
- [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](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