openms-insight 0.1.2__py3-none-any.whl → 0.1.3__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.
@@ -6,7 +6,7 @@ import polars as pl
6
6
 
7
7
  from ..core.base import BaseComponent
8
8
  from ..core.registry import register_component
9
- from ..preprocessing.filtering import compute_dataframe_hash, filter_and_collect_cached
9
+ from ..preprocessing.filtering import filter_and_collect_cached
10
10
 
11
11
 
12
12
  @register_component("table")
@@ -57,14 +57,14 @@ class Table(BaseComponent):
57
57
  regenerate_cache: bool = False,
58
58
  column_definitions: Optional[List[Dict[str, Any]]] = None,
59
59
  title: Optional[str] = None,
60
- index_field: str = 'id',
60
+ index_field: str = "id",
61
61
  go_to_fields: Optional[List[str]] = None,
62
- layout: str = 'fitDataFill',
62
+ layout: str = "fitDataFill",
63
63
  default_row: int = 0,
64
64
  initial_sort: Optional[List[Dict[str, Any]]] = None,
65
65
  pagination: bool = True,
66
66
  page_size: int = 100,
67
- **kwargs
67
+ **kwargs,
68
68
  ):
69
69
  """
70
70
  Initialize the Table component.
@@ -138,7 +138,7 @@ class Table(BaseComponent):
138
138
  initial_sort=initial_sort,
139
139
  pagination=pagination,
140
140
  page_size=page_size,
141
- **kwargs
141
+ **kwargs,
142
142
  )
143
143
 
144
144
  def _get_cache_config(self) -> Dict[str, Any]:
@@ -149,10 +149,29 @@ class Table(BaseComponent):
149
149
  Dict of config values that affect preprocessing
150
150
  """
151
151
  return {
152
- 'column_definitions': self._column_definitions,
153
- 'index_field': self._index_field,
152
+ "column_definitions": self._column_definitions,
153
+ "index_field": self._index_field,
154
+ "title": self._title,
155
+ "go_to_fields": self._go_to_fields,
156
+ "layout": self._layout,
157
+ "default_row": self._default_row,
158
+ "initial_sort": self._initial_sort,
159
+ "pagination": self._pagination,
160
+ "page_size": self._page_size,
154
161
  }
155
162
 
163
+ def _restore_cache_config(self, config: Dict[str, Any]) -> None:
164
+ """Restore component-specific configuration from cached config."""
165
+ self._column_definitions = config.get("column_definitions")
166
+ self._index_field = config.get("index_field", "id")
167
+ self._title = config.get("title")
168
+ self._go_to_fields = config.get("go_to_fields")
169
+ self._layout = config.get("layout", "fitDataFill")
170
+ self._default_row = config.get("default_row", 0)
171
+ self._initial_sort = config.get("initial_sort")
172
+ self._pagination = config.get("pagination", True)
173
+ self._page_size = config.get("page_size", 100)
174
+
156
175
  def _get_row_group_size(self) -> int:
157
176
  """
158
177
  Get optimal row group size for parquet writing.
@@ -195,31 +214,40 @@ class Table(BaseComponent):
195
214
  self._column_definitions = []
196
215
  for name, dtype in zip(schema.names(), schema.dtypes()):
197
216
  col_def: Dict[str, Any] = {
198
- 'field': name,
199
- 'title': name.replace('_', ' ').title(),
200
- 'headerTooltip': True,
217
+ "field": name,
218
+ "title": name.replace("_", " ").title(),
219
+ "headerTooltip": True,
201
220
  }
202
221
  # Set sorter based on data type
203
- if dtype in (pl.Int8, pl.Int16, pl.Int32, pl.Int64,
204
- pl.UInt8, pl.UInt16, pl.UInt32, pl.UInt64,
205
- pl.Float32, pl.Float64):
206
- col_def['sorter'] = 'number'
207
- col_def['hozAlign'] = 'right'
222
+ if dtype in (
223
+ pl.Int8,
224
+ pl.Int16,
225
+ pl.Int32,
226
+ pl.Int64,
227
+ pl.UInt8,
228
+ pl.UInt16,
229
+ pl.UInt32,
230
+ pl.UInt64,
231
+ pl.Float32,
232
+ pl.Float64,
233
+ ):
234
+ col_def["sorter"] = "number"
235
+ col_def["hozAlign"] = "right"
208
236
  elif dtype == pl.Boolean:
209
- col_def['sorter'] = 'boolean'
237
+ col_def["sorter"] = "boolean"
210
238
  elif dtype in (pl.Date, pl.Datetime, pl.Time):
211
- col_def['sorter'] = 'date'
239
+ col_def["sorter"] = "date"
212
240
  else:
213
- col_def['sorter'] = 'string'
241
+ col_def["sorter"] = "string"
214
242
 
215
243
  self._column_definitions.append(col_def)
216
244
 
217
245
  # Store column definitions in preprocessed data for serialization
218
- self._preprocessed_data['column_definitions'] = self._column_definitions
246
+ self._preprocessed_data["column_definitions"] = self._column_definitions
219
247
 
220
248
  # Store LazyFrame for streaming to disk (filter happens at render time)
221
249
  # Base class will use sink_parquet() to stream without full materialization
222
- self._preprocessed_data['data'] = data # Keep lazy
250
+ self._preprocessed_data["data"] = data # Keep lazy
223
251
 
224
252
  def _get_columns_to_select(self) -> Optional[List[str]]:
225
253
  """Get list of columns needed for this table."""
@@ -227,9 +255,9 @@ class Table(BaseComponent):
227
255
  return None
228
256
 
229
257
  columns_to_select = [
230
- col_def['field']
258
+ col_def["field"]
231
259
  for col_def in self._column_definitions
232
- if 'field' in col_def
260
+ if "field" in col_def
233
261
  ]
234
262
  # Always include index field for row identification
235
263
  if self._index_field and self._index_field not in columns_to_select:
@@ -249,11 +277,11 @@ class Table(BaseComponent):
249
277
 
250
278
  def _get_vue_component_name(self) -> str:
251
279
  """Return the Vue component name."""
252
- return 'TabulatorTable'
280
+ return "TabulatorTable"
253
281
 
254
282
  def _get_data_key(self) -> str:
255
283
  """Return the key used to send primary data to Vue."""
256
- return 'tableData'
284
+ return "tableData"
257
285
 
258
286
  def _prepare_vue_data(self, state: Dict[str, Any]) -> Dict[str, Any]:
259
287
  """
@@ -272,7 +300,7 @@ class Table(BaseComponent):
272
300
  columns = self._get_columns_to_select()
273
301
 
274
302
  # Get cached data (DataFrame or LazyFrame)
275
- data = self._preprocessed_data.get('data')
303
+ data = self._preprocessed_data.get("data")
276
304
  if data is None:
277
305
  # Fallback to raw data if available
278
306
  data = self._raw_data
@@ -290,7 +318,7 @@ class Table(BaseComponent):
290
318
  filter_defaults=self._filter_defaults,
291
319
  )
292
320
 
293
- return {'tableData': df_pandas, '_hash': data_hash}
321
+ return {"tableData": df_pandas, "_hash": data_hash}
294
322
 
295
323
  def _get_component_args(self) -> Dict[str, Any]:
296
324
  """
@@ -302,29 +330,29 @@ class Table(BaseComponent):
302
330
  # Get column definitions (may have been loaded from cache)
303
331
  column_defs = self._column_definitions
304
332
  if column_defs is None:
305
- column_defs = self._preprocessed_data.get('column_definitions', [])
333
+ column_defs = self._preprocessed_data.get("column_definitions", [])
306
334
 
307
335
  args: Dict[str, Any] = {
308
- 'componentType': self._get_vue_component_name(),
309
- 'columnDefinitions': column_defs,
310
- 'tableIndexField': self._index_field,
311
- 'tableLayoutParam': self._layout,
312
- 'defaultRow': self._default_row,
336
+ "componentType": self._get_vue_component_name(),
337
+ "columnDefinitions": column_defs,
338
+ "tableIndexField": self._index_field,
339
+ "tableLayoutParam": self._layout,
340
+ "defaultRow": self._default_row,
313
341
  # Pass interactivity so Vue knows which identifier to update on row click
314
- 'interactivity': self._interactivity,
342
+ "interactivity": self._interactivity,
315
343
  # Pagination settings
316
- 'pagination': self._pagination,
317
- 'pageSize': self._page_size,
344
+ "pagination": self._pagination,
345
+ "pageSize": self._page_size,
318
346
  }
319
347
 
320
348
  if self._title:
321
- args['title'] = self._title
349
+ args["title"] = self._title
322
350
 
323
351
  if self._go_to_fields:
324
- args['goToFields'] = self._go_to_fields
352
+ args["goToFields"] = self._go_to_fields
325
353
 
326
354
  if self._initial_sort:
327
- args['initialSort'] = self._initial_sort
355
+ args["initialSort"] = self._initial_sort
328
356
 
329
357
  # Add any extra config options
330
358
  args.update(self._config)
@@ -335,8 +363,8 @@ class Table(BaseComponent):
335
363
  self,
336
364
  field: str,
337
365
  formatter: str,
338
- formatter_params: Optional[Dict[str, Any]] = None
339
- ) -> 'Table':
366
+ formatter_params: Optional[Dict[str, Any]] = None,
367
+ ) -> "Table":
340
368
  """
341
369
  Add or update a column formatter.
342
370
 
@@ -349,10 +377,10 @@ class Table(BaseComponent):
349
377
  Self for method chaining
350
378
  """
351
379
  for col_def in self._column_definitions or []:
352
- if col_def.get('field') == field:
353
- col_def['formatter'] = formatter
380
+ if col_def.get("field") == field:
381
+ col_def["formatter"] = formatter
354
382
  if formatter_params:
355
- col_def['formatterParams'] = formatter_params
383
+ col_def["formatterParams"] = formatter_params
356
384
  break
357
385
  return self
358
386
 
@@ -360,10 +388,10 @@ class Table(BaseComponent):
360
388
  self,
361
389
  field: str,
362
390
  precision: int = 2,
363
- symbol: str = '',
364
- thousand: str = ',',
365
- decimal: str = '.'
366
- ) -> 'Table':
391
+ symbol: str = "",
392
+ thousand: str = ",",
393
+ decimal: str = ".",
394
+ ) -> "Table":
367
395
  """
368
396
  Format a column as currency/money.
369
397
 
@@ -379,13 +407,13 @@ class Table(BaseComponent):
379
407
  """
380
408
  return self.with_column_formatter(
381
409
  field,
382
- 'money',
410
+ "money",
383
411
  {
384
- 'precision': precision,
385
- 'symbol': symbol,
386
- 'thousand': thousand,
387
- 'decimal': decimal,
388
- }
412
+ "precision": precision,
413
+ "symbol": symbol,
414
+ "thousand": thousand,
415
+ "decimal": decimal,
416
+ },
389
417
  )
390
418
 
391
419
  def with_progress_bar(
@@ -393,8 +421,8 @@ class Table(BaseComponent):
393
421
  field: str,
394
422
  min_val: float = 0,
395
423
  max_val: float = 100,
396
- color: Optional[str] = None
397
- ) -> 'Table':
424
+ color: Optional[str] = None,
425
+ ) -> "Table":
398
426
  """
399
427
  Format a column as a progress bar.
400
428
 
@@ -407,7 +435,7 @@ class Table(BaseComponent):
407
435
  Returns:
408
436
  Self for method chaining
409
437
  """
410
- params: Dict[str, Any] = {'min': min_val, 'max': max_val}
438
+ params: Dict[str, Any] = {"min": min_val, "max": max_val}
411
439
  if color:
412
- params['color'] = color
413
- return self.with_column_formatter(field, 'progress', params)
440
+ params["color"] = color
441
+ return self.with_column_formatter(field, "progress", params)
@@ -1,9 +1,9 @@
1
1
  """Core infrastructure for openms_insight."""
2
2
 
3
3
  from .base import BaseComponent
4
- from .state import StateManager
5
- from .registry import register_component, get_component_class
6
4
  from .cache import CacheMissError
5
+ from .registry import get_component_class, register_component
6
+ from .state import StateManager
7
7
 
8
8
  __all__ = [
9
9
  "BaseComponent",
@@ -1,11 +1,12 @@
1
1
  """Base component class for all visualization components."""
2
2
 
3
- from abc import ABC, abstractmethod
4
- from datetime import datetime
5
3
  import hashlib
6
4
  import json
5
+ from abc import ABC, abstractmethod
6
+ from datetime import datetime
7
7
  from pathlib import Path
8
- from typing import Any, Dict, List, Optional, TYPE_CHECKING
8
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
9
+
9
10
  import polars as pl
10
11
 
11
12
  from .cache import CacheMissError, get_cache_dir
@@ -50,15 +51,24 @@ class BaseComponent(ABC):
50
51
  interactivity: Optional[Dict[str, str]] = None,
51
52
  cache_path: str = ".",
52
53
  regenerate_cache: bool = False,
53
- **kwargs
54
+ **kwargs,
54
55
  ):
55
56
  """
56
57
  Initialize the component.
57
58
 
59
+ Components can be created in two modes:
60
+
61
+ 1. **Creation mode** (data provided): Creates cache with specified config.
62
+ All configuration (filters, interactivity, component-specific) is stored.
63
+
64
+ 2. **Reconstruction mode** (no data): Loads everything from cache.
65
+ Only cache_id and cache_path are needed. All configuration is restored
66
+ from the cached manifest. Any other parameters passed are ignored.
67
+
58
68
  Args:
59
69
  cache_id: Unique identifier for this component's cache (MANDATORY).
60
70
  Creates a folder {cache_path}/{cache_id}/ for cached data.
61
- data: Polars LazyFrame with source data. Optional if cache exists.
71
+ data: Polars LazyFrame with source data. Required for creation mode.
62
72
  data_path: Path to parquet file with source data. Preferred over
63
73
  data= for large datasets as preprocessing runs in a subprocess
64
74
  to ensure memory is released after cache creation.
@@ -84,23 +94,51 @@ class BaseComponent(ABC):
84
94
 
85
95
  self._cache_id = cache_id
86
96
  self._cache_dir = get_cache_dir(cache_path, cache_id)
87
- self._filters = filters or {}
88
- self._filter_defaults = filter_defaults or {}
89
- self._interactivity = interactivity or {}
90
97
  self._preprocessed_data: Dict[str, Any] = {}
91
- self._config = kwargs
92
98
 
93
- # Check if we should load from cache or preprocess
94
- if regenerate_cache or not self._is_cache_valid():
95
- if data is None and data_path is None:
99
+ # Determine mode: reconstruction (no data) or creation (data provided)
100
+ has_data = data is not None or data_path is not None
101
+
102
+ # Check if any configuration arguments were explicitly provided
103
+ # Note: We only check filters/interactivity/filter_defaults because component-
104
+ # specific kwargs always have default values passed by subclasses
105
+ has_config = (
106
+ filters is not None
107
+ or filter_defaults is not None
108
+ or interactivity is not None
109
+ )
110
+
111
+ if not has_data and not regenerate_cache:
112
+ # Reconstruction mode - only cache_id and cache_path allowed
113
+ if has_config:
114
+ raise CacheMissError(
115
+ "Configuration arguments (filters, interactivity, filter_defaults) "
116
+ "require data= or data_path= to be provided. "
117
+ "For reconstruction from cache, use only cache_id and cache_path."
118
+ )
119
+ if not self._cache_exists():
120
+ raise CacheMissError(
121
+ f"Cache not found at '{self._cache_dir}'. "
122
+ f"Provide data= or data_path= to create the cache."
123
+ )
124
+ self._raw_data = None
125
+ self._load_from_cache()
126
+ else:
127
+ # Creation mode - use provided config
128
+ if not has_data:
96
129
  raise CacheMissError(
97
- f"Cache not found at '{self._cache_dir}' and no data provided. "
98
- f"Either provide data=, data_path=, or ensure cache exists."
130
+ "regenerate_cache=True requires data= or data_path= to be provided."
99
131
  )
100
132
 
133
+ self._filters = filters or {}
134
+ self._filter_defaults = filter_defaults or {}
135
+ self._interactivity = interactivity or {}
136
+ self._config = kwargs
137
+
101
138
  if data_path is not None:
102
139
  # Subprocess preprocessing - memory released after cache creation
103
140
  from .subprocess_preprocess import preprocess_component
141
+
104
142
  preprocess_component(
105
143
  type(self),
106
144
  data_path=data_path,
@@ -109,20 +147,16 @@ class BaseComponent(ABC):
109
147
  filters=filters,
110
148
  filter_defaults=filter_defaults,
111
149
  interactivity=interactivity,
112
- **kwargs
150
+ **kwargs,
113
151
  )
114
152
  self._raw_data = None
115
153
  self._load_from_cache()
116
154
  else:
117
- # In-process preprocessing (backward compatible)
155
+ # In-process preprocessing
118
156
  self._raw_data = data
119
157
  self._validate_mappings()
120
158
  self._preprocess()
121
159
  self._save_to_cache()
122
- else:
123
- # Load from valid cache
124
- self._raw_data = None
125
- self._load_from_cache()
126
160
 
127
161
  def _validate_mappings(self) -> None:
128
162
  """Validate that filter and interactivity columns exist in the data schema."""
@@ -163,7 +197,7 @@ class BaseComponent(ABC):
163
197
  config_dict = {
164
198
  "filters": self._filters,
165
199
  "interactivity": self._interactivity,
166
- **self._get_cache_config()
200
+ **self._get_cache_config(),
167
201
  }
168
202
  config_str = json.dumps(config_dict, sort_keys=True, default=str)
169
203
  return hashlib.sha256(config_str.encode()).hexdigest()
@@ -176,15 +210,17 @@ class BaseComponent(ABC):
176
210
  """Get path to preprocessed data directory."""
177
211
  return self._cache_dir / "preprocessed"
178
212
 
179
- def _is_cache_valid(self) -> bool:
213
+ def _cache_exists(self) -> bool:
180
214
  """
181
- Check if cache is valid and can be loaded.
215
+ Check if a valid cache exists that can be loaded.
182
216
 
183
- Cache is valid when:
184
- 1. manifest.json exists
217
+ Cache exists when:
218
+ 1. manifest.json exists and is readable
185
219
  2. version matches current CACHE_VERSION
186
220
  3. component_type matches
187
- 4. config_hash matches current config
221
+
222
+ Note: This does NOT check config hash. In reconstruction mode,
223
+ all configuration is restored from the cache manifest.
188
224
  """
189
225
  manifest_path = self._get_manifest_path()
190
226
  if not manifest_path.exists():
@@ -204,24 +240,32 @@ class BaseComponent(ABC):
204
240
  if manifest.get("component_type") != self._component_type:
205
241
  return False
206
242
 
207
- # Check config hash
208
- current_hash = self._compute_config_hash()
209
- if manifest.get("config_hash") != current_hash:
210
- return False
211
-
212
243
  return True
213
244
 
214
245
  def _load_from_cache(self) -> None:
215
- """Load preprocessed data from cache."""
246
+ """Load all configuration and preprocessed data from cache.
247
+
248
+ Restores:
249
+ - filters mapping
250
+ - filter_defaults mapping
251
+ - interactivity mapping
252
+ - Component-specific configuration via _restore_cache_config()
253
+ - All preprocessed data files
254
+ """
216
255
  manifest_path = self._get_manifest_path()
217
256
  preprocessed_dir = self._get_preprocessed_dir()
218
257
 
219
258
  with open(manifest_path) as f:
220
259
  manifest = json.load(f)
221
260
 
222
- # Load filters and interactivity from manifest
261
+ # Restore filters, filter_defaults, and interactivity from manifest
223
262
  self._filters = manifest.get("filters", {})
263
+ self._filter_defaults = manifest.get("filter_defaults", {})
224
264
  self._interactivity = manifest.get("interactivity", {})
265
+ self._config = manifest.get("config", {})
266
+
267
+ # Restore component-specific configuration
268
+ self._restore_cache_config(manifest.get("config", {}))
225
269
 
226
270
  # Load preprocessed data files
227
271
  data_files = manifest.get("data_files", {})
@@ -235,9 +279,25 @@ class BaseComponent(ABC):
235
279
  for key, value in data_values.items():
236
280
  self._preprocessed_data[key] = value
237
281
 
282
+ @abstractmethod
283
+ def _restore_cache_config(self, config: Dict[str, Any]) -> None:
284
+ """
285
+ Restore component-specific configuration from cached config dict.
286
+
287
+ Called during reconstruction mode to restore all component attributes
288
+ that were stored in the manifest's config section.
289
+
290
+ Args:
291
+ config: The config dict from manifest (result of _get_cache_config())
292
+ """
293
+ pass
294
+
238
295
  def _save_to_cache(self) -> None:
239
296
  """Save preprocessed data to cache."""
240
- from ..preprocessing.filtering import optimize_for_transfer, optimize_for_transfer_lazy
297
+ from ..preprocessing.filtering import (
298
+ optimize_for_transfer,
299
+ optimize_for_transfer_lazy,
300
+ )
241
301
 
242
302
  # Create directories
243
303
  self._cache_dir.mkdir(parents=True, exist_ok=True)
@@ -252,6 +312,7 @@ class BaseComponent(ABC):
252
312
  "config_hash": self._compute_config_hash(),
253
313
  "config": self._get_cache_config(),
254
314
  "filters": self._filters,
315
+ "filter_defaults": self._filter_defaults,
255
316
  "interactivity": self._interactivity,
256
317
  "data_files": {},
257
318
  "data_values": {},
@@ -267,14 +328,14 @@ class BaseComponent(ABC):
267
328
  # Apply streaming-safe optimization (Float64→Float32 only)
268
329
  # Int64 bounds checking would require collect(), breaking streaming
269
330
  value = optimize_for_transfer_lazy(value)
270
- value.sink_parquet(filepath, compression='zstd')
331
+ value.sink_parquet(filepath, compression="zstd")
271
332
  manifest["data_files"][key] = filename
272
333
  elif isinstance(value, pl.DataFrame):
273
334
  filename = f"{key}.parquet"
274
335
  filepath = preprocessed_dir / filename
275
336
  # Full optimization including Int64→Int32 with bounds checking
276
337
  value = optimize_for_transfer(value)
277
- value.write_parquet(filepath, compression='zstd')
338
+ value.write_parquet(filepath, compression="zstd")
278
339
  manifest["data_files"][key] = filename
279
340
  elif self._is_json_serializable(value):
280
341
  manifest["data_values"][key] = value
@@ -332,10 +393,7 @@ class BaseComponent(ABC):
332
393
  pass
333
394
 
334
395
  @abstractmethod
335
- def _prepare_vue_data(
336
- self,
337
- state: Dict[str, Any]
338
- ) -> Dict[str, Any]:
396
+ def _prepare_vue_data(self, state: Dict[str, Any]) -> Dict[str, Any]:
339
397
  """
340
398
  Prepare data payload for Vue component.
341
399
 
@@ -404,8 +462,8 @@ class BaseComponent(ABC):
404
462
  def __call__(
405
463
  self,
406
464
  key: Optional[str] = None,
407
- state_manager: Optional['StateManager'] = None,
408
- height: Optional[int] = None
465
+ state_manager: Optional["StateManager"] = None,
466
+ height: Optional[int] = None,
409
467
  ) -> Any:
410
468
  """
411
469
  Render the component in Streamlit.
@@ -419,17 +477,14 @@ class BaseComponent(ABC):
419
477
  Returns:
420
478
  The value returned by the Vue component (usually selection state)
421
479
  """
422
- from .state import get_default_state_manager
423
480
  from ..rendering.bridge import render_component
481
+ from .state import get_default_state_manager
424
482
 
425
483
  if state_manager is None:
426
484
  state_manager = get_default_state_manager()
427
485
 
428
486
  return render_component(
429
- component=self,
430
- state_manager=state_manager,
431
- key=key,
432
- height=height
487
+ component=self, state_manager=state_manager, key=key, height=height
433
488
  )
434
489
 
435
490
  def __repr__(self) -> str:
@@ -1,12 +1,12 @@
1
1
  """Component type registry for serialization and deserialization."""
2
2
 
3
- from typing import Dict, Type, TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Dict, Type
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from .base import BaseComponent
7
7
 
8
8
  # Global registry mapping component type names to their classes
9
- _COMPONENT_REGISTRY: Dict[str, Type['BaseComponent']] = {}
9
+ _COMPONENT_REGISTRY: Dict[str, Type["BaseComponent"]] = {}
10
10
 
11
11
 
12
12
  def register_component(name: str):
@@ -24,7 +24,8 @@ def register_component(name: str):
24
24
  class Table(BaseComponent):
25
25
  ...
26
26
  """
27
- def decorator(cls: Type['BaseComponent']) -> Type['BaseComponent']:
27
+
28
+ def decorator(cls: Type["BaseComponent"]) -> Type["BaseComponent"]:
28
29
  if name in _COMPONENT_REGISTRY:
29
30
  raise ValueError(
30
31
  f"Component type '{name}' is already registered to "
@@ -37,7 +38,7 @@ def register_component(name: str):
37
38
  return decorator
38
39
 
39
40
 
40
- def get_component_class(name: str) -> Type['BaseComponent']:
41
+ def get_component_class(name: str) -> Type["BaseComponent"]:
41
42
  """
42
43
  Get a component class by its registered name.
43
44
 
@@ -59,7 +60,7 @@ def get_component_class(name: str) -> Type['BaseComponent']:
59
60
  return _COMPONENT_REGISTRY[name]
60
61
 
61
62
 
62
- def list_registered_components() -> Dict[str, Type['BaseComponent']]:
63
+ def list_registered_components() -> Dict[str, Type["BaseComponent"]]:
63
64
  """
64
65
  Get all registered component types.
65
66