Skip to content

sob.model

This module defines the building blocks of an sob based data model.

Model

Model()

Bases: sob.abc.Model

This class serves as a base class for sob.Object, sob.Dictionary, and sob.Array. This class should not be instantiated or sub-classed directly.

Source code in src/sob/model.py
86
87
88
89
90
def __init__(self) -> None:
    self._instance_meta: abc.Meta | None = None
    self._instance_hooks: abc.Hooks | None = None
    self._url: str | None = None
    self._pointer: str | None = None

Array

Array(
    items: (
        sob.abc.Array
        | collections.abc.Iterable[
            sob.abc.MarshallableTypes
        ]
        | str
        | bytes
        | sob.abc.Readable
        | None
    ) = None,
    item_types: (
        collections.abc.Iterable[type | sob.abc.Property]
        | sob.abc.Types
        | type
        | sob.abc.Property
        | None
    ) = None,
)

Bases: sob.model.Model, sob.abc.Array, sob.abc.Model

This class may either be instantiated directly or serve as a base class for defining typed JSON arrays (python lists).

Typing can be set at the instance level by providing the keyword argument item_types when initializing an instance of sob.Array, or by assigning item types to the class or instance metadata.

Example:

from __future__ import annotations
from io import StringIO
from typing import IO, Iterable
import sob
from datetime import datetime, date

class ObjectA(sob.Object):
    __slots__: tuple[str, ...] = (
        "name",
        "iso8601_datetime",
    )

    def __init__(
        self,
        _data: str | IO | dict | None = None,
        name: str | None = None,
        iso8601_datetime: datetime | None = None,
    ) -> None:
        self.name: str | None = name
        self.iso8601_datetime: datetime | None = iso8601_datetime
        super().__init__(_data)


sob.get_writable_object_meta(ObjectA).properties = sob.Properties([
    ("name", sob.StringProperty()),
    (
        "iso8601_datetime",
        sob.DateTimeProperty(name="iso8601DateTime")
    ),
])

class ObjectB(sob.Object):
    __slots__: tuple[str, ...] = (
        "name",
        "iso8601_date",
    )

    def __init__(
        self,
        _data: str | IO | dict | None = None,
        name: str | None = None,
        iso8601_date: date | None = None,
    ) -> None:
        self.name: str | None = name
        self.iso8601_date: date | None = iso8601_date
        super().__init__(_data)


sob.get_writable_object_meta(ObjectB).properties = sob.Properties([
    ("name", sob.StringProperty()),
    ("iso8601_date", sob.DateProperty(name="iso8601Date")),
])

class ArrayA(sob.Array):
    def __init__(
        self,
        items: (
            Iterable[ObjectA|ObjectB|dict]
            | IO
            | str
            | bytes
            | None
        ) = None,
    ) -> None:
        super().__init__(items)


sob.get_writable_array_meta(ArrayA).item_types = sob.Types([
    ObjectA, ObjectB
])


# Instances can be initialized using attribute parameters
array_a_instance_1: ArrayA = ArrayA(
    [
        ObjectA(
            name="Object A",
            iso8601_datetime=datetime(1999, 12, 31, 23, 59, 59),
        ),
        ObjectB(
            name="Object B",
            iso8601_date=date(1999, 12, 31),
        ),
    ]
)

# ...or by passing the JSON data, either as a string, bytes, sequence,
# or file-like object, as the first positional argument when
# initializing the class:
assert array_a_instance_1 == ArrayA(
    """
    [
        {
            "name": "Object A",
            "iso8601DateTime": "1999-12-31T23:59:59Z"
        },
        {
            "name": "Object B",
            "iso8601Date": "1999-12-31"
        }
    ]
    """
) == ArrayA(
    [
        {
            "name": "Object A",
            "iso8601DateTime": datetime(1999, 12, 31, 23, 59, 59)
        },
        {
            "name": "Object B",
            "iso8601Date": date(1999, 12, 31)
        }
    ]
) == ArrayA(
    StringIO(
        """
        [
            {
                "name": "Object A",
                "iso8601DateTime": "1999-12-31T23:59:59Z"
            },
            {
                "name": "Object B",
                "iso8601Date": "1999-12-31"
            }
        ]
        """
    )
)

# An array instance can be serialized to JSON using the `sob.serialize`
# function, or by simply casting it as a string

assert sob.serialize(array_a_instance_1, indent=4) == """
[
    {
        "name": "Object A",
        "iso8601DateTime": "1999-12-31T23:59:59Z"
    },
    {
        "name": "Object B",
        "iso8601Date": "1999-12-31"
    }
]
""".strip()

assert str(array_a_instance_1) == (
    '[{"name": "Object A", "iso8601DateTime": "1999-12-31T23:59:59Z"}'
    ', {"name": "Object B", "iso8601Date": "1999-12-31"}]'
)

