Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebase on stricter types for fields #14

Merged
merged 10 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions docs/models/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ The Table Schema model allows to manipulate a Pydantic model in Python according
## Usage

```python
from dplib.models import Schema, Field
from dplib.models import Schema, IntegerField

schema = Schema()
schema.add_field(Field(name='id', type='integer'))
schema.add_field(IntegerField(name='id'))
schema.missingValues = ['-']
print(schema.to_text(format="json"))
```
Expand All @@ -28,7 +28,29 @@ print(schema.to_text(format="json"))
## Reference

::: dplib.models.Schema
::: dplib.models.Field
::: dplib.models.Constraints
::: dplib.models.IFieldsMatch
::: dplib.models.ForeignKey
::: dplib.models.ForeignKeyReference
::: dplib.models.BaseField
::: dplib.models.Field
::: dplib.models.AnyField
::: dplib.models.ArrayField
::: dplib.models.BooleanField
::: dplib.models.DateField
::: dplib.models.DatetimeField
::: dplib.models.DurationField
::: dplib.models.GeojsonField
::: dplib.models.GeopointField
::: dplib.models.IntegerField
::: dplib.models.ListField
::: dplib.models.NumberField
::: dplib.models.ObjectField
::: dplib.models.StringField
::: dplib.models.TimeField
::: dplib.models.YearField
::: dplib.models.YearmonthField
::: dplib.models.BaseConstraints
::: dplib.models.CollectionConstraints
::: dplib.models.JsonConstraints
::: dplib.models.StringConstraints
::: dplib.models.ValueConstraints
4 changes: 2 additions & 2 deletions dplib/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from .contributor import Contributor
from .dialect import Dialect
from .field import Constraints, Field
from .field import *
from .license import License
from .package import Package
from .resource import Resource
from .schema import ForeignKey, ForeignKeyReference, Schema
from .schema import *
from .source import Source
4 changes: 3 additions & 1 deletion dplib/models/field/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from .constraints import Constraints
from .constraints import *
from .datatypes import *
from .field import Field
from .types import IField
6 changes: 3 additions & 3 deletions dplib/models/field/__spec__/test_field.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from dplib.models import Field
from dplib.models import AnyField, IntegerField


def test_field_defaults():
field = Field()
field = AnyField()
assert field.type == "any"
assert field.missingValues == [""]


def test_field_constraints():
field = Field()
field = IntegerField()
field.constraints.minimum = 1
assert field.constraints.minimum == 1
assert field.to_dict() == {"constraints": {"minimum": 1}}
21 changes: 0 additions & 21 deletions dplib/models/field/constraints.py

This file was deleted.

5 changes: 5 additions & 0 deletions dplib/models/field/constraints/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .base import BaseConstraints
from .collection import CollectionConstraints
from .json import JsonConstraints
from .string import StringConstraints
from .value import ValueConstraints
13 changes: 13 additions & 0 deletions dplib/models/field/constraints/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import annotations

from typing import Generic, List, Optional, TypeVar, Union

from ....system import Model

NativeType = TypeVar("NativeType")


class BaseConstraints(Model, Generic[NativeType]):
required: Optional[bool] = None
unique: Optional[bool] = None
enum: Optional[List[Union[str, NativeType]]] = None
10 changes: 10 additions & 0 deletions dplib/models/field/constraints/collection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from __future__ import annotations

from typing import Optional

from .base import BaseConstraints


class CollectionConstraints(BaseConstraints[str]):
minLength: Optional[int] = None
maxLength: Optional[int] = None
9 changes: 9 additions & 0 deletions dplib/models/field/constraints/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from __future__ import annotations

from typing import Any, Dict, Optional

from .collection import CollectionConstraints


class JsonConstraints(CollectionConstraints):
jsonSchema: Optional[Dict[str, Any]] = None
9 changes: 9 additions & 0 deletions dplib/models/field/constraints/string.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from __future__ import annotations

from typing import Optional

from .collection import CollectionConstraints


class StringConstraints(CollectionConstraints):
pattern: Optional[str] = None
15 changes: 15 additions & 0 deletions dplib/models/field/constraints/value.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

from typing import Generic, Optional, TypeVar, Union

from .base import BaseConstraints

NativeType = TypeVar("NativeType")


# TODO: tweak serialization if needed
class ValueConstraints(BaseConstraints[NativeType], Generic[NativeType]):
minimum: Optional[Union[str, NativeType]] = None
maximum: Optional[Union[str, NativeType]] = None
exclusiveMinimum: Optional[Union[str, NativeType]] = None
exclusiveMaximum: Optional[Union[str, NativeType]] = None
17 changes: 17 additions & 0 deletions dplib/models/field/datatypes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from .any import AnyField
from .array import ArrayField
from .base import BaseField
from .boolean import BooleanField
from .date import DateField
from .datetime import DatetimeField
from .duration import DurationField
from .geojson import GeojsonField
from .geopoint import GeopointField
from .integer import IntegerField
from .list import ListField
from .number import NumberField
from .object import ObjectField
from .string import StringField
from .time import TimeField
from .year import YearField
from .yearmonth import YearmonthField
16 changes: 16 additions & 0 deletions dplib/models/field/datatypes/any.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import annotations

