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.
@@ -0,0 +1,8 @@
1
+ @staticmethod
2
+ def normalize_dtype(dtype) -> str:
3
+ """Normalize dtype to string representation."""
4
+ return (
5
+ str(dtype)
6
+ if not hasattr(dtype, "name") or getattr(dtype, "names", None) is not None
7
+ else dtype.name
8
+ )
mlschema/mls.py ADDED
@@ -0,0 +1,68 @@
1
+ from typing import Any
2
+
3
+ from pandas import DataFrame
4
+
5
+ from mlschema.core.app import Service, Strategy
6
+
7
+
8
+ class MLSchema:
9
+ """Facade that orchestrates strategy registration and schema generation.
10
+
11
+ The class wraps an internal :class:`mlschema.core.app.Service` instance and
12
+ surfaces a minimal, stable API for client code. It is therefore the
13
+ canonical entry point when integrating **mlschema** into an application or
14
+ pipeline.
15
+
16
+ Attributes:
17
+ field_service: Internal service component that performs the heavy lifting (registry management and JSON payload generation).
18
+ """
19
+
20
+ def __init__(self) -> None:
21
+ self.field_service = Service()
22
+
23
+ def register(self, strategy: Strategy) -> None:
24
+ """Register a **new** strategy.
25
+
26
+ Args:
27
+ strategy: Instance of a concrete :class:`mlschema.core.app.Strategy`.
28
+
29
+ Raises:
30
+ StrategyNameAlreadyRegisteredError: If a strategy with the same name is already registered.
31
+ StrategyDtypeAlreadyRegisteredError: If a strategy with the same dtype is already registered.
32
+ """
33
+ self.field_service.register(strategy)
34
+
35
+ def unregister(self, strategy: Strategy) -> None:
36
+ """Remove a **previously registered** strategy.
37
+
38
+ Args:
39
+ strategy: Strategy instance to be removed from the registry.
40
+ """
41
+ self.field_service.unregister(strategy)
42
+
43
+ def update(self, strategy: Strategy) -> None:
44
+ """Replace an existing strategy **in-place**.
45
+
46
+ If either the ``type_name`` or any of the advertised ``dtypes``
47
+ already exist, they are overwritten with the supplied strategy.
48
+
49
+ Args:
50
+ strategy: Instance of `Strategy` to update.
51
+ """
52
+ self.field_service.update(strategy)
53
+
54
+ def build(self, df: DataFrame) -> dict[str, list[dict[str, Any]]]:
55
+ """Translate a DataFrame into a JSON-serialisable form schema
56
+
57
+ Args:
58
+ df: Source data whose columns will be analysed and mapped to field definitions.
59
+
60
+ Returns:
61
+ Dictionary with the schema information, where keys are field names
62
+
63
+ Raises:
64
+ EmptyDataFrameError: If the DataFrame is empty.
65
+ FallbackStrategyMissingError: If no fallback strategy is available for the DataFrame.
66
+ PydanticCustomError: If there are validation errors in the schema.
67
+ """
68
+ return self.field_service.build_schema(df)
mlschema/py.typed ADDED
File without changes
@@ -0,0 +1,41 @@
1
+ """Strategies sub-package for **MLSchema**
2
+
3
+ This namespace aggregates the concrete `Strategy` implementations that map
4
+ pandas dtypes to **validated JSON field definitions**. All classes inherit from
5
+ `mlschema.core.Strategy` and are **opt-in**, they become active only after an
6
+ explicit `MLSchema.register()` call.
7
+
8
+ Strategies Available:
9
+ | Class | Description |
10
+ |---------------------|----------------------------------------------------|
11
+ | BooleanStrategy | Strategy for handling boolean data types. |
12
+ | CategoryStrategy | Strategy for handling categorical data types. |
13
+ | DateStrategy | Strategy for handling date and datetime data types.|
14
+ | NumberStrategy | Strategy for handling numeric data types. |
15
+ | TextStrategy | Strategy for handling text and string data types. |
16
+
17
+ Design notes
18
+ ------------
19
+
20
+ | Principle | Description |
21
+ |---------------------------|-------------------------------------------------------------------------------------------------------------------------------- |
22
+ | **Single-responsibility** | Each strategy handles *one* logical field type. |
23
+ | **Pluggable** | New strategies register via `MLSchema.register()`, replace via `MLSchema.update()`, and deregister via `MLSchema.unregister()`. |
24
+ | **Declarative output** | Strategies emit validated `BaseField` subclasses, ensuring schema integrity from ingestion to UI rendering. |
25
+ """
26
+
27
+ from .app import (
28
+ BooleanStrategy,
29
+ CategoryStrategy,
30
+ DateStrategy,
31
+ NumberStrategy,
32
+ TextStrategy,
33
+ )
34
+
35
+ __all__ = [
36
+ "BooleanStrategy",
37
+ "DateStrategy",
38
+ "TextStrategy",
39
+ "NumberStrategy",
40
+ "CategoryStrategy",
41
+ ]
@@ -0,0 +1,13 @@
1
+ from .boolean_strategy import BooleanStrategy
2
+ from .category_strategy import CategoryStrategy
3
+ from .date_strategy import DateStrategy
4
+ from .number_strategy import NumberStrategy
5
+ from .text_strategy import TextStrategy
6
+
7
+ __all__ = [
8
+ "BooleanStrategy",
9
+ "DateStrategy",
10
+ "TextStrategy",
11
+ "NumberStrategy",
12
+ "CategoryStrategy",
13
+ ]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from mlschema.core import Strategy
4
+ from mlschema.strategies.domain import BooleanField, FieldTypes
5
+
6
+
7
+ class BooleanStrategy(Strategy):
8
+ """Instance of Strategy for boolean fields.
9
+
10
+ Name:
11
+ `boolean`
12
+
13
+ Dtypes:
14
+ | Name | Type |
15
+ | -------- | ----------------- |
16
+ | bool | `BooleanDtype` |
17
+ | boolean | `BooleanDtype` |
18
+
19
+ Model Attributes:
20
+ | Name | Type | Description |
21
+ | ----------- | -------------------- | ------------------------------------------ |
22
+ | type | `Literal["boolean"]` | Fixed type for the strategy. |
23
+ | value | `bool | None` | The current value of the field. |
24
+
25
+ """
26
+
27
+ def __init__(self) -> None:
28
+ super().__init__(
29
+ type_name=FieldTypes.BOOLEAN,
30
+ schema_cls=BooleanField,
31
+ dtypes=("bool", "boolean"),
32
+ )
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from pandas import CategoricalDtype, Series
4
+
5
+ from mlschema.core import Strategy
6
+ from mlschema.strategies.domain import CategoryField, FieldTypes
7
+
8
+
9
+ class CategoryStrategy(Strategy):
10
+ """Instance of Strategy for category fields.
11
+
12
+ Name:
13
+ `category`
14
+
15
+ Dtypes:
16
+ | Name | Type |
17
+ | -------- | ----------------- |
18
+ | category | `CategoricalDtype`|
19
+
20
+ Model Attributes:
21
+ | Name | Type | Description |
22
+ | ------- | --------------------- | -------------------------------- |
23
+ | type | `Literal["category"]` | Fixed type for the strategy. |
24
+ | options | `list[str]` | List of allowed categories. |
25
+ | value | `str | None` | Current value of the field. |
26
+
27
+ Model Restrictions:
28
+ | Description | Error Type | Error Message |
29
+ | --------------------- | --------------------- | ------------------------------------------------- |
30
+ | `value` in `options` | `PydanticCustomError` | `value {value} must be in options {options}` |
31
+
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ super().__init__(
36
+ type_name=FieldTypes.CATEGORY,
37
+ schema_cls=CategoryField,
38
+ dtypes=("category",),
39
+ )
40
+
41
+ def attributes_from_series(self, series: Series) -> dict:
42
+ """Derives the list of *options* from the series.
43
+
44
+ Args:
45
+ series: Pandas series with categorical values.
46
+
47
+ Returns:
48
+ Dictionary with the ``options`` key and the list of unique values.
49
+ """
50
+ if isinstance(series.dtype, CategoricalDtype):
51
+ options = list(series.cat.categories)
52
+ else:
53
+ options = list(series.dropna().unique())
54
+ return {"options": options}
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from mlschema.core import Strategy
4
+ from mlschema.strategies.domain import DateField, FieldTypes
5
+
6
+
7
+ class DateStrategy(Strategy):
8
+ """Instance of Strategy for date fields.
9
+
10
+ Name:
11
+ `date`
12
+
13
+ Dtypes:
14
+ | Name | Type |
15
+ | -------------- | ------------------- |
16
+ | datetime64[ns] | `DatetimeTZDtype` |
17
+ | datetime64 | `DatetimeDtype` |
18
+
19
+ Model Attributes:
20
+ | Name | Type | Description |
21
+ | ----------- | ------------------- | ------------------------------------------ |
22
+ | type | `Literal["date"]` | Fixed type for the strategy. |
23
+ | value | `date | None` | The current value of the field. |
24
+ | min | `date | None` | Minimum allowed date. |
25
+ | max | `date | None` | Maximum allowed date. |
26
+ | step | `PositiveInt` | Increment in days. |
27
+
28
+ Model Restrictions:
29
+ | Description | Error Type | Error Message |
30
+ | --------------------- | --------------------- | ------------------------------------------------- |
31
+ | `min` ≤ `max` | `PydanticCustomError` | `min {min} must be ≤ max {max}` |
32
+ | `value` ≥ `min` | `PydanticCustomError` | `value {value} must be ≥ min {min}` |
33
+ | `value` ≤ `max` | `PydanticCustomError` | `value {value} must be ≤ max {max}` |
34
+
35
+ """
36
+
37
+ def __init__(self) -> None:
38
+ super().__init__(
39
+ type_name=FieldTypes.DATE,
40
+ schema_cls=DateField,
41
+ dtypes=("datetime64[ns]", "datetime64"),
42
+ )
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from pandas import Series, api
4
+
5
+ from mlschema.core import Strategy
6
+ from mlschema.strategies.domain import FieldTypes, NumberField
7
+
8
+
9
+ class NumberStrategy(Strategy):
10
+ """Instance of Strategy for number fields.
11
+
12
+ Name:
13
+ `number`
14
+
15
+ Dtypes:
16
+ | Name | Type |
17
+ | -------- | ----------------- |
18
+ | int64 | `Int64Dtype` |
19
+ | float64 | `Float64Dtype` |
20
+ | int32 | `Int32Dtype` |
21
+ | float32 | `Float32Dtype` |
22
+
23
+ Model Attributes:
24
+ | Name | Type | Description |
25
+ | ----------- | ------------------- | ------------------------------------------ |
26
+ | type | `Literal["number"]` | Fixed type for the strategy. |
27
+ | value | `int | float | None`| The current value of the field. |
28
+ | step | `float | int` | Increment for numeric values. |
29
+ | min | `int | float | None`| Minimum allowed value. |
30
+ | max | `int | float | None`| Maximum allowed value. |
31
+ | unit | `str | None` | Unit of measurement for the numeric value. |
32
+ | placeholder | `str | None` | Placeholder text for the field. |
33
+
34
+ Model Restrictions:
35
+ | Description | Error Type | Error Message |
36
+ | --------------------- | --------------------- | ------------------------------------------------- |
37
+ | `min` ≤ `max` | `PydanticCustomError` | `min {min} must be ≤ max {max}` |
38
+ | `value` ≥ `min` | `PydanticCustomError` | `value {value} must be ≥ min {min}` |
39
+ | `value` ≤ `max` | `PydanticCustomError` | `value {value} must be ≤ max {max}` |
40
+
41
+ """
42
+
43
+ def __init__(self) -> None:
44
+ super().__init__(
45
+ type_name=FieldTypes.NUMBER,
46
+ schema_cls=NumberField,
47
+ dtypes=("int64", "float64", "int32", "float32"),
48
+ )
49
+
50
+ def attributes_from_series(self, series: Series) -> dict:
51
+ """Derives the ``step`` attribute from the ``dtype``.
52
+
53
+ Args:
54
+ series: Pandas series with numeric values.
55
+
56
+ Returns:
57
+ Dictionary with the ``step`` key.
58
+ """
59
+ # Default step: 0.1 for floats, 1 for integers
60
+ step = 0.1 if api.types.is_float_dtype(series.dtype) else 1
61
+ return {"step": step}
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from mlschema.core import Strategy
4
+ from mlschema.strategies.domain import FieldTypes, TextField
5
+
6
+
7
+ class TextStrategy(Strategy):
8
+ """Instance of Strategy for text fields.
9
+
10
+ Name:
11
+ `text`
12
+
13
+ Dtypes:
14
+ | Name | Type |
15
+ | -------- | ----------------- |
16
+ | object | `object` |
17
+ | string | `StringDtype` |
18
+
19
+ Model Attributes:
20
+ | Name | Type | Description |
21
+ | ----------- | ----------------- | ------------------------------------------ |
22
+ | type | `Literal["text"]` | Fixed type for the strategy. |
23
+ | value | `str | None` | The current value of the field. |
24
+ | placeholder | `str | None` | Placeholder text for the field. |
25
+ | min_length | `int | None` | Minimum length of the text. |
26
+ | max_length | `int | None` | Maximum length of the text. |
27
+ | pattern | `str | None` | Regular expression pattern for validation. |
28
+
29
+ Model Restrictions:
30
+ | Description | Error Type | Error Message |
31
+ | ----------------------------- | --------------------- | ------------------------------------------------------------- |
32
+ | `min_length` ≤ `max_length` | `PydanticCustomError` | `minLength {minLength} must be ≤ maxLength {maxLength}` |
33
+ | `value` length ≥ `min_length` | `PydanticCustomError` | `value length {value_length} must be ≥ minLength {minLength}` |
34
+ | `value` length ≤ `max_length` | `PydanticCustomError` | `value length {value_length} must be ≤ maxLength {maxLength}` |
35
+
36
+ """
37
+
38
+ def __init__(self) -> None:
39
+ super().__init__(
40
+ type_name=FieldTypes.TEXT,
41
+ schema_cls=TextField,
42
+ dtypes=("object", "string"),
43
+ )
@@ -0,0 +1,15 @@
1
+ from .boolean_field import BooleanField
2
+ from .category_field import CategoryField
3
+ from .date_field import DateField
4
+ from .field_types import FieldTypes
5
+ from .number_field import NumberField
6
+ from .text_field import TextField
7
+
8
+ __all__ = [
9
+ "BooleanField",
10
+ "DateField",
11
+ "NumberField",
12
+ "CategoryField",
13
+ "TextField",
14
+ "FieldTypes",
15
+ ]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from mlschema.core.domain import BaseField
6
+ from mlschema.strategies.domain.field_types import FieldTypes
7
+
8
+
9
+ class BooleanField(BaseField):
10
+ type: Literal[FieldTypes.BOOLEAN] = FieldTypes.BOOLEAN
11
+ value: bool | None = None
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, Literal
4
+
5
+ from pydantic import Field, model_validator
6
+ from pydantic_core import PydanticCustomError
7
+
8
+ from mlschema.core.domain import BaseField
9
+ from mlschema.strategies.domain.field_types import FieldTypes
10
+
11
+
12
+ class CategoryField(BaseField):
13
+ type: Literal[FieldTypes.CATEGORY] = FieldTypes.CATEGORY
14
+ value: str | None = None
15
+ options: Annotated[list[str], Field(min_length=1)]
16
+
17
+ @model_validator(mode="after")
18
+ def _check_value_in_options(self) -> CategoryField:
19
+ if self.value is not None and self.value not in self.options:
20
+ raise PydanticCustomError(
21
+ "value_error", "Value must match one of the allowed options"
22
+ )
23
+ return self
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+ from typing import Literal
5
+
6
+ from pydantic import PositiveInt, model_validator
7
+ from pydantic_core import PydanticCustomError
8
+
9
+ from mlschema.core.domain import BaseField
10
+ from mlschema.strategies.domain.field_types import FieldTypes
11
+
12
+
13
+ class DateField(BaseField):
14
+ type: Literal[FieldTypes.DATE] = FieldTypes.DATE
15
+ value: date | None = None
16
+ min: date | None = None
17
+ max: date | None = None
18
+ step: PositiveInt = 1
19
+
20
+ @model_validator(mode="after")
21
+ def _check_dates(self) -> DateField:
22
+ if self.min and self.max and self.min > self.max:
23
+ raise PydanticCustomError(
24
+ "date_range_error",
25
+ "Minimum date must be earlier than or equal to maximum date",
26
+ )
27
+
28
+ if self.value:
29
+ if self.min and self.value < self.min:
30
+ raise PydanticCustomError(
31
+ "date_min_error", "Date must be later than or equal to minimum date"
32
+ )
33
+ if self.max and self.value > self.max:
34
+ raise PydanticCustomError(
35
+ "date_max_error",
36
+ "Date must be earlier than or equal to maximum date",
37
+ )
38
+ return self
@@ -0,0 +1,9 @@
1
+ from enum import Enum
2
+
3
+
4
+ class FieldTypes(str, Enum):
5
+ TEXT = "text"
6
+ NUMBER = "number"
7
+ BOOLEAN = "boolean"
8
+ CATEGORY = "category"
9
+ DATE = "date"
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import model_validator
6
+ from pydantic_core import PydanticCustomError
7
+
8
+ from mlschema.core.domain import BaseField
9
+ from mlschema.strategies.domain.field_types import FieldTypes
10
+
11
+
12
+ class NumberField(BaseField):
13
+ type: Literal[FieldTypes.NUMBER] = FieldTypes.NUMBER
14
+ min: float | None = None
15
+ max: float | None = None
16
+ step: float | None = 1
17
+ placeholder: str | None = None
18
+ value: float | None = None
19
+ unit: str | None = None
20
+
21
+ @model_validator(mode="after")
22
+ def _check_numeric_constraints(self) -> NumberField:
23
+ if self.min is not None and self.max is not None and self.min > self.max:
24
+ raise PydanticCustomError(
25
+ "min_max_constraint",
26
+ "min ({min}) must be ≤ max ({max})",
27
+ {"min": self.min, "max": self.max},
28
+ )
29
+
30
+ if self.value is not None:
31
+ if self.min is not None and self.value < self.min:
32
+ raise PydanticCustomError(
33
+ "value_min_constraint",
34
+ "value ({value}) must be ≥ min ({min})",
35
+ {"value": self.value, "min": self.min},
36
+ )
37
+ if self.max is not None and self.value > self.max:
38
+ raise PydanticCustomError(
39
+ "value_max_constraint",
40
+ "value ({value}) must be ≤ max ({max})",
41
+ {"value": self.value, "max": self.max},
42
+ )
43
+ return self
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import Field, model_validator
6
+ from pydantic_core import PydanticCustomError
7
+
8
+ from mlschema.core.domain import BaseField
9
+ from mlschema.strategies.domain.field_types import FieldTypes
10
+
11
+
12
+ class TextField(BaseField):
13
+ type: Literal[FieldTypes.TEXT] = FieldTypes.TEXT
14
+ value: str | None = None
15
+ placeholder: str | None = None
16
+ minLength: int | None = None
17
+ maxLength: int | None = None
18
+ pattern: str | None = Field(
19
+ default=None,
20
+ description=(
21
+ "Regular expression in Python notation; Pydantic does not preserve "
22
+ "re.Pattern instances in JSON serialization."
23
+ ),
24
+ )
25
+
26
+ @model_validator(mode="after")
27
+ def _check_lengths(self) -> TextField:
28
+ if (
29
+ self.minLength is not None
30
+ and self.maxLength is not None
31
+ and self.minLength > self.maxLength
32
+ ):
33
+ raise PydanticCustomError(
34
+ "length_validation",
35
+ "minLength ({min_length}) must be ≤ maxLength ({max_length})",
36
+ {"min_length": self.minLength, "max_length": self.maxLength},
37
+ )
38
+
39
+ if (
40
+ self.value is not None
41
+ and self.minLength is not None
42
+ and len(self.value) < self.minLength
43
+ ):
44
+ raise PydanticCustomError(
45
+ "min_length_validation",
46
+ "value length ({value_length}) must be ≥ minLength ({min_length})",
47
+ {"value_length": len(self.value), "min_length": self.minLength},
48
+ )
49
+
50
+ if (
51
+ self.value is not None
52
+ and self.maxLength is not None
53
+ and len(self.value) > self.maxLength
54
+ ):
55
+ raise PydanticCustomError(
56
+ "max_length_validation",
57
+ "value length ({value_length}) must be ≤ maxLength ({max_length})",
58
+ {"value_length": len(self.value), "max_length": self.maxLength},
59
+ )
60
+
61
+ return self