mongo-aggro 0.1.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

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 (53) hide show
  1. mongo_aggro/__init__.py +400 -0
  2. mongo_aggro/accumulators.py +30 -12
  3. mongo_aggro/base.py +49 -9
  4. mongo_aggro/expressions/__init__.py +396 -0
  5. mongo_aggro/expressions/arithmetic.py +329 -0
  6. mongo_aggro/expressions/array.py +425 -0
  7. mongo_aggro/expressions/base.py +180 -0
  8. mongo_aggro/expressions/bitwise.py +84 -0
  9. mongo_aggro/expressions/comparison.py +161 -0
  10. mongo_aggro/expressions/conditional.py +117 -0
  11. mongo_aggro/expressions/date.py +665 -0
  12. mongo_aggro/expressions/encrypted.py +116 -0
  13. mongo_aggro/expressions/logical.py +72 -0
  14. mongo_aggro/expressions/object.py +122 -0
  15. mongo_aggro/expressions/set.py +150 -0
  16. mongo_aggro/expressions/size.py +48 -0
  17. mongo_aggro/expressions/string.py +365 -0
  18. mongo_aggro/expressions/trigonometry.py +283 -0
  19. mongo_aggro/expressions/type.py +205 -0
  20. mongo_aggro/expressions/variable.py +73 -0
  21. mongo_aggro/expressions/window.py +327 -0
  22. mongo_aggro/operators/__init__.py +65 -0
  23. mongo_aggro/operators/array.py +41 -0
  24. mongo_aggro/operators/base.py +15 -0
  25. mongo_aggro/operators/bitwise.py +81 -0
  26. mongo_aggro/operators/comparison.py +82 -0
  27. mongo_aggro/operators/element.py +32 -0
  28. mongo_aggro/operators/geo.py +171 -0
  29. mongo_aggro/operators/logical.py +111 -0
  30. mongo_aggro/operators/misc.py +102 -0
  31. mongo_aggro/operators/regex.py +25 -0
  32. mongo_aggro/stages/__init__.py +110 -0
  33. mongo_aggro/stages/array.py +69 -0
  34. mongo_aggro/stages/change.py +109 -0
  35. mongo_aggro/stages/core.py +170 -0
  36. mongo_aggro/stages/geo.py +93 -0
  37. mongo_aggro/stages/group.py +154 -0
  38. mongo_aggro/stages/join.py +221 -0
  39. mongo_aggro/stages/misc.py +45 -0
  40. mongo_aggro/stages/output.py +136 -0
  41. mongo_aggro/stages/search.py +315 -0
  42. mongo_aggro/stages/session.py +111 -0
  43. mongo_aggro/stages/stats.py +152 -0
  44. mongo_aggro/stages/transform.py +136 -0
  45. mongo_aggro/stages/window.py +139 -0
  46. mongo_aggro-0.2.2.dist-info/METADATA +193 -0
  47. mongo_aggro-0.2.2.dist-info/RECORD +49 -0
  48. {mongo_aggro-0.1.0.dist-info → mongo_aggro-0.2.2.dist-info}/WHEEL +1 -1
  49. mongo_aggro/operators.py +0 -247
  50. mongo_aggro/stages.py +0 -990
  51. mongo_aggro-0.1.0.dist-info/METADATA +0 -537
  52. mongo_aggro-0.1.0.dist-info/RECORD +0 -9
  53. {mongo_aggro-0.1.0.dist-info → mongo_aggro-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,110 @@
1
+ """MongoDB aggregation pipeline stages.
2
+
3
+ This package provides typed stage classes for building MongoDB aggregation
4
+ pipelines. All stages are re-exported here for convenient access.
5
+ """
6
+
7
+ from mongo_aggro.stages.array import Unwind
8
+ from mongo_aggro.stages.change import ChangeStream, ChangeStreamSplitLargeEvent
9
+ from mongo_aggro.stages.core import (
10
+ Count,
11
+ Group,
12
+ Limit,
13
+ Match,
14
+ Project,
15
+ Skip,
16
+ Sort,
17
+ )
18
+ from mongo_aggro.stages.geo import GeoNear
19
+ from mongo_aggro.stages.group import Bucket, BucketAuto, Facet, SortByCount
20
+ from mongo_aggro.stages.join import GraphLookup, Lookup, UnionWith
21
+ from mongo_aggro.stages.misc import ListClusterCatalog, QuerySettings
22
+ from mongo_aggro.stages.output import Documents, Merge, Out, Sample
23
+ from mongo_aggro.stages.search import (
24
+ ListSearchIndexes,
25
+ RankFusion,
26
+ Search,
27
+ SearchMeta,
28
+ VectorSearch,
29
+ )
30
+ from mongo_aggro.stages.session import (
31
+ ListLocalSessions,
32
+ ListSampledQueries,
33
+ ListSessions,
34
+ )
35
+ from mongo_aggro.stages.stats import (
36
+ CollStats,
37
+ CurrentOp,
38
+ IndexStats,
39
+ PlanCacheStats,
40
+ )
41
+ from mongo_aggro.stages.transform import (
42
+ AddFields,
43
+ Redact,
44
+ ReplaceRoot,
45
+ ReplaceWith,
46
+ Set,
47
+ Unset,
48
+ )
49
+ from mongo_aggro.stages.window import Densify, Fill, SetWindowFields
50
+
51
+ __all__ = [
52
+ # core
53
+ "Match",
54
+ "Project",
55
+ "Group",
56
+ "Sort",
57
+ "Limit",
58
+ "Skip",
59
+ "Count",
60
+ # array
61
+ "Unwind",
62
+ # join
63
+ "Lookup",
64
+ "UnionWith",
65
+ "GraphLookup",
66
+ # transform
67
+ "AddFields",
68
+ "Set",
69
+ "Unset",
70
+ "ReplaceRoot",
71
+ "ReplaceWith",
72
+ "Redact",
73
+ # group
74
+ "Facet",
75
+ "Bucket",
76
+ "BucketAuto",
77
+ "SortByCount",
78
+ # output
79
+ "Out",
80
+ "Merge",
81
+ "Sample",
82
+ "Documents",
83
+ # window
84
+ "SetWindowFields",
85
+ "Densify",
86
+ "Fill",
87
+ # geo
88
+ "GeoNear",
89
+ # stats
90
+ "CollStats",
91
+ "IndexStats",
92
+ "PlanCacheStats",
93
+ "CurrentOp",
94
+ # session
95
+ "ListSessions",
96
+ "ListLocalSessions",
97
+ "ListSampledQueries",
98
+ # change
99
+ "ChangeStream",
100
+ "ChangeStreamSplitLargeEvent",
101
+ # search
102
+ "Search",
103
+ "SearchMeta",
104
+ "VectorSearch",
105
+ "ListSearchIndexes",
106
+ "RankFusion",
107
+ # misc
108
+ "ListClusterCatalog",
109
+ "QuerySettings",
110
+ ]
@@ -0,0 +1,69 @@
1
+ """Array-related MongoDB aggregation pipeline stages.
2
+
3
+ This module contains stages for working with array fields, primarily Unwind.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+
11
+ class Unwind(BaseModel):
12
+ """
13
+ $unwind stage - deconstructs an array field.
14
+
15
+ Example:
16
+ >>> Unwind(path="cars").model_dump()
17
+ {"$unwind": "$cars"}
18
+
19
+ >>> # With options
20
+ >>> Unwind(
21
+ ... path="items",
22
+ ... include_array_index="itemIndex",
23
+ ... preserve_null_and_empty=True
24
+ ... ).model_dump()
25
+ {"$unwind": {
26
+ "path": "$items",
27
+ "includeArrayIndex": "itemIndex",
28
+ "preserveNullAndEmptyArrays": true
29
+ }}
30
+ """
31
+
32
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
33
+
34
+ path: str = Field(..., description="Array field path (without $)")
35
+ include_array_index: str | None = Field(
36
+ default=None,
37
+ validation_alias="includeArrayIndex",
38
+ serialization_alias="includeArrayIndex",
39
+ description="Name of index field",
40
+ )
41
+ preserve_null_and_empty: bool | None = Field(
42
+ default=None,
43
+ validation_alias="preserveNullAndEmptyArrays",
44
+ serialization_alias="preserveNullAndEmptyArrays",
45
+ description="Output doc if array is null/empty/missing",
46
+ )
47
+
48
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
49
+ field_path = (
50
+ f"${self.path}" if not self.path.startswith("$") else self.path
51
+ )
52
+
53
+ if (
54
+ self.include_array_index is None
55
+ and self.preserve_null_and_empty is None
56
+ ):
57
+ return {"$unwind": field_path}
58
+
59
+ result: dict[str, Any] = {"path": field_path}
60
+ if self.include_array_index is not None:
61
+ result["includeArrayIndex"] = self.include_array_index
62
+ if self.preserve_null_and_empty is not None:
63
+ result["preserveNullAndEmptyArrays"] = self.preserve_null_and_empty
64
+ return {"$unwind": result}
65
+
66
+
67
+ __all__ = [
68
+ "Unwind",
69
+ ]
@@ -0,0 +1,109 @@
1
+ """Change stream MongoDB aggregation pipeline stages.
2
+
3
+ This module contains stages for change stream operations:
4
+ ChangeStream and ChangeStreamSplitLargeEvent.
5
+ """
6
+
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+
12
+ class ChangeStream(BaseModel):
13
+ """
14
+ $changeStream stage - returns a change stream cursor.
15
+
16
+ Must be the first stage in the pipeline.
17
+
18
+ Example:
19
+ >>> ChangeStream().model_dump()
20
+ {"$changeStream": {}}
21
+
22
+ >>> ChangeStream(full_document="updateLookup").model_dump()
23
+ {"$changeStream": {"fullDocument": "updateLookup"}}
24
+ """
25
+
26
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
27
+
28
+ full_document: (
29
+ Literal["default", "updateLookup", "whenAvailable", "required"] | None
30
+ ) = Field(
31
+ default=None,
32
+ serialization_alias="fullDocument",
33
+ description="Full document option for update events",
34
+ )
35
+ full_document_before_change: (
36
+ Literal["off", "whenAvailable", "required"] | None
37
+ ) = Field(
38
+ default=None,
39
+ serialization_alias="fullDocumentBeforeChange",
40
+ description="Include pre-image of modified document",
41
+ )
42
+ resume_after: dict[str, Any] | None = Field(
43
+ default=None,
44
+ serialization_alias="resumeAfter",
45
+ description="Resume token to resume change stream",
46
+ )
47
+ start_after: dict[str, Any] | None = Field(
48
+ default=None,
49
+ serialization_alias="startAfter",
50
+ description="Resume token to start after",
51
+ )
52
+ start_at_operation_time: Any | None = Field(
53
+ default=None,
54
+ serialization_alias="startAtOperationTime",
55
+ description="Timestamp to start watching changes",
56
+ )
57
+ all_changes_for_cluster: bool | None = Field(
58
+ default=None,
59
+ serialization_alias="allChangesForCluster",
60
+ description="Watch all changes for the cluster",
61
+ )
62
+ show_expanded_events: bool | None = Field(
63
+ default=None,
64
+ serialization_alias="showExpandedEvents",
65
+ description="Show expanded change events",
66
+ )
67
+
68
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
69
+ result: dict[str, Any] = {}
70
+ if self.full_document is not None:
71
+ result["fullDocument"] = self.full_document
72
+ if self.full_document_before_change is not None:
73
+ result["fullDocumentBeforeChange"] = (
74
+ self.full_document_before_change
75
+ )
76
+ if self.resume_after is not None:
77
+ result["resumeAfter"] = self.resume_after
78
+ if self.start_after is not None:
79
+ result["startAfter"] = self.start_after
80
+ if self.start_at_operation_time is not None:
81
+ result["startAtOperationTime"] = self.start_at_operation_time
82
+ if self.all_changes_for_cluster is not None:
83
+ result["allChangesForCluster"] = self.all_changes_for_cluster
84
+ if self.show_expanded_events is not None:
85
+ result["showExpandedEvents"] = self.show_expanded_events
86
+ return {"$changeStream": result}
87
+
88
+
89
+ class ChangeStreamSplitLargeEvent(BaseModel):
90
+ """
91
+ $changeStreamSplitLargeEvent stage - splits large change events.
92
+
93
+ Must be the last stage in a $changeStream pipeline.
94
+
95
+ Example:
96
+ >>> ChangeStreamSplitLargeEvent().model_dump()
97
+ {"$changeStreamSplitLargeEvent": {}}
98
+ """
99
+
100
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
101
+
102
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
103
+ return {"$changeStreamSplitLargeEvent": {}}
104
+
105
+
106
+ __all__ = [
107
+ "ChangeStream",
108
+ "ChangeStreamSplitLargeEvent",
109
+ ]
@@ -0,0 +1,170 @@
1
+ """Core MongoDB aggregation pipeline stages.
2
+
3
+ This module contains the fundamental stages used in most aggregation pipelines:
4
+ Match, Project, Group, Sort, Limit, Skip, and Count.
5
+ """
6
+
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+
12
+ class Match(BaseModel):
13
+ """
14
+ $match stage - filters documents by specified criteria.
15
+
16
+ Example:
17
+ >>> Match(query={"status": "active"}).model_dump()
18
+ {"$match": {"status": "active"}}
19
+
20
+ >>> # With logical operators
21
+ >>> Match(query={"$and": [{"status": "active"}, {"age": {"$gt": 18}}]})
22
+ """
23
+
24
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
25
+
26
+ query: dict[str, Any] = Field(..., description="Query filter conditions")
27
+
28
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
29
+ return {"$match": self.query}
30
+
31
+
32
+ class Project(BaseModel):
33
+ """
34
+ $project stage - shapes documents by including/excluding fields.
35
+
36
+ Example:
37
+ >>> Project(fields={"name": 1, "year": 1, "_id": 0}).model_dump()
38
+ {"$project": {"name": 1, "year": 1, "_id": 0}}
39
+
40
+ >>> # With expressions
41
+ >>> Project(fields={"fullName": {"$concat": ["$first", " ", "$last"]}})
42
+ """
43
+
44
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
45
+
46
+ fields: dict[str, Any] = Field(
47
+ ..., description="Field projections (1=include, 0=exclude, or expr)"
48
+ )
49
+
50
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
51
+ return {"$project": self.fields}
52
+
53
+
54
+ class Group(BaseModel):
55
+ """
56
+ $group stage - groups documents by specified expression.
57
+
58
+ Example:
59
+ >>> Group(
60
+ ... id="$category",
61
+ ... total={"$sum": "$quantity"},
62
+ ... count={"$sum": 1}
63
+ ... ).model_dump()
64
+ {
65
+ "$group": {
66
+ "_id": "$category",
67
+ "total": {"$sum": "$quantity"},
68
+ "count": {"$sum": 1}
69
+ }
70
+ }
71
+ """
72
+
73
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
74
+
75
+ id: Any = Field(
76
+ ...,
77
+ validation_alias="_id",
78
+ serialization_alias="_id",
79
+ description="Grouping expression",
80
+ )
81
+ accumulators: dict[str, Any] = Field(
82
+ default_factory=dict,
83
+ description="Accumulator expressions (e.g., $sum, $avg)",
84
+ )
85
+
86
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
87
+ result = {"_id": self.id}
88
+ result.update(self.accumulators)
89
+ return {"$group": result}
90
+
91
+
92
+ class Sort(BaseModel):
93
+ """
94
+ $sort stage - sorts documents.
95
+
96
+ Example:
97
+ >>> Sort(fields={"age": -1, "name": 1}).model_dump()
98
+ {"$sort": {"age": -1, "name": 1}}
99
+ """
100
+
101
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
102
+
103
+ fields: dict[str, Literal[-1, 1]] = Field(
104
+ ..., description="Sort specification (1=asc, -1=desc)"
105
+ )
106
+
107
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
108
+ return {"$sort": self.fields}
109
+
110
+
111
+ class Limit(BaseModel):
112
+ """
113
+ $limit stage - limits the number of documents.
114
+
115
+ Example:
116
+ >>> Limit(count=10).model_dump()
117
+ {"$limit": 10}
118
+ """
119
+
120
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
121
+
122
+ count: int = Field(..., gt=0, description="Maximum number of documents")
123
+
124
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
125
+ return {"$limit": self.count}
126
+
127
+
128
+ class Skip(BaseModel):
129
+ """
130
+ $skip stage - skips a number of documents.
131
+
132
+ Example:
133
+ >>> Skip(count=5).model_dump()
134
+ {"$skip": 5}
135
+ """
136
+
137
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
138
+
139
+ count: int = Field(..., ge=0, description="Number of documents to skip")
140
+
141
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
142
+ return {"$skip": self.count}
143
+
144
+
145
+ class Count(BaseModel):
146
+ """
147
+ $count stage - counts documents.
148
+
149
+ Example:
150
+ >>> Count(field="total").model_dump()
151
+ {"$count": "total"}
152
+ """
153
+
154
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
155
+
156
+ field: str = Field(..., description="Output field name for count")
157
+
158
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
159
+ return {"$count": self.field}
160
+
161
+
162
+ __all__ = [
163
+ "Match",
164
+ "Project",
165
+ "Group",
166
+ "Sort",
167
+ "Limit",
168
+ "Skip",
169
+ "Count",
170
+ ]
@@ -0,0 +1,93 @@
1
+ """Geospatial MongoDB aggregation pipeline stages.
2
+
3
+ This module contains stages for geospatial queries: GeoNear.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+
11
+ class GeoNear(BaseModel):
12
+ """
13
+ $geoNear stage - returns documents near a geographic point.
14
+
15
+ Example:
16
+ >>> GeoNear(
17
+ ... near={"type": "Point", "coordinates": [-73.99, 40.73]},
18
+ ... distance_field="dist.calculated",
19
+ ... spherical=True,
20
+ ... max_distance=5000
21
+ ... ).model_dump()
22
+ """
23
+
24
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
25
+
26
+ near: dict[str, Any] | list[float] = Field(
27
+ ..., description="GeoJSON point or legacy coordinates"
28
+ )
29
+ distance_field: str = Field(
30
+ ...,
31
+ validation_alias="distanceField",
32
+ serialization_alias="distanceField",
33
+ description="Field for calculated distance",
34
+ )
35
+ spherical: bool | None = Field(
36
+ default=None, description="Use spherical geometry"
37
+ )
38
+ max_distance: float | None = Field(
39
+ default=None,
40
+ validation_alias="maxDistance",
41
+ serialization_alias="maxDistance",
42
+ description="Max distance in meters",
43
+ )
44
+ min_distance: float | None = Field(
45
+ default=None,
46
+ validation_alias="minDistance",
47
+ serialization_alias="minDistance",
48
+ description="Min distance in meters",
49
+ )
50
+ query: dict[str, Any] | None = Field(
51
+ default=None, description="Additional query filter"
52
+ )
53
+ distance_multiplier: float | None = Field(
54
+ default=None,
55
+ validation_alias="distanceMultiplier",
56
+ serialization_alias="distanceMultiplier",
57
+ description="Multiplier for distances",
58
+ )
59
+ include_locs: str | None = Field(
60
+ default=None,
61
+ validation_alias="includeLocs",
62
+ serialization_alias="includeLocs",
63
+ description="Field for matched location",
64
+ )
65
+ key: str | None = Field(
66
+ default=None, description="Geospatial index to use"
67
+ )
68
+
69
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
70
+ result: dict[str, Any] = {
71
+ "near": self.near,
72
+ "distanceField": self.distance_field,
73
+ }
74
+ if self.spherical is not None:
75
+ result["spherical"] = self.spherical
76
+ if self.max_distance is not None:
77
+ result["maxDistance"] = self.max_distance
78
+ if self.min_distance is not None:
79
+ result["minDistance"] = self.min_distance
80
+ if self.query is not None:
81
+ result["query"] = self.query
82
+ if self.distance_multiplier is not None:
83
+ result["distanceMultiplier"] = self.distance_multiplier
84
+ if self.include_locs is not None:
85
+ result["includeLocs"] = self.include_locs
86
+ if self.key is not None:
87
+ result["key"] = self.key
88
+ return {"$geoNear": result}
89
+
90
+
91
+ __all__ = [
92
+ "GeoNear",
93
+ ]
@@ -0,0 +1,154 @@
1
+ """Grouping and bucketing MongoDB aggregation pipeline stages.
2
+
3
+ This module contains stages for advanced grouping operations:
4
+ Facet, Bucket, BucketAuto, and SortByCount.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from mongo_aggro.base import Pipeline
12
+
13
+
14
+ class SortByCount(BaseModel):
15
+ """
16
+ $sortByCount stage - groups and counts by field, sorted by count.
17
+
18
+ Example:
19
+ >>> SortByCount(field="category").model_dump()
20
+ {"$sortByCount": "$category"}
21
+ """
22
+
23
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
24
+
25
+ field: str = Field(..., description="Field to group and count by")
26
+
27
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
28
+ field_path = (
29
+ f"${self.field}" if not self.field.startswith("$") else self.field
30
+ )
31
+ return {"$sortByCount": field_path}
32
+
33
+
34
+ class Facet(BaseModel):
35
+ """
36
+ $facet stage - processes multiple pipelines within a single stage.
37
+
38
+ Example:
39
+ >>> Facet(pipelines={
40
+ ... "byCategory": Pipeline([Group(id="$category")]),
41
+ ... "byYear": Pipeline([Group(id="$year")])
42
+ ... }).model_dump()
43
+ {"$facet": {
44
+ "byCategory": [{"$group": {"_id": "$category"}}],
45
+ "byYear": [{"$group": {"_id": "$year"}}]
46
+ }}
47
+ """
48
+
49
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
50
+
51
+ pipelines: dict[str, Pipeline | list[dict[str, Any]]] = Field(
52
+ ..., description="Named pipelines"
53
+ )
54
+
55
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
56
+ result: dict[str, list[dict[str, Any]]] = {}
57
+ for name, pipeline in self.pipelines.items():
58
+ if isinstance(pipeline, Pipeline):
59
+ result[name] = pipeline.to_list()
60
+ else:
61
+ result[name] = pipeline
62
+ return {"$facet": result}
63
+
64
+
65
+ class Bucket(BaseModel):
66
+ """
67
+ $bucket stage - categorizes documents into buckets.
68
+
69
+ Example:
70
+ >>> Bucket(
71
+ ... group_by="$price",
72
+ ... boundaries=[0, 100, 500, 1000],
73
+ ... default="Other",
74
+ ... output={"count": {"$sum": 1}}
75
+ ... ).model_dump()
76
+ {"$bucket": {
77
+ "groupBy": "$price",
78
+ "boundaries": [0, 100, 500, 1000],
79
+ "default": "Other",
80
+ "output": {"count": {"$sum": 1}}
81
+ }}
82
+ """
83
+
84
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
85
+
86
+ group_by: str | dict[str, Any] = Field(
87
+ ...,
88
+ validation_alias="groupBy",
89
+ serialization_alias="groupBy",
90
+ description="Expression to group by",
91
+ )
92
+ boundaries: list[Any] = Field(..., description="Bucket boundaries")
93
+ default: Any | None = Field(
94
+ default=None, description="Default bucket for non-matching docs"
95
+ )
96
+ output: dict[str, Any] | None = Field(
97
+ default=None, description="Output document specification"
98
+ )
99
+
100
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
101
+ result: dict[str, Any] = {
102
+ "groupBy": self.group_by,
103
+ "boundaries": self.boundaries,
104
+ }
105
+ if self.default is not None:
106
+ result["default"] = self.default
107
+ if self.output is not None:
108
+ result["output"] = self.output
109
+ return {"$bucket": result}
110
+
111
+
112
+ class BucketAuto(BaseModel):
113
+ """
114
+ $bucketAuto stage - automatically categorizes into specified buckets.
115
+
116
+ Example:
117
+ >>> BucketAuto(group_by="$age", buckets=5).model_dump()
118
+ {"$bucketAuto": {"groupBy": "$age", "buckets": 5}}
119
+ """
120
+
121
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
122
+
123
+ group_by: str | dict[str, Any] = Field(
124
+ ...,
125
+ validation_alias="groupBy",
126
+ serialization_alias="groupBy",
127
+ description="Expression to group by",
128
+ )
129
+ buckets: int = Field(..., gt=0, description="Number of buckets")
130
+ output: dict[str, Any] | None = Field(
131
+ default=None, description="Output document specification"
132
+ )
133
+ granularity: str | None = Field(
134
+ default=None, description="Preferred number series"
135
+ )
136
+
137
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
138
+ result: dict[str, Any] = {
139
+ "groupBy": self.group_by,
140
+ "buckets": self.buckets,
141
+ }
142
+ if self.output is not None:
143
+ result["output"] = self.output
144
+ if self.granularity is not None:
145
+ result["granularity"] = self.granularity
146
+ return {"$bucketAuto": result}
147
+
148
+
149
+ __all__ = [
150
+ "Facet",
151
+ "Bucket",
152
+ "BucketAuto",
153
+ "SortByCount",
154
+ ]