# An array can be converted into a list of JSON-serializable
# python objects using `sob.marshal`
assert sob.marshal(array_a_instance_1) == [
    {
        "name": "Object A",
        "iso8601DateTime": "1999-12-31T23:59:59Z"
    },
    {
        "name": "Object B",
        "iso8601Date": "1999-12-31"
    }
]

Parameters:

Source code in src/sob/model.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
def __init__(
    self,
    items: (
        abc.Array
        | Iterable[abc.MarshallableTypes]
        | str
        | bytes
        | abc.Readable
        | None
    ) = None,
    item_types: (
        Iterable[type | abc.Property]
        | abc.Types
        | type
        | abc.Property
        | None
    ) = None,
) -> None:
    """
    Parameters:
        items:
        item_types:
    """
    Model.__init__(self)
    self._instance_meta: abc.ArrayMeta | None = None
    self._instance_hooks: abc.ArrayHooks | None = None
    self._list: list[abc.MarshallableTypes] = []
    self._init_url(items)
    deserialized_items: (
        Iterable[abc.MarshallableTypes] | abc.Model | None
    ) = self._init_format(items)
    if not isinstance(deserialized_items, (NoneType, Iterable)):
        raise TypeError(deserialized_items)
    self._init_item_types(deserialized_items, item_types)
    self._init_items(deserialized_items)
    self._init_pointer()

Dictionary

Dictionary(
    items: (
        sob.abc.Dictionary
        | collections.abc.Mapping[
            str, sob.abc.MarshallableTypes
        ]
        | collections.abc.Iterable[
            tuple[str, sob.abc.MarshallableTypes]
        ]
        | sob.abc.Readable
        | str
        | bytes
        | None
    ) = None,
    value_types: (
        collections.abc.Iterable[type | sob.abc.Property]
        | type
        | sob.abc.Property
        | sob.abc.Types
        | None
    ) = None,
)

Bases: sob.model.Model, sob.abc.Dictionary, sob.abc.Model

This class may either be instantiated directly or serve as a base class for defining JSON objects for which there is not a predetermined set of properties/attributes, but for which there may be a pre-determined set of permitted value types.

Typing can be set at the instance level by providing the keyword argument value_types when initializing an instance of sob.Dictionary, or by assigning value types to the class or instance metadata.

Example:

from __future__ import annotations
import sob
from io import StringIO
from typing import IO, Any, Iterable, Mapping
from datetime import datetime, date

class ObjectA(sob.Object):
    __slots__: tuple[str, ...] = (
        "name",
        "iso8601_datetime",
    )

    def __init__(
        self,
        _data: str | IO | dict | None = None,
        name: str | None = None,
        iso8601_datetime: datetime | None = None,
    ) -> None:
        self.name: str | None = name
        self.iso8601_datetime: datetime | None = iso8601_datetime
        super().__init__(_data)


sob.get_writable_object_meta(ObjectA).properties = sob.Properties([
    ("name", sob.StringProperty()),
    (
        "iso8601_datetime",
        sob.DateTimeProperty(name="iso8601DateTime")
    ),
])

class ObjectB(sob.Object):
    __slots__: tuple[str, ...] = (
        "name",
        "iso8601_date",
    )

    def __init__(
        self,
        _data: str | IO | dict | None = None,
        name: str | None = None,
        iso8601_date: date | None = None,
    ) -> None:
        self.name: str | None = name
        self.iso8601_date: date | None = iso8601_date
        super().__init__(_data)


sob.get_writable_object_meta(ObjectB).properties = sob.Properties([
    ("name", sob.StringProperty()),
    ("iso8601_date", sob.DateProperty(name="iso8601Date")),
])

class DictionaryA(sob.Dictionary):
    def __init__(
        self,
        items: (
            Mapping[str, Any]
            | Iterable[tuple[str, ObjectA|ObjectB|dict]]
            | IO
            | str
            | bytes
            | None
        ) = None,
    ) -> None:
        super().__init__(items)


sob.get_writable_dictionary_meta(DictionaryA).value_types = sob.Types([
    ObjectA, ObjectB
])


# Instances can be initialized with a dictionary
dictionary_a_instance_1: DictionaryA = DictionaryA(
    {
        "a": ObjectA(
            name="Object A",
            iso8601_datetime=datetime(1999, 12, 31, 23, 59, 59),
        ),
        "b": ObjectB(
            name="Object B",
            iso8601_date=date(1999, 12, 31),
        ),
    }
)

