Module xmodel.remote.model

Expand source code
from typing import TypeVar, Generic, TYPE_CHECKING

from xmodel.base.model import BaseModel, Self
from xsentinels.default import Default


if TYPE_CHECKING:
    from xmodel.remote.api import RemoteApi

# Can't forward-ref the bound type here (ie: 'RemoteModel'), so put BaseModel in at least...
M = TypeVar("M", bound=BaseModel)


def _lazy_load_types(cls):
    """
    Lazy import BaseApi into module, helps resolve BaseApi forward-refs;
    ie: `api: "RemoteApi[T]"`

    We need to resolve these forward-refs due to use of `get_type_hints()` in
    BaseModel.__init_subclass__; so get_type_hints can find the correct type for the
    forward-ref string in out `RemoteModel` class below.

    Sets it in such a way so IDE's such as pycharm don't get confused + pydoc3
    can still find it and use the type forward-reference.

    See `xmodel.base.model.BaseModel.__init_subclass__` for more details.
    """
    if 'BaseApi' not in globals():
        from xmodel.remote.api import RemoteApi
        globals()['RemoteApi'] = RemoteApi


class RemoteModel(BaseModel, lazy_loader=_lazy_load_types):
    api: 'RemoteApi[Self]' = None

    # todo: I think we will want to make the type on the `id` field a TypeVar of some sort,
    #       sometimes we will want to use a `str` for it's type [etc].
    id: int = None
    """ Primary identifier for object, used with API endpoint. """

    def __init__(self, *args, id=Default, **initial_values):

        super().__init__(*args, **initial_values)

        if id is not Default:
            self.id = id

    def __repr__(self):
        message = super().__repr__().split("(")[1][:-1]

        response_state = self.api.response_state
        response_state_attrs = []

        if response_state.had_error is not None:
            response_state_attrs.append('had_error')

        if response_state.response_code is not None and response_state.response_code != 200:
            response_state_attrs.append('response_code')

        if response_state.errors is not None:
            response_state_attrs.append('errors')

        for attr in response_state_attrs:
            message += f', __{attr}={getattr(response_state, attr, None)}'

        return f"{self.__class__.__name__}({message})"

Classes

class RemoteModel (*args, id=Default, **initial_values)

Used as the abstract base-class for classes/object that communicate with our REST API.

This is one of the main classes, and it's highly recommend you read the SDK Library Overview first, if you have not already. That document has many basic examples of using this class along with other related classes.

Attributes that start with _ or don't have a type-hint are not considered fields on the object that automatically get mapped to/from the JSON that is passed in. For more details see Type Hints.

When you sub-class BaseModel, you can create your own Model class, with your own fields/attrs. You can pass class arguments/paramters in when you declare your sub-class. The Model-subclass can provide parameters to the super class during class construction.

In the example below, notice the base_url part. That's a class argument, that is used by the super-class during the construction of the sub-class (before any instances are created). In this case it takes this and stores it on xmodel.rest.RestStructure.base_model_url as part of the structure information for the BaseModel subclass.

See Basic Model Example for an example of what class arguments are or look at this example below using a RestModel:

>>> # 'base_url' part is a class argument:
>>> from xmodel.rest import RestModel
>>> class Account(RestModel["Account"], base_url='/account'):
>>>    id: str
>>>    name: str

These class arguments are sent to a special method BaseStructure.configure_for_model_type(). See that methods docs for a list of avaliable class-arguments.

See BaseModel.__init_subclass__ for more on the internal details of how this works exactly.

Note: In the case of base_url example above, it's the base-url-endpoint for the model.

If you want to know more about that see xmodel.rest.RestClient.url_for_endpoint. It has details on how the final request Url is constructed.

This class also allows you to more easily with with JSON data via:

Other important related classes are listed below.

  • BaseApi Accessable via BaseModel.api.
  • xmodel.rest.RestClient: Accessable via xmodel.base.api.BaseApi.client.
  • xmodel.rest.settings.RestSettings: Accessable via xmodel.base.api.BaseApi.settings.
  • BaseStructure: Accessable via BaseApi.structure
  • xmodel.base.auth.BaseAuth: Accessable via xmodel.base.api.BaseApi.auth

Tip: For all of the above, you can change what class is allocated for each one

by changing the type-hint on a subclass.

Creates a new model object. The first/second params need to be passed as positional arguments. The rest must be sent as key-word arguments. Everything is optional.

Args

id
Specify the BaseModel.id attribute, if you know it. If left as Default, nothing will be set on it. It could be set to something via args[0] (ie: a JSON dict). If you do provide a value, it be set last after everything else has been set.
*args

I don't want to take names from what you could put into 'initial_values', so I keep it as position-only *args. Once Python 3.8 comes out, we can use a new feature where you can specify some arguments as positional-only and not keyword-able.

FirstArg - If Dict:

If raw dictionary parsed from JSON string. It just calls self.api.update_from_json(args[0]) for you.

FirstArt - If BaseModel:

If a BaseModel, will copy fields over that have the same name. You can use this to duplicate a Model object, if you want to copy it. Or can be used to copy fields from one model type into another, on fields that are the same name.

Will ignore fields that are present on one but not the other. Only copy fields that are on both models types.

**initial_values
Let's you specify other attribute values for convenience. They will be set into the object the same way you would normally doing it: ie: model_obj.some_attr = v is the same as ModelClass(some_attr=v).
Expand source code
class RemoteModel(BaseModel, lazy_loader=_lazy_load_types):
    api: 'RemoteApi[Self]' = None

    # todo: I think we will want to make the type on the `id` field a TypeVar of some sort,
    #       sometimes we will want to use a `str` for it's type [etc].
    id: int = None
    """ Primary identifier for object, used with API endpoint. """

    def __init__(self, *args, id=Default, **initial_values):

        super().__init__(*args, **initial_values)

        if id is not Default:
            self.id = id

    def __repr__(self):
        message = super().__repr__().split("(")[1][:-1]

        response_state = self.api.response_state
        response_state_attrs = []

        if response_state.had_error is not None:
            response_state_attrs.append('had_error')

        if response_state.response_code is not None and response_state.response_code != 200:
            response_state_attrs.append('response_code')

        if response_state.errors is not None:
            response_state_attrs.append('errors')

        for attr in response_state_attrs:
            message += f', __{attr}={getattr(response_state, attr, None)}'

        return f"{self.__class__.__name__}({message})"

Ancestors

Class variables

var apiRemoteApi[BaseModel]

Inherited from: BaseModel.api

Used to access the api class, which is used to retrieve/send objects to/from api …

var id : int

Primary identifier for object, used with API endpoint.

Static methods

def __init_subclass__(*, lazy_loader: Callable[[Type[~M]], None] = None, **kwargs)

Inherited from: BaseModel.__init_subclass__

We take all arguments (except lazy_loader) passed into here and send them to the method on our structure: …