Skip to content

Commit 4af38dd

Browse files
authored
Add support for find functions, replace functions, count_documents, and other CRUD helpers (#6)
1 parent 7a77964 commit 4af38dd

File tree

4 files changed

+325
-168
lines changed

4 files changed

+325
-168
lines changed

pymongoexplain/commands.py

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,21 @@
2020

2121
from bson.son import SON
2222
from pymongo.collection import Collection
23+
from .utils import convert_to_camelcase
24+
2325

2426
Document = Union[dict, SON]
2527

28+
2629
class BaseCommand():
27-
def __init__(self, dictionary):
28-
if type(self) == BaseCommand:
29-
raise NotImplementedError
30-
self.command_document = self.convert_to_camelcase(dictionary)
31-
32-
def convert_to_camelcase(self, d: dict) -> dict:
33-
ret = dict()
34-
for key in d.keys():
35-
if d[key] is None:
36-
continue
37-
new_key = key
38-
if "_" in key:
39-
new_key = key.split("_")[0] + ''.join(
40-
[i.capitalize() for i in key.split("_")[1:]])
41-
if type(d[key]) == list:
42-
ret[new_key] = [self.convert_to_camelcase(i) for i in d[key]
43-
if type(i) == dict]
44-
elif type(d[key]) == dict:
45-
ret[new_key] = self.convert_to_camelcase(d[key])
46-
else:
47-
ret[new_key] = d[key]
48-
return ret
30+
def __init__(self, collection):
31+
self.command_document = {}
32+
self.collection = collection
33+
34+
@property
35+
def command_name(self):
36+
"""New command classes will specify the command name here."""
37+
return None
4938

5039
def get_SON(self):
5140
cmd = SON([(self.command_name, self.collection)])
@@ -56,72 +45,99 @@ def get_SON(self):
5645
class UpdateCommand(BaseCommand):
5746
def __init__(self, collection: Collection, filter, update,
5847
kwargs):
59-
super().__init__(kwargs)
60-
self.command_name = "update"
61-
self.collection = collection.name
62-
return_dictionary = {"updates":[{"q": filter, "u": update}]}
63-
for key, value in self.command_document.items():
64-
if key == "bypassDocumentValidation":
65-
return_dictionary[key] = value
48+
super().__init__(collection.name)
49+
return_document = {"updates":[{"q": filter, "u": update}]}
50+
for key in kwargs:
51+
value = kwargs[key]
52+
if key == "bypass_document_validation":
53+
return_document[key] = value
6654
else:
67-
return_dictionary["updates"][0][key] = value
68-
self.command_document = return_dictionary
55+
return_document["updates"][0][key] = value
56+
self.command_document = convert_to_camelcase(return_document)
57+
58+
@property
59+
def command_name(self):
60+
return "update"
6961

7062

7163
class DistinctCommand(BaseCommand):
7264
def __init__(self, collection: Collection, key, filter, session,
7365
kwargs):
74-
self.command_name = "distinct"
75-
self.collection = collection.name
66+
super().__init__(collection.name)
7667
self.command_document = {"key": key, "query": filter}
7768
for key, value in kwargs.items():
7869
self.command_document[key] = value
79-
super().__init__(self.command_document)
70+
self.command_document = convert_to_camelcase(self.command_document)
71+
72+
@property
73+
def command_name(self):
74+
return "distinct"
8075

8176

8277
class AggregateCommand(BaseCommand):
8378
def __init__(self, collection: Collection, pipeline, session,
8479
cursor_options,
85-
kwargs):
86-
self.command_name = "aggregate"
87-
self.collection = collection.name
80+
kwargs, exclude_keys = []):
81+
super().__init__(collection.name)
8882
self.command_document = {"pipeline": pipeline, "cursor": cursor_options}
8983
for key, value in kwargs.items():
9084
self.command_document[key] = value
91-
super().__init__(self.command_document)
9285

86+
self.command_document = convert_to_camelcase(
87+
self.command_document, exclude_keys=exclude_keys)
88+
89+
@property
90+
def command_name(self):
91+
return "aggregate"
9392

9493
class CountCommand(BaseCommand):
9594
def __init__(self, collection: Collection, filter,
9695
kwargs):
97-
self.command_name = "count"
98-
self.collection = collection.name
96+
super().__init__(collection.name)
9997
self.command_document = {"query": filter}
10098
for key, value in kwargs.items():
101-
self.dictionary[key] = value
102-
super().__init__(self.command_document)
99+
self.command_document[key] = value
100+
self.command_document = convert_to_camelcase(self.command_document)
101+
102+
@property
103+
def command_name(self):
104+
return "count"
103105

104106