# ...or by passing the JSON data, either as a string, bytes, sequence,
# or file-like object, as the first positional argument when
# initializing the class:
assert dictionary_a_instance_1 == DictionaryA(
    """
    {
        "a": {
            "name": "Object A",
            "iso8601DateTime": "1999-12-31T23:59:59Z"
        },
        "b": {
            "name": "Object B",
            "iso8601Date": "1999-12-31"
        }
    }
    """
) == DictionaryA(
    StringIO(
        """
        {
            "a": {
                "name": "Object A",
                "iso8601DateTime": "1999-12-31T23:59:59Z"
            },
            "b": {
                "name": "Object B",
                "iso8601Date": "1999-12-31"
            }
        }
        """
    )
) == DictionaryA(
    (
        (
            "a",
            ObjectA(
                name="Object A",
                iso8601_datetime=datetime(1999, 12, 31, 23, 59, 59),
            )
        ),
        (
            "b",
            ObjectB(
                name="Object B",
                iso8601_date=date(1999, 12, 31),
            )
        ),
    )
)

# A dictionary instance can be serialized to JSON using the
# `sob.serialize` function, or by simply casting it as a string
assert sob.serialize(dictionary_a_instance_1, indent=4) == """
{
    "a": {
        "name": "Object A",
        "iso8601DateTime": "1999-12-31T23:59:59Z"
    },
    "b": {
        "name": "Object B",
        "iso8601Date": "1999-12-31"
    }
}
""".strip()

assert str(dictionary_a_instance_1) == (
    '{"a": {"name": "Object A", "iso8601DateTime": '
    '"1999-12-31T23:59:59Z"}, "b": {"name": "Object B", '
    '"iso8601Date": "1999-12-31"}}'
)

# A dictionary can be converted into a JSON-serializable
# objects using `sob.marshal`
assert sob.marshal(dictionary_a_instance_1) == {
    "a": {
        "name": "Object A",
        "iso8601DateTime": "1999-12-31T23:59:59Z"
    },
    "b": {
        "name": "Object B",
        "iso8601Date": "1999-12-31"
    }
}
Source code in src/sob/model.py
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
def __init__(
    self,
    items: (
        abc.Dictionary
        | Mapping[str, abc.MarshallableTypes]
        | Iterable[tuple[str, abc.MarshallableTypes]]
        | abc.Readable
        | str
        | bytes
        | None
    ) = None,
    value_types: (
        Iterable[type | abc.Property]
        | type
        | abc.Property
        | abc.Types
        | None
    ) = None,
) -> None:
    Model.__init__(self)
    self._instance_hooks: abc.DictionaryHooks | None = None
    self._instance_meta: abc.DictionaryMeta | None = None
    self._dict: dict[str, abc.MarshallableTypes] = {}
    self._init_url(items)
    deserialized_items: (
        Iterable[abc.MarshallableTypes]
        | Mapping[str, abc.MarshallableTypes]
        | abc.Model
        | None
    ) = self._init_format(items)
    self._init_value_types(deserialized_items, value_types)  # type: ignore
    self._init_items(deserialized_items)  # type: ignore
    self._init_pointer()

Object

Object(
    _data: (
        sob.abc.Object
        | sob.abc.Dictionary
        | collections.abc.Mapping[
            str, sob.abc.MarshallableTypes
        ]
        | collections.abc.Iterable[
            tuple[str, sob.abc.MarshallableTypes]
        ]
        | sob.abc.Readable
        | str
        | bytes
        | None
    ) = None,
)

Bases: sob.model.Model, sob.abc.Object, sob.abc.Model

This class serves as a base for defining models for JSON objects (python dictionaries) which have a predetermined set of properties (attributes). This class should not be instantiated directly, but rather sub-classed to create object models.

Example:

from __future__ import annotations
from io import StringIO
from typing import IO, Iterable
import sob
from datetime import datetime, date

class ObjectA(sob.Object):
    __slots__: tuple[str, ...] = (
        "boolean",
        "boolean_or_string",
        "integer",
        "number",
        "object_a",
        "iso8601_datetime",
        "iso8601_date",
    )

    def __init__(
        self,
        _data: str | IO | dict | Iterable | None = None,
        boolean: bool | None = None,
        boolean_or_string: bool | str | None = None,
        integer: int | None = None,
        enumerated: int | None = None,
        number: float | None = None,
        object_a: ObjectA | None = None,
        iso8601_datetime: datetime | None = None,
        iso8601_date: date | None = None,
    ) -> None:
        self.boolean: bool | None = boolean
        self.boolean_or_string: bool | str | None = boolean_or_string
        self.integer: int | None = integer
        self.enumerated: int | None = enumerated
        self.number: float | None = integer
        self.object_a: ObjectA | None = None
        self.iso8601_datetime: datetime | None = iso8601_datetime
        self.iso8601_date: date | None = iso8601_date
        super().__init__(_data)


sob.get_writable_object_meta(ObjectA).properties = sob.Properties([
    ("boolean", sob.BooleanProperty()),
    (
        "boolean_or_string",
        sob.Property(
            name="booleanOrString",
            types=sob.Types([bool, str])
        )
    ),
    ("integer", sob.IntegerProperty()),
    ("enumerated", sob.EnumeratedProperty(values=(1, 2, 3))),
    ("number", sob.NumberProperty()),
    (
        "iso8601_datetime",
        sob.DateTimeProperty(name="iso8601DateTime")
    ),
    ("iso8601_date", sob.DateProperty(name="iso8601Date")),
])

