eegdash 0.4.0.dev173498563__py3-none-any.whl → 0.4.1__py3-none-any.whl

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.

Potentially problematic release.


This version of eegdash might be problematic. Click here for more details.

eegdash/__init__.py CHANGED
@@ -18,4 +18,4 @@ _init_mongo_client()
18
18
 
19
19
  __all__ = ["EEGDash", "EEGDashDataset", "EEGChallengeDataset", "preprocessing"]
20
20
 
21
- __version__ = "0.4.0.dev173498563"
21
+ __version__ = "0.4.1"
eegdash/api.py CHANGED
@@ -16,7 +16,7 @@ from typing import Any, Mapping
16
16
 
17
17
  import mne
18
18
  from docstring_inheritance import NumpyDocstringInheritanceInitMeta
19
- from dotenv import load_dotenv
19
+ from mne.utils import _soft_import
20
20
  from mne_bids import find_matching_paths
21
21
  from pymongo import InsertOne, UpdateOne
22
22
  from rich.console import Console
@@ -89,7 +89,8 @@ class EEGDash:
89
89
  except Exception:
90
90
  DB_CONNECTION_STRING = None
91
91
  else:
92
- load_dotenv()
92
+ dotenv = _soft_import("dotenv", "eegdash[full] is necessary.")
93
+ dotenv.load_dotenv()
93
94
  DB_CONNECTION_STRING = os.getenv("DB_CONNECTION_STRING")
94
95
 
95
96
  # Use singleton to get MongoDB client, database, and collection
@@ -212,17 +213,22 @@ class EEGDash:
212
213
  return doc is not None
213
214
 
214
215
  def _validate_input(self, record: dict[str, Any]) -> dict[str, Any]:
215
- """Internal method to validate the input record against the expected schema.
216
+ """Validate the input record against the expected schema.
216
217
 
217
218
  Parameters
218
219
  ----------
219
- record: dict
220
+ record : dict
220
221
  A dictionary representing the EEG data record to be validated.
221
222
 
222
223
  Returns
223
224
  -------
224
- dict:
225
- Returns the record itself on success, or raises a ValueError if the record is invalid.
225
+ dict
226
+ The record itself on success.
227
+
228
+ Raises
229
+ ------
230
+ ValueError
231
+ If the record is missing required keys or has values of the wrong type.
226
232
 
227
233
  """
228
234
  input_types = {
@@ -252,20 +258,44 @@ class EEGDash:
252
258
  return record
253
259
 
254
260
  def _build_query_from_kwargs(self, **kwargs) -> dict[str, Any]:
255
- """Internal helper to build a validated MongoDB query from keyword args.
261
+ """Build a validated MongoDB query from keyword arguments.
262
+
263
+ This delegates to the module-level builder used across the package.
264
+
265
+ Parameters
266
+ ----------
267
+ **kwargs
268
+ Keyword arguments to convert into a MongoDB query.
269
+
270
+ Returns
271
+ -------
272
+ dict
273
+ A MongoDB query dictionary.
256
274
 
257
- This delegates to the module-level builder used across the package and
258
- is exposed here for testing and convenience.
259
275
  """
260
276
  return build_query_from_kwargs(**kwargs)
261
277
 
262
- # --- Query merging and conflict detection helpers ---
263
- def _extract_simple_constraint(self, query: dict[str, Any], key: str):
278
+ def _extract_simple_constraint(
279
+ self, query: dict[str, Any], key: str
280
+ ) -> tuple[str, Any] | None:
264
281
  """Extract a simple constraint for a given key from a query dict.
265
282
 
266
- Supports only top-level equality (key: value) and $in (key: {"$in": [...]})
267
- constraints. Returns a tuple (kind, value) where kind is "eq" or "in". If the
268
- key is not present or uses other operators, returns None.
283
+ Supports top-level equality (e.g., ``{'subject': '01'}``) and ``$in``
284
+ (e.g., ``{'subject': {'$in': ['01', '02']}}``) constraints.
285
+
286
+ Parameters
287
+ ----------
288
+ query : dict
289
+ The MongoDB query dictionary.
290
+ key : str
291
+ The key for which to extract the constraint.
292
+
293
+ Returns
294
+ -------
295
+ tuple or None
296
+ A tuple of (kind, value) where kind is "eq" or "in", or None if the
297
+ constraint is not present or unsupported.
298
+
269
299
  """