from typing import Literal, Optional

import pydantic

from ..constraints import BaseConstraints
from .base import BaseField


class AnyField(BaseField):
"""The field contains values of a unspecified or mixed type."""

type: Literal["any"] = "any"
format: Optional[Literal["default"]] = None
constraints: BaseConstraints[str] = pydantic.Field(default_factory=BaseConstraints)
16 changes: 16 additions & 0 deletions dplib/models/field/datatypes/array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import annotations

from typing import Literal, Optional

import pydantic

from ..constraints import JsonConstraints
from .base import BaseField


class ArrayField(BaseField):
"""The field contains a valid JSON array."""

type: Literal["array"] = "array"
format: Optional[Literal["default"]] = None
constraints: JsonConstraints = pydantic.Field(default_factory=JsonConstraints)
48 changes: 48 additions & 0 deletions dplib/models/field/datatypes/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from typing import List, Optional

import pydantic

from .... import types
from ....system import Model


class BaseField(Model):
"""Base Field"""

name: Optional[str] = None
"""
The field descriptor MUST contain a name property.
"""

title: Optional[str] = None
"""
A human readable label or title for the field
"""

description: Optional[str] = None
"""
A description for this field e.g. “The recipient of the funds”
"""

missingValues: List[str] = [""]
"""
A list of field values to consider as null values
"""

# Compat

@pydantic.model_validator(mode="before")
@classmethod
def compat(cls, data: types.IData):
if not isinstance(data, dict): # type: ignore
return data

# field.format
format = data.get("format")
if format:
if format.startswith("fmt:"):
data["format"] = format[4:]

return data
26 changes: 26 additions & 0 deletions dplib/models/field/datatypes/boolean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

from typing import List, Literal, Optional

import pydantic

from ..constraints import BaseConstraints
from .base import BaseField


class BooleanField(BaseField):
"""The field contains boolean (true/false) data."""

type: Literal["boolean"] = "boolean"
format: Optional[Literal["default"]] = None
constraints: BaseConstraints[bool] = pydantic.Field(default_factory=BaseConstraints)

trueValues: List[str] = ["true", "True", "TRUE", "1"]
"""
Values to be interpreted as “true” for boolean fields
"""

falseValues: List[str] = ["false", "False", "FALSE", "0"]
"""
Values to be interpreted as “false” for boolean fields
"""
19 changes: 19 additions & 0 deletions dplib/models/field/datatypes/date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

import datetime
from typing import Literal, Optional

import pydantic

from ..constraints import ValueConstraints
from .base import BaseField


class DateField(BaseField):
"""he field contains a date without a time."""

type: Literal["date"] = "date"
format: Optional[str] = None
constraints: ValueConstraints[datetime.date] = pydantic.Field(
default_factory=ValueConstraints
)
19 changes: 19 additions & 0 deletions dplib/models/field/datatypes/datetime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

import datetime
from typing import Literal, Optional

import pydantic

from ..constraints import ValueConstraints
from .base import BaseField


class DatetimeField(BaseField):
"""The field contains a date with a time."""

type: Literal["datetime"] = "datetime"
format: Optional[str] = None
constraints: ValueConstraints[datetime.datetime] = pydantic.Field(
default_factory=ValueConstraints
)
16 changes: 16 additions & 0 deletions dplib/models/field/datatypes/duration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import annotations

from typing import Literal, Optional

import pydantic

from ..constraints import ValueConstraints
from .base import BaseField


class DurationField(BaseField):
"""The field contains a duration of time."""

type: Literal["duration"] = "duration"
format: Optional[Literal["default"]] = None
constraints: ValueConstraints[str] = pydantic.Field(default_factory=ValueConstraints)
21 changes: 21 additions & 0 deletions dplib/models/field/datatypes/geojson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from typing import Literal, Optional, Union

import pydantic

from ..constraints import BaseConstraints
from .base import BaseField

IGeojsonFormat = Union[
Literal["default"],
Literal["topojson"],
]


class GeojsonField(BaseField):
"""The field contains a JSON object according to GeoJSON or TopoJSON spec."""

type: Literal["geojson"] = "geojson"
format: Optional[IGeojsonFormat] = None
constraints: BaseConstraints[str] = pydantic.Field(default_factory=BaseConstraints)
22 changes: 22 additions & 0 deletions dplib/models/field/datatypes/geopoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

from typing import Literal, Optional, Union

import pydantic

from ..constraints import BaseConstraints
from .base import BaseField

IGeojsonFormat = Union[
Literal["default"],
Literal["array"],
Literal["object"],
]


class GeopointField(BaseField):
"""The field contains data describing a geographic point."""

type: Literal["geopoint"] = "geopoint"
format: Optional[IGeojsonFormat] = None
constraints: BaseConstraints[str] = pydantic.Field(default_factory=BaseConstraints)
Loading
Loading