105107
class FindCommand(BaseCommand):
106108
def __init__(self, collection: Collection,
107109
kwargs):
108-
self.command_name = "find"
109-
self.collection = collection.name
110-
self.command_document={}
110+
super().__init__(collection.name)
111111
for key, value in kwargs.items():
112112
self.command_document[key] = value
113-
super().__init__(self.command_document)
113+
self.command_document = convert_to_camelcase(self.command_document)
114+
115+
@property
116+
def command_name(self):
117+
return "find"
118+
119+
class FindAndModifyCommand(BaseCommand):
120+
def __init__(self, collection: Collection,
121+
kwargs):
122+
super().__init__(collection.name)
123+
for key, value in kwargs.items():
124+
self.command_document[key] = value
125+
self.command_document = convert_to_camelcase(self.command_document)
126+
127+
@property
128+
def command_name(self):
129+
return "findAndModify"
114130

115131

116132
class DeleteCommand(BaseCommand):
117133
def __init__(self, collection: Collection, filter,
118134
limit, collation, kwargs):
119-
super().__init__(kwargs)
120-
self.command_name = "delete"
121-
self.collection = collection.name
122-
return_dictionary = {"deletes": [{"q": filter, "limit": limit,
123-
"collation": collation}]}
124-
125-
for key, value in self.command_document.items():
126-
return_dictionary[key] = value
127-
self.command_document = return_dictionary
135+
super().__init__(collection.name)
136+
self.command_document = {"deletes": [SON({"q": filter, "limit": limit})]}
137+
for key, value in kwargs.items():
138+
self.command_document[key] = value
139+
self.command_document = convert_to_camelcase(self.command_document)
140+
141+
@property
142+
def command_name(self):
143+
return "delete"

pymongoexplain/explainable_collection.py

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@
1313
# limitations under the License.
1414

1515

16-
from typing import Union, List
16+
from typing import Union, List, Dict
1717

1818
import pymongo
1919
from bson.son import SON
2020

2121
from .commands import AggregateCommand, FindCommand, CountCommand, \
22-
UpdateCommand, DistinctCommand, DeleteCommand
23-
22+
UpdateCommand, DistinctCommand, DeleteCommand, FindAndModifyCommand
2423

2524
Document = Union[dict, SON]
2625

@@ -29,10 +28,11 @@ def __init__(self, collection):
2928
self.collection = collection
3029
self.last_cmd_payload = None
3130

32-
def _explain_command(self, command: Document):
33-
explain_command = SON([("explain", command.get_SON())])
31+
def _explain_command(self, command):
32+
command_son = command.get_SON()
33+
explain_command = SON([("explain", command_son)])
3434
explain_command["verbosity"] = "queryPlanner"
35-
self.last_cmd_payload = explain_command
35+
self.last_cmd_payload = command_son
3636
return self.collection.database.command(explain_command)
3737