270
300
  if not isinstance(query, dict) or key not in query:
271
301
  return None
@@ -275,16 +305,28 @@ class EEGDash:
275
305
  return ("in", list(val["$in"]))
276
306
  return None # unsupported operator shape for conflict checking
277
307
  else:
278
- return ("eq", val)
308
+ return "eq", val
279
309
 
280
310
  def _raise_if_conflicting_constraints(
281
311
  self, raw_query: dict[str, Any], kwargs_query: dict[str, Any]
282
312
  ) -> None:
283
- """Raise ValueError if both query sources define incompatible constraints.
313
+ """Raise ValueError if query sources have incompatible constraints.
314
+
315
+ Checks for mutually exclusive constraints on the same field to avoid
316
+ silent empty results.
317
+
318
+ Parameters
319
+ ----------
320
+ raw_query : dict
321
+ The raw MongoDB query dictionary.
322
+ kwargs_query : dict
323
+ The query dictionary built from keyword arguments.
324
+
325
+ Raises
326
+ ------
327
+ ValueError
328
+ If conflicting constraints are found.
284
329
 
285
- We conservatively check only top-level fields with simple equality or $in
286
- constraints. If a field appears in both queries and constraints are mutually
287
- exclusive, raise an explicit error to avoid silent empty result sets.
288
330
  """
289
331
  if not raw_query or not kwargs_query:
290
332
  return
@@ -388,12 +430,31 @@ class EEGDash:
388
430
  logger.info("Upserted: %s", result.upserted_count)
389
431
  logger.info("Errors: %s ", result.bulk_api_result.get("writeErrors", []))
390
432
 
391
- def _add_request(self, record: dict):
392
- """Internal helper method to create a MongoDB insertion request for a record."""
433
+ def _add_request(self, record: dict) -> InsertOne:
434
+ """Create a MongoDB insertion request for a record.
435
+
436
+ Parameters
437
+ ----------
438
+ record : dict
439
+ The record to insert.
440
+
441
+ Returns
442
+ -------
443
+ InsertOne
444
+ A PyMongo ``InsertOne`` object.
445
+
446
+ """
393
447
  return InsertOne(record)
394
448
 
395
- def add(self, record: dict):
396
- """Add a single record to the MongoDB collection."""
449
+ def add(self, record: dict) -> None:
450
+ """Add a single record to the MongoDB collection.
451
+
452
+ Parameters
453
+ ----------
454
+ record : dict
455
+ The record to add.
456
+
457
+ """
397
458
  try:
398
459
  self.__collection.insert_one(record)
399
460
  except ValueError as e:
@@ -405,11 +466,23 @@ class EEGDash:
405
466
  )
406
467
  logger.debug("Add operation failed", exc_info=exc)
407
468
 
408
- def _update_request(self, record: dict):
409
- """Internal helper method to create a MongoDB update request for a record."""
469
+ def _update_request(self, record: dict) -> UpdateOne:
470
+ """Create a MongoDB update request for a record.
471
+
472
+ Parameters
473
+ ----------
474
+ record : dict
475
+ The record to update.
476
+
477
+ Returns
478
+ -------
479
+ UpdateOne
480
+ A PyMongo ``UpdateOne`` object.
481
+
482
+ """
410
483
  return UpdateOne({"data_name": record["data_name"]}, {"$set": record})
411
484
 
412
- def update(self, record: dict):
485
+ def update(self, record: dict) -> None:
413
486
  """Update a single record in the MongoDB collection.
414
487
 
415
488
  Parameters
@@ -429,58 +502,81 @@ class EEGDash:
429
502
  logger.debug("Update operation failed", exc_info=exc)
430
503
 
431
504
  def exists(self, query: dict[str, Any]) -> bool:
432
- """Alias for :meth:`exist` provided for API clarity."""
505
+ """Check if at least one record matches the query.
506
+
507
+ This is an alias for :meth:`exist`.
508
+
509
+ Parameters
510
+ ----------
511
+ query : dict
512
+ MongoDB query to check for existence.
513
+
514
+ Returns
515
+ -------
516
+ bool
517
+ True if a matching record exists, False otherwise.
518
+
519
+ """
433
520
  return self.exist(query)
434
521
 
435
- def remove_field(self, record, field):
436
- """Remove a specific field from a record in the MongoDB collection.
522
+ def remove_field(self, record: dict, field: str) -> None:
523
+ """Remove a field from a specific record in the MongoDB collection.
437
524
 