# Instances can be initialized using attribute parameters
object_a_instance_1: ObjectA = ObjectA(
    boolean=True,
    boolean_or_string="Maybe",
    integer=99,
    enumerated=2,
    number=3.14,
    iso8601_datetime=datetime(1999, 12, 31, 23, 59, 59),
    iso8601_date=date(1999, 12, 31),
)

# ...or by passing the JSON data, either as a string, bytes, dict, or
# file-like object, as the first positional argument when initializing
# the class:
assert object_a_instance_1 == ObjectA(
    """
    {
        "boolean": true,
        "booleanOrString": "Maybe",
        "integer": 99,
        "enumerated": 2,
        "number": 99,
        "iso8601DateTime": "1999-12-31T23:59:59Z",
        "iso8601Date": "1999-12-31"
    }
    """
) == ObjectA(
    {
        "boolean": True,
        "booleanOrString": "Maybe",
        "integer": 99,
        "enumerated": 2,
        "number": 99,
        "iso8601DateTime": datetime(1999, 12, 31, 23, 59, 59),
        "iso8601Date": date(1999, 12, 31)
    }
) == ObjectA(
    (
        ("boolean", True),
        ("booleanOrString", "Maybe"),
        ("integer", 99),
        ("enumerated", 2),
        ("number", 99),
        ("iso8601DateTime", datetime(1999, 12, 31, 23, 59, 59)),
        ("iso8601Date", date(1999, 12, 31))
    )
) == ObjectA(
    StringIO(
        """
        {
            "boolean": true,
            "booleanOrString": "Maybe",
            "integer": 99,
            "enumerated": 2,
            "number": 99,
            "iso8601DateTime": "1999-12-31T23:59:59Z",
            "iso8601Date": "1999-12-31"
        }
        """
    )
)

# An object instance can be serialized to JSON using the
# `sob.serialize` function, or by simply casting it as a string

assert sob.serialize(object_a_instance_1, indent=4) == """
{
    "boolean": true,
    "booleanOrString": "Maybe",
    "integer": 99,
    "enumerated": 2,
    "number": 99,
    "iso8601DateTime": "1999-12-31T23:59:59Z",
    "iso8601Date": "1999-12-31"
}
""".strip()

assert str(object_a_instance_1) == (
    '{"boolean": true, "booleanOrString": "Maybe", "integer": 99, '
    '"enumerated": 2, "number": 99, '
    '"iso8601DateTime": "1999-12-31T23:59:59Z", '
    '"iso8601Date": "1999-12-31"}'
)

# An object can be converted into a dictionary of JSON-serializable
# python objects using `sob.marshal`
assert sob.marshal(object_a_instance_1) == {
    "boolean": True,
    "booleanOrString": "Maybe",
    "integer": 99,
    "enumerated": 2,
    "number": 99,
    "iso8601DateTime": "1999-12-31T23:59:59Z",
    "iso8601Date": "1999-12-31"
}

Parameters:

  • _data (sob.abc.Object | sob.abc.Dictionary | collections.abc.Mapping[str, sob.abc.MarshallableTypes] | collections.abc.Iterable[tuple[str, sob.abc.MarshallableTypes]] | sob.abc.Readable | str | bytes | None, default: None ) –

    JSON data with which to initialize this object. This may be a dictionary/mapping, a JSON string or bytes, a file-like object containing JSON data, or an iterable of key/value tuples.

Source code in src/sob/model.py
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
def __init__(
    self,
    _data: (
        abc.Object
        | abc.Dictionary
        | Mapping[str, abc.MarshallableTypes]
        | Iterable[tuple[str, abc.MarshallableTypes]]
        | abc.Readable
        | str
        | bytes
        | None
    ) = None,
) -> None:
    """
    Parameters:
        _data: JSON data with which to initialize this object. This may
            be a dictionary/mapping, a JSON string or bytes, a
            file-like object containing JSON data, or an iterable of
            key/value tuples.
    """
    self._instance_meta: abc.ObjectMeta | None = None
    self._instance_hooks: abc.ObjectHooks | None = None
    self._extra: dict[str, abc.MarshallableTypes] | None = None
    Model.__init__(self)
    self._init_url(_data)
    deserialized_data: (
        Iterable[abc.MarshallableTypes]
        | Mapping[str, abc.MarshallableTypes]
        | abc.Model
        | None
    ) = self._init_format(_data)
    if not (
        isinstance(
            deserialized_data, (abc.Object, abc.Dictionary, dict, Mapping)
        )
        or (deserialized_data is None)
    ):
        raise TypeError(deserialized_data)
    self._data_init(deserialized_data)
    self._init_pointer()

