pydantic-linq 0.1.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 (70) hide show
  1. pydantic_linq-0.1.0/.claude/scheduled_tasks.lock +1 -0
  2. pydantic_linq-0.1.0/.github/workflows/ci.yml +44 -0
  3. pydantic_linq-0.1.0/.github/workflows/release.yml +63 -0
  4. pydantic_linq-0.1.0/.gitignore +5 -0
  5. pydantic_linq-0.1.0/LICENSE +21 -0
  6. pydantic_linq-0.1.0/PKG-INFO +90 -0
  7. pydantic_linq-0.1.0/README.md +65 -0
  8. pydantic_linq-0.1.0/examples/01_basics.py +37 -0
  9. pydantic_linq-0.1.0/examples/02_sorting_paging.py +36 -0
  10. pydantic_linq-0.1.0/examples/03_grouping_aggregation.py +47 -0
  11. pydantic_linq-0.1.0/examples/04_joins_sets.py +60 -0
  12. pydantic_linq-0.1.0/examples/05_pydantic_integration.py +54 -0
  13. pydantic_linq-0.1.0/examples/06_async.py +67 -0
  14. pydantic_linq-0.1.0/examples/07_sales_report.py +100 -0
  15. pydantic_linq-0.1.0/pyproject.toml +41 -0
  16. pydantic_linq-0.1.0/src/pydantic_linq/__init__.py +34 -0
  17. pydantic_linq-0.1.0/src/pydantic_linq/aio/__init__.py +1 -0
  18. pydantic_linq-0.1.0/src/pydantic_linq/aio/base.py +97 -0
  19. pydantic_linq-0.1.0/src/pydantic_linq/aio/enumerable.py +49 -0
  20. pydantic_linq-0.1.0/src/pydantic_linq/aio/grouping.py +19 -0
  21. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/__init__.py +33 -0
  22. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/aggregation.py +92 -0
  23. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/concatenation.py +30 -0
  24. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/conversion.py +51 -0
  25. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/element.py +76 -0
  26. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/equality.py +19 -0
  27. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/filtering.py +23 -0
  28. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/generation.py +40 -0
  29. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/grouping.py +55 -0
  30. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/joining.py +79 -0
  31. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/partitioning.py +91 -0
  32. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/projection.py +64 -0
  33. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/quantifiers.py +27 -0
  34. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/sets.py +59 -0
  35. pydantic_linq-0.1.0/src/pydantic_linq/aio/operators/sorting.py +47 -0
  36. pydantic_linq-0.1.0/src/pydantic_linq/aio/ordered.py +38 -0
  37. pydantic_linq-0.1.0/src/pydantic_linq/core/__init__.py +8 -0
  38. pydantic_linq-0.1.0/src/pydantic_linq/core/equality.py +53 -0
  39. pydantic_linq-0.1.0/src/pydantic_linq/core/errors.py +8 -0
  40. pydantic_linq-0.1.0/src/pydantic_linq/core/grouping.py +12 -0
  41. pydantic_linq-0.1.0/src/pydantic_linq/core/sentinel.py +4 -0
  42. pydantic_linq-0.1.0/src/pydantic_linq/core/sorting.py +28 -0
  43. pydantic_linq-0.1.0/src/pydantic_linq/integrations/__init__.py +1 -0
  44. pydantic_linq-0.1.0/src/pydantic_linq/integrations/builtins.py +54 -0
  45. pydantic_linq-0.1.0/src/pydantic_linq/integrations/pydantic.py +17 -0
  46. pydantic_linq-0.1.0/src/pydantic_linq/py.typed +0 -0
  47. pydantic_linq-0.1.0/src/pydantic_linq/sync/__init__.py +1 -0
  48. pydantic_linq-0.1.0/src/pydantic_linq/sync/base.py +56 -0
  49. pydantic_linq-0.1.0/src/pydantic_linq/sync/enumerable.py +54 -0
  50. pydantic_linq-0.1.0/src/pydantic_linq/sync/grouping.py +49 -0
  51. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/__init__.py +33 -0
  52. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/aggregation.py +101 -0
  53. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/concatenation.py +24 -0
  54. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/conversion.py +58 -0
  55. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/element.py +70 -0
  56. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/equality.py +16 -0
  57. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/filtering.py +19 -0
  58. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/generation.py +57 -0
  59. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/grouping.py +57 -0
  60. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/joining.py +77 -0
  61. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/partitioning.py +64 -0
  62. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/projection.py +55 -0
  63. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/quantifiers.py +23 -0
  64. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/sets.py +57 -0
  65. pydantic_linq-0.1.0/src/pydantic_linq/sync/operators/sorting.py +49 -0
  66. pydantic_linq-0.1.0/src/pydantic_linq/sync/ordered.py +28 -0
  67. pydantic_linq-0.1.0/tests/test_async_enumerable.py +130 -0
  68. pydantic_linq-0.1.0/tests/test_builtins.py +40 -0
  69. pydantic_linq-0.1.0/tests/test_enumerable.py +214 -0
  70. pydantic_linq-0.1.0/uv.lock +244 -0
