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.
- {refscan-0.2.1 → refscan-0.2.2}/PKG-INFO +1 -1
- {refscan-0.2.1 → refscan-0.2.2}/pyproject.toml +4 -1
- refscan-0.2.2/refscan/lib/Finder.py +139 -0
- refscan-0.2.1/refscan/lib/Finder.py +0 -62
- {refscan-0.2.1 → refscan-0.2.2}/LICENSE +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/README.md +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/__init__.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/cli/cli.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/cli/graph.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/cli/scan.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/cli/version.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/grapher.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/lib/Reference.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/lib/ReferenceList.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/lib/Violation.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/lib/ViolationList.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/lib/constants.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/lib/helpers.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/scanner.py +0 -0
- {refscan-0.2.1 → refscan-0.2.2}/refscan/templates/graph.template.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: refscan
|
|
3
|
-
Version: 0.2.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|