-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ENH: Parallel specification of next API
- Loading branch information
Showing
5 changed files
with
221 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from .api1 import BIDSDataset, BIDSFile, File, Index, Label | ||
from .enums import Query | ||
from .utils import PaddedInt | ||
|
||
NONE, REQUIRED, OPTIONAL = tuple(Query) | ||
|
||
__all__ = ( | ||
"BIDSDataset", | ||
"BIDSFile", | ||
"File", | ||
"Index", | ||
"Label", | ||
"NONE", | ||
"OPTIONAL", | ||
"REQUIRED", | ||
"Query", | ||
"PaddedInt", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
"""PyBIDS 1.0 API specification""" | ||
|
||
from pathlib import Path | ||
from typing import Any, Dict, List, Optional, Protocol, TypeVar, Union | ||
|
||
from .utils import PaddedInt | ||
|
||
try: | ||
from typing import TypeAlias | ||
except ImportError: | ||
from typing_extensions import TypeAlias | ||
|
||
|
||
# Datasets should be parameterizable on some kind of schema object. | ||
# External API users should not depend on it, so this is bound to Any, | ||
# but once a Schema type is defined for an API implementation, type checkers | ||
# should be able to introspect it. | ||
SchemaT = TypeVar("SchemaT") | ||
|
||
|
||
Index: TypeAlias = PaddedInt | ||
Label: TypeAlias = str | ||
|
||
|
||
class File(Protocol[SchemaT]): | ||
"""Generic file holder | ||
This serves as a base class for :class:`BIDSFile` and can represent | ||
non-BIDS files. | ||
""" | ||
|
||
path: Path | ||
relative_path: Path | ||
dataset: Optional["BIDSDataset[SchemaT]"] | ||
|
||
def __fspath__(self) -> str: | ||
... | ||
|
||
|
||
class BIDSFile(File[SchemaT], Protocol): | ||
"""BIDS file | ||
This provides access to BIDS concepts such as path components | ||
and sidecar metadata. | ||
BIDS paths take the form:: | ||
[sub-<label>/[ses-<label>/]<datatype>/]<entities>_<suffix><extension> | ||
""" | ||
|
||
entities: Dict[str, Union[Label, Index]] | ||
datatype: Optional[str] | ||
suffix: Optional[str] | ||
extension: Optional[str] | ||
|
||
metadata: Dict[str, Any] | ||
"""Sidecar metadata aggregated according to inheritance principle""" | ||
|
||
|
||
class BIDSDataset(Protocol[SchemaT]): | ||
"""Interface to a single BIDS dataset. | ||
This structure does not consider the contents of sub-datasets | ||
such as `sourcedata/` or `derivatives/`. | ||
""" | ||
root: Path | ||
schema: SchemaT | ||
|
||
dataset_description: Dict[str, Any] | ||
"""Contents of dataset_description.json""" | ||
|
||
ignored: List[File[SchemaT]] | ||
"""Invalid files found in dataset""" | ||
|
||
files: List[BIDSFile[SchemaT]] | ||
"""Valid files found in dataset""" | ||
|
||
datatypes: List[str] | ||
"""Datatype directories found in dataset""" | ||
|
||
modalities: List[str] | ||
"""BIDS "modalities" found in dataset""" | ||
|
||
subjects: List[str] | ||
"""Subject/participant identifiers found in the dataset""" | ||
|
||
entities: List[str] | ||
"""Entities (long names) found in any filename in the dataset""" | ||
|
||
def get(self, **filters) -> List[BIDSFile[SchemaT]]: | ||
"""Query dataset for files""" | ||
|
||
def get_entities(self, entity: str, **filters) -> List[Label | Index]: | ||
"""Query dataset for entity values""" | ||
|
||
def get_metadata(self, term: str, **filters) -> List[Any]: | ||
"""Query dataset for metdata values""" | ||
|
||
|
||
class DatasetCollection(BIDSDataset[SchemaT], Protocol): | ||
"""Interface to a collection of BIDS dataset. | ||
This structure allows the user to construct a single view of | ||
multiple datasets, such as including source or derivative datasets. | ||
""" | ||
primary: BIDSDataset[SchemaT] | ||
datasets: List[BIDSDataset[SchemaT]] | ||
|
||
def add_dataset(self, dataset: BIDSDataset[SchemaT]) -> None: | ||
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
from enum import Enum | ||
|
||
|
||
class Query(Enum): | ||
"""Special arguments for dataset querying | ||
* `Query.NONE` - The field MUST NOT be present | ||
* `Query.REQUIRED` - The field MUST be present, but may take any value | ||
* `Query.OPTIONAL` - The field MAY be present, and may take any value | ||
`Query.ANY` is a synonym for `Query.REQUIRED`. Its use is discouraged | ||
and may be removed in the future. | ||
""" | ||
|
||
NONE = 1 | ||
REQUIRED = ANY = 2 | ||
OPTIONAL = 3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import typing as ty | ||
|
||
|
||
class PaddedInt(int): | ||
"""Integer type that preserves zero-padding | ||
Acts like an int in almost all ways except that string formatting | ||
will keep the original zero-padding. Numeric format specifiers will | ||
work with the integer value. | ||
>>> PaddedInt(1) | ||
1 | ||
>>> p2 = PaddedInt("02") | ||
>>> p2 | ||
02 | ||
>>> str(p2) | ||
'02' | ||
>>> p2 == 2 | ||
True | ||
>>> p2 in range(3) | ||
True | ||
>>> f"{p2}" | ||
'02' | ||
>>> f"{p2:s}" | ||
'02' | ||
>>> f"{p2!s}" | ||
'02' | ||
>>> f"{p2!r}" | ||
'02' | ||
>>> f"{p2:d}" | ||
'2' | ||
>>> f"{p2:03d}" | ||
'002' | ||
>>> f"{p2:f}" | ||
'2.000000' | ||
>>> {2: "val"}.get(p2) | ||
'val' | ||
>>> {p2: "val"}.get(2) | ||
'val' | ||
Note that arithmetic will break the padding. | ||
>>> str(p2 + 1) | ||
'3' | ||
""" | ||
|
||
def __init__(self, val: ty.Union[str, int]) -> None: | ||
self.sval = str(val) | ||
if not self.sval.isdigit(): | ||
raise TypeError( | ||
f"{self.__class__.__name__}() argument must be a string of digits " | ||
f"or int, not {val.__class__.__name__!r}" | ||
) | ||
|
||
def __eq__(self, val: object) -> bool: | ||
return val == self.sval or super().__eq__(val) | ||
|
||
def __str__(self) -> str: | ||
return self.sval | ||
|
||
def __repr__(self) -> str: | ||
return self.sval | ||
|
||
def __format__(self, format_spec: str) -> str: | ||
"""Format a padded integer | ||
If a format spec can be used on a string, apply it to the zero-padded string. | ||
Otherwise format as an integer. | ||
""" | ||
try: | ||
return format(self.sval, format_spec) | ||
except ValueError: | ||
return super().__format__(format_spec) | ||
|
||
def __hash__(self) -> int: | ||
return super().__hash__() |