@@ -0,0 +1 @@
1
+ {"sessionId":"f6433955-3966-47db-80fb-3ba5bc285b56","pid":24568,"procStart":"134378175680740197","acquiredAt":1793345293783}
@@ -0,0 +1,44 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_call:
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ group: ci-${{ github.ref }}
12
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ lint:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+ - uses: astral-sh/setup-uv@v7
23
+ - run: uvx ruff check
24
+ - run: uv sync --locked && uvx pyright src
25
+
26
+ test:
27
+ name: test (${{ matrix.os }}, py${{ matrix.python }})
28
+ runs-on: ${{ matrix.os }}
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ os: [windows-latest, ubuntu-latest]
33
+ python: ["3.12", "3.13", "3.14"]
34
+ steps:
35
+ - uses: actions/checkout@v7
36
+ - uses: astral-sh/setup-uv@v7
37
+ with:
38
+ python-version: ${{ matrix.python }}
39
+ enable-cache: true
40
+ - run: uv sync --locked
41
+ - run: uv run pytest -q
42
+ - name: Examples
43
+ shell: bash
44
+ run: for f in examples/*.py; do uv run python "$f" > /dev/null || exit 1; done
@@ -0,0 +1,63 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ ci:
12
+ uses: ./.github/workflows/ci.yml
13
+
14
+ build:
15
+ needs: ci
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ - uses: astral-sh/setup-uv@v7
20
+ - name: Tag matches pyproject version
21
+ run: |
22
+ version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
23
+ test "v$version" = "$GITHUB_REF_NAME" || { echo "::error::tag $GITHUB_REF_NAME != pyproject v$version"; exit 1; }
24
+ - run: uv build
25
+ - name: Smoke-test the wheel
26
+ run: uv run --isolated --no-project --with ./dist/*.whl python -c "import pydantic_linq; assert [1, 2].select(str).to_list() == ['1', '2']"
27
+ - uses: actions/upload-artifact@v7
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ pypi:
33
+ # PyPI trusted publisher: owner codejunction, repo pydantic-linq, workflow release.yml, environment pypi
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment:
37
+ name: pypi
38
+ url: https://pypi.org/project/pydantic-linq/
39
+ permissions:
40
+ id-token: write
41
+ steps:
42
+ - uses: actions/download-artifact@v8
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ - uses: pypa/gh-action-pypi-publish@release/v1
47
+ with:
48
+ skip-existing: true
49
+
50
+ github-release:
51
+ needs: pypi
52
+ runs-on: ubuntu-latest
53
+ permissions:
54
+ contents: write
55
+ steps:
56
+ - uses: actions/checkout@v7
57
+ - uses: actions/download-artifact@v8
58
+ with:
59
+ name: dist
60
+ path: dist/
61
+ - env:
62
+ GH_TOKEN: ${{ github.token }}
63
+ run: gh release create "$GITHUB_REF_NAME" dist/* --title "pydantic-linq $GITHUB_REF_NAME" --generate-notes
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .pytest_cache/
5
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abhishek Dutta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.5
2
+ Name: pydantic-linq
3
+ Version: 0.1.0
4
+ Summary: C# LINQ (Enumerable + AsyncEnumerable) for Python, typed and Pydantic-aware
5
+ Project-URL: Homepage, https://github.com/codejunction/pydantic-linq
6
+ Project-URL: Source, https://github.com/codejunction/pydantic-linq
7
+ Project-URL: Issues, https://github.com/codejunction/pydantic-linq/issues
8
+ Author-email: Abhishek Dutta <phoenixdutta@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: async,collections,functional,iterator,linq,pydantic,query
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Pydantic :: 2
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: forbiddenfruit>=0.1.4
23
+ Requires-Dist: pydantic>=2.7
24
+ Description-Content-Type: text/markdown
25
+
26
+ # pydantic-linq
27
+
28
+ C# LINQ (`System.Linq.Enumerable`) for Python. It is typed and works with Pydantic.
29
+
30
+ ```python
31
+ from pydantic import BaseModel
32
+ import pydantic_linq # adds LINQ operators to list / tuple / set / frozenset
33
+
34
+ class Row(BaseModel):
35
+ id: int
36
+ tag: str
37
+
38
+ rows = [Row(id=1, tag="a"), Row(id=2, tag="b"), Row(id=3, tag="a")]
39
+
40
+ rows.where(lambda r: r.tag == "a").select(lambda r: r.id).to_list() # [1, 3]
41
+ rows.group_by(lambda r: r.tag).select(lambda g: (g.key, len(g))).to_list()
42
+ rows.order_by_descending(lambda r: r.id).then_by(lambda r: r.tag).first()
43
+ ```
44
+
45
+ ## Async
46
+
47
+ `AsyncEnumerable` covers System.Linq.Async. It wraps any sync or async iterable. Sequence operators are deferred, and terminal operators are awaited:
48
+
49
+ ```python
50
+ async def load_age(r: Row) -> int: ...
51
+
52
+ ages = await rows.to_async_enumerable().where(lambda r: r.id > 1).select(load_age).to_list()
53
+ ```
54
+
55
+ A selector or predicate can be a plain function or an `async def`, so there are no separate `*Await` overloads. Terminal operators keep the sync names (`to_list`, `first`, `count`) and are awaited. The C# `Async` suffix is dropped.
56
+
57
+ ## Conventions
58
+
59
+ - Method names are the C# names in snake_case: `OrderByDescending` becomes `order_by_descending`. `Except` becomes `except_`, because `except` is a Python keyword.
60
+ - Operators that return a sequence are deferred and can be enumerated again, as in .NET.
61
+ - Equality uses value semantics (`==`). Non-frozen (unhashable) pydantic models work in `distinct`, `union` and the other set operators.
62
+ - `cast(Model)` validates each element with a pydantic `TypeAdapter`, so dicts become models. `of_type(Model)` filters with `isinstance`.
63
+ - `Enumerable[Model]` can be a pydantic field type. It validates like `list[Model]` and serializes as a list.
64
+ - C# `KeyValuePair` becomes a `(key, value)` tuple. `IComparer` becomes a `cmp(a, b) -> int` function. A C# `Range` or `Index` (for example `^1`) becomes a slice or a negative int.
65
+ - Lambda overloads that take an index (`Select((x, i) => ...)`) map to `.index()`, followed by the operator.
66
+ - Empty-sequence errors raise `InvalidOperationError`, which subclasses `ValueError`.
67
+
68
+ ## Builtin patching
69
+
70
+ Importing the package uses [forbiddenfruit](https://github.com/clarete/forbiddenfruit) to patch `list`, `tuple`, `set` and `frozenset`. A builtin method that already exists keeps its Python meaning, for example `list.count(x)` and `list.reverse()`. To get the LINQ version, use `rows.as_enumerable().count()`. Type checkers do not see the patched methods. Where you need static types, use `Enumerable(rows)`. To remove the patches, call `pydantic_linq.uninstall()`.
71
+
72
+ ## Layout
73
+
74
+ ```
75
+ src/pydantic_linq/
76
+ core/ errors, value-equality set, Grouping (no internal deps)
77
+ sync/
78
+ base.py EnumerableBase, deferred execution
79
+ operators/ one mixin per LINQ category: filtering, projection, partitioning, sets, sorting, ...
80
+ enumerable.py Enumerable = all mixins
81
+ ordered.py OrderedEnumerable (then_by)
82
+ grouping.py Grouping (an Enumerable with .key), Lookup
83
+ aio/ AsyncEnumerable, same layout as sync/
84
+ integrations/ pydantic field schema, builtin patching
85
+ ```
86
+
87
+ ## Examples
88
+
89
+ `examples/` runs from simple to complex: `01_basics` through `07_sales_report`, an end-to-end report pipeline. Run any of them with `uv run python examples/01_basics.py`.
90
+
@@ -0,0 +1,65 @@
1
+ # pydantic-linq
2
+
3
+ C# LINQ (`System.Linq.Enumerable`) for Python. It is typed and works with Pydantic.
4
+
5
+ ```python
6
+ from pydantic import BaseModel
7
+ import pydantic_linq # adds LINQ operators to list / tuple / set / frozenset
8
+
9
+ class Row(BaseModel):
10
+ id: int
11
+ tag: str
12
+
13
+ rows = [Row(id=1, tag="a"), Row(id=2, tag="b"), Row(id=3, tag="a")]
14
+
15
+ rows.where(lambda r: r.tag == "a").select(lambda r: r.id).to_list() # [1, 3]
16
+ rows.group_by(lambda r: r.tag).select(lambda g: (g.key, len(g))).to_list()
17
+ rows.order_by_descending(lambda r: r.id).then_by(lambda r: r.tag).first()
18
+ ```
19
+
20
+ ## Async
21
+
22
+ `AsyncEnumerable` covers System.Linq.Async. It wraps any sync or async iterable. Sequence operators are deferred, and terminal operators are awaited:
23
+
24
+ ```python
25
+ async def load_age(r: Row) -> int: ...
26
+
27
+ ages = await rows.to_async_enumerable().where(lambda r: r.id > 1).select(load_age).to_list()
28
+ ```
29
+
30
+ A selector or predicate can be a plain function or an `async def`, so there are no separate `*Await` overloads. Terminal operators keep the sync names (`to_list`, `first`, `count`) and are awaited. The C# `Async` suffix is dropped.
31
+
32
+ ## Conventions
33
+
34
+ - Method names are the C# names in snake_case: `OrderByDescending` becomes `order_by_descending`. `Except` becomes `except_`, because `except` is a Python keyword.
35
+ - Operators that return a sequence are deferred and can be enumerated again, as in .NET.
36
+ - Equality uses value semantics (`==`). Non-frozen (unhashable) pydantic models work in `distinct`, `union` and the other set operators.
37
+ - `cast(Model)` validates each element with a pydantic `TypeAdapter`, so dicts become models. `of_type(Model)` filters with `isinstance`.
38
+ - `Enumerable[Model]` can be a pydantic field type. It validates like `list[Model]` and serializes as a list.
39
+ - C# `KeyValuePair` becomes a `(key, value)` tuple. `IComparer` becomes a `cmp(a, b) -> int` function. A C# `Range` or `Index` (for example `^1`) becomes a slice or a negative int.
40
+ - Lambda overloads that take an index (`Select((x, i) => ...)`) map to `.index()`, followed by the operator.
41
+ - Empty-sequence errors raise `InvalidOperationError`, which subclasses `ValueError`.
42
+
43
+ ## Builtin patching
44
+
45
+ Importing the package uses [forbiddenfruit](https://github.com/clarete/forbiddenfruit) to patch `list`, `tuple`, `set` and `frozenset`. A builtin method that already exists keeps its Python meaning, for example `list.count(x)` and `list.reverse()`. To get the LINQ version, use `rows.as_enumerable().count()`. Type checkers do not see the patched methods. Where you need static types, use `Enumerable(rows)`. To remove the patches, call `pydantic_linq.uninstall()`.
46
+
47
+ ## Layout
48
+
49
+ ```
50
+ src/pydantic_linq/
51
+ core/ errors, value-equality set, Grouping (no internal deps)
52
+ sync/
53
+ base.py EnumerableBase, deferred execution
54
+ operators/ one mixin per LINQ category: filtering, projection, partitioning, sets, sorting, ...
55
+ enumerable.py Enumerable = all mixins
56
+ ordered.py OrderedEnumerable (then_by)
57
+ grouping.py Grouping (an Enumerable with .key), Lookup
58
+ aio/ AsyncEnumerable, same layout as sync/
59
+ integrations/ pydantic field schema, builtin patching
60
+ ```
61
+
62
+ ## Examples
63
+
64
+ `examples/` runs from simple to complex: `01_basics` through `07_sales_report`, an end-to-end report pipeline. Run any of them with `uv run python examples/01_basics.py`.
65
+
@@ -0,0 +1,37 @@
1
+ """Basics: filter, project and pick elements from a plain list of pydantic models."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+ import pydantic_linq # noqa: F401 (adds the LINQ operators to list / tuple / set)
6
+ from pydantic_linq import Enumerable
7
+
8
+
9
+ class Product(BaseModel):
10
+ name: str
11
+ price: float
12
+ stock: int
13
+
14
+
15
+ products = [
16
+ Product(name="Keyboard", price=49.0, stock=12),
17
+ Product(name="Mouse", price=19.5, stock=0),
18
+ Product(name="Monitor", price=219.0, stock=4),
19
+ Product(name="Cable", price=5.0, stock=120),
20
+ ]
21
+
22
+ # Where + Select, called straight on the list
23
+ in_stock = products.where(lambda p: p.stock > 0).select(lambda p: p.name).to_list()
24
+ print("in stock:", in_stock)
25
+
26
+ # Element operators
27
+ print("first over $100:", products.first(lambda p: p.price > 100).name)
28
+ print("first over $1000:", products.first_or_default(lambda p: p.price > 1000))
29
+
30
+ # Quantifiers and counting. list.count(x) keeps its Python meaning, so wrap for the LINQ count().
31
+ print("any sold out:", products.any(lambda p: p.stock == 0))
32
+ print("sold out count:", Enumerable(products).count(lambda p: p.stock == 0))
33
+
34
+ # Queries are deferred: nothing runs until you enumerate
35
+ cheap = products.where(lambda p: p.price < 20)
36
+ products.append(Product(name="Sticker", price=1.0, stock=500))
37
+ print("cheap (evaluated now):", cheap.select(lambda p: p.name).to_list())
@@ -0,0 +1,36 @@
1
+ """Sorting and paging: OrderBy / ThenBy, Skip / Take, Chunk."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+ import pydantic_linq # noqa: F401
6
+
7
+
8
+ class Employee(BaseModel):
9
+ name: str
10
+ dept: str
11
+ salary: int
12
+
13
+
14
+ staff = [
15
+ Employee(name="Ava", dept="Eng", salary=120),
16
+ Employee(name="Ben", dept="Ops", salary=90),
17
+ Employee(name="Cleo", dept="Eng", salary=120),
18
+ Employee(name="Dan", dept="Eng", salary=105),
19
+ Employee(name="Eve", dept="Ops", salary=95),
20
+ ]
21
+
22
+ # Multi-key, stable sort: dept ascending, then salary descending, then name
23
+ ordered = staff.order_by(lambda e: e.dept).then_by_descending(lambda e: e.salary).then_by(lambda e: e.name)
24
+ print([f"{e.dept}:{e.name}:{e.salary}" for e in ordered])
25
+
26
+ # Paging
27
+ page_size = 2
28
+ for page in range(3):
29
+ names = ordered.skip(page * page_size).take(page_size).select(lambda e: e.name).to_list()
30
+ print(f"page {page + 1}:", names)
31
+
32
+ # Chunk does the same in one call
33
+ print("chunks:", ordered.select(lambda e: e.name).chunk(2).to_list())
34
+
35
+ # Take with a range: slice(1, -1) is C#'s Take(1..^1)
36
+ print("without top and bottom:", ordered.take(slice(1, -1)).select(lambda e: e.name).to_list())
@@ -0,0 +1,47 @@
1
+ """Grouping and aggregation: GroupBy, ToLookup, CountBy, AggregateBy, Sum / Average / MaxBy."""
2
+
3
+ from datetime import date
4
+
5
+ from pydantic import BaseModel
6
+
7
+ import pydantic_linq # noqa: F401
8
+
9
+
10
+ class Sale(BaseModel):
11
+ region: str
12
+ rep: str
13
+ amount: float
14
+ day: date
15
+
16
+
17
+ sales = [
18
+ Sale(region="North", rep="Ann", amount=120.0, day=date(2026, 1, 3)),
19
+ Sale(region="North", rep="Bob", amount=80.0, day=date(2026, 1, 4)),
20
+ Sale(region="South", rep="Cid", amount=200.0, day=date(2026, 1, 4)),
21
+ Sale(region="North", rep="Ann", amount=60.0, day=date(2026, 2, 1)),
22
+ Sale(region="South", rep="Dee", amount=40.0, day=date(2026, 2, 2)),
23
+ ]
24
+
25
+ # GroupBy with a result selector: one summary row per region
26
+ summary = sales.group_by(
27
+ lambda s: s.region,
28
+ result_selector=lambda region, items: (region, items.count(), items.sum(lambda s: s.amount)),
29
+ ).to_list()
30
+ print("region, orders, total:", summary)
31
+
32
+ # Each Grouping is itself an Enumerable with a .key
33
+ for g in sales.group_by(lambda s: s.day.month):
34
+ print(f"month {g.key}: avg {g.average(lambda s: s.amount):.2f}, reps {g.select(lambda s: s.rep).distinct().to_list()}")
35
+
36
+ # A Lookup is an indexable group set; a missing key gives an empty sequence
37
+ by_rep = sales.to_lookup(lambda s: s.rep, lambda s: s.amount)
38
+ print("Ann:", by_rep["Ann"].to_list(), "Zed:", by_rep["Zed"].to_list())
39
+
40
+ # .NET 9 CountBy / AggregateBy
41
+ print("orders per rep:", sales.count_by(lambda s: s.rep).to_list())
42
+ print("revenue per rep:", sales.aggregate_by(lambda s: s.rep, 0.0, lambda acc, s: acc + s.amount).to_list())
43
+
44
+ # Scalar aggregates
45
+ print("total:", sales.sum(lambda s: s.amount))
46
+ print("biggest sale:", sales.max_by(lambda s: s.amount).rep)
47
+ print("running product:", [1, 2, 3, 4].aggregate(lambda a, b: a * b))
@@ -0,0 +1,60 @@
1
+ """Joins and set operations across two model lists."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+ import pydantic_linq # noqa: F401
6
+
7
+
8
+ class Customer(BaseModel):
9
+ id: int
10
+ name: str
11
+
12
+
13
+ class Order(BaseModel):
14
+ customer_id: int
15
+ item: str
16
+
17
+
18
+ customers = [Customer(id=1, name="Ann"), Customer(id=2, name="Bob"), Customer(id=3, name="Cid")]
19
+ orders = [
20
+ Order(customer_id=1, item="book"),
21
+ Order(customer_id=1, item="pen"),
22
+ Order(customer_id=3, item="lamp"),
23
+ Order(customer_id=9, item="ghost"),
24
+ ]
25
+
26
+ # Inner join
27
+ print("join:", customers.join(orders, lambda c: c.id, lambda o: o.customer_id, lambda c, o: (c.name, o.item)).to_list())
28
+
29
+ # GroupJoin: every customer with their orders (possibly none)
30
+ print(
31
+ "group join:",
32
+ customers.group_join(
33
+ orders, lambda c: c.id, lambda o: o.customer_id, lambda c, os: (c.name, os.select(lambda o: o.item).to_list())
34
+ ).to_list(),
35
+ )
36
+
37
+ # .NET 10 LeftJoin / RightJoin: the missing side is None
38
+ print(
39
+ "left join:",
40
+ customers.left_join(
41
+ orders, lambda c: c.id, lambda o: o.customer_id, lambda c, o: (c.name, o.item if o else None)
42
+ ).to_list(),
43
+ )
44
+ print(
45
+ "right join:",
46
+ customers.right_join(
47
+ orders, lambda c: c.id, lambda o: o.customer_id, lambda c, o: (c.name if c else None, o.item)
48
+ ).to_list(),
49
+ )
50
+
51
+ # Set operations use value equality, so equal (non-frozen) models count as duplicates
52
+ batch_a = [Customer(id=1, name="Ann"), Customer(id=2, name="Bob")]
53
+ batch_b = [Customer(id=2, name="Bob"), Customer(id=4, name="Dee")]
54
+ print("union:", batch_a.union(batch_b).select(lambda c: c.name).to_list())
55
+ print("intersect:", batch_a.intersect(batch_b).select(lambda c: c.name).to_list())
56
+ print("except:", batch_a.except_(batch_b).select(lambda c: c.name).to_list())
57
+
58
+ # *By variants compare by a key; the second sequence holds keys
59
+ print("customers without orders:", customers.except_by(orders.select(lambda o: o.customer_id), lambda c: c.id)
60
+ .select(lambda c: c.name).to_list())
@@ -0,0 +1,54 @@
1
+ """Pydantic integration: validate raw data with cast(), use Enumerable[Model] as a field, round-trip JSON."""
2
+
3
+ from pydantic import BaseModel, ValidationError
4
+
5
+ import pydantic_linq # noqa: F401
6
+ from pydantic_linq import Enumerable
7
+
8
+
9
+ class LineItem(BaseModel):
10
+ sku: str
11
+ qty: int
12
+ unit_price: float
13
+
14
+
15
+ class Invoice(BaseModel):
16
+ number: str
17
+ lines: Enumerable[LineItem] # validated like list[LineItem]; behaves like LINQ
18
+
19
+ @property
20
+ def total(self) -> float:
21
+ return self.lines.sum(lambda line: line.qty * line.unit_price)
22
+
23
+
24
+ # Raw rows, e.g. from an API or CSV: strings get coerced, dicts become models
25
+ raw = [
26
+ {"sku": "A-1", "qty": "2", "unit_price": "9.99"},
27
+ {"sku": "B-7", "qty": 1, "unit_price": 120},
28
+ ]
29
+ items = raw.cast(LineItem).to_list()
30
+ print("cast:", items)
31
+
32
+ # strict=True disables coercion
33
+ try:
34
+ raw.cast(LineItem, strict=True).to_list()
35
+ except ValidationError as e:
36
+ print("strict cast failed:", e.error_count(), "errors")
37
+
38
+ invoice = Invoice(number="INV-1", lines=items)
39
+ print("total:", round(invoice.total, 2))
40
+ print("big lines:", invoice.lines.where(lambda line: line.unit_price > 50).select(lambda line: line.sku).to_list())
41
+
42
+ # Serializes as a plain list; validates back into an Enumerable
43
+ payload = invoice.model_dump_json()
44
+ print("json:", payload)
45
+ again = Invoice.model_validate_json(payload)
46
+ print("round-trip type:", type(again.lines).__name__, "count:", again.lines.count())
47
+
48
+ # Value equality: non-frozen models are unhashable, distinct() still works
49
+ dupes = [LineItem(sku="A-1", qty=1, unit_price=1.0)] * 3
50
+ print("distinct:", dupes.distinct().count())
51
+
52
+ # of_type filters by class, like OfType<T>
53
+ mixed = [items[0], "noise", 42, items[1]]
54
+ print("of_type:", mixed.of_type(LineItem).select(lambda line: line.sku).to_list())
@@ -0,0 +1,67 @@
1
+ """Async: AsyncEnumerable over async sources, with sync or async selectors."""
2
+
3
+ import asyncio
4
+
5
+ from pydantic import BaseModel
6
+
7
+ import pydantic_linq # noqa: F401
8
+ from pydantic_linq import AsyncEnumerable
9
+
10
+
11
+ class User(BaseModel):
12
+ id: int
13
+ name: str
14
+
15
+
16
+ class Profile(BaseModel):
17
+ user_id: int
18
+ followers: int
19
+
20
+
21
+ async def fetch_users():
22
+ """An async generator source, e.g. a paginated API or a DB cursor."""
23
+ for i, name in enumerate(["ann", "bob", "cid", "dee"], start=1):
24
+ await asyncio.sleep(0.01)
25
+ yield User(id=i, name=name)
26
+
27
+
28
+ async def fetch_profile(user: User) -> Profile:
29
+ """An async selector, the System.Linq.Async SelectAwait overload."""
30
+ await asyncio.sleep(0.01)
31
+ return Profile(user_id=user.id, followers=user.id * 150)
32
+
33
+
34
+ async def is_active(user: User) -> bool:
35
+ await asyncio.sleep(0)
36
+ return user.id != 2
37
+
38
+
39
+ async def main() -> None:
40
+ users = AsyncEnumerable(fetch_users())
41
+
42
+ # Async predicate + sync projection
43
+ names = await users.where(is_active).select(lambda u: u.name.title()).to_list()
44
+ print("active:", names)
45
+
46
+ # Async generator sources are one-shot (as in Python), so re-wrap for a second query
47
+ users = AsyncEnumerable(fetch_users())
48
+ popular = await (
49
+ users.select(fetch_profile)
50
+ .where(lambda p: p.followers > 200)
51
+ .order_by_descending(lambda p: p.followers)
52
+ .select(lambda p: p.user_id)
53
+ .to_list()
54
+ )
55
+ print("popular ids:", popular)
56
+
57
+ # Sync lists enter async with to_async_enumerable(); terminal operators are awaited
58
+ local = [User(id=10, name="eve"), User(id=11, name="fay")]
59
+ print("total followers:", await local.to_async_enumerable().select(fetch_profile).sum(lambda p: p.followers))
60
+ print("first:", (await local.to_async_enumerable().first(lambda u: u.id > 10)).name)
61
+
62
+ # Grouping yields AsyncGrouping, an AsyncEnumerable with a .key
63
+ async for g in AsyncEnumerable(fetch_users()).group_by(lambda u: u.id % 2 == 0):
64
+ print("even" if g.key else "odd", await g.select(lambda u: u.name).to_list())
65
+
66
+
67
+ asyncio.run(main())