marshal

marshal(
    data: sob.abc.MarshallableTypes,
    types: (
        collections.abc.Iterable[type | sob.abc.Property]
        | sob.abc.Types
        | None
    ) = None,
    value_types: (
        collections.abc.Iterable[type | sob.abc.Property]
        | sob.abc.Types
        | None
    ) = None,
    item_types: (
        collections.abc.Iterable[type | sob.abc.Property]
        | sob.abc.Types
        | None
    ) = None,
) -> typing.Any

This function recursively converts data which is not serializable using json.dumps into data which can be represented as JSON.

Parameters:

  • data (sob.abc.MarshallableTypes) –

    The data to be marshalled, typically an instance of sob.Model.

  • types (collections.abc.Iterable[type | sob.abc.Property] | sob.abc.Types | None, default: None ) –

    Property definitions or type(s) associated with this data. This is typically only used for recursive calls, so not typically provided explicitly by client applications.

  • value_types (collections.abc.Iterable[type | sob.abc.Property] | sob.abc.Types | None, default: None ) –

    Property definitions or type(s) associated with this objects' dictionary values. This is typically only used for recursive calls, so not typically provided explicitly by client applications.

  • item_types (collections.abc.Iterable[type | sob.abc.Property] | sob.abc.Types | None, default: None ) –

    Property definitions or type(s) associated with this array's items. This is typically only used for recursive calls, so not typically provided explicitly by client applications.

Source code in src/sob/model.py
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
def marshal(  # noqa: C901
    data: abc.MarshallableTypes,
    types: Iterable[type | abc.Property] | abc.Types | None = None,
    value_types: Iterable[type | abc.Property] | abc.Types | None = None,
    item_types: Iterable[type | abc.Property] | abc.Types | None = None,
) -> Any:
    """
    This function recursively converts data which is not serializable using
    `json.dumps` into data which *can* be represented as JSON.

    Parameters:
        data: The data to be marshalled, typically an instance of `sob.Model`.
        types: Property definitions or type(s) associated with this data.
            This is typically only used for recursive calls, so not typically
            provided explicitly by client applications.
        value_types: Property definitions or type(s) associated with this
            objects' dictionary values. This is typically only used for
            recursive calls, so not typically provided explicitly by client
            applications.
        item_types: Property definitions or type(s) associated with this
            array's items. This is typically only used for recursive calls,
            so not typically provided explicitly by client applications.
    """
    marshalled_data: abc.JSONTypes
    if isinstance(data, Decimal):
        # Instances of `decimal.Decimal` can'ts be serialized as JSON, so we
        # convert them to `float`
        marshalled_data = float(data)
    elif (data is None) or isinstance(data, (str, int, float)):
        # Don't do anything with `None`--this just means an attributes is not
        # used for this instance (an explicit `null` would be passed as
        # `sob.properties.types.NULL`).
        marshalled_data = data
    elif data is NULL:
        marshalled_data = None
    elif isinstance(data, abc.Model):
        marshalled_data = data._marshal()  # noqa: SLF001
    elif types is not None:
        marshalled_data = _marshal_typed(data, types)
    elif isinstance(data, datetime):
        marshalled_data = datetime2str(data)
    elif isinstance(data, date):
        marshalled_data = date2str(data)
    elif isinstance(data, (bytes, bytearray)):
        # Convert `bytes` to base-64 encoded strings
        marshalled_data = str(b64encode(data), "ascii")
    elif isinstance(data, Collection):
        marshalled_data = _marshal_collection(
            data, value_types=value_types, item_types=item_types
        )
    elif isinstance(data, SupportsBytes):
        # Convert objects which can be *cast* as `bytes` to
        # base-64 encoded strings
        marshalled_data = str(b64encode(bytes(data)), "ascii")
    else:
        message: str = f"Cannot unmarshal: {data!r}"
        raise ValueError(message)
    return marshalled_data

unmarshal

unmarshal(
    data: sob.abc.MarshallableTypes,
    types: (
        collections.abc.Iterable[type | sob.abc.Property]
        | type
        | sob.abc.Property
        | sob.abc.Types
    ) = (),
    value_types: (
        collections.abc.Iterable[type | sob.abc.Property]
        | type
        | sob.abc.Property
        | sob.abc.Types
    ) = (),
    item_types: (
        collections.abc.Iterable[type | sob.abc.Property]
        | type
        | sob.abc.Property
        | sob.abc.Types
    ) = (),
) -> typing.Any

Converts deserialized data into one of the provided types.