3838
def update_one(self, filter, update, upsert=False,
@@ -41,6 +41,9 @@ def update_one(self, filter, update, upsert=False,
4141
session=None, **kwargs):
4242
kwargs.update(locals())
4343
del kwargs["self"], kwargs["kwargs"], kwargs["filter"], kwargs["update"]
44+
kwargs["multi"] = False
45+
if bypass_document_validation == False:
46+
del kwargs["bypass_document_validation"]
4447
command = UpdateCommand(self.collection, filter, update, kwargs)
4548
return self._explain_command(command)
4649

@@ -49,6 +52,8 @@ def update_many(self, filter: Document, update: Document, upsert=False,
4952
kwargs.update(locals())
5053
del kwargs["self"], kwargs["kwargs"], kwargs["filter"], kwargs["update"]
5154
kwargs["multi"] = True
55+
if bypass_document_validation == False:
56+
del kwargs["bypass_document_validation"]
5257
command = UpdateCommand(self.collection, filter, update, kwargs)
5358
return self._explain_command(command)
5459

@@ -61,8 +66,19 @@ def aggregate(self, pipeline: List[Document], session=None, **kwargs):
6166
{},kwargs)
6267
return self._explain_command(command)
6368

64-
def count_documents(self, filter: Document, session=None, **kwargs):
65-
command = CountCommand(self.collection, filter,kwargs)
69+
def estimated_document_count(self,
70+
**kwargs):
71+
72+
command = CountCommand(self.collection, None, kwargs)
73+
return self._explain_command(command)
74+
75+
def count_documents(self, filter: Document, session=None,
76+
**kwargs):
77+
78+
command = AggregateCommand(self.collection, [{'$match': filter},
79+
{'$group': {'n': {'$sum': 1}, '_id': 1}}],
80+
session, {}, kwargs,
81+
exclude_keys=filter.keys())
6682
return self._explain_command(command)
6783

6884
def delete_one(self, filter: Document, collation=None, session=None,
@@ -73,8 +89,11 @@ def delete_one(self, filter: Document, collation=None, session=None,
7389
return self._explain_command(command)
7490

7591
def delete_many(self, filter: Document, collation=None,
76-
session=None, **kwargs):
92+
session=None, **kwargs: Dict[str, Union[int, str,
93+
Document,
94+
bool]]):
7795
limit = 0
96+
kwargs["session"] = session
7897
command = DeleteCommand(self.collection, filter, limit, collation,
7998
kwargs)
8099
return self._explain_command(command)
@@ -100,20 +119,79 @@ def watch(self, pipeline: Document = None, full_document: Document = None,
100119
max_await_time_ms})
101120
return self._explain_command(command)
102121

103-
def find(self, filter: Document = None, projection: list = None,
104-
skip: int = 0, limit: int = 0, no_cursor_timeout: bool = False,
105-
sort: Document = None, allow_partial_results: bool = False,
106-
oplog_replay: bool = False, batch_size: int=0,
107-
collation: Document = None, hint: Union[Document, str] = None,
108-
max_time_ms: int = None, max: Document = None, min: Document =
109-
None, return_key: bool = False,
110-
show_record_id: bool = False, comment: str = None,
111-
session:Document = None, **kwargs: Union[int, str, Document,
112-
bool]):
122+
def find(self, filter: Document = None,
123+
**kwargs: Dict[str, Union[int, str,Document, bool]]):
113124
kwargs.update(locals())
114125
del kwargs["self"], kwargs["kwargs"]
115126
command = FindCommand(self.collection,
116127
kwargs)
117128
return self._explain_command(command)
118129

130+
def find_one(self, filter: Document = None, **kwargs: Dict[str,
131+
Union[int, str,
132+
Document, bool]]):
133+
kwargs.update(locals())
134+
del kwargs["self"], kwargs["kwargs"]
135+
kwargs["limit"] = 1
136+
command = FindCommand(self.collection, kwargs)
137+
return self._explain_command(command)
138+
139+
def find_one_and_delete(self, filter: Document, projection: list = None,
140+
sort: Document=None, session=None,
141+
**kwargs):
142+
kwargs["query"] = filter
143+
kwargs["fields"] = projection
144+
kwargs["sort"] = sort
145+
kwargs["remove"] = True
146+
kwargs["session"] = session
147+
148+
command = FindAndModifyCommand(self.collection,
149+
kwargs)
150+
return self._explain_command(command)
151+
152+
def find_one_and_replace(self, filter: Document, replacement: Document,
153+
projection: list = None, sort=None,
154+
return_document=pymongo.ReturnDocument.BEFORE,
155+
session=None, **kwargs):
156+
kwargs["query"] = filter
157+
kwargs["fields"] = projection
158+
kwargs["sort"] = sort
159+
kwargs["new"] = False
160+
kwargs["update"] = replacement
161+
kwargs["session"] = session
162+
command = FindAndModifyCommand(self.collection,
163+
kwargs)
164+
return self._explain_command(command)
165+
166+
def find_one_and_update(self, filter: Document, replacement: Document,
167+
projection: list = None, sort=None,
168+
return_document=pymongo.ReturnDocument.BEFORE,
169+
session=None, **kwargs):
170+
kwargs["query"] = filter
171+
kwargs["fields"] = projection
172+
kwargs["sort"] = sort
173+
kwargs["upsert"] = False
174+
kwargs["update"] = replacement
175+
kwargs["session"] = session
176+
177+
command = FindAndModifyCommand(self.collection,
178+
kwargs)
179+
return self._explain_command(command)
180+
181+
def replace_one(self, filter: Document, replacement: Document,
182+
upsert=False, bypass_document_validation=False,
183+
collation=None, session=None, **kwargs):
184+
kwargs.update(locals())
185+
del kwargs["self"], kwargs["kwargs"], kwargs["filter"], kwargs[
186+
"replacement"]
187+
kwargs["multi"] = False
188+
if not bypass_document_validation:
189+
del kwargs["bypass_document_validation"]
190+
update = replacement
191+
command = UpdateCommand(self.collection, filter, update, kwargs)
192+
193+
return self._explain_command(command)
194+
195+
196+
119197

pymongoexplain/utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright 2020-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
"""Utility functions"""
17+
18+
19+
def convert_to_camelcase(d, exclude_keys=[]):
20+
if not isinstance(d, dict):
21+
return d
22+
ret = dict()
23+
for key in d.keys():
24+
if d[key] is None:
25+
continue
26+
if key in exclude_keys:
27+
ret[key] = d[key]
28+
continue
29+
new_key = key
30+
if "_" in key and key[0] != "_":
31+
new_key = key.split("_")[0] + ''.join(
32+
[i.capitalize() for i in key.split("_")[1:]])
33+
if isinstance(d[key], list):
34+
ret[new_key] = [convert_to_camelcase(
35+
i, exclude_keys=exclude_keys) for i in d[key]]
36+
elif isinstance(d[key], dict):
37+
ret[new_key] = convert_to_camelcase(d[key],
38+
exclude_keys=exclude_keys)
39+
else:
40+
ret[new_key] = d[key]
41+
return ret

0 commit comments

Comments
 (0)