refscan 0.2.2__tar.gz → 0.2.3__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.2 → refscan-0.2.3}/PKG-INFO +1 -1
- {refscan-0.2.2 → refscan-0.2.3}/pyproject.toml +1 -1
- {refscan-0.2.2 → refscan-0.2.3}/refscan/grapher.py +21 -2
- refscan-0.2.3/refscan/lib/Finder.py +174 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/templates/graph.template.html +78 -1
- refscan-0.2.2/refscan/lib/Finder.py +0 -139
- {refscan-0.2.2 → refscan-0.2.3}/LICENSE +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/README.md +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/__init__.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/cli/cli.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/cli/graph.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/cli/scan.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/cli/version.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/lib/Reference.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/lib/ReferenceList.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/lib/Violation.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/lib/ViolationList.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/lib/constants.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/lib/helpers.py +0 -0
- {refscan-0.2.2 → refscan-0.2.3}/refscan/scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: refscan
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -48,6 +48,18 @@ def encode_json_value_as_base64_str(json_value: Union[dict, list]) -> str:
|
|
|
48
48
|
return encoded_bytes_as_string
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def is_class_abstract(class_name: str, schema_view: linkml_runtime.SchemaView) -> bool:
|
|
52
|
+
r"""
|
|
53
|
+
Returns whether the specified schema class is abstract.
|
|
54
|
+
|
|
55
|
+
Note: The `abstract` property can contain `True`, `False`, or `None`.
|
|
56
|
+
|
|
57
|
+
Reference: https://linkml.io/linkml/schemas/inheritance.html#abstract-classes-and-slots
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
return schema_view.get_class(class_name).abstract is True
|
|
61
|
+
|
|
62
|
+
|
|
51
63
|
def graph(
|
|
52
64
|
schema_view: linkml_runtime.SchemaView,
|
|
53
65
|
subject: Subject,
|
|
@@ -106,12 +118,17 @@ def graph(
|
|
|
106
118
|
source_name = r.source_collection_name if subject == Subject.collection else r.source_class_name
|
|
107
119
|
target_name = r.target_collection_name if subject == Subject.collection else r.target_class_name
|
|
108
120
|
|
|
121
|
+
# If the subject of the graph is "class", determine whether the source class is abstract. Same for the target class.
|
|
122
|
+
# Note: This will allow us to display abstract classes differently from concrete classes in the diagram.
|
|
123
|
+
is_source_abstract = is_class_abstract(source_name, schema_view) if subject == Subject.class_ else None
|
|
124
|
+
is_target_abstract = is_class_abstract(target_name, schema_view) if subject == Subject.class_ else None
|
|
125
|
+
|
|
109
126
|
# Append a node for the source subject if we haven't already done so. Same for the target subject.
|
|
110
127
|
node_ids = [n["data"]["id"] for n in nodes]
|
|
111
128
|
if source_name not in node_ids:
|
|
112
|
-
nodes.append(dict(data=dict(id=source_name)))
|
|
129
|
+
nodes.append(dict(data=dict(id=source_name, is_abstract=is_source_abstract)))
|
|
113
130
|
if target_name not in node_ids:
|
|
114
|
-
nodes.append(dict(data=dict(id=target_name)))
|
|
131
|
+
nodes.append(dict(data=dict(id=target_name, is_abstract=is_target_abstract)))
|
|
115
132
|
|
|
116
133
|
# Append an edge for this source subject-to-target subject relationship if we haven't already done so.
|
|
117
134
|
edge_id = f"{source_name}__to__{target_name}"
|
|
@@ -147,6 +164,8 @@ def graph(
|
|
|
147
164
|
dict(
|
|
148
165
|
app_version=get_package_metadata("Version"),
|
|
149
166
|
schema_version=schema_view.schema.version,
|
|
167
|
+
subject_singular="class" if subject == Subject.class_ else "collection",
|
|
168
|
+
subject_plural="classes" if subject == Subject.class_ else "collections",
|
|
150
169
|
)
|
|
151
170
|
)
|
|
152
171
|
html_result = html_template.replace("{{ graph_data_json_base64 }}", graph_data_json_base64_str)
|
|
@@ -0,0 +1,174 @@
|
|
|
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, in a way that uses past searches to try to
|
|
9
|
+
speed up future searches.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, database: Database):
|
|
13
|
+
self.db = database
|
|
14
|
+
|
|
15
|
+
# Initialize our cache of names of collections we most recently found referenced document `id`s in.
|
|
16
|
+
#
|
|
17
|
+
# Note: This is a queue we will use to keep track of which collections we most recently found referenced
|
|
18
|
+
# document `id`s in. This will enable us to _begin_ searching in those same collections for subsequent
|
|
19
|
+
# searches. The idea that this will speed things up is based on the assumption that references emanating
|
|
20
|
+
# from source documents that we process _temporally close_ to one another, likely point to target
|
|
21
|
+
# documents residing _in the same collections_ as one another.
|
|
22
|
+
#
|
|
23
|
+
self.cached_collection_names_where_recently_found = []
|
|
24
|
+
self.cache_size = 2 # we'll cache up to 2 collection names
|
|
25
|
+
|
|
26
|
+
# Initialize our cache of document `id` presences/absences by collection.
|
|
27
|
+
#
|
|
28
|
+
# Note: This is a dictionary we will use to keep track of the referenced `id`s we find in each collection,
|
|
29
|
+
# and the referenced `id`s we fail to find (i.e. confirm do _not_ exist) in each collection. This will
|
|
30
|
+
# enable us to avoid querying the database for the same information that we have previously obtained
|
|
31
|
+
# by querying the database.
|
|
32
|
+
#
|
|
33
|
+
# Note: The top-level items in the dictionary have keys that are collection names and values that are
|
|
34
|
+
# dictionaries. Each of those dictionaries has keys that are document `id`s and values that are boolean
|
|
35
|
+
# flags, which indicate whether we know that a document having that `id` exists in that collection
|
|
36
|
+
# (`True`) or we know that a document having that `id` does _not_ exist in that collection (`False`).
|
|
37
|
+
# Here's an example value of this attribute:
|
|
38
|
+
# ```
|
|
39
|
+
# {
|
|
40
|
+
# "biosample_set": {"nmdc:bsm-00-000001": True, "nmdc:bsm-00-000002": False},
|
|
41
|
+
# "study_set": {"nmdc:sty-00-000001": True},
|
|
42
|
+
# }
|
|
43
|
+
# ```
|
|
44
|
+
#
|
|
45
|
+
# Note: If this program ever leads to memory consumption (RAM usage) issues in practice, consider
|
|
46
|
+
# redesigning this cache and/or allowing the user to disable its use.
|
|
47
|
+
#
|
|
48
|
+
self.cached_id_presence_by_collection: Dict[str, Dict[str, bool]] = {}
|
|
49
|
+
|
|
50
|
+
def _set_name_of_collection_most_recently_found_in(self, collection_name: str):
|
|
51
|
+
r"""
|
|
52
|
+
Helper function that updates our cache of collection names in which target documents were most recently found,
|
|
53
|
+
so that the specified collection is at the front/start of the list. That will make it so it gets searched first
|
|
54
|
+
next time.
|
|
55
|
+
"""
|
|
56
|
+
# Make a concise alias for the instance attribute (both will refer to the same data structure).
|
|
57
|
+
queue = self.cached_collection_names_where_recently_found
|
|
58
|
+
|
|
59
|
+
# Check whether either (a) the queue is empty or (b) this collection name is not at the front of the queue.
|
|
60
|
+
# Example: [] is empty, or "b_set" is not the first item in ["a_set", "b_set", "c_set"]
|
|
61
|
+
if len(queue) == 0 or queue[0] != collection_name:
|
|
62
|
+
|
|
63
|
+
# If this collection name is already (elsewhere) in the queue, remove it from that position, automatically
|
|
64
|
+
# updating the list indexes of subsequent list items (so there is no "gap" in the list).
|
|
65
|
+
# Example: Removing "b_set" from ["a_set", "b_set", "c_set"] → ["a_set", "c_set"]
|
|
66
|
+
if collection_name in queue:
|
|
67
|
+
queue.remove(collection_name)
|
|
68
|
+
|
|
69
|
+
# Prepend this collection name to the beginning of the queue, dropping any excess collection names.
|
|
70
|
+
# Example: Prepending ["b_set"] to ["a_set", "c_set"] → ["b_set", "a_set", "c_set"]
|
|
71
|
+
queue = [collection_name] + queue[0 : self.cache_size - 1]
|
|
72
|
+
|
|
73
|
+
# Note: This unnecessary assignment is here only to tell my IDE that `queue` isn't an unused variable.
|
|
74
|
+
self.cached_collection_names_where_recently_found = queue
|
|
75
|
+
|
|
76
|
+
def _optimize_collection_search_order(self, names_of_eligible_collections: List[str]) -> List[str]:
|
|
77
|
+
r"""
|
|
78
|
+
Helper function that returns an optimized order in which the specified collections could be searched, in order
|
|
79
|
+
to take advantage of things the instance "remembers" from recent searches.
|
|
80
|
+
"""
|
|
81
|
+
ordered_collection_names = names_of_eligible_collections.copy()
|
|
82
|
+
|
|
83
|
+
# Iterate over the collection names in the queue, from back (oldest) to front (newest).
|
|
84
|
+
for cached_collection_name in reversed(self.cached_collection_names_where_recently_found):
|
|
85
|
+
|
|
86
|
+
# If this cached collection name is among the eligible ones, move it to the front of the list.
|
|
87
|
+
# Example: Since "b_set" is in ["a_set", "b_set", "c_set"] → ["b_set, "a_set", "c_set"]
|
|
88
|
+
if cached_collection_name in names_of_eligible_collections:
|
|
89
|
+
ordered_collection_names.remove(cached_collection_name)
|
|
90
|
+
ordered_collection_names.insert(0, cached_collection_name)
|
|
91
|
+
|
|
92
|
+
return ordered_collection_names
|
|
93
|
+
|
|
94
|
+
def _set_cached_id_presence_in_collection(self, collection_name: str, document_id: str, is_present: bool) -> None:
|
|
95
|
+
r"""
|
|
96
|
+
Helper function that updates our cache of document `id` presences/absences, setting the presence flag for the
|
|
97
|
+
specified document `id` in the specified collection.
|
|
98
|
+
"""
|
|
99
|
+
if collection_name not in self.cached_id_presence_by_collection:
|
|
100
|
+
self.cached_id_presence_by_collection[collection_name] = {} # creates key if it does not exist
|
|
101
|
+
self.cached_id_presence_by_collection[collection_name][document_id] = is_present
|
|
102
|
+
|
|
103
|
+
def _get_cached_id_presence_in_collection(self, collection_name: str, document_id: str) -> Optional[bool]:
|
|
104
|
+
r"""
|
|
105
|
+
Helper function that checks our cache of document `id` presences/absences, returning the presence/absence flag,
|
|
106
|
+
if any, for the specified document `id` in the specified collection.
|
|
107
|
+
"""
|
|
108
|
+
if (
|
|
109
|
+
collection_name not in self.cached_id_presence_by_collection
|
|
110
|
+
or document_id not in self.cached_id_presence_by_collection[collection_name]
|
|
111
|
+
):
|
|
112
|
+
return None
|
|
113
|
+
else:
|
|
114
|
+
return self.cached_id_presence_by_collection[collection_name][document_id]
|
|
115
|
+
|
|
116
|
+
def check_whether_document_having_id_exists_among_collections(
|
|
117
|
+
self, document_id: str, collection_names: List[str]
|
|
118
|
+
) -> Optional[str]:
|
|
119
|
+
r"""
|
|
120
|
+
Checks whether any document in any of the specified collections has the specified value in its `id` field.
|
|
121
|
+
Returns the name of the first collection, if any, containing such a document. If none of the collections
|
|
122
|
+
contain such a document, the function returns `None`.
|
|
123
|
+
|
|
124
|
+
References:
|
|
125
|
+
- https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find_one
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# Initialize the name of the containing collection to `None`.
|
|
129
|
+
name_of_collection_containing_target_document = None
|
|
130
|
+
|
|
131
|
+
# Optimize the search order of the collections, based upon the outcomes of recent searches.
|
|
132
|
+
ordered_collection_names = self._optimize_collection_search_order(collection_names)
|
|
133
|
+
|
|
134
|
+
# Search the collections.
|
|
135
|
+
query_filter = dict(id=document_id)
|
|
136
|
+
for collection_name in ordered_collection_names:
|
|
137
|
+
|
|
138
|
+
# Before querying the database, check our cache of document `id` presences/absences per collection.
|
|
139
|
+
is_id_present: Optional[bool] = self._get_cached_id_presence_in_collection(
|
|
140
|
+
collection_name=collection_name, document_id=document_id
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# If we already know it's _not_ in this collection, stop evaluating this collection and, instead,
|
|
144
|
+
# proceed to evaluate the next collection.
|
|
145
|
+
if is_id_present is False:
|
|
146
|
+
continue # stop evaluating this collection
|
|
147
|
+
|
|
148
|
+
# If we already know it _is_ in this collection, record the collection name, update the cache of names
|
|
149
|
+
# of collections where a referenced document was recently found, and skip searching additional collections.
|
|
150
|
+
if is_id_present is True:
|
|
151
|
+
name_of_collection_containing_target_document = collection_name
|
|
152
|
+
self._set_name_of_collection_most_recently_found_in(collection_name=collection_name)
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
# If we find it in the collection in the database, record the collection name, update both (a) the cache
|
|
156
|
+
# of which documents we have found in which collections, and (b) the cache of names of collections where
|
|
157
|
+
# a referenced document was recently found, and skip searching additional collections.
|
|
158
|
+
if self.db.get_collection(collection_name).find_one(query_filter, projection=["_id"]) is not None:
|
|
159
|
+
name_of_collection_containing_target_document = collection_name
|
|
160
|
+
|
|
161
|
+
# Record that a document having this `id` is _present_ in this collection.
|
|
162
|
+
self._set_cached_id_presence_in_collection(
|
|
163
|
+
collection_name=collection_name, document_id=document_id, is_present=True
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self._set_name_of_collection_most_recently_found_in(collection_name=collection_name)
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
# If we made it this far, record that a document having this `id` does _not_ exist in this collection.
|
|
170
|
+
self._set_cached_id_presence_in_collection(
|
|
171
|
+
collection_name=collection_name, document_id=document_id, is_present=False
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return name_of_collection_containing_target_document
|
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
list-style-type: none;
|
|
20
20
|
margin: 0;
|
|
21
21
|
}
|
|
22
|
+
.text-button {
|
|
23
|
+
color: inherit;
|
|
24
|
+
text-decoration: underline;
|
|
25
|
+
background: none;
|
|
26
|
+
border: none;
|
|
27
|
+
margin: 0;
|
|
28
|
+
padding: 0;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
}
|
|
22
31
|
</style>
|
|
23
32
|
</head>
|
|
24
33
|
<body>
|
|
@@ -82,6 +91,8 @@
|
|
|
82
91
|
li.textContent = `refscan version ${graphMetadata["app_version"]}`;
|
|
83
92
|
metadataListEl.appendChild(li);
|
|
84
93
|
}
|
|
94
|
+
let subjectSingular = graphMetadata["subject_singular"];
|
|
95
|
+
let subjectPlural = graphMetadata["subject_plural"];
|
|
85
96
|
metadataContainerEl.appendChild(metadataListEl);
|
|
86
97
|
|
|
87
98
|
// Base64-decode the graph data and parse it as JSON.
|
|
@@ -174,7 +185,19 @@
|
|
|
174
185
|
color: "#0000FF", // text color
|
|
175
186
|
backgroundColor: "#0000FF"
|
|
176
187
|
}
|
|
177
|
-
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
selector: "node.isAbstract",
|
|
191
|
+
style: {
|
|
192
|
+
opacity: 0.5,
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
selector: "edge.isAbstract",
|
|
197
|
+
style: {
|
|
198
|
+
opacity: 0.5,
|
|
199
|
+
}
|
|
200
|
+
},
|
|
178
201
|
],
|
|
179
202
|
});
|
|
180
203
|
|
|
@@ -214,6 +237,60 @@
|
|
|
214
237
|
.removeClass("incomer")
|
|
215
238
|
.removeClass("inFocus");
|
|
216
239
|
});
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Sets up a text button that can be used to toggle the visibility of
|
|
243
|
+
* nodes that represent abstract things.
|
|
244
|
+
*/
|
|
245
|
+
function setupAbstractNodeVisibilityToggler() {
|
|
246
|
+
// Get a collection of all nodes that represent abstract things.
|
|
247
|
+
// Note: For conciseness, we refer to them as "abstract nodes" here.
|
|
248
|
+
// Reference: https://js.cytoscape.org/#eles.filter
|
|
249
|
+
const abstractNodes = cy.filter(function(el, idx, allEls) {
|
|
250
|
+
return el.isNode() && el.data("is_abstract");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// If there are any abstract nodes, give them (and the edges connected to them)
|
|
254
|
+
// a specific "stylesheet" class and set up their visibility toggler.
|
|
255
|
+
if (abstractNodes.length > 0) {
|
|
256
|
+
abstractNodes.addClass("isAbstract");
|
|
257
|
+
abstractNodes.connectedEdges().addClass("isAbstract");
|
|
258
|
+
|
|
259
|
+
// Set up a clickable element that can be used to show/hide the abstract nodes.
|
|
260
|
+
// Note: When a node gets hidden, its connected edges will be automatically hidden also.
|
|
261
|
+
const strings = {
|
|
262
|
+
prefix: `Abstract ${subjectPlural} are `,
|
|
263
|
+
visible: "visible",
|
|
264
|
+
hidden: "hidden",
|
|
265
|
+
};
|
|
266
|
+
let removedNodes = cy.collection(); // initially empty
|
|
267
|
+
const prefixEl = document.createElement("span");
|
|
268
|
+
prefixEl.textContent =strings.prefix;
|
|
269
|
+
const togglerEl = document.createElement("button");
|
|
270
|
+
togglerEl.classList.add("text-button");
|
|
271
|
+
togglerEl.title = "Toggle visibility";
|
|
272
|
+
const togglerTextEl = document.createElement("span");
|
|
273
|
+
togglerTextEl.textContent = strings.visible;
|
|
274
|
+
togglerEl.appendChild(togglerTextEl);
|
|
275
|
+
togglerEl.addEventListener("click", () => {
|
|
276
|
+
if (removedNodes.length > 0) {
|
|
277
|
+
// Add them back into the graph.
|
|
278
|
+
removedNodes.restore();
|
|
279
|
+
removedNodes = cy.collection();
|
|
280
|
+
togglerTextEl.textContent = strings.visible;
|
|
281
|
+
} else {
|
|
282
|
+
// Remove them from the graph, but save them in memory.
|
|
283
|
+
removedNodes = cy.remove(abstractNodes);
|
|
284
|
+
togglerTextEl.textContent = strings.hidden;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
metadataContainerEl.appendChild(prefixEl);
|
|
288
|
+
metadataContainerEl.appendChild(togglerEl);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Set up the abstract node visibility toggler.
|
|
293
|
+
setupAbstractNodeVisibilityToggler();
|
|
217
294
|
})();
|
|
218
295
|
</script>
|
|
219
296
|
</body>
|
|
@@ -1,139 +0,0 @@
|
|
|
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
|
|
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
|