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.
- mongo_aggro/__init__.py +400 -0
- mongo_aggro/accumulators.py +30 -12
- mongo_aggro/base.py +49 -9
- mongo_aggro/expressions/__init__.py +396 -0
- mongo_aggro/expressions/arithmetic.py +329 -0
- mongo_aggro/expressions/array.py +425 -0
- mongo_aggro/expressions/base.py +180 -0
- mongo_aggro/expressions/bitwise.py +84 -0
- mongo_aggro/expressions/comparison.py +161 -0
- mongo_aggro/expressions/conditional.py +117 -0
- mongo_aggro/expressions/date.py +665 -0
- mongo_aggro/expressions/encrypted.py +116 -0
- mongo_aggro/expressions/logical.py +72 -0
- mongo_aggro/expressions/object.py +122 -0
- mongo_aggro/expressions/set.py +150 -0
- mongo_aggro/expressions/size.py +48 -0
- mongo_aggro/expressions/string.py +365 -0
- mongo_aggro/expressions/trigonometry.py +283 -0
- mongo_aggro/expressions/type.py +205 -0
- mongo_aggro/expressions/variable.py +73 -0
- mongo_aggro/expressions/window.py +327 -0
- mongo_aggro/operators/__init__.py +65 -0
- mongo_aggro/operators/array.py +41 -0
- mongo_aggro/operators/base.py +15 -0
- mongo_aggro/operators/bitwise.py +81 -0
- mongo_aggro/operators/comparison.py +82 -0
- mongo_aggro/operators/element.py +32 -0
- mongo_aggro/operators/geo.py +171 -0
- mongo_aggro/operators/logical.py +111 -0
- mongo_aggro/operators/misc.py +102 -0
- mongo_aggro/operators/regex.py +25 -0
- mongo_aggro/stages/__init__.py +110 -0
- mongo_aggro/stages/array.py +69 -0
- mongo_aggro/stages/change.py +109 -0
- mongo_aggro/stages/core.py +170 -0
- mongo_aggro/stages/geo.py +93 -0
- mongo_aggro/stages/group.py +154 -0
- mongo_aggro/stages/join.py +221 -0
- mongo_aggro/stages/misc.py +45 -0
- mongo_aggro/stages/output.py +136 -0
- mongo_aggro/stages/search.py +315 -0
- mongo_aggro/stages/session.py +111 -0
- mongo_aggro/stages/stats.py +152 -0
- mongo_aggro/stages/transform.py +136 -0
- mongo_aggro/stages/window.py +139 -0
- mongo_aggro-0.2.2.dist-info/METADATA +193 -0
- mongo_aggro-0.2.2.dist-info/RECORD +49 -0
- {mongo_aggro-0.1.0.dist-info → mongo_aggro-0.2.2.dist-info}/WHEEL +1 -1
- mongo_aggro/operators.py +0 -247
- mongo_aggro/stages.py +0 -990
- mongo_aggro-0.1.0.dist-info/METADATA +0 -537
- mongo_aggro-0.1.0.dist-info/RECORD +0 -9
- {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
|
+
]
|