Parameters:

  • data (sob.abc.MarshallableTypes) –
  • types (collections.abc.Iterable[type | sob.abc.Property] | type | sob.abc.Property | sob.abc.Types, default: () ) –

    Property definitions or type(s) into which to attempt to un-marshal the data. If multiple types are provided, the first which does not raise an error or contain extraneous attributes is accepted. If the data has extraneous attributes for all types, the type with the fewest extraneous attributes is accepted.

  • value_types (collections.abc.Iterable[type | sob.abc.Property] | type | sob.abc.Property | sob.abc.Types, default: () ) –

    For dictionary-like objects, values will be un-marshalled as one of the provided property definitions or types.

  • item_types (collections.abc.Iterable[type | sob.abc.Property] | type | sob.abc.Property | sob.abc.Types, default: () ) –

    For sequences (lists/tuples), items will be un-marshalled as one of the provided property definitions or types.

Source code in src/sob/model.py
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
def unmarshal(
    data: abc.MarshallableTypes,
    types: (
        Iterable[type | abc.Property] | type | abc.Property | abc.Types
    ) = (),
    value_types: (
        Iterable[type | abc.Property] | type | abc.Property | abc.Types
    ) = (),
    item_types: (
        Iterable[type | abc.Property] | type | abc.Property | abc.Types
    ) = (),
) -> Any:
    """
    Converts deserialized data into one of the provided types.

    Parameters:
        data:
        types: Property definitions or type(s) into which to attempt to
            un-marshal the data. If multiple types are provided,
            the first which does not raise an error or contain extraneous
            attributes is accepted. If the data has extraneous attributes
            for all types, the type with the fewest extraneous attributes is
            accepted.
        value_types: For dictionary-like objects, values will be un-marshalled
            as one of the provided property definitions or types.
        item_types: For sequences (lists/tuples), items will be un-marshalled
            as one of the provided property definitions or types.
    """
    return _Unmarshal(
        data, types=types, value_types=value_types, item_types=item_types
    )()

serialize

serialize(
    data: sob.abc.MarshallableTypes,
    indent: int | None = None,
) -> str

This function serializes data, particularly instances of sob.Model sub-classes, into JSON encoded strings.

Parameters:

  • data (sob.abc.MarshallableTypes) –
  • indent (int | None, default: None ) –

    The number of spaces to use for indentation. If None, the JSON will be compacted (no line breaks or indentation).

Source code in src/sob/model.py
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
def serialize(
    data: abc.MarshallableTypes,
    indent: int | None = None,
) -> str:
    """
    This function serializes data, particularly instances of `sob.Model`
    sub-classes, into JSON encoded strings.

    Parameters:
        data:
        indent: The number of spaces to use for indentation. If `None`,
            the JSON will be compacted (no line breaks or indentation).
    """
    string_data: str
    if isinstance(data, abc.Model):
        before_serialize: Callable[[abc.JSONTypes], abc.JSONTypes] | None
        after_serialize: Callable[[str], str] | None
        before_serialize, after_serialize = _get_serialize_instance_hooks(data)
        marshalled_data: abc.JSONTypes = marshal(data)
        if before_serialize is not None:
            marshalled_data = before_serialize(marshalled_data)
        string_data = json.dumps(marshalled_data, indent=indent)
        if after_serialize is not None:
            string_data = after_serialize(string_data)
    else:
        if not isinstance(data, abc.JSON_TYPES):
            raise TypeError(data)
        string_data = json.dumps(data, indent=indent)
    return string_data

deserialize

deserialize(
    data: str | bytes | sob.abc.Readable | None,
    coerce_unparseable: type[str | bytes] | None = None,
) -> typing.Any

This function deserializes JSON encoded data from a string, bytes, or a file-like object.

Parameters:

  • data (str | bytes | sob.abc.Readable | None) –

    This can be a string or file-like object containing JSON serialized data.

  • coerce_unparseable (type[str | bytes] | None, default: None ) –

    If str or bytes are provided, and the data provided cannot be parsed as JSON, it will be returned as the specified type. If None (the default), an error will be raised if the data cannot be parsed as JSON.

This function returns None (for JSON null values), or an instance of str, dict, list, int, float or bool.

Source code in src/sob/model.py
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
def deserialize(
    data: str | bytes | abc.Readable | None,
    coerce_unparseable: type[str | bytes] | None = None,
) -> Any:
    """
    This function deserializes JSON encoded data from a string, bytes,
    or a file-like object.

    Parameters:
        data: This can be a string or file-like object
            containing JSON serialized data.
        coerce_unparseable: If `str` or `bytes` are provided, and
            the data provided cannot be parsed as JSON, it will be returned
            as the specified type. If `None` (the default), an error
            will be raised if the data cannot be parsed as JSON.

    This function returns `None` (for JSON null values), or an instance of
    `str`, `dict`, `list`, `int`, `float` or `bool`.
    """
    deserialized_data: abc.JSONTypes
    if isinstance(data, str):
        try:
            deserialized_data = json.loads(
                data,
                strict=False,
            )
        except ValueError as error:
            if coerce_unparseable:
                return data
            raise DeserializeError(
                data=data,
                message=get_exception_text(),
            ) from error
    elif isinstance(data, bytes):
        str_data: str = str(data, encoding="utf-8")
        try:
            deserialized_data = deserialize(str_data)
        except DeserializeError as error:
            if coerce_unparseable:
                if issubclass(coerce_unparseable, bytes):
                    return data
                return str_data
            raise DeserializeError(
                data=data,
                message=get_exception_text(),
            ) from error
    else:
        if not isinstance(data, abc.Readable):
            raise TypeError(data)
        deserialized_data = deserialize(
            read(data), coerce_unparseable=coerce_unparseable
        )
    return deserialized_data

