mlschema 0.1.0__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.
mlschema/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """Production-grade orchestration for translating **pandas** DataFrames into
2
+ validated JSON field schemas.
3
+
4
+ The package exports a single façade—:class:`mlschema.MLSchema`
5
+ (alias :pydata:`mlform.MLForm`) which wraps the internal *Service/Registry*
6
+ subsystem. Client code typically:
7
+
8
+ 1. **Registers** concrete field strategies.
9
+ 2. **Builds** a JSON-serialisable schema from a DataFrame.
10
+
11
+ Public surface:
12
+ * `MLSchema` — canonical entry point
13
+ * `mlschema.core.Strategy` — extension contract (advanced)
14
+ * `mlschema.core.BaseField` — Pydantic base model (advanced)
15
+ * All runtime errors derive from `mlschema.core.MLSchemaError`.
16
+
17
+ Example:
18
+ ```python
19
+ from mlschema import MLSchema
20
+ from mlschema.strategies import NumberStrategy
21
+ import pandas as pd
22
+
23
+ ms = MLSchema()
24
+ ms.register(NumberStrategy())
25
+
26
+ df = pd.DataFrame({"age": [22, 37, 29]})
27
+ schema = ms.build(df)
28
+ ```
29
+ """
30
+
31
+ from .mls import MLSchema
32
+
33
+ __all__ = ["MLSchema"]
@@ -0,0 +1,33 @@
1
+ """Core abstractions and error contracts for **MLSchema**.
2
+
3
+ This module defines the *extension surface* on which all custom behaviour
4
+ is built. Integrators subclass the abstractions below to introduce new data
5
+ types or override default processing logic, and they trap the accompanying
6
+ exceptions to maintain deterministic error handling across the pipeline.
7
+ """
8
+
9
+ from mlschema.core.app import Strategy
10
+ from mlschema.core.domain import BaseField
11
+ from mlschema.core.exceptions import (
12
+ EmptyDataFrameError,
13
+ FallbackStrategyMissingError,
14
+ FieldRegistryError,
15
+ FieldServiceError,
16
+ InvalidValueError,
17
+ MLSchemaError,
18
+ StrategyDtypeAlreadyRegisteredError,
19
+ StrategyNameAlreadyRegisteredError,
20
+ )
21
+
22
+ __all__ = [
23
+ "Strategy",
24
+ "BaseField",
25
+ "MLSchemaError",
26
+ "InvalidValueError",
27
+ "FieldRegistryError",
28
+ "FieldServiceError",
29
+ "EmptyDataFrameError",
30
+ "FallbackStrategyMissingError",
31
+ "StrategyDtypeAlreadyRegisteredError",
32
+ "StrategyNameAlreadyRegisteredError",
33
+ ]
@@ -0,0 +1,5 @@
1
+ from .registry import Registry
2
+ from .service import Service
3
+ from .strategy import Strategy
4
+
5
+ __all__ = ["Strategy", "Registry", "Service"]
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from numpy import dtype as np_dtype
4
+
5
+ from mlschema.core.app.strategy import Strategy
6
+ from mlschema.core.exceptions import (
7
+ StrategyDtypeAlreadyRegisteredError,
8
+ StrategyNameAlreadyRegisteredError,
9
+ )
10
+ from mlschema.core.util import normalize_dtype
11
+
12
+
13
+ class Registry:
14
+ """Manages the lifecycle of registered *Strategy* instances."""
15
+
16
+ def __init__(self) -> None:
17
+ """Initialize empty internal containers.
18
+
19
+ The registry maintains two parallel indexes for efficient lookups:
20
+
21
+ * ``_by_name`` - Maps logical type names (e.g., "number") to Strategy instances
22
+ * ``_by_dtype`` - Maps normalized dtype names (e.g., "float64") to Strategy instances
23
+
24
+ Both structures are kept coherent in each registration, update, or
25
+ removal operation. The registry is not thread-safe by design (the
26
+ expected usage is write-once, read-many in inference processes).
27
+ """
28
+ self._by_name: dict[str, Strategy] = {}
29
+ self._by_dtype: dict[str, Strategy] = {}
30
+
31
+ def register(self, strategy: Strategy, *, overwrite: bool = False) -> None:
32
+ """Register a new strategy.
33
+
34
+ Args:
35
+ strategy: Instance of :class:`Strategy` to register.
36
+ overwrite: If True, an existing registration with the same type_name or dtype will be replaced instead of raising an exception.
37
+
38
+ Raises:
39
+ StrategyNameAlreadyRegisteredError: If a strategy already exists for that type_name and overwrite is False.
40
+ StrategyDtypeAlreadyRegisteredError: If a strategy already exists for any of its dtype and overwrite is False.
41
+ """
42
+ if not overwrite and strategy.type_name in self._by_name:
43
+ raise StrategyNameAlreadyRegisteredError(strategy.type_name)
44
+
45
+ normalized_keys = set(map(normalize_dtype, strategy.dtypes))
46
+
47
+ for key in normalized_keys:
48
+ if not overwrite and key in self._by_dtype:
49
+ raise StrategyDtypeAlreadyRegisteredError(key)
50
+
51
+ self._by_name[strategy.type_name] = strategy
52
+
53
+ for key in normalized_keys:
54
+ self._by_dtype[key] = strategy
55
+
56
+ def update(self, strategy: Strategy) -> None:
57
+ """Replace the existing strategy with the same ``type_name``.
58
+
59
+ Equivalent to calling :meth:`register` with ``overwrite=True``.
60
+
61
+ Args:
62
+ strategy: Instance of :class:`Strategy` to update.
63
+ """
64
+ self.register(strategy, overwrite=True)
65
+
66
+ def unregister(self, type_name: str) -> None:
67
+ """Remove a strategy from the registry.
68
+
69
+ Args:
70
+ type_name: The logical identifier of the strategy to remove.
71
+ """
72
+ strat = self._by_name.pop(type_name, None)
73
+ if strat is not None:
74
+ self._by_dtype = {k: v for k, v in self._by_dtype.items() if v is not strat}
75
+
76
+ def strategy_for_name(self, type_name: str) -> Strategy | None:
77
+ """Return the strategy associated with ``type_name`` or ``None``.
78
+
79
+ Args:
80
+ type_name: Logical identifier of the field type to look up.
81
+
82
+ Returns:
83
+ Strategy instance for the given type name, or None if not found.
84
+ """
85
+ return self._by_name.get(type_name)
86
+
87
+ def strategy_for_dtype(self, dtype: str | np_dtype) -> Strategy | None:
88
+ """Return the strategy capable of handling ``dtype`` or ``None``.
89
+
90
+ Args:
91
+ dtype: NumPy dtype string or dtype object to look up.
92
+
93
+ Returns:
94
+ Strategy instance that handles the given dtype, or None if not found.
95
+ """
96
+ return self._by_dtype.get(normalize_dtype(dtype))
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from pandas import DataFrame, Series
4
+
5
+ from mlschema.core.app.registry import Registry
6
+ from mlschema.core.exceptions.service import (
7
+ EmptyDataFrameError,
8
+ FallbackStrategyMissingError,
9
+ )
10
+ from mlschema.core.util import normalize_dtype
11
+
12
+ from .strategy import Strategy
13
+
14
+
15
+ class Service:
16
+ """Translates a :class:`pandas.DataFrame` to the JSON spec consumed by the front-end.
17
+
18
+ The Service class acts as the main orchestrator for converting pandas DataFrame
19
+ structures into JSON schema payloads. It uses a registry-based strategy pattern
20
+ to handle different data types and their corresponding schema representations.
21
+
22
+ The service generates JSON payloads in the format:
23
+ {"inputs": [...], "outputs": [...]}
24
+
25
+ Attributes:
26
+ _registry: Internal registry that manages field strategies for different data types.
27
+ """
28
+
29
+ def __init__(self) -> None:
30
+ """Initialize the Service with an empty Registry."""
31
+ self._registry = Registry()
32
+
33
+ def register(self, strategy: Strategy) -> None:
34
+ """Register a new field strategy.
35
+
36
+ Args:
37
+ strategy: Instance of :class:`Strategy` to register.
38
+
39
+ Raises:
40
+ StrategyNameAlreadyRegisteredError: If a strategy with the same type_name already exists.
41
+ StrategyDtypeAlreadyRegisteredError: If a strategy with the same dtype already exists.
42
+ """
43
+
44
+ self._registry.register(strategy)
45
+
46
+ def unregister(self, strategy: Strategy) -> None:
47
+ """Unregister a previously registered strategy.
48
+
49
+ Args:
50
+ strategy: Instance of :class:`Strategy` to unregister.
51
+ """
52
+ self._registry.unregister(strategy.type_name)
53
+
54
+ def update(self, strategy: Strategy) -> None:
55
+ """Update an already registered strategy.
56
+
57
+ If the strategy doesn't exist, it's registered as new.
58
+
59
+ Args:
60
+ strategy: Instance of :class:`Strategy` to update.
61
+ """
62
+ self._registry.update(strategy)
63
+
64
+ # ------------------------------------------------------------------ #
65
+ # Internal helpers #
66
+ # ------------------------------------------------------------------ #
67
+ def _field_payload(self, series: Series) -> dict:
68
+ """Generate the schema for a specific column.
69
+
70
+ If the series dtype is not associated with any strategy,
71
+ a fallback strategy under the ``type_name`` "text" is attempted.
72
+
73
+ Args:
74
+ series: Pandas series to inspect.
75
+
76
+ Returns:
77
+ Dictionary with the column schema.
78
+
79
+ Raises:
80
+ FallbackStrategyMissingError: If no fallback strategy exists.
81
+ """
82
+ dtype = normalize_dtype(series.dtype)
83
+ strat = self._registry.strategy_for_dtype(
84
+ dtype
85
+ ) or self._registry.strategy_for_name("text")
86
+ if strat is None:
87
+ raise FallbackStrategyMissingError(dtype)
88
+ return strat.build_dict(series)
89
+
90
+ # ------------------------------------------------------------------ #
91
+ # Public API #
92
+ # ------------------------------------------------------------------ #
93
+ def _schema_payload(self, df: DataFrame) -> list[dict]:
94
+ """Build the list of schemas for each column.
95
+
96
+ Args:
97
+ df: Source DataFrame.
98
+
99
+ Returns:
100
+ Ordered list of schemas.
101
+
102
+ Raises:
103
+ EmptyDataFrameError: If the DataFrame has no columns or is empty.
104
+ """
105
+ if df.columns.empty or df.empty:
106
+ raise EmptyDataFrameError(df)
107
+ return [self._field_payload(col) for _, col in df.items()]
108
+
109
+ def build_schema(self, df: DataFrame) -> dict[str, list[dict]]:
110
+ """Return the final payload ready for injection into the front-end.
111
+
112
+ Args:
113
+ df: Source DataFrame.
114
+
115
+ Returns:
116
+ JSON payload with the schema of each column.
117
+ """
118
+ return {"inputs": self._schema_payload(df), "outputs": []}
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ from numpy import dtype as np_dtype
6
+ from pandas import Series
7
+
8
+ from mlschema.core.domain import BaseField
9
+ from mlschema.core.util import normalize_dtype
10
+
11
+
12
+ class Strategy:
13
+ """
14
+ Abstract base class for all MLSchema field strategies.
15
+
16
+ Each concrete strategy maps a *single* pandas dtype (or group of dtypes) to a
17
+ validated JSON field specification. Strategies are **opt-in**: they influence
18
+ schema generation only after being registered via `MLSchema.register()`.
19
+
20
+ Usage contract:
21
+ * Do **not** mutate the incoming `Series`; treat it as read-only.
22
+ * Subclasses should override `attributes_from_series()` to emit extra
23
+ metadata, but must avoid the reserved keys: `"title"`, `"type"`,
24
+ `"required"`, `"description"`.
25
+ * Registration is idempotent—duplicate `type_name`'s must be replaced via
26
+ `MLSchema.update()`.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ type_name: str,
33
+ schema_cls: type[BaseField],
34
+ dtypes: Sequence[str | np_dtype],
35
+ ) -> None:
36
+ """
37
+
38
+ Args:
39
+ type_name: Identifier for the strategy type.
40
+ schema_cls: Pydantic class that models the field.
41
+ dtypes: Sequence of ``dtype`` (instances or names) to which the strategy applies.
42
+ """
43
+ self._type_name: str = type_name
44
+ self._schema_cls: type[BaseField] = schema_cls
45
+ self._dtypes: tuple[str, ...] = tuple(
46
+ normalize_dtype(dtype) for dtype in dtypes
47
+ )
48
+
49
+ @property
50
+ def type_name(self) -> str:
51
+ """Identifier for the strategy type."""
52
+ return self._type_name
53
+
54
+ @property
55
+ def schema_cls(self) -> type[BaseField]:
56
+ """Pydantic class used to serialize the schema."""
57
+ return self._schema_cls
58
+
59
+ @property
60
+ def dtypes(self) -> tuple[str, ...]:
61
+ """Tuple of supported ``dtype`` names."""
62
+ return self._dtypes
63
+
64
+ def attributes_from_series(self, series: Series) -> dict:
65
+ """Calculate field-specific attributes.
66
+
67
+ This method can be overridden by subclasses to add
68
+ implementation-specific metadata to the schema.
69
+
70
+ Args:
71
+ series: Dataframe column to analyze.
72
+
73
+ Returns:
74
+ Dictionary with additional attributes; never includes the standard keys ``title``, ``type``, ``required``, ``description``.
75
+ """
76
+ return {}
77
+
78
+ def build_dict(self, series: Series) -> dict:
79
+ """Create the JSON representation of the schema.
80
+
81
+ Combines the standard attributes with those returned by `attributes_from_series` and serializes the result with the associated Pydantic class.
82
+
83
+ Args:
84
+ series: Dataframe column to analyze.
85
+
86
+ Returns:
87
+ JSON with the field schema.
88
+ """
89
+ base_attrs: dict = {
90
+ "title": series.name,
91
+ "required": not series.isna().any(),
92
+ "description": None,
93
+ "type": self.type_name,
94
+ }
95
+
96
+ # Incorporate implementation-specific attributes
97
+ base_attrs.update(self.attributes_from_series(series))
98
+
99
+ # Instantiate the Pydantic class and dump to JSON
100
+ return self._schema_cls(**base_attrs).model_dump(
101
+ mode="json",
102
+ exclude_unset=False,
103
+ exclude_none=True,
104
+ )
@@ -0,0 +1,3 @@
1
+ from .base_field import BaseField
2
+
3
+ __all__ = ["BaseField"]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class BaseField(BaseModel):
9
+ """Standard metadata present in **all** fields.
10
+
11
+ Extend this class to define custom field types.
12
+
13
+ Attributes:
14
+ title: Human-readable field identifier (1-100 characters).
15
+ description: Optional description (max. 500 characters).
16
+ required: True if the original column contains no null values.
17
+ """
18
+
19
+ model_config = ConfigDict(extra="forbid", frozen=False)
20
+
21
+ title: Annotated[str, Field(min_length=1, max_length=100)]
22
+ description: Annotated[str | None, Field(max_length=500)] = None
23
+ required: bool = True
@@ -0,0 +1,22 @@
1
+ from mlschema.core.exceptions._base import InvalidValueError, MLSchemaError
2
+ from mlschema.core.exceptions.registry import (
3
+ FieldRegistryError,
4
+ StrategyDtypeAlreadyRegisteredError,
5
+ StrategyNameAlreadyRegisteredError,
6
+ )
7
+ from mlschema.core.exceptions.service import (
8
+ EmptyDataFrameError,
9
+ FallbackStrategyMissingError,
10
+ FieldServiceError,
11
+ )
12
+
13
+ __all__ = [
14
+ "MLSchemaError",
15
+ "InvalidValueError",
16
+ "FieldRegistryError",
17
+ "FieldServiceError",
18
+ "StrategyNameAlreadyRegisteredError",
19
+ "StrategyDtypeAlreadyRegisteredError",
20
+ "EmptyDataFrameError",
21
+ "FallbackStrategyMissingError",
22
+ ]
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class MLSchemaError(Exception):
7
+ """Project-root for every **mlschema** runtime failure.
8
+
9
+ All domain-specific exceptions ultimately derive from this class,
10
+ enabling both narrow and broad interception patterns:
11
+
12
+ ```python
13
+ try:
14
+ schema = ms.build(df)
15
+ except MLSchemaError as exc: # catch-all
16
+ logger.error("Schema failure: %s", exc, exc_info=True)
17
+ raise HTTPException(422, detail=str(exc)) from exc
18
+ ```
19
+
20
+ Attributes:
21
+ context: Optional, machine-friendly diagnostics (e.g., offending
22
+ `dtype`, column name, strategy ID). Contents are *stable* only
23
+ for the public leaf exceptions; treat additional keys as
24
+ informational.
25
+ """
26
+
27
+ def __init__(
28
+ self, message: str | None = None, *, context: dict[str, Any] | None = None
29
+ ) -> None:
30
+ super().__init__(message or self.__class__.__name__)
31
+ self.context: dict[str, Any] | None = context
32
+
33
+
34
+ class InvalidValueError(MLSchemaError, ValueError):
35
+ """Standard signal for configuration or user-input violations.
36
+
37
+ Raised when a supplied argument, configuration value, or runtime
38
+ artefact fails validation. Subclasses narrow the scope to
39
+ specific domains (e.g., *registry* vs. *service* faults).
40
+
41
+ Args
42
+ ----
43
+ param: Logical argument name that triggered the failure
44
+ (``"dtype"``, ``"type_name"``, …).
45
+ value: Offending value already *normalised* by the caller.
46
+ message: Human-readable description. If *None*, a neutral default is
47
+ auto-generated.
48
+ context: Arbitrary diagnostics for observability pipelines
49
+ (e.g., ``{"strategy": "NumberStrategy"}``).
50
+
51
+ Attributes:
52
+ param: Same as the *param* constructor argument.
53
+ value: Same as the *value* constructor argument.
54
+ context: Same as the *context* constructor argument.
55
+ Same as the *context* constructor argument.
56
+
57
+ Examples
58
+ --------
59
+ ```python
60
+ if dtype_key in registry:
61
+ raise InvalidValueError(
62
+ param="dtype",
63
+ value=dtype_key,
64
+ message=f"dtype {dtype_key!r} already mapped",
65
+ context={"registered_strategy": registry[dtype_key]},
66
+ )
67
+ ```
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ param: str,
73
+ value: Any,
74
+ message: str | None = None,
75
+ *,
76
+ context: dict[str, Any] | None = None,
77
+ ) -> None:
78
+ self.param: str = param
79
+ self.value: Any = value
80
+ default = f'Invalid value for "{param}": {value!r}.'
81
+ super().__init__(message or default, context=context)
@@ -0,0 +1,73 @@
1
+ # mlschema/core/exceptions/registry.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from mlschema.core.exceptions._base import InvalidValueError
7
+
8
+
9
+ class FieldRegistryError(InvalidValueError):
10
+ """Domain-root for **registry-layer** validation failures.
11
+
12
+ Triggered when an operation on the strategy registry receives an
13
+ invalid, conflicting, or otherwise disallowed value.
14
+
15
+ Args:
16
+ param: Logical argument name that caused the fault
17
+ value: Offending value (already normalised by the caller).
18
+ message: Human-readable description. If *None*, a neutral default is autogenerated.
19
+ context: Arbitrary diagnostics—module, strategy ID, etc.
20
+
21
+ Attributes:
22
+ param: Same as the *param* constructor argument.
23
+ value: Same as the *value* constructor argument.
24
+ context: Same as the *context* constructor argument.
25
+
26
+ Example
27
+ -------
28
+ ```python
29
+ if type_name in registry:
30
+ raise FieldRegistryError(
31
+ param="type_name",
32
+ value=type_name,
33
+ message=f"Strategy {type_name!r} already exists",
34
+ context={"existing_cls": registry[type_name]},
35
+ )
36
+ ```
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ param: str,
42
+ value: Any,
43
+ message: str | None = None,
44
+ *,
45
+ context: dict[str, Any] | None = None,
46
+ ) -> None:
47
+ # build default fallback only if the caller omitted a custom message
48
+ default_msg = f"Registry received an invalid value for {param!r}: {value!r}."
49
+ super().__init__(param, value, message or default_msg, context=context)
50
+
51
+
52
+ class StrategyNameAlreadyRegisteredError(FieldRegistryError):
53
+ """Conflict: two strategies contend for the same **type_name**."""
54
+
55
+ def __init__(self, type_name: str) -> None:
56
+ super().__init__(
57
+ param="type_name",
58
+ value=type_name,
59
+ message=f'Strategy for type_name "{type_name}" is already registered.',
60
+ context={"offender": type_name},
61
+ )
62
+
63
+
64
+ class StrategyDtypeAlreadyRegisteredError(FieldRegistryError):
65
+ """Conflict: two strategies contend for the same **dtype** key."""
66
+
67
+ def __init__(self, dtype_key: str) -> None:
68
+ super().__init__(
69
+ param="dtype",
70
+ value=dtype_key,
71
+ message=f'Strategy for dtype "{dtype_key}" is already registered.',
72
+ context={"offender": dtype_key},
73
+ )
@@ -0,0 +1,81 @@
1
+ # mlschema/core/exceptions/service.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from ._base import InvalidValueError
7
+
8
+
9
+ class FieldServiceError(InvalidValueError):
10
+ """Domain-root for **service-layer** validation failures.
11
+
12
+ Triggered when runtime data or configuration supplied to the *Service*
13
+ component is missing, malformed, or otherwise unusable.
14
+
15
+ Args:
16
+ param: Logical argument name that caused the fault
17
+ (e.g., ``"dataframe"``, ``"dtype"``, ``"fallback_strategy"``).
18
+ value: Offending value—typically the incoming DataFrame, a dtype string,
19
+ or a strategy identifier.
20
+ message: Human-readable description. If *None*, a neutral default is
21
+ autogenerated.
22
+ context: Arbitrary diagnostics for logs or metrics
23
+ (row/column counts, offending dtype, etc.).
24
+
25
+ Attributes:
26
+ param: Mirrors the *param* constructor argument.
27
+ value: Mirrors the *value* constructor argument.
28
+ context: Mirrors the *context* constructor argument.
29
+
30
+ Example
31
+ -------
32
+ ```python
33
+ if df.empty:
34
+ raise FieldServiceError(
35
+ param="dataframe",
36
+ value=df,
37
+ message="Input DataFrame is empty",
38
+ context={"rows": 0, "cols": 0},
39
+ )
40
+ ```
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ param: str,
46
+ value: Any,
47
+ message: str | None = None,
48
+ *,
49
+ context: dict[str, Any] | None = None,
50
+ ) -> None:
51
+ default = f"Service received an invalid value for {param!r}: {value!r}."
52
+ super().__init__(param, value, message or default, context=context)
53
+
54
+
55
+ class EmptyDataFrameError(FieldServiceError):
56
+ """Conflict: Service received a DataFrame with **zero** rows *or* columns."""
57
+
58
+ def __init__(self, df) -> None:
59
+ super().__init__(
60
+ param="dataframe",
61
+ value=df,
62
+ message="DataFrame contains no columns or rows.",
63
+ context={"rows": len(df.index), "cols": len(df.columns)},
64
+ )
65
+
66
+
67
+ class FallbackStrategyMissingError(FieldServiceError):
68
+ """
69
+ Conflict: No registered strategy matches the column dtype, and the fallback ``"text"`` strategy is absent.
70
+ """
71
+
72
+ def __init__(self, dtype_str: str) -> None:
73
+ super().__init__(
74
+ param="dtype",
75
+ value=dtype_str,
76
+ message=(
77
+ f'No strategy registered for dtype "{dtype_str}"; attempted '
78
+ 'fallback "text" but it is not available.'
79
+ ),
80
+ context={"dtype": dtype_str},
81
+ )
@@ -0,0 +1,3 @@
1
+ from mlschema.core.util.dtypes import normalize_dtype
2
+
3
+ __all__ = ["normalize_dtype"]