refscan 0.2.5__tar.gz → 0.3.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.5
3
+ Version: 0.3.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
@@ -313,6 +313,16 @@ While editing the tool's source code, you can run the tool as you normally would
313
313
  poetry run refscan --help
314
314
  ```
315
315
 
316
+ ### Check types
317
+
318
+ We use [mypy](https://mypy.readthedocs.io/en/stable/) as the static type checker for `refscan`.
319
+
320
+ You can perform static type checking by running the following command from the root directory of the repository:
321
+
322
+ ```shell
323
+ poetry run mypy
324
+ ```
325
+
316
326
  ### Run tests
317
327
 
318
328
  We use [pytest](https://docs.pytest.org/en/8.2.x/) as the testing framework for `refscan`.
@@ -292,6 +292,16 @@ While editing the tool's source code, you can run the tool as you normally would
292
292
  poetry run refscan --help
293
293
  ```
294
294
 
295
+ ### Check types
296
+
297
+ We use [mypy](https://mypy.readthedocs.io/en/stable/) as the static type checker for `refscan`.
298
+
299
+ You can perform static type checking by running the following command from the root directory of the repository:
300
+
301
+ ```shell
302
+ poetry run mypy
303
+ ```
304
+
295
305
  ### Run tests
296
306
 
297
307
  We use [pytest](https://docs.pytest.org/en/8.2.x/) as the testing framework for `refscan`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  [tool.poetry]
4
4
  name = "refscan"
5
- version = "v0.2.5"
5
+ version = "v0.3.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"
@@ -43,6 +43,10 @@ pytest-cov = "^5.0.0"
43
43
  # We use `mongomock` to test MongoDB interaction without involving a real MongoDB instance.
44
44
  # Docs: https://github.com/mongomock/mongomock
45
45
  mongomock = "^4.3.0"
46
+ # We use `mypy` to perform static type checking.
47
+ # Usage: $ poetry run mypy
48
+ # Docs: https://mypy.readthedocs.io/en/stable/
49
+ mypy = "^1.16.0"
46
50
 
47
51
  [tool.poetry.scripts]
48
52
  # Reference: https://python-poetry.org/docs/pyproject#scripts
@@ -61,6 +65,25 @@ addopts = "--doctest-modules --cov=refscan"
61
65
  # Reference: https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-format
62
66
  line-length = 120
63
67
 
68
+ [tool.mypy]
69
+ # Make it so, when someone runs `$ poetry run mypy`, these default CLI options are used.
70
+ # Reference: https://mypy.readthedocs.io/en/stable/config_file.html
71
+ files = "."
72
+
73
+ [[tool.mypy.overrides]]
74
+ # Silence the following error:
75
+ # > Skipping analyzing "linkml_runtime": module is installed, but missing library stubs or py.typed marker [import-untyped]
76
+ #
77
+ # Note: We first tried using the following, but it seemed to have no effect on the behavior of mypy when run via `$ poetry run mypy`.
78
+ # ```
79
+ # [mypy-linkml_runtime]
80
+ # ignore_missing_imports = true
81
+ # ```
82
+ # So, we—instead—used the syntax shown on: https://stackoverflow.com/a/74198016
83
+ #
84
+ module = "linkml_runtime"
85
+ ignore_missing_imports = true
86
+
64
87
  [build-system]
65
88
  requires = ["poetry-core"]
66
89
  build-backend = "poetry.core.masonry.api"
@@ -12,7 +12,7 @@ def get_package_metadata(key: str) -> str:
12
12
  metadata_value = ""
13
13
  try:
14
14
  package_metadata = metadata("refscan")
15
- metadata_value = package_metadata.get(key)
15
+ metadata_value = package_metadata.get(key, "")
16
16
  except PackageNotFoundError:
17
17
  pass
18
18
  return metadata_value
@@ -172,7 +172,6 @@ def scan(
172
172
  db=db,
173
173
  schema_view=schema_view,
174
174
  references=references,
175
- collection_names=collection_names,
176
175
  names_of_source_collections_to_skip=names_of_source_collections_to_skip,
177
176
  user_wants_to_locate_misplaced_documents=user_wants_to_locate_misplaced_documents,
178
177
  verbose=verbose,
@@ -1,5 +1,5 @@
1
1
  from enum import Enum
2
- from typing import Union
2
+ from typing import Union, List, Dict, Any
3
3
  import json
4
4
  import base64
5
5
  from importlib import resources
@@ -15,6 +15,11 @@ from refscan.lib.helpers import (
15
15
  identify_references,
16
16
  )
17
17
 
18
+ # Define some type aliases that we can reference later, to simplify the latter type hints.
19
+ # TODO: Update these type hints to not use `Any`.
20
+ NodeType = Dict[str, Dict[str, Any]]
21
+ EdgeType = Dict[str, Dict[str, Any]]
22
+
18
23
 
19
24
  class Subject(str, Enum):
20
25
  r"""The subject (i.e. focal point) of the graph."""
@@ -105,8 +110,8 @@ def graph(
105
110
  #
106
111
  # Reference: https://js.cytoscape.org/#notation/elements-json
107
112
  #
108
- nodes = []
109
- edges = []
113
+ nodes: List[NodeType] = []
114
+ edges: List[EdgeType] = []
110
115
  for r in references:
111
116
  # Get the source and target names depending upon the subject of the graph.
112
117
  source_name = r.source_collection_name if subject == Subject.collection else r.source_class_name
@@ -24,8 +24,8 @@ class Finder:
24
24
  # from source documents that we process _temporally close_ to one another, likely point to target
25
25
  # documents residing _in the same collections_ as one another.
26
26
  #
27
- self.cached_collection_names_where_recently_found = []
28
- self.cache_size = 2 # we'll cache up to 2 collection names
27
+ self.cached_collection_names_where_recently_found: List[str] = []
28
+ self.cache_size: int = 2 # we'll cache up to 2 collection names
29
29
 
30
30
  # Initialize our cache of document `id` presences/absences by collection.
31
31
  #
@@ -1,4 +1,4 @@
1
- from typing import List, Dict, Union
1
+ from typing import List, Dict, Union, Optional
2
2
  from pathlib import Path
3
3
  from dataclasses import fields, astuple
4
4
  from collections import UserList
@@ -18,6 +18,17 @@ class ReferenceList(UserList):
18
18
  One thing it does is enable sorting via `sorted(the_list)`.
19
19
  """
20
20
 
21
+ # A cached dictionary that maps source class names to a list of the names of
22
+ # the fields of that class that contain references. Initialized to `None` to
23
+ # indicate that it hasn't been computed/cached yet.
24
+ #
25
+ # Example: {"Study": ["part_of"]}
26
+ #
27
+ # TODO: Consider implementing caching via functools's `@cached_property` decorator instead.
28
+ # Reference: https://docs.python.org/3/library/functools.html#functools.cached_property
29
+ #
30
+ _reference_field_names_by_source_class_name: Optional[Dict[str, List[str]]] = None
31
+
21
32
  def get_source_collection_names(self) -> list[str]:
22
33
  """
23
34
  Returns the distinct `source_collection_names` values among all references in the list.
@@ -101,11 +112,16 @@ class ReferenceList(UserList):
101
112
 
102
113
  def get_reference_field_names_by_source_class_name(self) -> Dict[str, List[str]]:
103
114
  r"""
104
- Returns a dictionary that maps source class names to a list of the
105
- names of the fields of that class that contain references.
115
+ Returns a dictionary that maps source class names to a list of the names of
116
+ the fields of that class that contain references.
106
117
 
107
118
  Example: {"Study": ["part_of"]}
108
119
  """
120
+
121
+ # If this dictionary has already been computed by this class, return that cached dictionary.
122
+ if isinstance(self._reference_field_names_by_source_class_name, dict):
123
+ return self._reference_field_names_by_source_class_name
124
+
109
125
  reference_field_names_by_source_class_name: Dict[str, List[str]] = {}
110
126
  for reference in self.data:
111
127
  source_class_name = reference.source_class_name
@@ -119,6 +135,9 @@ class ReferenceList(UserList):
119
135
  if source_field_name not in reference_field_names_by_source_class_name[source_class_name]:
120
136
  reference_field_names_by_source_class_name[source_class_name].append(source_field_name)
121
137
 
138
+ # Cache the dictionary for subsequent invocations of this method.
139
+ self._reference_field_names_by_source_class_name = reference_field_names_by_source_class_name
140
+
122
141
  return reference_field_names_by_source_class_name
123
142
 
124
143
  def get_by_target_class_name(self, target_class_name: str) -> "ReferenceList":
@@ -18,6 +18,8 @@ def connect_to_database(mongo_uri: str, database_name: str, verbose: bool = True
18
18
  mongo_client: MongoClient = MongoClient(host=mongo_uri, directConnection=True)
19
19
 
20
20
  with timeout(5): # if any message exchange takes > 5 seconds, this will raise an exception
21
+ if mongo_client.address is None:
22
+ raise ValueError("Failed to determine MongoDB server hostname or port number.")
21
23
  (host, port_number) = mongo_client.address
22
24
 
23
25
  if verbose:
@@ -8,6 +8,7 @@ from linkml_runtime import SchemaView
8
8
  from refscan.lib.Finder import Finder
9
9
  from refscan.lib.helpers import (
10
10
  derive_schema_class_name_from_document,
11
+ get_collection_names_from_schema,
11
12
  translate_class_uri_into_schema_class_name,
12
13
  translate_schema_class_name_into_class_uri,
13
14
  init_progress_bar,
@@ -51,6 +52,10 @@ def identify_referring_documents(
51
52
  # Initialize a list of descriptors of documents that reference the specified document.
52
53
  referring_document_descriptors = []
53
54
 
55
+ # If the specified document does not have an `id` field, we know it cannot be referenced.
56
+ if "id" not in document:
57
+ return referring_document_descriptors
58
+
54
59
  # Get the specified document's `id` and derive its schema class name.
55
60
  document_id = document["id"]
56
61
  document_class_name = derive_schema_class_name_from_document(schema_view, document)
@@ -92,6 +97,17 @@ def identify_referring_documents(
92
97
 
93
98
  # For each referring document, store a descriptor of it.
94
99
  for referring_document in referring_documents:
100
+
101
+ # Handle the case where the source document does not have an `id`.
102
+ #
103
+ # Note: As a reminder, according to the NMDC Schema (as of version v11.8.0), documents in the
104
+ # `functional_annotation_agg` collection do not have an `id` field.
105
+ # Reference: https://microbiomedata.github.io/nmdc-schema/FunctionalAnnotationAggMember/
106
+ #
107
+ source_document_id = referring_document.get("id", None)
108
+ if not isinstance(source_document_id, str):
109
+ source_document_id = ""
110
+
95
111
  source_class_name = translate_class_uri_into_schema_class_name(
96
112
  schema_view=schema_view,
97
113
  class_uri=referring_document["type"],
@@ -100,7 +116,7 @@ def identify_referring_documents(
100
116
  source_collection_name=collection_name,
101
117
  source_class_name=source_class_name,
102
118
  source_document_object_id=referring_document["_id"],
103
- source_document_id=referring_document["id"],
119
+ source_document_id=source_document_id,
104
120
  )
105
121
  referring_document_descriptors.append(document_descriptor)
106
122
 
@@ -110,11 +126,8 @@ def identify_referring_documents(
110
126
  def scan_outgoing_references(
111
127
  document: dict,
112
128
  schema_view: SchemaView,
113
- # TODO: Cache this dictionary, since it is derived from the schema alone.
114
- reference_field_names_by_source_class_name: Dict[str, List[str]],
115
129
  references: ReferenceList,
116
130
  finder: Finder,
117
- collection_names: List[str],
118
131
  source_collection_name: str,
119
132
  client_session: Optional[ClientSession] = None,
120
133
  user_wants_to_locate_misplaced_documents: bool = False,
@@ -125,10 +138,8 @@ def scan_outgoing_references(
125
138
 
126
139
  :param document: The source document from which the references emanate
127
140
  :param schema_view: A SchemaView bound to the schema
128
- :param reference_field_names_by_source_class_name: Dictionary mapping class names to lists of reference field names
129
141
  :param references: A `ReferenceList` derived from the schema
130
142
  :param finder: A `Finder` bound to the database being scanned
131
- :param collection_names: List of collection names that are described by the schema
132
143
  :param source_collection_name: Name of collection in which source document resides (this is included in the
133
144
  violation report in an attempt to facilitate investigation of violations)
134
145
  :param client_session: A `pymongo.client_session.ClientSession` instance that, if specified, will be used when
@@ -145,14 +156,20 @@ def scan_outgoing_references(
145
156
 
146
157
  # Get the document's `id` so that we can include it in this script's output.
147
158
  source_document_object_id = document["_id"]
148
- source_document_id = document["id"] if "id" in document else None
159
+ source_document_id: Optional[str] = document["id"] if "id" in document else None
149
160
 
150
161
  # Get the document's schema class name so that we can interpret its fields accordingly.
151
162
  source_class_name = derive_schema_class_name_from_document(schema_view, document)
163
+ if source_class_name is None:
164
+ raise ValueError(f"Failed to derive schema class name from document having `id`: {source_document_id}")
152
165
 
153
166
  # Get the names of that class's fields that can contain references.
167
+ reference_field_names_by_source_class_name = references.get_reference_field_names_by_source_class_name()
154
168
  names_of_reference_fields = reference_field_names_by_source_class_name.get(source_class_name, [])
155
169
 
170
+ # Get the names of collections that are described by the schema.
171
+ collection_names = get_collection_names_from_schema(schema_view=schema_view)
172
+
156
173
  # Check each field that both (a) exists in the document and (b) can contain a reference.
157
174
  for field_name in names_of_reference_fields:
158
175
  if field_name in document:
@@ -191,6 +208,15 @@ def scan_outgoing_references(
191
208
  )
192
209
  )
193
210
 
211
+ # Handle the case where the source document does not have an `id`.
212
+ #
213
+ # Note: As a reminder, according to the NMDC Schema (as of version v11.8.0), documents in the
214
+ # `functional_annotation_agg` collection do not have an `id` field.
215
+ # Reference: https://microbiomedata.github.io/nmdc-schema/FunctionalAnnotationAggMember/
216
+ #
217
+ if not isinstance(source_document_id, str):
218
+ source_document_id = ""
219
+
194
220
  # Instantiate a `Violation` containing information about this referential integrity violation.
195
221
  violation = Violation(
196
222
  source_collection_name=source_collection_name,
@@ -212,7 +238,6 @@ def scan(
212
238
  db: Database,
213
239
  schema_view: SchemaView,
214
240
  references: ReferenceList,
215
- collection_names: List[str],
216
241
  names_of_source_collections_to_skip: List[str],
217
242
  user_wants_to_locate_misplaced_documents: bool = False,
218
243
  console: Console = default_console,
@@ -226,7 +251,6 @@ def scan(
226
251
  :param schema_view: A SchemaView bound to the schema with which that database complies
227
252
  (except that it may not be compliant in terms of referential integrity)
228
253
  :param references: A `ReferenceList` derived from the schema
229
- :param collection_names: List of collection names that are described by the schema
230
254
  :param names_of_source_collections_to_skip: List of source collections to skip
231
255
  :param user_wants_to_locate_misplaced_documents: Whether the user wants the function to proceed to search illegal
232
256
  collections after failing to find the referenced document among
@@ -235,9 +259,6 @@ def scan(
235
259
  :param verbose: Whether you want the function to print a higher-than-normal amount of information to the console
236
260
  """
237
261
 
238
- # Get a dictionary that maps source class names to the names of their fields that can contain references.
239
- reference_field_names_by_source_class_name = references.get_reference_field_names_by_source_class_name()
240
-
241
262
  # Initialize a progress bar.
242
263
  custom_progress = init_progress_bar()
243
264
 
@@ -317,10 +338,8 @@ def scan(
317
338
  violations = scan_outgoing_references(
318
339
  document=document,
319
340
  schema_view=schema_view,
320
- reference_field_names_by_source_class_name=reference_field_names_by_source_class_name,
321
341
  references=references,
322
342
  finder=finder,
323
- collection_names=collection_names,
324
343
  source_collection_name=source_collection_name,
325
344
  user_wants_to_locate_misplaced_documents=user_wants_to_locate_misplaced_documents,
326
345
  )
File without changes
File without changes
File without changes
File without changes