validate

validate(
    data: typing.Any,
    types: (
        sob.abc.Types
        | collections.abc.Iterable[type | sob.abc.Property]
        | None
    ) = None,
    *,
    raise_errors: bool = True
) -> collections.abc.Sequence[str]

This function verifies that all properties/items/values of a model instance are of the correct data type(s), and that all required attributes are present (if applicable).

Parameters:

  • data (typing.Any) –
  • types (sob.abc.Types | collections.abc.Iterable[type | sob.abc.Property] | None, default: None ) –

    Property definitions or types against which to attempt to validate the data.

  • raise_errors (bool, default: True ) –

    If True, a validation error will be raised if the validation fails. If False, a list of error message strings will be returned.

If raise_errors is True (this is the default), violations will result in a validation error being raised. If raise_errors is False, a list of error messages will be returned.

Source code in src/sob/model.py
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
def validate(
    data: Any,
    types: abc.Types | Iterable[type | abc.Property] | None = None,
    *,
    raise_errors: bool = True,
) -> Sequence[str]:
    """
    This function verifies that all properties/items/values of a model instance
    are of the correct data type(s), and that all required attributes are
    present (if applicable).

    Parameters:
        data:
        types: Property definitions or types against which to attempt to
            validate the data.
        raise_errors: If `True`, a validation error will be raised if
            the validation fails. If `False`, a list of error message strings
            will be returned.

    If `raise_errors` is `True` (this is the default), violations will result
    in a validation error being raised. If `raise_errors` is `False`, a list
    of error messages will be returned.
    """
    if isinstance(data, GeneratorType):
        data = tuple(data)
    error_messages: list[str] = []
    if types is not None:
        error_messages.extend(_validate_typed(data, types))
    error_messages.extend(_call_validate_method(data))
    if raise_errors and error_messages:
        data_representation: str = f"\n\n    {indent_(represent(data))}"
        error_messages_representation: str = "\n\n".join(error_messages)
        if data_representation not in error_messages_representation:
            error_messages_representation = (
                f"{data_representation}\n\n{error_messages_representation}"
            )
        raise errors.ValidationError(error_messages_representation)
    return error_messages

replace_model_nulls

replace_model_nulls(
    model_instance: sob.abc.Model,
    replacement_value: sob.abc.MarshallableTypes = None,
) -> None

This function replaces all instances of sob.properties.types.NULL.

Parameters:

  • model_instance (sob.model.Model)
  • replacement_value (typing.Any): The value with which nulls will be replaced. This defaults to None.
Source code in src/sob/model.py
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
def replace_model_nulls(
    model_instance: abc.Model,
    replacement_value: abc.MarshallableTypes = None,
) -> None:
    """
    This function replaces all instances of `sob.properties.types.NULL`.

    Parameters:

    - model_instance (sob.model.Model)
    - replacement_value (typing.Any):
      The value with which nulls will be replaced. This defaults to `None`.
    """
    if isinstance(model_instance, abc.Object):
        _replace_object_nulls(model_instance, replacement_value)
    elif isinstance(model_instance, abc.Array):
        _replace_array_nulls(model_instance, replacement_value)
    elif isinstance(model_instance, abc.Dictionary):
        _replace_dictionary_nulls(model_instance, replacement_value)

get_model_from_meta

get_model_from_meta(
    name: str,
    metadata: sob.abc.Meta,
    module: str | None = None,
    docstring: str | None = None,
    pre_init_source: str = "",
    post_init_source: str = "",
) -> type[sob.abc.Model]

Constructs an sob.Object, sob.Array, or sob.Dictionary sub-class from an instance of sob.ObjectMeta, sob.ArrayMeta, or sob.DictionaryMeta.

Parameters:

  • name (str) –

    The name of the class.

  • class_meta
  • module (str | None, default: None ) –

    Specify the value for the class definition's __module__ property. The invoking module will be used if this is not specified. Note: If using the result of this function with sob.utilities.get_source to generate static code--this should be set to "main". The default behavior is only appropriate when using this function as a factory.

  • docstring (str | None, default: None ) –

    A docstring to associate with the class definition.

  • pre_init_source (str, default: '' ) –

    Source code to insert before the __init__ function in the class definition.

  • post_init_source (str, default: '' ) –

    Source code to insert after the __init__ function in the class definition.

