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
354
355
356
357
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
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
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
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
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
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
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
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
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
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
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
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
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,
) -> 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.

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
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
def deserialize(
    data: str | bytes | abc.Readable | 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.

    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:
            raise DeserializeError(
                data=data,
                message=get_exception_text(),
            ) from error
    elif isinstance(data, bytes):
        deserialized_data = deserialize(str(data, encoding="utf-8"))
    else:
        if not isinstance(data, abc.Readable):
            raise TypeError(data)
        deserialized_data = deserialize(read(data))
    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
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
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
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
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
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
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
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
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
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}"