438
525
  Parameters
439
526
  ----------
440
527
  record : dict
441
- Record identifying object with ``data_name``.
528
+ Record-identifying object with a ``data_name`` key.
442
529
  field : str
443
- Field name to remove.
530
+ The name of the field to remove.
444
531
 
445
532
  """
446
533
  self.__collection.update_one(
447
534
  {"data_name": record["data_name"]}, {"$unset": {field: 1}}
448
535
  )
449
536
 
450
- def remove_field_from_db(self, field):
451
- """Remove a field from all records (destructive).
537
+ def remove_field_from_db(self, field: str) -> None:
538
+ """Remove a field from all records in the database.
539
+
540
+ .. warning::
541
+ This is a destructive operation and cannot be undone.
452
542
 
453
543
  Parameters
454
544
  ----------
455
545
  field : str
456
- Field name to remove from every document.
546
+ The name of the field to remove from all documents.
457
547
 
458
548
  """
459
549
  self.__collection.update_many({}, {"$unset": {field: 1}})
460
550
 
461
551
  @property
462
552
  def collection(self):
463
- """Return the MongoDB collection object."""
464
- return self.__collection
553
+ """The underlying PyMongo ``Collection`` object.
465
554
 
466
- def close(self):
467
- """Backward-compatibility no-op; connections are managed globally.
555
+ Returns
556
+ -------
557
+ pymongo.collection.Collection
558
+ The collection object used for database interactions.
468
559
 
469
- Notes
470
- -----
471
- Connections are managed by :class:`MongoConnectionManager`. Use
472
- :meth:`close_all_connections` to explicitly close all clients.
560
+ """
561
+ return self.__collection
473
562
 
563
+ def close(self) -> None:
564
+ """Close the MongoDB connection.
565
+
566
+ .. deprecated:: 0.1
567
+ Connections are now managed globally by :class:`MongoConnectionManager`.
568
+ This method is a no-op and will be removed in a future version.
569
+ Use :meth:`EEGDash.close_all_connections` to close all clients.
474
570
  """
475
571
  # Individual instances no longer close the shared client
476
572
  pass
477
573
 
478
574
  @classmethod
479
- def close_all_connections(cls):
480
- """Close all MongoDB client connections managed by the singleton."""
575
+ def close_all_connections(cls) -> None:
576
+ """Close all MongoDB client connections managed by the singleton manager."""
481
577
  MongoConnectionManager.close_all()
482
578
 
483
- def __del__(self):
579
+ def __del__(self) -> None:
484
580
  """Destructor; no explicit action needed due to global connection manager."""
485
581
  # No longer needed since we're using singleton pattern
486
582
  pass
@@ -775,45 +871,30 @@ class EEGDashDataset(BaseConcatDataset, metaclass=NumpyDocstringInheritanceInitM
775
871
  ) -> list[dict]:
776
872
  """Discover local BIDS EEG files and build minimal records.
777
873
 
778
- This helper enumerates EEG recordings under ``dataset_root`` via
779
- ``mne_bids.find_matching_paths`` and applies entity filters to produce a
780
- list of records suitable for ``EEGDashBaseDataset``. No network access
781
- is performed and files are not read.
874
+ Enumerates EEG recordings under ``dataset_root`` using
875
+ ``mne_bids.find_matching_paths`` and applies entity filters to produce
876
+ records suitable for :class:`EEGDashBaseDataset`. No network access is
877
+ performed, and files are not read.
782
878
 
783
879
  Parameters
784
880
  ----------
785
881
  dataset_root : Path
786
- Local dataset directory. May be the plain dataset folder (e.g.,
787
- ``ds005509``) or a suffixed cache variant (e.g.,
788
- ``ds005509-bdf-mini``).
789
- filters : dict of {str, Any}
790
- Query filters. Must include ``'dataset'`` with the dataset id (without
791
- local suffixes). May include BIDS entities ``'subject'``,
792
- ``'session'``, ``'task'``, and ``'run'``. Each value can be a scalar
793
- or a sequence of scalars.
882
+ Local dataset directory (e.g., ``/path/to/cache/ds005509``).
883
+ filters : dict
884
+ Query filters. Must include ``'dataset'`` and may include BIDS
885
+ entities like ``'subject'``, ``'session'``, etc.
794
886
 
