mongo-pipebuilder 0.3.1__tar.gz → 0.5.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 (23) hide show
  1. mongo_pipebuilder-0.5.0/MANIFEST.in +3 -0
  2. {mongo_pipebuilder-0.3.1/src/mongo_pipebuilder.egg-info → mongo_pipebuilder-0.5.0}/PKG-INFO +88 -6
  3. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/README.md +85 -1
  4. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/pyproject.toml +48 -8
  5. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/src/mongo_pipebuilder/__init__.py +1 -1
  6. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/src/mongo_pipebuilder/builder.py +304 -140
  7. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0/src/mongo_pipebuilder.egg-info}/PKG-INFO +88 -6
  8. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/src/mongo_pipebuilder.egg-info/SOURCES.txt +5 -0
  9. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/tests/test_builder.py +9 -10
  10. mongo_pipebuilder-0.5.0/tests/test_builder_add_stages.py +95 -0
  11. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/tests/test_builder_debug.py +51 -50
  12. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/tests/test_builder_insert.py +22 -21
  13. mongo_pipebuilder-0.5.0/tests/test_builder_lookup_let.py +167 -0
  14. mongo_pipebuilder-0.5.0/tests/test_builder_match_expr.py +69 -0
  15. mongo_pipebuilder-0.5.0/tests/test_builder_union_with.py +95 -0
  16. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/tests/test_builder_validation.py +13 -14
  17. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/tests/test_builder_validation_existing.py +2 -1
  18. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/tests/test_builder_validation_new.py +1 -0
  19. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/LICENSE +0 -0
  20. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/setup.cfg +0 -0
  21. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/src/mongo_pipebuilder.egg-info/dependency_links.txt +0 -0
  22. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/src/mongo_pipebuilder.egg-info/requires.txt +0 -0
  23. {mongo_pipebuilder-0.3.1 → mongo_pipebuilder-0.5.0}/src/mongo_pipebuilder.egg-info/top_level.txt +0 -0
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mongo-pipebuilder
3
- Version: 0.3.1
3
+ Version: 0.5.0
4
4
  Summary: Type-safe, fluent MongoDB aggregation pipeline builder
5
5
  Author-email: seligoroff <seligoroff@gmail.com>
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/seligoroff/mongo-pipebuilder
8
8
  Project-URL: Documentation, https://github.com/seligoroff/mongo-pipebuilder#readme
9
9
  Project-URL: Repository, https://github.com/seligoroff/mongo-pipebuilder
@@ -11,16 +11,14 @@ Project-URL: Issues, https://github.com/seligoroff/mongo-pipebuilder/issues
11
11
  Keywords: mongodb,aggregation,pipeline,builder,query
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
14
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.8
17
15
  Classifier: Programming Language :: Python :: 3.9
18
16
  Classifier: Programming Language :: Python :: 3.10
19
17
  Classifier: Programming Language :: Python :: 3.11
20
18
  Classifier: Programming Language :: Python :: 3.12
21
19
  Classifier: Topic :: Database
22
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
- Requires-Python: >=3.8
21
+ Requires-Python: >=3.9
24
22
  Description-Content-Type: text/markdown
25
23
  License-File: LICENSE
26
24
  Requires-Dist: typing_extensions>=4.0.0; python_version < "3.11"
@@ -29,7 +27,7 @@ Dynamic: license-file
29
27
  # mongo-pipebuilder
30
28
 
