refscan 0.2.1__tar.gz → 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: refscan
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Command-line program that scans the NMDC MongoDB database for referential integrity violations
5
5
  Keywords: mongodb,mongo,relationships,reference,database,data,referential integrity,scan
6
6
  Requires-Python: >=3.9,<4.0
@@ -2,7 +2,7 @@
2
2
 
3
3
  [tool.poetry]
4
4
  name = "refscan"
5
- version = "v0.2.1"
5
+ version = "v0.2.2"
6
6
  description = "Command-line program that scans the NMDC MongoDB database for referential integrity violations"
7
7
  authors = []
8
8
  readme = "README.md"
@@ -40,6 +40,9 @@ pyinstrument = "^4.6.2"
40
40
  # Docs: https://docs.pytest.org/en/stable/contents.html
41
41
  pytest = "^8.2.2"
42
42
  pytest-cov = "^5.0.0"
43
+ # We use `mongomock` to test MongoDB interaction without involving a real MongoDB instance.
44
+ # Docs: https://github.com/mongomock/mongomock
45
+ mongomock = "^4.3.0"
43
46
 
44
47
  [tool.poetry.scripts]
45
48
  # Reference: https://python-poetry.org/docs/pyproject#scripts
@@ -0,0 +1,139 @@
1
+ from typing import List, Optional, Dict
2
+
3
+ from pymongo.database import Database
4
+
5
+
6
+ class Finder:
7
+ r"""
8
+ A class that can be used to find a document in a MongoDB database,
9
+ in a way that uses past searches to try to speed up future searches.
10
+ """
11
+
12
+ def __init__(self, database: Database):
13
+ self.db = database
14
+
15
+ # Initialize a variable we can use to keep track of the collections in which we most recently found targeted
16
+ # documents.
17
+ #
18
+ # Note: This will enable us to _start_ searching in these same collections next time.
19
+ #
20
+ self.names_of_collections_most_recently_found_in = []
21
+ self.cache_size = 2 # we'll cache up to 2 collection names
22
+
23
+ # Initialize a dictionary we can use to keep track of the referenced `id`s we have found in a given collection,
24
+ # and the referenced `id`s we have confirmed do _not_ exist in a given collection.
25
+ #
26
+ # The top-level items have keys that are collection names and values that are dictionaries. The latter
27
+ # dictionaries have keys that are `id` values and values that are boolean flags. The boolean flags indicate
28
+ # whether we have found a document having that `id` in that collection; where `True` means we have,
29
+ # and `False` means we search and _did not_ find such a document in that collection.
30
+ #
31
+ # Example:
32
+ # ```
33
+ # {
34
+ # "biosample_set": {"bsm-00-000001": True, "bsm-00-000002": False},
35
+ # "study_set": {"sty-00-000001": True},
36
+ # }
37
+ # ```
38
+ #
39
+ # Note: This will enable us to _avoid_ searching for the same `id` values in the same collections in the future,
40
+ # decreasing execution time (in exchange for memory consumption).
41
+ #
42
+ # TODO: Measure memory usage when scanning the NMDC database, reverting this perf. optimization if necessary.
43
+ #
44
+ self.cached_id_presence_by_collection: Dict[str, Dict[str, bool]] = {}
45
+
46
+ def _set_cached_id_presence_in_collection(self, collection_name: str, document_id: str, is_present: bool) -> None:
47
+ r"""
48
+ Helper function that updates our cache of `id` presences/absences, setting the presence flag for the specified
49
+ document `id` in the specified collection.
50
+ """
51
+ if collection_name not in self.cached_id_presence_by_collection:
52
+ self.cached_id_presence_by_collection[collection_name] = {}
53
+ self.cached_id_presence_by_collection[collection_name][document_id] = is_present
54
+
55
+ def _get_cached_id_presence_in_collection(self, collection_name: str, document_id: str) -> Optional[bool]:
56
+ r"""
57
+ Helper function that checks our cache of `id` presences/absences, getting the presence/absence flag, if any,
58
+ for the specified document `id` in the specified collection.
59
+ """
60
+ if (
61
+ collection_name not in self.cached_id_presence_by_collection
62
+ or document_id not in self.cached_id_presence_by_collection[collection_name]
63
+ ):
64
+ return None
65
+ else:
66
+ return self.cached_id_presence_by_collection[collection_name][document_id]
67
+
68
+ def check_whether_document_having_id_exists_among_collections(
69
+ self, document_id: str, collection_names: List[str]
70
+ ) -> Optional[str]:
71
+ r"""
72
+ Checks whether any document in any of the specified collections has the specified value in its `id` field.
73
+ Returns the name of the first collection, if any, containing such a document. If none of the collections
74
+ contain such a document, the function returns `None`.
75
+
76
+ TODO: This function refers to "cache" as though we are only dealing with one—but we are now dealing with two
77
+ (see the two instance attributes of this class). The outstanding task here is to update the comments
78
+ in this function to be more specific about which cache is being referenced in a given statement.
79
+
80
+ References:
81
+ - https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find_one
82
+ """
83
+ names_of_collections_to_search = collection_names.copy()
84
+
85
+ # Make a concise alias.
86
+ cache = self.names_of_collections_most_recently_found_in
87
+
88
+ # If any cached collection name is among the ones eligible to search, move it to the front of list.
89
+ for cached_collection_name in reversed(cache): # goes in reverse, so latest addition ends up in front
90
+ if cached_collection_name in collection_names:
91
+ names_of_collections_to_search.remove(cached_collection_name)
92
+ names_of_collections_to_search.insert(0, cached_collection_name) # makes it the first item
93
+
94
+ # Search the collections in their current order.
95
+ name_of_collection_containing_target_document = None
96
+ query_filter = dict(id=document_id)
97
+ for collection_name in names_of_collections_to_search:
98
+
99
+ # Before querying the database, check our in-memory record of `id` presences in this collection.
100
+ is_id_present: Optional[bool] = self._get_cached_id_presence_in_collection(
101
+ collection_name=collection_name, document_id=document_id
102
+ )
103
+
104
+ # If we already know it's _not_ in this collection, stop evaluating this collection and, instead,
105
+ # proceed to evaluate the next collection.
106
+ if is_id_present is False:
107
+ continue # stop evaluating this collection
108
+
109
+ # If we _don't know_ whether it is in the collection or not, we can't take any shortcuts. Aw, shucks.
110
+ if is_id_present is None:
111
+ pass
112
+
113
+ # If it _is_ in this collection—based on either what we already know, or what we find now—cache
114
+ # the collection name and skip searching additional collections for documents having this `id`.
115
+ collection = self.db.get_collection(collection_name)
116
+ if is_id_present or collection.find_one(query_filter, projection=["_id"]) is not None:
117
+ name_of_collection_containing_target_document = collection_name
118
+
119
+ # Put this collection name at the front/start of the cache.
120
+ if len(cache) == 0 or cache[0] != collection_name:
121
+ if collection_name in cache: # if it's elsewhere in the cache, remove it from the cache
122
+ cache.remove(collection_name)
123
+ cache = [collection_name] + cache[0 : self.cache_size - 1] # prepends item, dropping any excess
124
+ self.names_of_collections_most_recently_found_in = cache # persists it to the instance attribute
125
+
126
+ # Record that a document having this `id` _exists_ in this collection.
127
+ # Note: This assignment will be redundant if we got here due to `is_id_present` being `True`.
128
+ self._set_cached_id_presence_in_collection(
129
+ collection_name=collection_name, document_id=document_id, is_present=True
130
+ )
131
+
132
+ break
133
+
134
+ # Record that a document having this `id` does _not_ exist in this collection.
135
+ self._set_cached_id_presence_in_collection(
136
+ collection_name=collection_name, document_id=document_id, is_present=False
137
+ )
138
+
139
+ return name_of_collection_containing_target_document
@@ -1,62 +0,0 @@
1
- from typing import List, Optional
2
-
3
- from pymongo.database import Database
4
-
5
-
6
- class Finder:
7
- r"""
8
- A class that can be used to find a document in a MongoDB database,
9
- in a way that uses past searches to try to speed up future searches.
10
- """
11
-
12
- def __init__(self, database: Database):
13
- self.db = database
14
-
15
- # Initialize a variable we can use to keep track of the collections in which we most recently found targeted
16
- # documents.
17
- #
18
- # Note: This will enable us to _start_ searching in these same collections next time.
19
- #
20
- self.names_of_collections_most_recently_found_in = []
21
- self.cache_size = 2 # we'll cache up to 2 collection names
22
-
23
- def check_whether_document_having_id_exists_among_collections(
24
- self, document_id: str, collection_names: List[str]
25
- ) -> Optional[str]:
26
- r"""
27
- Checks whether any document in any of the specified collections has the specified value in its `id` field.
28
- Returns the name of the first collection, if any, containing such a document. If none of the collections
29
- contain such a document, the function returns `None`.
30
-
31
- References:
32
- - https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find_one
33
- """
34
- names_of_collections_to_search = collection_names.copy()
35
-
36
- # Make a concise alias.
37
- cache = self.names_of_collections_most_recently_found_in
38
-
39
- # If any cached collection name is among the ones eligible to search, move it to the front of list.
40
- for cached_collection_name in reversed(cache): # goes in reverse, so latest addition ends up in front
41
- if cached_collection_name in collection_names:
42
- names_of_collections_to_search.remove(cached_collection_name)
43
- names_of_collections_to_search.insert(0, cached_collection_name) # makes it the first item
44
-
45
- # Search the collections in their current order.
46
- name_of_collection_containing_target_document = None
47
- query_filter = dict(id=document_id)
48
- for collection_name in names_of_collections_to_search:
49
-
50
- # If we found the document, cache the collection name and stop searching.
51
- if self.db.get_collection(collection_name).find_one(query_filter, projection=["_id"]) is not None:
52
- name_of_collection_containing_target_document = collection_name
53
-
54
- # Put this collection name at the front/start of the cache.
55
- if len(cache) == 0 or cache[0] != collection_name:
56
- if collection_name in cache: # if it's elsewhere in the cache, remove it from the cache
57
- cache.remove(collection_name)
58
- cache = [collection_name] + cache[0 : self.cache_size - 1] # prepends item, dropping any excess
59
- self.names_of_collections_most_recently_found_in = cache # persists it to the instance attribute
60
- break
61
-
62
- return name_of_collection_containing_target_document
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes