scistack-db 0.1.9.dev0__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.
@@ -0,0 +1,166 @@
1
+ """Column selection wrapper for variable types in for_each (DB-backed)."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class ColumnSelection:
7
+ """
8
+ Wraps a variable class with column selection for use in for_each() inputs.
9
+
10
+ Created automatically by BaseVariable.__class_getitem__ when using bracket
11
+ syntax:
12
+
13
+ MyVar["col_name"] # single column -> numpy array
14
+ MyVar[["col_a", "col_b"]] # multiple columns -> DataFrame subset
15
+
16
+ After loading, only the specified columns are extracted from the loaded
17
+ DataFrame. Single column selection returns a numpy array; multiple columns
18
+ return a DataFrame subset.
19
+
20
+ When ``iterate=True`` (constructed via ``MyVar.for_columns(...)``) the
21
+ selection means "run fn once per column and reassemble the per-column
22
+ results into a single wide output variable" rather than "pass the columns
23
+ as one argument." An **empty** ``columns`` (``[]``, the default) means "all
24
+ data columns", resolved at for_each time. ``None`` is accepted as an alias
25
+ for the empty/all sentinel for backward compatibility.
26
+
27
+ For standalone DataFrame usage, see scifor.ColumnSelection.
28
+ """
29
+
30
+ def __init__(
31
+ self, var_type: type, columns: "list[str] | None" = None, iterate: bool = False
32
+ ):
33
+ """
34
+ Args:
35
+ var_type: The variable class to load.
36
+ columns: List of column names to extract after loading. An empty
37
+ list ``[]`` (the default) means "all data columns", resolved at
38
+ for_each time (only meaningful with ``iterate=True``). ``None``
39
+ is accepted as an alias for ``[]``.
40
+ iterate: If True, iterate over the columns (one fn call each) and
41
+ reassemble into one wide output; if False (default), pass the
42
+ column(s) as a single argument.
43
+ """
44
+ if columns is None:
45
+ columns = []
46
+ self.var_type = var_type
47
+ # Normalize the all-columns sentinel to an empty list (copying any
48
+ # provided list so a shared default object is never mutated).
49
+ self.columns = list(columns) if columns else []
50
+ self.iterate = iterate
51
+
52
+ @property
53
+ def __name__(self) -> str:
54
+ """Return a display name for format_inputs and error messages."""
55
+ var_name = getattr(self.var_type, "__name__", type(self.var_type).__name__)
56
+ suffix = ", iterate" if self.iterate else ""
57
+ if not self.columns:
58
+ return f"{var_name}[<all columns>{suffix}]"
59
+ if len(self.columns) == 1 and not self.iterate:
60
+ return f'{var_name}["{self.columns[0]}"]'
61
+ cols = ", ".join(f'"{c}"' for c in self.columns)
62
+ return f"{var_name}[{cols}{suffix}]"
63
+
64
+ def load(self, **metadata) -> Any:
65
+ """Load from the underlying var_type, then apply column selection."""
66
+ return self.var_type.load(**metadata)
67
+
68
+ def to_csv(self, filename: str, *args, **kwargs) -> None:
69
+ """Export the selected column(s) to a CSV file in flat table format.
70
+
71
+ Writes one row per schema_id with the selected columns as value
72
+ columns. The underlying variable must be a single-row table per
73
+ schema_id. ``filename`` must end with ``.csv``. ``kwargs`` mirror
74
+ ``load()`` (``where=``, ``version=``, ``db=``, metadata).
75
+
76
+ Example:
77
+ GaitData["Speed"].to_csv("speed.csv", subject=1)
78
+ GaitData[["Speed", "Cadence"]].to_csv("gait.csv", subject=1)
79
+ """
80
+ from scidb.csv_export import export_csv
81
+
82
+ export_csv(self, filename, *args, **kwargs)
83
+
84
+ # --- Comparison operators that produce ColumnFilter objects ---
85
+
86
+ def __eq__(self, other):
87
+ try:
88
+ from scidb.filters import ColumnFilter
89
+
90
+ return ColumnFilter(self.var_type, self.columns[0], "==", other)
91
+ except ImportError:
92
+ raise NotImplementedError(
93
+ "Comparison operators on ColumnSelection require scidb."
94
+ )
95
+
96
+ def __ne__(self, other):
97
+ try:
98
+ from scidb.filters import ColumnFilter
99
+
100
+ return ColumnFilter(self.var_type, self.columns[0], "!=", other)
101
+ except ImportError:
102
+ raise NotImplementedError(
103
+ "Comparison operators on ColumnSelection require scidb."
104
+ )
105
+
106
+ def __lt__(self, other):
107
+ try:
108
+ from scidb.filters import ColumnFilter
109
+
110
+ return ColumnFilter(self.var_type, self.columns[0], "<", other)
111
+ except ImportError:
112
+ raise NotImplementedError(
113
+ "Comparison operators on ColumnSelection require scidb."
114
+ )
115
+
116
+ def __le__(self, other):
117
+ try:
118
+ from scidb.filters import ColumnFilter
119
+
120
+ return ColumnFilter(self.var_type, self.columns[0], "<=", other)
121
+ except ImportError:
122
+ raise NotImplementedError(
123
+ "Comparison operators on ColumnSelection require scidb."
124
+ )
125
+
126
+ def __gt__(self, other):
127
+ try:
128
+ from scidb.filters import ColumnFilter
129
+
130
+ return ColumnFilter(self.var_type, self.columns[0], ">", other)
131
+ except ImportError:
132
+ raise NotImplementedError(
133
+ "Comparison operators on ColumnSelection require scidb."
134
+ )
135
+
136
+ def __ge__(self, other):
137
+ try:
138
+ from scidb.filters import ColumnFilter
139
+
140
+ return ColumnFilter(self.var_type, self.columns[0], ">=", other)
141
+ except ImportError:
142
+ raise NotImplementedError(
143
+ "Comparison operators on ColumnSelection require scidb."
144
+ )
145
+
146
+ def isin(self, values):
147
+ """Create an InFilter for set membership testing."""
148
+ try:
149
+ from scidb.filters import InFilter
150
+
151
+ return InFilter(self.var_type, self.columns[0], list(values))
152
+ except ImportError:
153
+ raise NotImplementedError("isin() on ColumnSelection requires scidb.")
154
+
155
+ def to_key(self) -> str:
156
+ """Return a canonical string for use as a version key.
157
+
158
+ Includes ``iterate`` and the resolved column list so that changing the
159
+ iterated column set (including the empty ``[]`` -> all-columns
160
+ resolution done before this is called) invalidates cached results.
161
+ """
162
+ name = getattr(self.var_type, "__name__", repr(self.var_type))
163
+ return f"{name}[{self.columns!r}, iterate={self.iterate}]"
164
+
165
+ def __hash__(self):
166
+ return hash((self.var_type, tuple(self.columns), self.iterate))
scidb/constant.py ADDED
@@ -0,0 +1,295 @@
1
+ """
2
+ Constant — a lightweight wrapper for pipeline configuration values.
3
+
4
+ Scientific pipelines accumulate magic numbers: sampling rates, bandpass limits,
5
+ epoch durations, regularization weights, etc. Wrapping them in ``constant(...)``
6
+ gives each value a description and a source location so the GUI sidebar can
7
+ surface them, while still letting the value behave transparently like its
8
+ underlying type.
9
+
10
+ Example:
11
+ from scidb import constant
12
+
13
+ SAMPLING_RATE_HZ = constant(
14
+ 1000,
15
+ description="Default sampling rate for all recordings",
16
+ )
17
+
18
+ DEFAULT_BANDPASS = constant(
19
+ (1.0, 40.0),
20
+ description="Standard LFP bandpass (Hz)",
21
+ )
22
+
23
+ # Transparent use:
24
+ duration = 5 * SAMPLING_RATE_HZ # 5000
25
+ low = DEFAULT_BANDPASS[0] # 1.0
26
+
27
+ # Discovery-side detection:
28
+ isinstance(SAMPLING_RATE_HZ, Constant) # True
29
+ SAMPLING_RATE_HZ.description # "Default sampling rate ..."
30
+ SAMPLING_RATE_HZ.source_file # "/path/to/module.py"
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import inspect
36
+ from typing import Any
37
+
38
+
39
+ class Constant:
40
+ """
41
+ Transparent wrapper around a pipeline configuration value.
42
+
43
+ ``Constant`` instances forward arithmetic, comparison, container, and
44
+ attribute access to the underlying value so they can be used in place of
45
+ the raw value at call sites. The wrapper additionally carries a human
46
+ description and the source file/line where it was constructed, which the
47
+ scidb discovery scanner uses to populate the GUI sidebar.
48
+
49
+ Prefer the :func:`constant` factory over instantiating this class directly
50
+ — the factory captures the caller's source location automatically.
51
+ """
52
+
53
+ __slots__ = ("_value", "description", "source_file", "source_line")
54
+
55
+ def __init__(
56
+ self,
57
+ value: Any,
58
+ description: str = "",
59
+ source_file: str = "",
60
+ source_line: int = 0,
61
+ ) -> None:
62
+ # Bypass __setattr__ on __slots__ via object.__setattr__ to make the
63
+ # wrapper feel as immutable as possible from the outside.
64
+ object.__setattr__(self, "_value", value)
65
+ object.__setattr__(self, "description", description)
66
+ object.__setattr__(self, "source_file", source_file)
67
+ object.__setattr__(self, "source_line", source_line)
68
+
69
+ # ------------------------------------------------------------------
70
+ # Introspection / debugging
71
+ # ------------------------------------------------------------------
72
+ @property
73
+ def value(self) -> Any:
74
+ """Return the wrapped underlying value."""
75
+ return self._value
76
+
77
+ def __repr__(self) -> str:
78
+ return f"Constant({self._value!r}, description={self.description!r})"
79
+
80
+ # ------------------------------------------------------------------
81
+ # Attribute passthrough
82
+ # ------------------------------------------------------------------
83
+ def __getattr__(self, name: str) -> Any:
84
+ # __getattr__ is only called when normal lookup fails, so our own
85
+ # slots (description, source_file, ...) take precedence.
86
+ return getattr(self._value, name)
87
+
88
+ # ------------------------------------------------------------------
89
+ # Conversions
90
+ # ------------------------------------------------------------------
91
+ def __bool__(self) -> bool:
92
+ return bool(self._value)
93
+
94
+ def __int__(self) -> int:
95
+ return int(self._value)
96
+
97
+ def __float__(self) -> float:
98
+ return float(self._value)
99
+
100
+ def __complex__(self) -> complex:
101
+ return complex(self._value)
102
+
103
+ def __str__(self) -> str:
104
+ return str(self._value)
105
+
106
+ def __format__(self, format_spec: str) -> str:
107
+ return format(self._value, format_spec)
108
+
109
+ def __bytes__(self) -> bytes:
110
+ return bytes(self._value)
111
+
112
+ def __index__(self) -> int:
113
+ # Needed for things like ``range(SAMPLING_RATE_HZ)`` and slicing.
114
+ return self._value.__index__()
115
+
116
+ def __hash__(self) -> int:
117
+ return hash(self._value)
118
+
119
+ # ------------------------------------------------------------------
120
+ # Comparison
121
+ # ------------------------------------------------------------------
122
+ @staticmethod
123
+ def _unwrap(other: Any) -> Any:
124
+ return other._value if isinstance(other, Constant) else other
125
+
126
+ def __eq__(self, other: Any) -> bool:
127
+ return self._value == self._unwrap(other)
128
+
129
+ def __ne__(self, other: Any) -> bool:
130
+ return self._value != self._unwrap(other)
131
+
132
+ def __lt__(self, other: Any) -> bool:
133
+ return self._value < self._unwrap(other)
134
+
135
+ def __le__(self, other: Any) -> bool:
136
+ return self._value <= self._unwrap(other)
137
+
138
+ def __gt__(self, other: Any) -> bool:
139
+ return self._value > self._unwrap(other)
140
+
141
+ def __ge__(self, other: Any) -> bool:
142
+ return self._value >= self._unwrap(other)
143
+
144
+ # ------------------------------------------------------------------
145
+ # Arithmetic (left-hand)
146
+ # ------------------------------------------------------------------
147
+ def __add__(self, other: Any) -> Any:
148
+ return self._value + self._unwrap(other)
149
+
150
+ def __sub__(self, other: Any) -> Any:
151
+ return self._value - self._unwrap(other)
152
+
153
+ def __mul__(self, other: Any) -> Any:
154
+ return self._value * self._unwrap(other)
155
+
156
+ def __truediv__(self, other: Any) -> Any:
157
+ return self._value / self._unwrap(other)
158
+
159
+ def __floordiv__(self, other: Any) -> Any:
160
+ return self._value // self._unwrap(other)
161
+
162
+ def __mod__(self, other: Any) -> Any:
163
+ return self._value % self._unwrap(other)
164
+
165
+ def __pow__(self, other: Any, mod: Any = None) -> Any:
166
+ if mod is None:
167
+ return self._value ** self._unwrap(other)
168
+ return pow(self._value, self._unwrap(other), mod)
169
+
170
+ def __matmul__(self, other: Any) -> Any:
171
+ return self._value @ self._unwrap(other)
172
+
173
+ def __lshift__(self, other: Any) -> Any:
174
+ return self._value << self._unwrap(other)
175
+
176
+ def __rshift__(self, other: Any) -> Any:
177
+ return self._value >> self._unwrap(other)
178
+
179
+ def __and__(self, other: Any) -> Any:
180
+ return self._value & self._unwrap(other)
181
+
182
+ def __xor__(self, other: Any) -> Any:
183
+ return self._value ^ self._unwrap(other)
184
+
185
+ def __or__(self, other: Any) -> Any:
186
+ return self._value | self._unwrap(other)
187
+
188
+ # ------------------------------------------------------------------
189
+ # Arithmetic (right-hand)
190
+ # ------------------------------------------------------------------
191
+ def __radd__(self, other: Any) -> Any:
192
+ return self._unwrap(other) + self._value
193
+
194
+ def __rsub__(self, other: Any) -> Any:
195
+ return self._unwrap(other) - self._value
196
+
197
+ def __rmul__(self, other: Any) -> Any:
198
+ return self._unwrap(other) * self._value
199
+
200
+ def __rtruediv__(self, other: Any) -> Any:
201
+ return self._unwrap(other) / self._value
202
+
203
+ def __rfloordiv__(self, other: Any) -> Any:
204
+ return self._unwrap(other) // self._value
205
+
206
+ def __rmod__(self, other: Any) -> Any:
207
+ return self._unwrap(other) % self._value
208
+
209
+ def __rpow__(self, other: Any) -> Any:
210
+ return self._unwrap(other) ** self._value
211
+
212
+ def __rmatmul__(self, other: Any) -> Any:
213
+ return self._unwrap(other) @ self._value
214
+
215
+ def __rlshift__(self, other: Any) -> Any:
216
+ return self._unwrap(other) << self._value
217
+
218
+ def __rrshift__(self, other: Any) -> Any:
219
+ return self._unwrap(other) >> self._value
220
+
221
+ def __rand__(self, other: Any) -> Any:
222
+ return self._unwrap(other) & self._value
223
+
224
+ def __rxor__(self, other: Any) -> Any:
225
+ return self._unwrap(other) ^ self._value
226
+
227
+ def __ror__(self, other: Any) -> Any:
228
+ return self._unwrap(other) | self._value
229
+
230
+ # ------------------------------------------------------------------
231
+ # Unary
232
+ # ------------------------------------------------------------------
233
+ def __neg__(self) -> Any:
234
+ return -self._value
235
+
236
+ def __pos__(self) -> Any:
237
+ return +self._value
238
+
239
+ def __abs__(self) -> Any:
240
+ return abs(self._value)
241
+
242
+ def __invert__(self) -> Any:
243
+ return ~self._value
244
+
245
+ def __round__(self, ndigits: int | None = None) -> Any:
246
+ if ndigits is None:
247
+ return round(self._value)
248
+ return round(self._value, ndigits)
249
+
250
+ # ------------------------------------------------------------------
251
+ # Container / iteration
252
+ # ------------------------------------------------------------------
253
+ def __len__(self) -> int:
254
+ return len(self._value)
255
+
256
+ def __iter__(self):
257
+ return iter(self._value)
258
+
259
+ def __contains__(self, item: Any) -> bool:
260
+ return self._unwrap(item) in self._value
261
+
262
+ def __getitem__(self, key: Any) -> Any:
263
+ return self._value[self._unwrap(key)]
264
+
265
+ def __reversed__(self):
266
+ return reversed(self._value)
267
+
268
+
269
+ def constant(value: Any, description: str = "") -> Constant:
270
+ """
271
+ Wrap ``value`` in a :class:`Constant`, capturing the caller's source
272
+ location so the GUI sidebar can link back to the definition.
273
+
274
+ Args:
275
+ value: The underlying value (scalar, tuple, list, dict, etc.).
276
+ description: Human-readable description of what the constant is for.
277
+
278
+ Returns:
279
+ A :class:`Constant` instance that behaves transparently as ``value``
280
+ for arithmetic, comparison, container, and attribute access.
281
+ """
282
+ # inspect.stack()[0] is this frame; [1] is the caller.
283
+ caller = inspect.stack()[1]
284
+ try:
285
+ source_file = caller.filename
286
+ source_line = caller.lineno
287
+ finally:
288
+ # Break the frame reference cycle that ``inspect.stack`` creates.
289
+ del caller
290
+ return Constant(
291
+ value,
292
+ description=description,
293
+ source_file=source_file,
294
+ source_line=source_line,
295
+ )