795
887
  Returns
796
888
  -------
797
- records : list of dict
798
- One record per matched EEG file with at least:
799
-
800
- - ``'data_name'``
801
- - ``'dataset'`` (dataset id, without suffixes)
802
- - ``'bidspath'`` (normalized to start with the dataset id)
803
- - ``'subject'``, ``'session'``, ``'task'``, ``'run'`` (may be None)
804
- - ``'bidsdependencies'`` (empty list)
805
- - ``'modality'`` (``"eeg"``)
806
- - ``'sampling_frequency'``, ``'nchans'``, ``'ntimes'`` (minimal
807
- defaults for offline usage)
889
+ list of dict
890
+ A list of records, one for each matched EEG file. Each record
891
+ contains BIDS entities, paths, and minimal metadata for offline use.
808
892
 
809
893
  Notes
810
894
  -----
811
- - Matching uses ``datatypes=['eeg']`` and ``suffixes=['eeg']``.
812
- - ``bidspath`` is constructed as
813
- ``<dataset_id> / <relative_path_from_dataset_root>`` to ensure the
814
- first path component is the dataset id (without local cache suffixes).
815
- - Minimal defaults are set for ``sampling_frequency``, ``nchans``, and
816
- ``ntimes`` to satisfy dataset length requirements offline.
895
+ Matching is performed for ``datatypes=['eeg']`` and ``suffixes=['eeg']``.
896
+ The ``bidspath`` is normalized to ensure it starts with the dataset ID,
897
+ even for suffixed cache directories.
817
898
 
818
899
  """
819
900
  dataset_id = filters["dataset"]
@@ -875,10 +956,22 @@ class EEGDashDataset(BaseConcatDataset, metaclass=NumpyDocstringInheritanceInitM
875
956
  return records_out
876
957
 
877
958
  def _find_key_in_nested_dict(self, data: Any, target_key: str) -> Any:
878
- """Recursively search for target_key in nested dicts/lists with normalized matching.
959
+ """Recursively search for a key in nested dicts/lists.
960
+
961
+ Performs a case-insensitive and underscore/hyphen-agnostic search.
962
+
963
+ Parameters
964
+ ----------
965
+ data : Any
966
+ The nested data structure (dicts, lists) to search.
967
+ target_key : str
968
+ The key to search for.
969
+
970
+ Returns
971
+ -------
972
+ Any
973
+ The value of the first matching key, or None if not found.
879
974
 
880
- This makes lookups tolerant to naming differences like "p-factor" vs "p_factor".
881
- Returns the first match or None.
882
975
  """
883
976
  norm_target = normalize_key(target_key)
884
977
  if isinstance(data, dict):
@@ -901,23 +994,25 @@ class EEGDashDataset(BaseConcatDataset, metaclass=NumpyDocstringInheritanceInitM
901
994
  description_fields: list[str],
902
995
  base_dataset_kwargs: dict,
903
996
  ) -> list[EEGDashBaseDataset]:
904
- """Helper method to find datasets in the MongoDB collection that satisfy the
905
- given query and return them as a list of EEGDashBaseDataset objects.
997
+ """Find and construct datasets from a MongoDB query.
998
+
999
+ Queries the database, then creates a list of
1000
+ :class:`EEGDashBaseDataset` objects from the results.
906
1001
 
907
1002
  Parameters
908
1003
  ----------
909
- query : dict
910
- The query object, as in EEGDash.find().
911
- description_fields : list[str]
912
- A list of fields to be extracted from the dataset records and included in
913
- the returned dataset description(s).
914
- kwargs: additional keyword arguments to be passed to the EEGDashBaseDataset
915
- constructor.
1004
+ query : dict, optional
1005
+ The MongoDB query to execute.
1006
+ description_fields : list of str
1007
+ Fields to extract from each record for the dataset description.
1008
+ base_dataset_kwargs : dict
1009
+ Additional keyword arguments to pass to the
1010
+ :class:`EEGDashBaseDataset` constructor.
916
1011
 
917
1012
  Returns
918
1013
  -------
919
- list :
920
- A list of EEGDashBaseDataset objects that match the query.
1014
+ list of EEGDashBaseDataset
1015
+ A list of dataset objects matching the query.
921
1016
 
922
1017
  """
923
1018
  datasets: list[EEGDashBaseDataset] = []