Source code in src/sob/model.py
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
def get_model_from_meta(
    name: str,
    metadata: abc.Meta,
    module: str | None = None,
    docstring: str | None = None,
    pre_init_source: str = "",
    post_init_source: str = "",
) -> type[abc.Model]:
    """
    Constructs an `sob.Object`, `sob.Array`, or `sob.Dictionary` sub-class
    from an instance of `sob.ObjectMeta`, `sob.ArrayMeta`, or
    `sob.DictionaryMeta`.

    Parameters:
        name: The name of the class.
        class_meta:
        module: Specify the value for the class definition's
            `__module__` property. The invoking module will be
            used if this is not specified. Note: If using the result of this
            function with `sob.utilities.get_source` to generate static
            code--this should be set to "__main__". The default behavior is
            only appropriate when using this function as a factory.
        docstring: A docstring to associate with the class definition.
        pre_init_source: Source code to insert *before* the `__init__`
            function in the class definition.
        post_init_source: Source code to insert *after* the `__init__`
            function in the class definition.
    """
    # For pickling to work, the __module__ variable needs to be set...
    module = module or get_calling_module_name(2)
    class_definition: str = _class_definition_from_meta(
        name,
        metadata,
        docstring=docstring,
        module=module,
        pre_init_source=pre_init_source,
        post_init_source=post_init_source,
    )
    namespace: dict[str, Any] = {"__name__": f"from_meta_{name}"}
    imports = [
        "from __future__ import annotations",
        "import typing",
    ]
    # `decimal.Decimal` may or may not be referenced in a given model--so
    # check first
    if re.search(r"\bdecimal\.Decimal\b", class_definition):
        imports.append("import decimal")
    # `datetime` may or may not be referenced in a given model--so check
    # first
    if re.search(r"\bdatetime\b", class_definition):
        imports.append("import datetime")
    imports.append("import sob")
    source: str = suffix_long_lines(
        "{}\n\n\n{}".format("\n".join(imports), class_definition)
    )
    error: Exception
    try:
        exec(source, namespace)  # noqa: S102
    except Exception as error:
        append_exception_text(error, f"\n\n{source}")
        raise
    model_class: type[abc.Model] = namespace[name]
    model_class._source = source  # noqa: SLF001
    model_class.__module__ = module
    model_class._class_meta = metadata  # noqa: SLF001
    return model_class

get_models_source

get_models_source(
    *model_classes: type[sob.abc.Model],
) -> str

Get source code for a series of model classes, organized as a module. This is useful for generating a module from classes generated using get_model_from_meta.

Source code in src/sob/model.py
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
def get_models_source(*model_classes: type[abc.Model]) -> str:
    """
    Get source code for a series of model classes, organized as a module.
    This is useful for generating a module from classes generated
    using `get_model_from_meta`.
    """
    import_source_lines: list[str] = []
    class_sources: list[str] = []
    model_class: type[abc.Model]
    class_names_metadata: dict[
        str, abc.ObjectMeta | abc.ArrayMeta | abc.DictionaryMeta
    ] = {}
    for model_class in model_classes:
        import_source: str
        class_source: str
        import_source, class_source = (
            get_source(model_class).strip().rpartition("\n\n\n")[::2]
        )
        import_source_lines.extend(import_source.splitlines())
        class_sources.append(class_source)
        meta_instance: abc.Meta | None = meta.read_model_meta(model_class)
        if not isinstance(
            meta_instance,
            (abc.ObjectMeta, abc.ArrayMeta, abc.DictionaryMeta),
        ):
            raise TypeError(meta_instance)
        class_names_metadata[model_class.__name__] = meta_instance
    class_name: str
    metadata: abc.ObjectMeta | abc.ArrayMeta | abc.DictionaryMeta
    metadata_sources: list[str] = []
    for class_name, metadata in class_names_metadata.items():
        if not isinstance(
            metadata, (abc.ObjectMeta, abc.ArrayMeta, abc.DictionaryMeta)
        ):
            raise TypeError(metadata)
        if isinstance(metadata, abc.ObjectMeta):
            metadata_sources.append(
                _get_class_meta_attribute_assignment_source(
                    class_name, "properties", metadata
                )
            )
        elif isinstance(metadata, abc.ArrayMeta):
            metadata_sources.append(
                _get_class_meta_attribute_assignment_source(
                    class_name, "item_types", metadata
                )
            )
        else:
            metadata_sources.append(
                _get_class_meta_attribute_assignment_source(
                    class_name, "value_types", metadata
                )
            )
    # De-duplicate imports while preserving order
    imports_source = "\n".join(dict.fromkeys(import_source_lines).keys())
    classes_source: str = "\n\n\n".join(class_sources)
    metadata_source: str = "\n".join(metadata_sources)
    return f"{imports_source}\n\n\n{classes_source}\n\n\n{metadata_source}"