Skip to content

Commit cde8511

Browse files
committed
ENH: Parallel specification of next API
1 parent 22d2eb5 commit cde8511

File tree

5 files changed

+221
-0
lines changed

5 files changed

+221
-0
lines changed

bids_ng/__init__.py

Whitespace-only changes.

bids_ng/types/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from .api1 import BIDSDataset, BIDSFile, File, Index, Label
2+
from .enums import Query
3+
from .utils import PaddedInt
4+
5+
NONE, REQUIRED, OPTIONAL = tuple(Query)
6+
7+
__all__ = (
8+
"BIDSDataset",
9+
"BIDSFile",
10+
"File",
11+
"Index",
12+
"Label",
13+
"NONE",
14+
"OPTIONAL",
15+
"REQUIRED",
16+
"Query",
17+
"PaddedInt",
18+
)

bids_ng/types/api1.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""PyBIDS 1.0 API specification"""
2+
3+
from pathlib import Path
4+
from typing import Any, Dict, List, Optional, Protocol, TypeVar, Union
5+
6+
from .utils import PaddedInt
7+
8+
try:
9+
from typing import TypeAlias
10+
except ImportError:
11+
from typing_extensions import TypeAlias
12+
13+
14+
# Datasets should be parameterizable on some kind of schema object.
15+
# External API users should not depend on it, so this is bound to Any,
16+
# but once a Schema type is defined for an API implementation, type checkers
17+
# should be able to introspect it.
18+
SchemaT = TypeVar("SchemaT")
19+
20+
21+
Index: TypeAlias = PaddedInt
22+
Label: TypeAlias = str
23+
24+
25+
class File(Protocol[SchemaT]):
26+
"""Generic file holder
27+
28+
This serves as a base class for :class:`BIDSFile` and can represent
29+
non-BIDS files.
30+
"""
31+
32+
path: Path
33+
relative_path: Path
34+
dataset: Optional["BIDSDataset[SchemaT]"]
35+
36+
def __fspath__(self) -> str:
37+
...
38+
39+
40+
class BIDSFile(File[SchemaT], Protocol):
41+
"""BIDS file
42+
43+
This provides access to BIDS concepts such as path components
44+
and sidecar metadata.
45+
46+
BIDS paths take the form::
47+
48+
[sub-<label>/[ses-<label>/]<datatype>/]<entities>_<suffix><extension>
49+
"""
50+
51+
entities: Dict[str, Union[Label, Index]]
52+
datatype: Optional[str]
53+
suffix: Optional[str]
54+
extension: Optional[str]
55+
56+
metadata: Dict[str, Any]
57+
"""Sidecar metadata aggregated according to inheritance principle"""
58+
59+
60+
class BIDSDataset(Protocol[SchemaT]):
61+
"""Interface to a single BIDS dataset.
62+
63+
This structure does not consider the contents of sub-datasets
64+
such as `sourcedata/` or `derivatives/`.
65+
"""
66+
root: Path
67+
schema: SchemaT
68+
69+
dataset_description: Dict[str, Any]
70+
"""Contents of dataset_description.json"""
71+
72+
ignored: List[File[SchemaT]]
73+
"""Invalid files found in dataset"""
74+
75+
files: List[BIDSFile[SchemaT]]
76+
"""Valid files found in dataset"""
77+
78+
datatypes: List[str]
79+
"""Datatype directories found in dataset"""
80+
81+
modalities: List[str]
82+
"""BIDS "modalities" found in dataset"""
83+
84+
subjects: List[str]
85+
"""Subject/participant identifiers found in the dataset"""
86+
87+
entities: List[str]
88+
"""Entities (long names) found in any filename in the dataset"""
89+
90+
def get(self, **filters) -> List[BIDSFile[SchemaT]]:
91+
"""Query dataset for files"""
92+
93+
def get_entities(self, entity: str, **filters) -> List[Label | Index]:
94+
"""Query dataset for entity values"""
95+
96+
def get_metadata(self, term: str, **filters) -> List[Any]:
97+
"""Query dataset for metdata values"""
98+
99+
100+
class DatasetCollection(BIDSDataset[SchemaT], Protocol):
101+
"""Interface to a collection of BIDS dataset.
102+
103+
This structure allows the user to construct a single view of
104+
multiple datasets, such as including source or derivative datasets.
105+
"""
106+
primary: BIDSDataset[SchemaT]
107+
datasets: List[BIDSDataset[SchemaT]]
108+
109+
def add_dataset(self, dataset: BIDSDataset[SchemaT]) -> None:
110+
...

bids_ng/types/enums.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from enum import Enum
2+
3+
4+
class Query(Enum):
5+
"""Special arguments for dataset querying
6+
7+
* `Query.NONE` - The field MUST NOT be present
8+
* `Query.REQUIRED` - The field MUST be present, but may take any value
9+
* `Query.OPTIONAL` - The field MAY be present, and may take any value
10+
11+
`Query.ANY` is a synonym for `Query.REQUIRED`. Its use is discouraged
12+
and may be removed in the future.
13+
"""
14+
15+
NONE = 1
16+
REQUIRED = ANY = 2
17+
OPTIONAL = 3

bids_ng/types/utils.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import typing as ty
2+
3+
4+
class PaddedInt(int):
5+
"""Integer type that preserves zero-padding
6+
7+
Acts like an int in almost all ways except that string formatting
8+
will keep the original zero-padding. Numeric format specifiers will
9+
work with the integer value.
10+
11+
>>> PaddedInt(1)
12+
1
13+
>>> p2 = PaddedInt("02")
14+
>>> p2
15+
02
16+
>>> str(p2)
17+
'02'
18+
>>> p2 == 2
19+
True
20+
>>> p2 in range(3)
21+
True
22+
>>> f"{p2}"
23+
'02'
24+
>>> f"{p2:s}"
25+
'02'
26+
>>> f"{p2!s}"
27+
'02'
28+
>>> f"{p2!r}"
29+
'02'
30+
>>> f"{p2:d}"
31+
'2'
32+
>>> f"{p2:03d}"
33+
'002'
34+
>>> f"{p2:f}"
35+
'2.000000'
36+
>>> {2: "val"}.get(p2)
37+
'val'
38+
>>> {p2: "val"}.get(2)
39+
'val'
40+
41+
Note that arithmetic will break the padding.
42+
43+
>>> str(p2 + 1)
44+
'3'
45+
"""
46+
47+
def __init__(self, val: ty.Union[str, int]) -> None:
48+
self.sval = str(val)
49+
if not self.sval.isdigit():
50+
raise TypeError(
51+
f"{self.__class__.__name__}() argument must be a string of digits "
52+
f"or int, not {val.__class__.__name__!r}"
53+
)
54+
55+
def __eq__(self, val: object) -> bool:
56+
return val == self.sval or super().__eq__(val)
57+
58+
def __str__(self) -> str:
59+
return self.sval
60+
61+
def __repr__(self) -> str:
62+
return self.sval
63+
64+
def __format__(self, format_spec: str) -> str:
65+
"""Format a padded integer
66+
67+
If a format spec can be used on a string, apply it to the zero-padded string.
68+
Otherwise format as an integer.
69+
"""
70+
try:
71+
return format(self.sval, format_spec)
72+
except ValueError:
73+
return super().__format__(format_spec)
74+
75+
def __hash__(self) -> int:
76+
return super().__hash__()

0 commit comments

Comments
 (0)