31
29
  [![PyPI version](https://badge.fury.io/py/mongo-pipebuilder.svg)](https://badge.fury.io/py/mongo-pipebuilder)
32
- [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
30
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
33
31
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
32
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
35
33
  [![Test Coverage](https://img.shields.io/badge/coverage-96%25-green.svg)](https://github.com/seligoroff/mongo-pipebuilder)
@@ -98,6 +96,15 @@ Adds a `$match` stage to filter documents.
98
96
  .match({"status": "active", "age": {"$gte": 18}})
99
97
  ```
100
98
 
99
+ ##### `match_expr(expr: Dict[str, Any]) -> Self`
100
+
101
+ Adds a `$match` stage with an `$expr` condition (expression-based filter; useful for comparing fields or using variables from `let` in subpipelines).
102
+
103
+ ```python
104
+ .match_expr({"$eq": ["$id", "$$teamId"]})
105
+ .match_expr({"$and": [{"$gte": ["$field", "$other"]}, {"$lte": ["$score", 100]}]})
106
+ ```
107
+
101
108
  ##### `lookup(from_collection: str, local_field: str, foreign_field: str, as_field: str, pipeline: Optional[List[Dict[str, Any]]] = None) -> Self`
102
109
 
103
110
  Adds a `$lookup` stage to join with another collection.
@@ -112,6 +119,43 @@ Adds a `$lookup` stage to join with another collection.
112
119
  )
113
120
  ```
114
121
 
122
+ ##### `lookup_let(from_collection: str, let: Dict[str, Any], pipeline: Union[List[Dict[str, Any]], PipelineBuilder], as_field: str) -> Self`
123
+
124
+ Adds a `$lookup` stage with `let` and `pipeline` (join by expression; variables from the current document are available in the subpipeline as `$$var`). Use this when the join condition is an expression (e.g. `$expr`) rather than equality of two fields.
125
+
126
+ ```python
127
+ # With list of stages
128
+ .lookup_let(
129
+ from_collection="teams",
130
+ let={"teamId": "$idTeam"},
131
+ pipeline=[
132
+ {"$match": {"$expr": {"$eq": ["$_id", "$$teamId"]}}},
133
+ {"$project": {"name": 1, "_id": 0}}
134
+ ],
135
+ as_field="team"
136
+ )
137
+
138
+ # With PipelineBuilder for the subpipeline (optionally using match_expr)
139
+ sub = PipelineBuilder().match_expr({"$eq": ["$_id", "$$teamId"]}).project({"name": 1, "_id": 0})
140
+ .lookup_let("teams", {"teamId": "$idTeam"}, sub, as_field="team")
141
+ ```
142
+
143
+ ##### `union_with(coll: str, pipeline: Optional[Union[List[Dict[str, Any]], PipelineBuilder]] = None) -> Self`
144
+
145
+ Adds a `$unionWith` stage to combine documents from the current pipeline with documents from another collection. Optionally runs a subpipeline on the other collection before merging.
146
+
147
+ ```python
148
+ # Union with another collection (no subpipeline)
149
+ .union_with("other_coll")
150
+
151
+ # With subpipeline as list of stages
152
+ .union_with("logs", [{"$match": {"level": "error"}}, {"$limit": 100}])
153
+
154
+ # With PipelineBuilder for the subpipeline
155
+ sub = PipelineBuilder().match({"source": "individual"}).project({"name": 1})
156
+ .union_with("sso_individual_statistics", sub)
157
+ ```
158
+
115
159
  ##### `add_fields(fields: Dict[str, Any]) -> Self`
116
160
 
117
161
  Adds a `$addFields` stage to add or modify fields.
@@ -237,6 +281,19 @@ Adds a custom stage for advanced use cases.
237
281
  }})
238
282
  ```
239
283
 
284
+ ##### `add_stages(stages: Iterable[Dict[str, Any]]) -> Self`
285
+
286
+ Adds multiple stages at once (e.g. a subpipeline from another builder). Empty dicts are skipped. Useful to avoid loops when inserting a ready-made list of stages.
287
+
288
+ ```python
289
+ # From a list
290
+ .add_stages([{"$match": {"level": "error"}}, {"$limit": 100}])
291
+
292
+ # From another builder
293
+ sub = PipelineBuilder().match({"source": "api"}).project({"name": 1})
294
+ .add_stages(sub.build())
295
+ ```
296
+
240
297
  ##### `prepend(stage: Dict[str, Any]) -> Self`
241
298
 
242
299
  Adds a stage at the beginning of the pipeline.
@@ -411,6 +468,31 @@ pipeline = (
411
468
  )
412
469
  ```
413
470
 
471
+ ### Lookup by expression (lookup_let)
472
+
473
+ When the join condition is an expression (e.g. `$expr`) rather than matching two fields, use `lookup_let`. The subpipeline can be built with `match_expr()`:
474
+
475
+ ```python
476
+ sub = (
477
+ PipelineBuilder()
478
+ .match_expr({"$eq": ["$_id", "$$teamId"]})
479
+ .project({"name": 1, "slug": 1, "_id": 0})
480
+ )
481
+ pipeline = (
482
+ PipelineBuilder()
483
+ .match({"status": "active"})
484
+ .lookup_let(
485
+ from_collection="teams",
486
+ let={"teamId": "$idTeam"},
487
+ pipeline=sub,
488
+ as_field="team"
489
+ )
490
+ .unwind("team", preserve_null_and_empty_arrays=True)
491
+ .project({"title": 1, "teamName": "$team.name"})
492
+ .build()
493
+ )
494
+ ```
495
+
414
496
  ### Aggregation with Grouping
415
497
 
416
498
  ```python
@@ -1,7 +1,7 @@
1
1
  # mongo-pipebuilder
2
2
 
3
3
  [![PyPI version](https://badge.fury.io/py/mongo-pipebuilder.svg)](https://badge.fury.io/py/mongo-pipebuilder)
4
- [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
4
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
7
7
  [![Test Coverage](https://img.shields.io/badge/coverage-96%25-green.svg)](https://github.com/seligoroff/mongo-pipebuilder)
@@ -70,6 +70,15 @@ Adds a `$match` stage to filter documents.
70
70
  .match({"status": "active", "age": {"$gte": 18}})
71
71
  ```
72
72
 
73
+ ##### `match_expr(expr: Dict[str, Any]) -> Self`
74
+
75
+ Adds a `$match` stage with an `$expr` condition (expression-based filter; useful for comparing fields or using variables from `let` in subpipelines).
76
+
77
+ ```python
78
+ .match_expr({"$eq": ["$id", "$$teamId"]})
79
+ .match_expr({"$and": [{"$gte": ["$field", "$other"]}, {"$lte": ["$score", 100]}]})
80
+ ```
81
+
73
82
  ##### `lookup(from_collection: str, local_field: str, foreign_field: str, as_field: str, pipeline: Optional[List[Dict[str, Any]]] = None) -> Self`
74
83
 
75
84
  Adds a `$lookup` stage to join with another collection.
@@ -84,6 +93,43 @@ Adds a `$lookup` stage to join with another collection.
84
93
  )
85
94
  ```
86
95
 
96
+ ##### `lookup_let(from_collection: str, let: Dict[str, Any], pipeline: Union[List[Dict[str, Any]], PipelineBuilder], as_field: str) -> Self`
97
+
98
+ Adds a `$lookup` stage with `let` and `pipeline` (join by expression; variables from the current document are available in the subpipeline as `$$var`). Use this when the join condition is an expression (e.g. `$expr`) rather than equality of two fields.
99
+
100
+ ```python
101
+ # With list of stages
102
+ .lookup_let(
103
+ from_collection="teams",
104
+ let={"teamId": "$idTeam"},
105
+ pipeline=[
106
+ {"$match": {"$expr": {"$eq": ["$_id", "$$teamId"]}}},
107
+ {"$project": {"name": 1, "_id": 0}}
108
+ ],
109
+ as_field="team"
110
+ )
111
+
112
+ # With PipelineBuilder for the subpipeline (optionally using match_expr)
113
+ sub = PipelineBuilder().match_expr({"$eq": ["$_id", "$$teamId"]}).project({"name": 1, "_id": 0})
114
+ .lookup_let("teams", {"teamId": "$idTeam"}, sub, as_field="team")
115
+ ```
116
+
117
+ ##### `union_with(coll: str, pipeline: Optional[Union[List[Dict[str, Any]], PipelineBuilder]] = None) -> Self`
118
+
119
+ Adds a `$unionWith` stage to combine documents from the current pipeline with documents from another collection. Optionally runs a subpipeline on the other collection before merging.
120
+
121
+ ```python
122
+ # Union with another collection (no subpipeline)
123
+ .union_with("other_coll")
124
+
125
+ # With subpipeline as list of stages
126
+ .union_with("logs", [{"$match": {"level": "error"}}, {"$limit": 100}])
127
+
128
+ # With PipelineBuilder for the subpipeline
129
+ sub = PipelineBuilder().match({"source": "individual"}).project({"name": 1})
130
+ .union_with("sso_individual_statistics", sub)
131
+ ```
132
+
87
133
  ##### `add_fields(fields: Dict[str, Any]) -> Self`
88
134
 
89
135
  Adds a `$addFields` stage to add or modify fields.
@@ -209,6 +255,19 @@ Adds a custom stage for advanced use cases.
209
255
  }})
210
256
  ```
211
257
 
258
+ ##### `add_stages(stages: Iterable[Dict[str, Any]]) -> Self`
259
+
260
+ Adds multiple stages at once (e.g. a subpipeline from another builder). Empty dicts are skipped. Useful to avoid loops when inserting a ready-made list of stages.
261
+
262
+ ```python
263
+ # From a list
264
+ .add_stages([{"$match": {"level": "error"}}, {"$limit": 100}])
265
+
266
+ # From another builder
267
+ sub = PipelineBuilder().match({"source": "api"}).project({"name": 1})
268
+ .add_stages(sub.build())
269
+ ```
270
+
212
271
  ##### `prepend(stage: Dict[str, Any]) -> Self`
213
272
 
214
273
  Adds a stage at the beginning of the pipeline.
@@ -383,6 +442,31 @@ pipeline = (
383
442
  )
384
443
  ```
385
444
 
445
+ ### Lookup by expression (lookup_let)
446
+
447
+ When the join condition is an expression (e.g. `$expr`) rather than matching two fields, use `lookup_let`. The subpipeline can be built with `match_expr()`:
448
+
449
+ ```python
450
+ sub = (
451
+ PipelineBuilder()
452
+ .match_expr({"$eq": ["$_id", "$$teamId"]})
453
+ .project({"name": 1, "slug": 1, "_id": 0})
454
+ )
455
+ pipeline = (
456
+ PipelineBuilder()
457
+ .match({"status": "active"})
458
+ .lookup_let(
459
+ from_collection="teams",
460
+ let={"teamId": "$idTeam"},
461
+ pipeline=sub,
462
+ as_field="team"
463
+ )
464
+ .unwind("team", preserve_null_and_empty_arrays=True)
465
+ .project({"title": 1, "teamName": "$team.name"})
466
+ .build()
467
+ )
468
+ ```
469
+
386
470
  ### Aggregation with Grouping
387
471
 
388
472
  ```python
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mongo-pipebuilder"
7
- version = "0.3.1"
7
+ version = "0.5.0"
8
8
  description = "Type-safe, fluent MongoDB aggregation pipeline builder"
9
9
  readme = "README.md"
10
- requires-python = ">=3.8"
10
+ requires-python = ">=3.9"
11
11
  dependencies = [
12
12
  "typing_extensions>=4.0.0; python_version<'3.11'",
13
13
  ]
14
- license = {text = "MIT"}
14
+ license = "MIT"
15
15
  authors = [
16
16
  {name = "seligoroff", email = "seligoroff@gmail.com"}
17
17
  ]
@@ -19,9 +19,7 @@ keywords = ["mongodb", "aggregation", "pipeline", "builder", "query"]
19
19
  classifiers = [
20
20
  "Development Status :: 3 - Alpha",
21
21
  "Intended Audience :: Developers",
22
- "License :: OSI Approved :: MIT License",
23
22
  "Programming Language :: Python :: 3",
24
- "Programming Language :: Python :: 3.8",
25
23
  "Programming Language :: Python :: 3.9",
26
24
  "Programming Language :: Python :: 3.10",
27
25
  "Programming Language :: Python :: 3.11",
@@ -51,12 +49,13 @@ addopts = [
51
49
  "--cov=src/mongo_pipebuilder",
52
50
  "--cov-report=term-missing",
53
51
  "--cov-report=html:tests/htmlcov",
54
- "--cov-report=json:coverage.json",
55
- "--cov-report=xml:coverage.xml",
52
+ "--cov-report=json:tests/coverage.json",
53
+ "--cov-report=xml:tests/coverage.xml",
56
54
  "--cov-branch",
57
55
  ]
58
56
 
59
57
  [tool.coverage.run]
58
+ data_file = "tests/.coverage"
60
59
  source = ["src/mongo_pipebuilder"]
61
60
  omit = [
62
61
  "*/tests/*",
@@ -84,7 +83,7 @@ skip_empty = true
84
83
  directory = "tests/htmlcov"
85
84
 
86
85
  [tool.mypy]
87
- python_version = "3.8"
86
+ python_version = "3.9"
88
87
  warn_return_any = true
89
88
  warn_unused_configs = true
90
89
  disallow_untyped_defs = false
@@ -99,3 +98,44 @@ warn_no_return = true
99
98
  module = "tests.*"
100
99
  disallow_untyped_defs = false
101
100
 
101
+ [tool.ruff]
102
+ line-length = 110
103
+ target-version = "py311"
104
+ # В Ruff лучше использовать .gitignore, но если нужно строго как было:
105
+ #respect-gitignore = false
106
+ #extend-exclude = ["venv", "deprecated"]
107
+
108
+ [tool.ruff.lint]
109
+ # E, F — база (Flake8), B — Bugbear (ошибки логики), I — Isort (импорты)
110
+ select = ["E", "F", "W", "B", "C901", "I", "Q", "T20", "COM"]
111
+ ignore = [
112
+ "B008", # Аргументы-функции в дефолтных значениях (часто нужно для FastAPI Depends)
113
+ "E266", # Слишком много # в комментариях
114
+ "E712", # Сравнение с True через == (иногда нужно в SQLAlchemy)
115
+ "COM812", # Конфликт с форматтером (запятые)
116
+ "COM819", # Trailing comma в type hints
117
+ "E203", # Пробел перед двоеточием (совместимость с Black)
118
+ "Q003", # Кавычки в f-строках
119
+ "PT006", # Типы параметров pytest
120
+ ]
121
+
122
+ [tool.ruff.lint.per-file-ignores]
123
+ # Игнорируем специфичные ошибки в тестах, скриптах, примерах и инициализации
124
+ "tests/*" = ["T201"]
125
+ "examples/*" = ["T201"]
126
+
127
+
128
+ [tool.ruff.lint.mccabe]
129
+ max-complexity = 12
130
+
131
+ [tool.ruff.lint.isort]
132
+ # Автоматически определяет внутренние модули, чтобы не мешать их с pip-пакетами
133
+ known-first-party = [
134
+ "api", "cli", "constants", "core", "helpers",
135
+ "infrastructure", "schemas", "scripts", "services",
136
+ "startup", "tasks", "tests"
137
+ ]
138
+
139
+ [tool.ruff.format]
140
+ quote-style = "double"
141
+ indent-style = "space"
@@ -9,6 +9,6 @@ Author: seligoroff
9
9
 
10
10
  from mongo_pipebuilder.builder import PipelineBuilder
11
11
 
12
- __version__ = "0.3.1"
12
+ __version__ = "0.5.0"
13
13
  __all__ = ["PipelineBuilder"]
14
14