Package xmodel

Provides easy way to map dict to/from Full-Fledged 'JsonModel' object.

Also, an abstract RemoteModel interface used in xmodel-rest and xdynamo along with some common code.

Important: Doc-comments in varius classes have out-of-date/broken refs; will be fixed soon.

Important: Docs Below Are OUT OF DATE!!!

Most of the docs below belong int xmodel-rest and xdynamo. We will revamp them soon, and put more into the README.md as well. Currently, I would look at the docs above or README.md for info on how to use JsonModel class, which is the main-class of this library.

Old Docs Below - Need A Lot Of Updates (ORM is old reference)

Used to be called the ORM library, that reference will be removed/updated as we get the docs back in shape here soon. For now, they are older references that may not be entirely accurate anymore.

ORM Library Overview

Library is intended to be used as a way to consolidate and standardize how we work with Model classes that can represent data from various different locations. It can be used as an easy way to communicate to/from our various places where we store models/information.

Right now this includes:

  • Our own API's, such as Account, Auth and Founder's club API's.
  • Dynamo tables.
  • Big commerce
  • Hubspot

Some reasons to use orm:

  • The foundation for xmodel-rest library's Model objects.
    • Let's us easily map objects into/out of various rest API's.
    • Handles breaking up requests to get objects by id transparently into several sub-requests.
    • Does pagination for you automatically
    • Can lazily and prefetch0in-bulk child objects.
  • Useful for accessing generally restful API's.
    • hubspot and bigcommerce projects use to to access their services api.
  • Consistant interface, works the same wherever it's used (vs one-off methods inside the project doing the same things in diffrent ways).
  • xmodel.dynamo: A library to map objects into Dynamo, we consolidated code from a few diffrent projects
    • Easily map objects into and out of Dynamo.
    • No need to duplicate code for things like paginating the results.
    • Add features or fix bugs in library, all other projects that use it benefit.
    • Can figure out best way to query dynamo for you automatically.

Some of the Models classes are kept directly in their respective projects (such as with hubspot). In these cases, you can import that project as a Library to utilize it's Model's and to also utilize it's other code/processes (which probably work closely with the Model's in question).

Model Fields

You can create your own Models. For a real example, see hubspot.api.api.Contact.

Note: (side-note: We should have put it in "hubspot.api.contact.Contact" or some such).

Here is a basic Model below that I will use for illustrative purposes. It's a basic model that is using the standard functionality, with the only customization being the base_url which is one of elements that construct's the url/path to it's endpoint.

Basic Model Example

>>> from xmodel import Field, BaseModel
>>> import datetime as dt
>>>
>>> class MyModel(
...    BaseModel,
...    base_url="accounts"  # <-- Class argument passed to underlying Structure object.
... ):
...    my_attribute: str  # <-- Automatically maps to API without extra effort
...    created_at: dt.datetime  # <-- Can auto-convert other types for you
...
...    other_attribute: int = Field(read_only=True)  # <-- Customize options
...
...    _will_not_map_to_api: int  # <-- Anything that begins with '_' won't map
...    also_will_not_map = None  # <-- No type-hint, 'Field' not auto-created

Type-hints

When Model classes are created, they will lazily find type-hinted attributes and determine if a xmodel.fields.Field should automatically be created for them. A xmodel.fields.Field is what is used by the sdk to map an attribute into it's corresponding JSON field that is sent/retrieved from the API service.

You can specify other types, such as datetime/date types. The SDK has a default converter in place for datetime/date types. You can define other default converters, see Type Converters for more details. Converters can also be used on a per-field basis via xmodel.fields.Field.converter.

The Model will enforce these type-hints, and use a converter if needed and one is available.

For example, if you try do do this:

>>> obj = MyModel()
>>> obj.my_attribute = 123
>>> obj.my_attribute
"123"

Notice how the output of the attribute is a str now and not an int. The Model will automatically realize that it needs to be a string and try to convert it into a string for you. If this is unsuccessful, it will raise an exception.

When this model is initialized from JSON, it does the same conversions. When grabbing this object from the API it will automatically create the Model from the JSON it receives from API.

If you wish, you can also pass in your own dict structure when creating an object. The Dict could have come from a JSON string.

>>> import json
>>> parsed_values = json.loads('{"my_attribute": 1234}')
>>> obj = MyModel(parsed_values)
>>> obj.my_attribute
"1234"

Field Objects

A BaseModel has a set of xmodel.fields.Field's that define how each field will map to an attribute to/from JSON, which we currently use for all of our APIs. See JSON for more details.

You can get a list of xmodel.fields.Field's via xmodel.base.api.BaseApi.fields. This will allow you do iterate over all the fields the system will use when interacting with JSON documents.

If you don't allocate a xmodel.fields.Field object directly on the Class at class definition time we will auto-generate them for you. It will only do this for fields that have a type-hint. If there is no type-hint, we won't auto-allocate a Field for it, and hence we won't map it to/from the JSON, enforce the types or auto-convert them. See Type Hints.

Field Subclasses

You can have your own Field sub-class if you wish. To guarantee all auto-generated fields use you class, you can set the type on xmodel.base.structure.Structure.field_type.

This needs to be set before any Model class that uses it is initialized. You can do that by subclassing xmodel.base.structure.Structure and setting it at class definition time. You then tell your BaseApi's to use your new structure.

For an example of doing all of this and also creating a custom xmodel.fields.Field subclass, see xmodel.dynamo.DynField. We use this in our Dynamo-related code to put additional options on model Fields that are only really useful for Dynamo models.

But the general idea is this:

>>> from xmodel import BaseStructure.....
# todo: Finish this example.

JSON

Right now all of our API's accept and return JSON. The Models handle JSON natively. This means you can also use Model's to more easily deal with JSON without necessarily having to use any of the xmodel.rest.RestClient aspects (send/receive). This is what we did with Dynamo at first, we simply grabbed the json via xmodel.base.api.json and send that directly into boto. Later on we put together a special xmodel.dynamo.DynClient to automatically send/receive it via boto (wraps boto).

Some the the reasons why it may be easier is due to the Model's in combination with the Fields. You can easily define a mapping and automatic type-conversion with only a simple Model defintion.

Hint: What about API's that use a non-JSON format?

If we ever have an API that has some other format, we would have a xmodel.rest.RestClient subclass that would handle mapping it to/from the JSON that we use on the Models. After RestClient gets a response, it would make a Dict out of it as if it got it from JSON and give that to the Model; and vic-versa (map from Dict into API format).

Model.api

You can also export or update an existing BaseModel object via methods under a special attribute BaseModel.api. This attribute has a reserved name on every Model. This attribute is how the Model interfaces with the rest of the SDK. That way the rest of the namespace for the Model attributes is available for use by Model subclass.

You can update an object via a dict from parse JSON via BaseApi.update_from_json(). Exporting JSON is easily done via BaseApi.json(). Both of these methods accept/return a xmodel.types.JsonDict, which is just a dict with str keys and Any value.

BaseModel.api is also how you can easily get/send objects to/from the API service.

There are various ways to change/customize a model, keep reading further.

BaseApi Class

One of the more important classes is BaseApi.

For an overview of the class see BaseApi Class Overview api.html#use-of-type-hints-for-changing-used-type

The class is a sort of central hub, it's where you can specify which types are allocated for each sub-class. This is done via type-hints (typehints are read and used to allocate correct class).

For more details see Use of Type Hints for Changing Type Used

Type Converters

The mapping of basic types to their converter function lives at BaseApi.default_converters. Normally you could customize this by by subclassing BaseApi with your own version for you Model(s). You can also change it dynamically via adjusting BaseApi.default_converters.

For the default converter map, see xmodel.converters.DEFAULT_CONVERTERS.

TODO

This is how I want it to work in the future:

Something to keep in mind is when the xmodel.base.api.BaseApi converts a type, and it needs a lookup a default converter it uses xmodel.base.api.BaseApi.get_default_converter. This method first checks it's self, and if type to convert is not in dict, it will check the superclasses default converters and so on until one is found.

This means you can override a type conversion from a super class, or let it be used if it works as needed. One example of this is how hubspot uses a time-stamp integer to communicate time but most other systems use the normal ISO date/time format. So for the BaseApi class that all hubspot Model's use, they have the datetime converter overriden with a special Hubspot version.

You can also set a converter per-field via a callback on a xmodel.fields.Field object.

All converters have a calling convention, see xmodel.fields.Converter for details.

RestClient Class

TODO

Section is unfinished, needs to be fleshed out more.

The config object that this api uses, can be customized per-model. All you have to do is this to make it a different type::

class MyClient():
    # Customize RestClient class in some way....
    my_custom_var: str = ConfigVar("MY_CUSTOM_ENVIRONMENTAL_VAR", "default")

class MyApi(base.BaseApi[T]):
    client: MyClient

class MyModel(base.model['MyModel'], endpoint_url="custom/path"):
    api: MyApi

The type-hints are enough to tell the system what types to use. They also will tell any IDE in use about what type it should be, for type-completion. So it's sort of doing double-duty to both tell IDE what it is and tell class what type to allocate for the attribute when creating the class/object.

.. todo: Make changing default_converters work as expected [api-class vs api-instance]; and then talk about te default type-converters here.

Going back to this example (from end of the ORM Library Overview section):

>>> from some_lib.account import Account
>>> account = Account.api.get_via_id(3)
>>> print(account.account_no)
"3"

We will look at the this Account model more closely, it has a good example of using child objects. Here is a simplified version of the Account Model:

class PhoneNumber(AccountModel['PhoneNumber'], base_url="account/phone_numbers"):
    account_id: int
    number: str
    description: str
    is_active: bool

class Account(AccountModel['Account'], base_url="accounts"):
    # Configure a more specific api to use with the Accounts endpoint.
    api: AccountsEndpointApi[Account]

    # This is generally very useful for Account objects,
    # don't exclude updated_at by default.
    updated_at: dt.datetime = Field(exclude=False)

    account_no: str
    first_name: str
    last_name: str
    preferred_name: str
    preferred_phone_number: PhoneNumber
    preferred_address: Address

Here is an example of getting that objects preferred phone number:

>>> print(account.preferred_phone_number.number)
"8015551234"

By default preferred_phone_number is currently None (internally), so the system knows that the PhoneNumber object has not been retrieved from the API yet. It also knows the id for the preferred_phone_number. It's stored on the account object via preferred_phone_number_id (via JSON from api). The ORM stored this number internally when the object was fetched.

If you define a field like this in the object:

>>> preferred_phone_number_id: int

Instead of storing the number internally, it would store it here instead (and you can get/set it as needed). You can also get/set it by setting the id field of the child object, like so:

>>> obj: Account
>>> obj.preferred_phone_number.id
123456

If this id is known to the Model, meaning that it was fetched when the object was retrieved from api (or set to something via preferred_phone_number_id); The sdk can lazily lookup the object on demand when it's asked for. It knows preferred_phone_number is a PhoneNumber model type (and that it's also associated with a diffrent api endpoint) and it knows the id, so it simply asks for it on demand/lazily via ChildType.api.get_via_id (aka: xmodel.base.api.BaseApi.get_via_id).

It automatically takes this `preferred_phone_number_id`` and looks-up the preferred phone number on the spot when you ask for it:

>>> obj.preferred_phone_number

This object is stored under preferred_phone_number so in the future it already has the object when something asks for it again.

Auto Prefetch Children

You can also pre-fetch these child objects in bulk if you have a collection of model objects (such as a List of some_lib.account.Account's) via xmodel.children.bulk_request_lazy_children. This is much more efficient if you have a lot of objects because it can grab many of the children pre-request.

TODO

At some point the xmodel-rest will probably fetch many child objects lazily in bulk. When someone accesses one lazily, it could grab more for other Model objects that don't have their children fetched yet. We just put in a weak-ref cache, so using this we could find the ones that we have fetched in the past and and are still around. We could fetch their children too at the same time in the same request in bulk (ie: so we fetch original child requested, along with 50 more or so via a single request).

You can also have the xmodel do this automatically as it receives pages of objects via xmodel.options.ApiOptions.auto_get_child_objects like so:

>>> Account.api.options.auto_get_child_objects = True

This sets this option for this Model type in the current XContext. If you make a new XContext, and then throw the XContext away, it will revert these option changes.

>>> from xinject.context import XContext
>>>
>>> # Starts out as False....
>>> assert not Account.api.options.auto_get_child_objects
>>>
>>> with XContext():
...     Account.api.options.auto_get_child_objects = True
...     # Returned generator will 'remember' the options at time of creation
...     accounts_gen_with_children_pre_fetched = Account.api.get(top=100)
>>>
>>> # After XContext is gone, it reverts back to what it was before
>>> assert not Account.api.options.auto_get_child_objects

You can use this to grab all of the accounts. The below returns a Generator and will grab a page of Account objects at a time, but still returning each object individually via the generator.

>>> [account.id for account in Account.api.get()]
[1, 2, 3, 4, 5, 118, 127, ...]

This is to help limit memory usage. It will also allow the sdk to asynchronously grab multiple pages in the future [pre-fetch them] without changing the sdk's public interface to other projects.

TODO

Asynchronously pre-fetch pages of objects as the generator iterates though result set.

Caching

There is a strong-ref and weak-ref caching system in the ORM you can take advantage of, depending on the situation.

By default, they are both disabled. They are explicitly an opt-in feature.

Strong-Ref Caching

You can enable strong-ref caching in three ways currently:

  • Set it on directly on xmodel.base.api.BaseApi.options.cache_by_id=True

  • Set cache_by_id=True as one of the model classes options, like so:

>>> from xmodel import ApiOptions
>>> class MyModel(RestModel['MyModel'], api_options=ApiOptions(cache_by_id=True)):
...     account_id: int
...     number: str
...     description: str
...     is_active: bool
  • Via a subclass of a structure, such as xmodel.rest.RestStructure, and setting it's xmodel.base.structure.BaseStructure.api_options to a default set that are used by default for new Model sub-classes that use that Structure subclass. Here is an example:
>>> from typing import TypeVar
>>> from xmodel import Field
>>>
>>> F = TypeVar(name="F", bound=Field)
>>> class AlwaysEnableCacheByIDStructure(RestStructure[F]):
...     # todo: Have ability to set a default value for api_options on the Api class
...     #   Right now you can only set this on a Model or Structure class.
...     api_options = ApiOptions(cache_by_id=True)

The strong cache is useful for caching objects that almost never change.

Weak-Ref Caching

Caching objects weakly is also disabled by default.

The weak-caching is nice, because there are situations where various object will reference the same object. Take for instance order and order-lines. The order-lines would have a one-to-one relationship back to the order object, and there is no need to lookup the same order object over and over again if you ask each order-line for it's order-object.

This is where the weak-cache can shine. The ORM can store temporary references to objects by 'id' and check this cache to retrieve them later instead of having to do an actual fetch-request.

Another place this can be useful is when query objects that are in a tree. And objects parent could be referenced by several children.

You can enable weak-caching via the xmodel.weak_cache_pool.WeakCachePool. xmodel imports this, so you can import it easily via:

>>> from xmodel import WeakCachePool

There is an enable property on it that you set to True to enable the caching. See xmodel.weak_cache_pool.WeakCachePool.enable.

It's a xinject.context.Dependency, and so can be used like any other normal resource. You can set the enable property on the current resource to make it more permently on.

>>> WeakCachePool.grab().enabled = True

Or you can temporarly enable it by creating a new xmodel.weak_cache_pool.WeakCachePool object and activating it temporarily.

>>> from xmodel import WeakCachePool
>>> @WeakCachePool(enabled=True)
>>> def lambda_event_handler(event, context):
...    pass

The most recent WeakCachePool is the one that is used, and the weak refrences are stored inside it. So when a WeakCachePool is deactivated and thrown-away, it will forgot anything that was weakly cached in it. Same thing happens when activating a new WeakCachePool, it will not use the previous pool for anything until the new WeakCachePool is deactivated.

This means, when you activate a new WeakCachePool, you are gurateed to always request new objects instead of using previously cached ones.

Expand source code
"""
Provides easy way to map dict to/from Full-Fledged 'JsonModel' object.

Also, an abstract RemoteModel interface used in xmodel-rest and xdynamo along with
some common code.

.. important:: Doc-comments in varius classes have out-of-date/broken refs; will be fixed soon.


.. important:: Docs Below Are OUT OF DATE!!!
    Most of the docs below belong int xmodel-rest and xdynamo.
    We will revamp them soon, and put more into the README.md as well.
    Currently, I would look at the docs above or README.md for info on how to use
    `JsonModel` class, which is the main-class of this library.


# Old Docs Below - Need A Lot Of Updates (ORM is old reference)

Used to be called the ORM library, that reference will be removed/updated as we get the docs
back in shape here soon. For now, they are older references that may not be entirely accurate
anymore.

## ORM Library Overview
[orm-library-overview]: #orm-library-overview

Library is intended to be used as a way to consolidate and standardize how we work with Model
classes  that can represent data from various different locations.  It can be used as an easy way
to communicate to/from our various places where we store models/information.

Right now this includes:

- Our own API's, such as Account, Auth and Founder's club API's.
- Dynamo tables.
- Big commerce
- Hubspot

Some reasons to use orm:

- The foundation for `xmodel-rest` library's Model objects.
    - Let's us easily map objects into/out of various rest API's.
    - Handles breaking up requests to get objects by id transparently into
        several sub-requests.
    - Does pagination for you automatically
    - Can lazily and prefetch0in-bulk child objects.
- Useful for accessing generally restful API's.
    - hubspot and bigcommerce projects use to to access their services api.
- Consistant interface, works the same wherever it's used
    (vs one-off methods inside the project doing the same things in diffrent ways).
- `xmodel.dynamo`: A library to map objects into Dynamo, we consolidated code from
    a few diffrent projects
    - Easily map objects into and out of Dynamo.
    - No need to duplicate code for things like paginating the results.
    - Add features or fix bugs in library, all other projects that use it benefit.
    - Can figure out best way to query dynamo for you automatically.

Some of the Models classes are kept directly in their respective projects (such as with hubspot).
In these cases, you can import that project as a Library to utilize it's Model's and to also
utilize it's other code/processes (which probably work closely with the Model's in question).

## Model Fields
[model-fields]: #model-fields

You can create your own Models. For a real example, see `hubspot.api.api.Contact`.

.. note:: (side-note: We should have put it in "hubspot.api.contact.Contact" or some such).

Here is a basic Model below that I will use for illustrative purposes.
It's a basic model that is using the standard functionality, with the only customization
being the `base_url` which is one of elements that construct's the url/path to it's endpoint.

### Basic Model Example
[model-fields]: #basic-model-example

>>> from xmodel import Field, BaseModel
>>> import datetime as dt
>>>
>>> class MyModel(
...    BaseModel,
...    base_url="accounts"  # <-- Class argument passed to underlying Structure object.
... ):
...    my_attribute: str  # <-- Automatically maps to API without extra effort
...    created_at: dt.datetime  # <-- Can auto-convert other types for you
...
...    other_attribute: int = Field(read_only=True)  # <-- Customize options
...
...    _will_not_map_to_api: int  # <-- Anything that begins with '_' won't map
...    also_will_not_map = None  # <-- No type-hint, 'Field' not auto-created

### Type-hints
[type-hints]: #type-hints

When Model classes are created, they will lazily find type-hinted attributes and determine
if a `xmodel.fields.Field` should automatically be created for them.
A `xmodel.fields.Field` is what is used by the sdk to map an attribute into it's corresponding
JSON field that is sent/retrieved from the API service.

You can specify other types, such as datetime/date types.
The SDK has a default converter in place for datetime/date types.
You can define other default converters, see [Type Converters](#type-converters) for more details.
Converters can also be used on a per-field basis via `xmodel.fields.Field.converter`.

The Model will enforce these type-hints, and use a converter if needed and one is available.

For example, if you try do do this:


>>> obj = MyModel()
>>> obj.my_attribute = 123
>>> obj.my_attribute
"123"


Notice how the output of the attribute is a str now and not an int. The Model will automatically
realize that it needs to be a string and try to convert it into a string for you.
If this is unsuccessful, it will raise an exception.

When this model is initialized from JSON, it does the same conversions. When grabbing this
object from the API it will automatically create the Model from the JSON it receives from API.

If you wish, you can also pass in your own dict structure when creating an object. The `Dict`
could have come from a JSON string.

>>> import json
>>> parsed_values = json.loads('{"my_attribute": 1234}')
>>> obj = MyModel(parsed_values)
>>> obj.my_attribute
"1234"

### Field Objects
[field-objects]: #field-objects

A `xmodel.base.model.BaseModel` has a set of `xmodel.fields.Field`'s that define how each
field will map to an attribute to/from JSON, which we currently use for all of our APIs.
See [JSON](#json) for more details.

You can get a list of `xmodel.fields.Field`'s via `xmodel.base.api.BaseApi.fields`.
This will allow you do iterate over all the fields the system will use when interacting
with JSON documents.

If you don't allocate a `xmodel.fields.Field` object directly on the Class at class definition
time we will auto-generate them for you. It will only do this for fields that have a type-hint.
If there is no type-hint, we won't auto-allocate a Field for it, and hence we won't
map it to/from the [JSON](#json), enforce the types or auto-convert them.
See [Type Hints](#type-hints).

#### Field Subclasses

You can have your own Field sub-class if you wish. To guarantee all auto-generated fields
use you class, you can set the type on `xmodel.base.structure.Structure.field_type`.

This needs to be set before any Model class that uses it is initialized.
You can do that by subclassing `xmodel.base.structure.Structure` and setting it at class
definition time. You then tell your BaseApi's to use your new structure.

For an example of doing all of this and also creating a custom `xmodel.fields.Field` subclass,
see `xmodel.dynamo.DynField`. We use this in our Dynamo-related code to put additional
options on model Fields that are only really useful for Dynamo models.

But the general idea is this:

>>> from xmodel import BaseStructure.....
# todo: Finish this example.

### JSON
[JSON]: #JSON

Right now all of our API's accept and return JSON.
The Models handle JSON natively.  This means you can also use Model's
to more easily deal with JSON without necessarily having to use any of the
`xmodel.rest.RestClient` aspects (send/receive).
This is what we did with Dynamo at first, we simply grabbed the json via `xmodel.base.api.json`
and send that directly into boto.
Later on we put together a special `xmodel.dynamo.DynClient`
to automatically send/receive it via boto (wraps boto).

Some the the reasons why it may be easier is due to the Model's in combination with the Fields.
You can easily define a mapping and automatic type-conversion with only a simple Model defintion.

.. hint:: What about API's that use a non-JSON format?
    If we ever have an API that has some other format, we would have a
    `xmodel.rest.RestClient` subclass that would handle mapping it to/from the JSON that
    we use on the Models. After RestClient gets a response, it would make a Dict out of it as if
    it got it from JSON and give that to the Model; and vic-versa (map from Dict into API format).


## Model.api

You can also export or update an existing `xmodel.base.model.BaseModel` object via methods
under a special attribute `xmodel.base.model.BaseModel.api`. This attribute has a reserved name
on every Model. This attribute is how the Model interfaces with the rest of the SDK.
That way the rest of the namespace for the Model attributes is available for use by Model subclass.

You can update an object via a dict from parse JSON via
`xmodel.base.api.BaseApi.update_from_json`.
Exporting JSON is easily done via `xmodel.base.api.BaseApi.json`.  Both of these methods
accept/return a `xmodel.types.JsonDict`, which is just a `dict` with `str`
keys and `Any` value.

`xmodel.base.model.BaseModel.api` is also how you can easily get/send objects to/from the
API service.


There are various ways to change/customize a model, keep reading further.

## BaseApi Class

One of the more important classes is `xmodel.base.api.BaseApi`.

For an overview of the class see [BaseApi Class Overview](./api.html#api-class-overview)
api.html#use-of-type-hints-for-changing-used-type

The class is a sort of central hub, it's where you can specify which types are allocated
for each sub-class. This is done via type-hints (typehints are read and used to allocate
correct class).

For more details see
[Use of Type Hints for Changing Type Used](./api.html#use-of-type-hints-for-changing-used-type)

### Type Converters
[type-converters]: #type-converters

The mapping of basic types to their converter function lives at
`xmodel.base.api.BaseApi.default_converters`. Normally you could customize this by
by subclassing `xmodel.base.api.BaseApi` with your own version for you Model(s).
You can also change it dynamically via adjusting `xmodel.base.api.BaseApi.default_converters`.

For the default converter map, see `xmodel.converters.DEFAULT_CONVERTERS`.

.. todo::
    ## This is how I want it to work in the future:

    Something to keep in mind is when the xmodel.base.api.BaseApi converts a type, and it needs
    a lookup a default converter it uses `xmodel.base.api.BaseApi.get_default_converter`.
    This method first checks it's self, and if type to convert is not in dict, it will check
    the superclasses default converters and so on until one is found.

    This means you can override a type conversion from a super class, or let it be used if it works
    as needed. One example of this is how hubspot uses a time-stamp integer to communicate time
    but most other systems use the normal ISO date/time format. So for the BaseApi class that all
    hubspot Model's use, they have the datetime converter overriden with a special Hubspot version.

You can also set a converter per-field via a callback on a `xmodel.fields.Field` object.

All converters have a calling convention, see `xmodel.fields.Converter` for details.

## RestClient Class

.. todo::: Section is unfinished, needs to be fleshed out more.

The config object that this api uses, can be customized per-model. All you have to
do is this to make it a different type::


    class MyClient():
        # Customize RestClient class in some way....
        my_custom_var: str = ConfigVar("MY_CUSTOM_ENVIRONMENTAL_VAR", "default")

    class MyApi(base.BaseApi[T]):
        client: MyClient

    class MyModel(base.model['MyModel'], endpoint_url="custom/path"):
        api: MyApi


The type-hints are enough to tell the system what types to use. They also will
tell any IDE in use about what type it should be, for type-completion.
So it's sort of doing double-duty to both tell IDE what it is and tell class what type to allocate
for the attribute when creating the class/object.

.. todo: Make changing default_converters work as expected [api-class vs api-instance];
    and then talk about te default type-converters here.

## Related Child Model's
[child-models]: #child-models

Going back to this example (from end of the [ORM Library Overview](#orm-library-overview) section):

>>> from some_lib.account import Account
>>> account = Account.api.get_via_id(3)
>>> print(account.account_no)
"3"

We will look at the this Account model more closely, it has a good example of using child objects.
Here is a simplified version of the Account Model:

```python
class PhoneNumber(AccountModel['PhoneNumber'], base_url="account/phone_numbers"):
    account_id: int
    number: str
    description: str
    is_active: bool

class Account(AccountModel['Account'], base_url="accounts"):
    # Configure a more specific api to use with the Accounts endpoint.
    api: AccountsEndpointApi[Account]

    # This is generally very useful for Account objects,
    # don't exclude updated_at by default.
    updated_at: dt.datetime = Field(exclude=False)

    account_no: str
    first_name: str
    last_name: str
    preferred_name: str
    preferred_phone_number: PhoneNumber
    preferred_address: Address
```

Here is an example of getting that objects preferred phone number:

>>> print(account.preferred_phone_number.number)
"8015551234"

By default `preferred_phone_number` is currently `None` (internally),
so the system knows that the `PhoneNumber` object has not been retrieved from the API yet.
It also knows the id for the preferred_phone_number. It's stored on the account object via
`preferred_phone_number_id` (via JSON from api).
The ORM stored this number internally when the object was fetched.

If you define a field like this in the object:

>>> preferred_phone_number_id: int

Instead of storing the number internally, it would store it here instead
(and you can get/set it as needed).
You can also get/set it by setting the `id` field of the child object, like so:

>>> obj: Account
>>> obj.preferred_phone_number.id
123456

If this `id` is known to the Model, meaning that it was fetched when the object was retrieved
from api (or set to something via `preferred_phone_number_id`); The sdk can lazily lookup
the object on demand when it's asked for.
It knows `preferred_phone_number` is a `PhoneNumber` model type
(and that it's also associated with a diffrent api endpoint) and it knows the id,
so it simply asks for it on demand/lazily via `ChildType.api.get_via_id`
(aka: `xmodel.base.api.BaseApi.get_via_id`).


It automatically takes this `preferred_phone_number_id`` and looks-up the preferred phone number
on the spot when you ask for it:

>>> obj.preferred_phone_number

This object is stored under `preferred_phone_number` so in the future it already has the object
when something asks for it again.

### Auto Prefetch Children
[auto-prefetch-children]: #auto-prefetch-children

You can also pre-fetch these child objects in bulk if you have a collection of model objects (such
as a `List` of `some_lib.account.Account`'s) via `xmodel.children.bulk_request_lazy_children`.
This is much more efficient if you have a lot of objects because it can grab many of the children
pre-request.

.. todo:: At some point the xmodel-rest will probably fetch many child objects lazily in bulk.
    When someone accesses one lazily, it could grab more for other Model objects that don't have
    their children fetched yet. We just put in a weak-ref cache, so using this we could
    find the ones that we have fetched in the past and and are still around.
    We could fetch their children too at the same time in the same request in bulk
    (ie: so we fetch original child requested, along with 50 more or so via a single request).


You can also have the xmodel do this automatically as it receives pages of objects via
`xmodel.options.ApiOptions.auto_get_child_objects` like so:

>>> Account.api.options.auto_get_child_objects = True

This sets this option for this Model type in the current `xinject.context.XContext`.
If you make a new XContext, and then throw the XContext away, it will revert these option changes.

>>> from xinject.context import XContext
>>>
>>> # Starts out as False....
>>> assert not Account.api.options.auto_get_child_objects
>>>
>>> with XContext():
...     Account.api.options.auto_get_child_objects = True
...     # Returned generator will 'remember' the options at time of creation
...     accounts_gen_with_children_pre_fetched = Account.api.get(top=100)
>>>
>>> # After XContext is gone, it reverts back to what it was before
>>> assert not Account.api.options.auto_get_child_objects

You can use this to grab all of the accounts. The below returns a Generator and will grab a page
of Account objects at a time, but still returning each object individually via the generator.

>>> [account.id for account in Account.api.get()]
[1, 2, 3, 4, 5, 118, 127, ...]

This is to help limit memory usage. It will also allow the sdk to asynchronously grab multiple
pages in the future [pre-fetch them] without changing the sdk's public interface to other projects.

.. todo:: Asynchronously pre-fetch pages of objects as the generator iterates though result set.

## Caching

There is a strong-ref and weak-ref caching system in the ORM you can take advantage of,
depending on the situation.

By default, they are both disabled.  They are explicitly an opt-in feature.

### Strong-Ref Caching

You can enable strong-ref caching in three ways currently:

-  Set it on directly on `xmodel.base.api.BaseApi.options.cache_by_id=True`

-  Set `cache_by_id=True` as one of the model classes options, like so:

>>> from xmodel import ApiOptions
>>> class MyModel(RestModel['MyModel'], api_options=ApiOptions(cache_by_id=True)):
...     account_id: int
...     number: str
...     description: str
...     is_active: bool


-  Via a subclass of a structure, such as `xmodel.rest.RestStructure`,
   and setting it's `xmodel.base.structure.BaseStructure.api_options` to a default set
   that are used by default for new Model sub-classes that use that Structure subclass.
   Here is an example:

>>> from typing import TypeVar
>>> from xmodel import Field
>>>
>>> F = TypeVar(name="F", bound=Field)
>>> class AlwaysEnableCacheByIDStructure(RestStructure[F]):
...     # todo: Have ability to set a default value for api_options on the Api class
...     #   Right now you can only set this on a Model or Structure class.
...     api_options = ApiOptions(cache_by_id=True)

The strong cache is useful for caching objects that almost never change.


### Weak-Ref Caching

Caching objects weakly is also disabled by default.

The weak-caching is nice, because there are situations where various object will reference
the same object. Take for instance order and order-lines.  The order-lines would have a
one-to-one relationship back to the order object, and there is no need to lookup the same
order object over and over again if you ask each order-line for it's order-object.

This is where the weak-cache can shine. The ORM can store temporary references to objects
by 'id' and check this cache to retrieve them later instead of having to do an actual
fetch-request.

Another place this can be useful is when query objects that are in a tree.
And objects parent could be referenced by several children.

You can enable weak-caching via the `xmodel.weak_cache_pool.WeakCachePool`.
`xmodel` imports this, so you can import it easily via:

>>> from xmodel import WeakCachePool

There is an enable property on it that you set to `True` to enable the caching.
See `xmodel.weak_cache_pool.WeakCachePool.enable`.

It's a `xinject.context.Dependency`, and so can be used like any other normal resource.
You can set the enable property on the current resource to make it more permently on.

>>> WeakCachePool.grab().enabled = True

Or you can temporarly enable it by creating a new `xmodel.weak_cache_pool.WeakCachePool`
object and activating it temporarily.

>>> from xmodel import WeakCachePool
>>> @WeakCachePool(enabled=True)
>>> def lambda_event_handler(event, context):
...    pass

The most recent WeakCachePool is the one that is used, and the weak refrences are stored inside it.
So when a WeakCachePool is deactivated and thrown-away, it will forgot anything that was weakly
cached in it. Same thing happens when activating a new WeakCachePool, it will not use the
previous pool for anything until the new WeakCachePool is deactivated.

This means, when you activate a new WeakCachePool, you are gurateed to always request new
objects instead of using previously cached ones.

"""

from .base import (
    BaseModel, BaseApi, BaseStructure
)
from .base.fields import Field, Converter
from .errors import XModelError
from .json import JsonModel
from .remote.weak_cache_pool import WeakCachePool


__all__ = [
    'BaseModel',
    'BaseApi',
    'BaseStructure',
    'Field',
    'Converter',
    'XModelError',
    'JsonModel'
]

__version__ = '1.0.1'

Sub-modules

xmodel.base
xmodel.common
xmodel.converters
xmodel.errors
xmodel.json

The purpose of the JsonModel is to be able to create model classes from json data we have retrieved from somewhere that is not an API endpoint or as a …

xmodel.remote
xmodel.util

Classes

class BaseApi (*, api: BaseApi[M] = None, model: BaseModel = None)

This class is a sort of "Central Hub" that ties all intrested parties together.

You can get the correct instance via BaseModel.

In order to reduce any name-collisions for other normal Model attributes, everything related to the BaseApi that the BaseModel needs is gotten though via this class.

You can get the BaseApi instance related to the model via BaseModel.api.

Example:

>>> obj = BaseModel.api.get_via_id(1)

For more information see BaseApi Class Overview.

Warning: You can probably skip the rest (below)

Most of the time you don't create BaseApi objects your self, and so for most people you can skip the following unless you want to know more about internal details.

Init Method Specifics

Normally you would not create an BaseApi object directly your self. BaseModel's know how to do this automatically. It happens in BaseModel.__init_subclass__().

Details about how the arguments you can pass are below.

BaseModel Class Construction:

If you provide an api arg without a model arg; we will copy the BaseApi.structure into new object, resetting the error status, and internal BaseApi._state to None. This api object is supposed to be the parent BaseModel's class api object.

If both api arg + model arg are None, the BaseModel is the root/generic BaseModel (ie: it has no parent BaseModel).

This is what is done by BaseModel classes while the class is lazily loading and creating/configuring the BaseModel class and it's associated BaseApi object (accessible via BaseModel.api)

BaseModel Instance Creation:

If you also pass in a model arg; this get you a special copy of the api you passed in for use just with that BaseModel instance. The model BaseApi._state will be allocated internally in the init'd BaseApi object. This is how a BaseModel instance get's it's own associated BaseApi object (that's a different instance vs the one set on BaseModel class when the BaseModel class was originally constructed).

All params are optional.

Args

api

The "parent" BaseApi obj to copy the basic structure from as a starting point, etc. The superclasses BaseApi class is passed via this arg. This is only used when allocating a new BaseApi object for a new BaseModel class (not an instance, a model class/type). This BaseApi object is used for the class-level BaseModel api object; ie: via "ModelClass.api"

See above "BaseModel Class Construction" for more details.

model

BaseModel to associate new BaseApi obj with. This is only used to create a new BaseApi object for a BaseModel instance for an already-existing type. ie: for BaseModel object instances.

See above "BaseModel Instance Creation" for more details.

Expand source code
class BaseApi(Generic[M]):
    """
        This class is a sort of "Central Hub" that ties all intrested parties together.

        You can get the correct instance via `xmodel.base.model.BaseModel`.

        In order to reduce any name-collisions for other normal Model attributes, everything
        related to the BaseApi that the `xmodel.base.model.BaseModel` needs is gotten though via
        this class.

        You can get the BaseApi instance related to the model via
        `xmodel.base.model.BaseModel.api`.

        Example:

        >>> obj = BaseModel.api.get_via_id(1)

        For more information see [BaseApi Class Overview](#api-class-overview).
    """

    # Defaults Types to use. When you sub-class BaseApi, you can declare/override these type-hints
    # and specify a different type... The system will allocate and use that new type instead
    # for you automatically on any instances created of the class.
    #
    # The BaseAuth won't modify the request to add auth; so it's safe to use as the base default.
    #
    # These are implemented via `@property` methods further below, but these are the type-hints.
    #
    # The properties and the __init__ method all use these type-hints in order to use the correct
    # type for each one on-demand as needed. For details on each one, see the @property method(s).
    #
    # PyCharm has some sort of issue, if I provide property type-hint and then a property function
    # that implements it. For some reason, this makes it ignore the type-hint in subclasses
    # but NOT in the current class.  It's some sort of bug. I get around this bug by using a
    # different initial-name for the property by pre-pending a `_` to it,
    # and then setting it to the correct name later.
    #
    # Example: `structure = _structure` is done after `def _structure(...)` is defined.

    # See `_structure` method for docs and method that gets this value.
    structure: BaseStructure[Field]

    @property
    def _structure(self):
        """
        Contain things that don't vary among the model instances;
        ie: This is the same object and applies to all instances of a particular BaseModel class.

        This object has a list of `xmodel.fields.Field` that apply to the
        `xmodel.base.model.BaseModel` you can get via
        `xmodel.base.structure.Structure.fields`; for example.

        This is currently created in `BaseApi.__init__`.

        BaseApi instance for a BaseModel is only created when first asked for via
        `xmodel.base.model.BaseModel.api`.

        Returns:
            BaseStructure: Structure with correct field and model type in it.
        """
        return self._structure

    # PyCharm has some sort of issue, if I provide property type-hint and then a property function
    # that implements it. For some reason, this makes it ignore the type-hint in subclasses
    # but NOT in the current class.  It's some sort of bug. This gets around it since pycharm
    # can't figure out what's going on here.
    structure = _structure

    _structure = None
    """ See `BaseApi.structure`.
    """

    # ------------------------------
    # --------- Properties ---------

    default_converters: Dict[Type[Any], Converter] = None
    """
    For an overview of type-converts, see
    [Type Converters Overview](./#type-converters).

    The class attribute defaults to `None`, but an instance/object will always have
    some sort of dict in place (happens during init call).

    Notice the `todo` note in the [overview](./#type-converters). I want it to work that way in the
    future (so via `BaseApi.set_default_converter` and `BaseApi.get_default_converter`).
    It's something coming in the future. For now you'll need to override
    `default_converters` and/or change it directly.

    You can provide your own values for this directly in a sub-class,
    when an BaseApi or subclass is created, we will merge converters in this order,
    with things later in the order taking precedence and override it:

    1. `xmodel.converters.DEFAULT_CONVERTERS`
    2. `BaseApi.default_converters` from `xmodel.base.model.BaseModel.api` from parent model.
        The parent model is the one the model is directly inheriting from.
    3. Finally, `BaseApi.default_converters` from the BaseApi subclass's class attribute
       (only looks on type/class directly for `default_converters`).

    It takes this final mapping and sets it on `self.default_converters`,
    and will be inherited as explained on on line number `2` above in the future.

    Default converters we have defined at the moment:

    - `xmodel.converters.convert_json_date`
    - `xmodel.converters.convert_json_datetime`
    - And a set of basic converters via `xmodel.converters.ConvertBasicType`, supports:
        - float
        - bool
        - str
        - int

    See `xmodel.converters.DEFAULT_CONVERTERS` to see the default converters map/dict.

    Maps type-hint to a default converter.  This converter will be used for `TypeValue.convert`
    when the model BaseStructure is create if none is provided for it at field definition time
    for a particular type-hint. If a type-hint is not in this converter, no convert is
    called for it.

    You don't need to provide one of these for a `xmodel.base.model.BaseModel` type-hint,
    as the system knows to call json/update_from_json on those types of objects.

    The default value provides a way to convert to/from a dt.date/dt.datetime and a string.
    """

    # def set_default_converter(self, type, converter):
    #     """ NOT IMPLEMENTED YET -
    #     .. Todo:: Josh: These were here to look up a converter from a parent if a child does not
    #         have one  I have not figured out what I want to do here quite yet...
    #
    #         See todo at [Type Converters](./#type-converters) for an explanation of what this may
    #         be in the future.
    #
    #     """
    #     raise NotImplementedError()
    #
    # def get_default_converter(self, type) -> Optional[Converter]:
    #     """ NOT IMPLEMENTED YET -
    #     .. Todo:: Josh: These were here to look up a converter from a parent if a child does not
    #         have one  I have not figured out what I want to do here quite yet...
    #
    #         See todo at [Type Converters](./#type-converters) for an explanation of what this may
    #         be in the future.
    #     """
    #     raise NotImplementedError()

    # ------------------------------
    # --------- Properties ---------

    @property
    def model_type(self) -> Type[M]:
        """ The same BaseApi class is meant to be re-used for any number of Models,
            and so a BaseModel specifies it's BaseApi type as generic `BaseApi[M]`. In this case
            is the BaseModel it's self.  That way we can have the type-system aware that different
            instances of the same BaseApi class can specify different associated BaseModel classes.

            This property will return the BaseModel type/class associated with this BaseApi
            instance.
        """
        # noinspection PyTypeChecker
        return self.structure.model_cls

    # used as an internal class property
    _CACHED_TYPE_HINTS = {}

    @classmethod
    def resolved_type_hints(cls) -> Dict[str, Type]:
        if hints := BaseApi._CACHED_TYPE_HINTS.get(cls):
            return hints

        hints = get_type_hints(cls)
        BaseApi._CACHED_TYPE_HINTS[cls] = hints
        return hints

    # ---------------------------
    # --------- Methods ---------

    # noinspection PyMissingConstructor
    def __init__(self, *, api: "BaseApi[M]" = None, model: BaseModel = None):
        """

        .. warning:: You can probably skip the rest (below)
            Most of the time you don't create `BaseApi` objects your self, and so for most people
            you can skip the following unless you want to know more about internal details.

        # Init Method Specifics

        Normally you would not create an `BaseApi` object directly your self.
        `xmodel.base.model.BaseModel`'s know how to do this automatically.
        It happens in `xmodel.base.model.BaseModel.__init_subclass__`.

        Details about how the arguments you can pass are below.

        ## BaseModel Class Construction:

        If you provide an `api` arg without a `model` arg; we will copy the `BaseApi.structure`
        into new object, resetting the error status, and internal `BaseApi._state` to None.
        This `api` object is supposed to be the parent BaseModel's class api object.

        If both `api` arg + `model` arg are `None`, the BaseModel is the root/generic BaseModel
        (ie: it has no parent BaseModel).

        This is what is done by BaseModel classes while the class is lazily loading and
        creating/configuring the BaseModel class and it's associated `BaseApi` object
        (accessible via `xmodel.base.model.BaseModel.api`)

        ## BaseModel Instance Creation:

        If you also pass in a `model` arg; this get you a special copy of the api you passed in
        for use just with that BaseModel instance. The model `BaseApi._state` will be allocated
        internally in the init'd BaseApi object. This is how a `xmodel.base.model.BaseModel`
        instance get's it's own associated `BaseApi` object
        (that's a different instance vs the one set on BaseModel class when the BaseModel class
        was originally constructed).

        All params are optional.

        Args:
            api: The "parent" BaseApi obj to copy the basic structure from as a starting point,
                etc.
                The superclasses BaseApi class is passed via this arg.
                This is only used when allocating a new `BaseApi` object for a new
                `xmodel.base.model.BaseModel` class (not an instance, a model class/type).
                This BaseApi object is used for the class-level BaseModel api object;
                ie: via "ModelClass.api"

                See above "BaseModel Class Construction" for more details.

            model:  BaseModel to associate new BaseApi obj with.
                This is only used to create a new BaseApi object for a
                `xmodel.base.model.BaseModel`
                instance for an already-existing type. ie: for BaseModel object instances.

                See above "BaseModel Instance Creation" for more details.
        """
        if api and model:
            raise XModelError(
                f"You can't pass in an BaseApi {api} and BaseModel {model} simultaneously."
            )

        if model:
            api = type(model).api

        if not api:
            assert not model, "You can't pass in a model without an associated api/model obj."

        if model:
            # If we have a model, the structure should be exactly the same as it's BaseModel type.
            self._structure = api.structure
            self._api_state = PrivateApiState(model=model)
            self.default_converters = api.default_converters
            return

        # If We don't have a BaseModel, then we need to copy the structure, it could change
        # because we are being allocated for a new BaseModel type at the class/type level;
        # this means we are not associated with a specific BaseModel instance, only a BaseModel
        # type.

        # We lookup the structure type that our associated model-type/class wants to use.
        structure_type = type(self).resolved_type_hints().get(
            'structure',
            BaseStructure[Field]
        )

        args = typing_inspect.get_args(structure_type)
        field_type = args[0] if args else Field

        # We have a root BaseModel with the abstract BaseModel as its super class,
        # in this case we need to allocate a blank structure object.
        # todo: allocate structure with new args
        # We look up the structure type that our associated model-type/class wants to use.
        existing_struct = api.structure if api else None
        self._structure = structure_type(
            parent=existing_struct,
            field_type=field_type
        )

        # default_converters is a mapping of type to convert too, and a converter callable.
        #
        # We want to inherit from the parent and converters they already have defined.
        #
        # Take any parent converters as they currently exist, and use them as a basis for our
        # converters. Then take any converters directly assigned to self and override the any
        # parent converters, when they both have a converter for the same key/type.
        self.default_converters = {
            **DEFAULT_CONVERTERS,
            **(api.default_converters or {} if api else {}),
            **(type(self).default_converters or {}),
        }

    # ----------------------------------------------------
    # --------- Things REQUIRING an Associated BaseModel -----

    @property
    def model(self) -> M:
        """ REQUIRES associated model object [see doc text below].

        Gives you back the model associated with this api. If this BaseApi obj is associated
        directly with the BaseModel class type and so there is no associated model, I will
        raise an exception.

        Some BaseApi methods are dependant on having an associated model, and when they ask for it
        and there is None, this will raise an exception for them. The first line of the doc
        comment tells you if it needs one.  Normally, it's pretty obvious if the method
        will need the model, due to what it will return to you (ie: if it would need model attrs).

        The methods that are dependant on a model are ones, like 'json', where it returns the
        JSON for a model.  It needs a model to get this data.

        If you access an object api via a BaseModel object, that will be the associated model.
        If you access it via a BaseModel type/class, it will be directly associated with the model
        class.

        Examples:
        >>> # Grab Account model from some_lib (as an example).
        >>> from some_lib.account import Account
        >>>
        >>> # api object is associated with MyModelClass class, not model obj.
        >>> Account.api
        >>>
        >>> account_obj = Account.api.get_via_id(3)
        >>> # api is associated with the account_obj model object.
        >>> account_obj.api
        >>>
        >>> # This sends object attributes to API, so it needs an associated
        >>> # BaseModel object, so this works:
        >>> account_obj.api.send()
        >>>
        >>> # This would produce an exception, since it would try to get BaseModel
        >>> # attributes to send. But there is no associated model.
        >>> Account.api.send()

        """
        api_state = self._api_state
        assert api_state, "BaseApi needs an attached model obj and there is no associated " \
                          "model api state."
        model = api_state.model
        assert model, "BaseApi needs an attached model obj and there is none."
        return model

    def get_child_without_lazy_lookup(
            self,
            child_field_name,
            *,
            false_if_not_set=False,
    ) -> Union[M, None, bool, NullType]:
        """ REQUIRES associated model object [see self.model].

        If the child is current set to Null, or an object, returns that value.
        Will NOT lazily lookup child, even if its possible to do so.

        :param child_field_name: The field name of the child object.
        :param false_if_not_set:
            Possible Values [Default: False]:
                * False: Return None if nothing is currently set.
                * True:  Return False if nothing is currently set. This lets you distinguish
                  between having a None value set on field vs nothing set at all.
                  Normally this distinction is only useful internally in this class,
                  external users probably don't need this option.
        """

        model = self.model

        if not self.structure.is_field_a_child(child_field_name):
            raise XModelError(
                f"Called get_child_without_lazy_lookup('{child_field_name}') but "
                f"field ({child_field_name}) is NOT a child field on model ({model}).")

        if child_field_name in model.__dict__:
            return getattr(model, child_field_name)

        if false_if_not_set:
            return False

        return None

    @property
    def have_changes(self) -> bool:
        """ Is True if `self.json(only_include_changes=True)` is not None;
            see json() method for more details.
        """
        log.debug(f"Checking Obj {self.model} to see if I have any changes [have_changes]")
        return self.json(only_include_changes=True) is not None

    def json(
        self,
        only_include_changes: bool = False,
        log_output: bool = False,
        include_removals: bool = False
    ) -> Optional[JsonDict]:
        """ REQUIRES associated model object (see `BaseApi.model` for details on this).

        Return associated model object as a JsonDict (str keys, any value), ready to be encoded
        via JSON encoder and sent to the API.

        Args:
            only_include_changes: If True, will only include what changed in the JsonDict result.
                Defaults to False.
                This is normally set to True if system is sending this object via PATCH, which is
                the  normal way the system sends objects to API.

                If only_include_changes is False (default), we always include everything that
                is not 'None'.
                When a `xmodel.base.client.BaseClient` subclass
                (such as `xmodel.rest.RestClient`)
                calls this method, it will pass in a value based on it's own
                `xmodel.rest.RestClient.enable_send_changes_only` is set to
                (defaults to False there too).
                You can override the RestClient.enable_send_changes_only at the BaseModel class
                level by making a RestClient subclass and setting `enable_send_changes_only` to
                default to `True`.

                There is a situations where we have to include all attributes, regardless:
                    1. If the 'id' field is set to a 'None' value. This indicates we need to create
                       a new object, and we are not partially updating an existing one, even if we
                       got updated via json at some point in the past.

                As always, properties set to None will *NOT* be included in returned JsonDict,
                regardless of what options have been set.

            log_output (bool): If False (default): won't log anything.
                If True: Logs what method returns at debug level.

           include_removals (bool): If False (default): won't include in response any fields
                that have been removed
                (vs when compared to the original JSON that updated this object).
                The value will be the special sentinel object `Remove`
                (see top of this module/file for `Remove` object, and it's `RemoveType` class).

        Returns:
            JsonDict: Will the needed attributes that should be sent to API.
                If returned value is None, that means only_include_changes is True
                and there were no changes.

                The returned dict is a copy and so can be mutated be the caller.
        """

        # todo: Refactor _get_fields() to return getter/setter closures for each field, and we
        #       can make this whole method more generic that way. We also can 'cache' the logic
        #       needed that way instead of having to figure it out each time, every time.

        structure = self.structure
        model = self.model
        api_state = self._api_state

        json: JsonDict = {}

        field_objs = structure.fields

        # Negate only_include_changes if we don't have any original update json to compare against.
        if only_include_changes and api_state.last_original_update_json is None:
            only_include_changes = False

        # noinspection PyDefaultArgument
        def set_value_into_json_dict(value, field_name, *, json=json):
            # Sets field value directly on json dict or passed in dict...
            if value is not None:
                # Convert Null into None (that's how JSON converter represents a Null).
                json[field_name] = value if value is not Null else None

        for field_obj in field_objs:
            # If we are read-only, no need to do anything more.
            if field_obj.read_only:
                continue

            # We deal with non-related types later.
            related_type = field_obj.related_type
            if not related_type:
                continue

            f = field_obj.name
            if field_obj.read_only:
                continue

            # todo: For now, the 'api-field-path' option can't be used at the same time as obj-r.
            if field_obj.json_path != field_obj.name:
                # I've put in some initial support for this below, but it's has not been tested
                # for now, keep raising an exception for this like we have been.
                # There is a work-around, see bottom part of the message in the below error:
                raise NotImplementedError(
                    f"Can't have xmodel.Field on BaseModel with related-type and a json_path "
                    f"that differ at the moment, for field ({field_obj}). "
                    f"It is something I want to support someday; the support is mostly in place "
                    f"already, but it needs some more careful thought, attention and testing "
                    f"before we should allow it. "
                    "Workaround:  Make an `{field.name}_id` field next to related field on the "
                    "model. Then, set `json_path` for that `{field.name}_id` field, set it to "
                    "what you want it to be. Finally, set the `{related_field.name}` to "
                    "read_only=True. This allows you to rename the `_id` field used to/from api "
                    "in the JSON input/output, but the Model can have an alternate name for the "
                    "related field. You can see a real-example of this at "
                    "`bigcommerce.api.orders._BcCommonOrderMetafield.order"
                )

            obj_type_structure = related_type.api.structure
            obj_type_has_id = obj_type_structure.has_id_field()

            if obj_type_has_id:
                # If the obj uses an 'id', then we have a {field_name}_id we want to
                # send instead of the full object as a json dict.
                #
                # This will grab the id from child obj if it exists, or from a defined field
                # of f"{f}_id" or finally from related id storage.

                # todo: If there is an object with no 'id' value, do we ignore it?
                #   or should we embed full object anyway?

                child_obj_id = api_state.get_related_field_id(f)

                # Method below should deal with None vs Null.
                set_value_into_json_dict(child_obj_id, f"{f}_id")
            else:
                obj: 'M' = getattr(model, f)

                # Related-object has no 'id', so get it's json dict and set that into the output.
                v = obj
                if obj is not Null and obj is not None:
                    # todo: a Field option to override this and always provide all
                    #   values (if object always needs to be fully embedded).
                    v = obj.api.json(only_include_changes=only_include_changes)

                # if it returns None (ie: no changes) and only_include_changes is enabled,
                # don't include the sub-object as a change.
                if v is not None or not only_include_changes:
                    # Method below should deal with None vs Null.
                    set_value_into_json_dict(v, f)

        for field_obj in field_objs:
            # If we are read-only, no need to do anything more.
            if field_obj.read_only:
                continue

            # We don't deal with related-types here.
            if field_obj.related_type:
                continue

            f = field_obj.name
            v = getattr(model, f)
            if v is not None and field_obj.converter:
                # Convert the value....
                v = field_obj.converter(
                    api=self,
                    direction=Converter.Direction.to_json,
                    field=field_obj,
                    value=v
                )

            path = field_obj.json_path
            if not path:
                set_value_into_json_dict(v, f)
                continue

            path_list = path.split(field_obj.json_path_separator)
            d = json
            for name in path_list[:-1]:
                d = d.setdefault(name, {})
            name = path_list[-1]

            # Sets field value into a sub-dictionary of the original `json` dict.
            set_value_into_json_dict(v, name, json=d)

        if include_removals:
            removals = self.fields_to_remove_for_json(json, field_objs)
            for f in removals:
                if f in json:
                    raise XynModelError(
                        f"Sanity check, we were about to overwrite real value with `Remove` "
                        f"in json field ({f}) for model ({self.model})."
                    )

                json[f] = Remove

        # If the `last_original_update_json` is None, then we never got update via JSON
        # so there is nothing to compare, include everything!
        if only_include_changes:
            log.debug(f"Checking Obj {model} for changes to include.")
            fields_to_pop = self.fields_to_pop_for_json(json, field_objs, log_output)

            for f in fields_to_pop:
                del json[f]

            if not json:
                # There were no changes, return None.
                return None
        else:
            due_to_msg = "unknown"
            if not only_include_changes:
                due_to_msg = "only_include_changes is False"
            if api_state.last_original_update_json is None:
                due_to_msg = "no original json value"

            if log_output:
                log.debug(f"Including everything for obj {model} due to {due_to_msg}.")

                # Log out at debug level what we are including in the JSON.
                for field, new_value in json.items():
                    log.debug(
                        f"   Included field ({field}) value ({new_value})"
                    )

        for k, v in json.items():
            # Must use list of JSON, convert any sets to a list.
            if type(v) is set:
                v = list(v)
                json[k] = v

        return json

    def fields_to_remove_for_json(self, json: dict, field_objs: List[Field]) -> Set[str]:
        """
        Returns set of fields that should be considered 'changed' because they were removed
        when compared to the original JSON values used to originally update this object.

        The names will be the fields json_path.
        """
        fields_to_remove = set()
        for field in field_objs:
            # A `None` in the `json` means a null, so we use `Default` as our sentinel type.
            new_value = json.get(field.json_path, Default)
            old_value = self._get_old_json_value(field=field.json_path, as_type=type(new_value))
            if new_value is Default and old_value is not Default:
                fields_to_remove.add(field.json_path)
        return fields_to_remove

    def fields_to_pop_for_json(
            self, json: dict, field_objs: List[Field], log_output: bool
    ) -> Set[Any]:
        """
        Goes through the list of fields (field_objs) to determine which ones have not changed in
        order to pop them out of the json representation. This method is used when we only want to
        include the changes in the json.

        :param json: dict representation of a model's fields and field values as they are currently
            set on the model.
        :param field_objs: List of fields and their values for a model
        :param log_output: boolean to determine if we should log the output or not
        :return: The field keys to remove from the json representation of the model.
        """
        fields_to_pop = set()
        for field, new_value in json.items():
            # json has simple strings, numbers, lists, dict;
            # so makes general comparison simpler.
            old_value = self._get_old_json_value(field=field, as_type=type(new_value))

            if old_value is Default:
                if log_output:
                    log.debug(
                        f"   Included field ({field}) with value "
                        f"({new_value}) because there is no original json value for it."
                    )
            elif self.should_include_field_in_json(
                    new_value=new_value,
                    old_value=old_value,
                    field=field
            ):
                if log_output:
                    log.debug(
                        f"   Included field ({field}) due to new value "
                        f"({new_value}) != old value ({old_value})."
                    )
            else:
                # We don't want to mutate dict while traversing it, remember this for later.
                fields_to_pop.add(field)

        # Map a field-key to what other fields should be included if field-key value is used.
        # For now we are NOT supporting `Field.json_path` to keep things simpler
        # when used in conjunction with `Field.include_with_fields`.
        # `Field` will raise an exception if json_path != field name and include_with_fields
        # is used at the same time.
        # It's something I would like to support in the future, but for now it's not needed.
        # We can assume that `field_obj.name == field_obj.json_path`
        for field_obj in field_objs:
            if not field_obj.include_with_fields:
                continue
            if field_obj.name not in fields_to_pop:
                continue
            if not (field_obj.include_with_fields <= fields_to_pop):
                fields_to_pop.remove(field_obj.name)

        return fields_to_pop

    def should_include_field_in_json(self, new_value: Any, old_value: Any, field: str) -> bool:
        """
        Returns True if the value for field should be included in the JSON.
        This only gets called if only_include_changes is True when passed to self.json::

            # Passed in like so:
            self.json(only_include_changes=True)

        This method is an easy way to change the comparison logic.

        `new_value` could be `xyn_types.default.Default`, to indicate that a value's
        absence is significant (ie: to remove an attribute from destination).

        Most of the time, a value's absence does not affect the destination when object
        is sent to API/Service because whatever value is currently there for the attribute
        is left intact/alone.

        But sometimes a service will remove the attribute if it does not exist.
        When this is the case, the absence of the value is significant for comparison purposes;
        ie: when deciding if a value has changed.


        :param new_value: New value that will be put into JSON.
        :param old_value:
            Old value originals in original JSON [normalized if possible to the same type as
            new_value.
        :param field: Field name.
        :return:
            If True: Will include the fields value in an update.
            If False: Won't include the fields value in an update.
        """
        # Convert old value to set if new value is set and old value is list (from original JSON).
        # If I was really cool :)... I would find out the inner type in case of int/str
        # and to a conversion to compare Apples to Apples.....
        # But trying to minimize changes so I don't conflict as much with soon to be
        # xdynamo feature.
        if type(new_value) is set and type(old_value) is list:
            old_value = set(old_value)

        return new_value != old_value

    def _get_old_json_value(self, *, field: str, as_type: Type = None) -> Optional[Any]:
        """ Returns the old field-values; Will return `Default` if there is no original value.  """
        original_json = self._api_state.last_original_update_json
        if original_json is None:
            # todo: Is there another value we could return here to indicate that we
            #       never got an original value in the first place?
            #
            # todo: Also, think about how we could do above todo ^ per-field
            #       [ie: if field was requested in the first place].
            return Default

        old_value = original_json.get(field, Default)
        if old_value is Default:
            # None is a valid value in JSON,
            # this indicates to do the Default thing/value with this field since we don't have any
            # original value for it.
            return Default

        # json has simple strings, numbers, lists, dict;
        # so makes general comparison simpler.
        old_type = type(old_value)
        if as_type != old_type:
            str_compatible_types = {str, int, float}
            if as_type in str_compatible_types and old_type in str_compatible_types:
                try:
                    # The 'id' field is a string and not an int [for example], so in
                    # general, we want to try and convert the old value into the new
                    # values type before comparison, if possible, for the basic types
                    # of str, int, float.
                    old_value = as_type(old_value)
                except ValueError:
                    # Just be sure it's the same value/type, should be but just in case.
                    old_value = original_json.get(field, None)
                    pass
        return old_value

    def copy_from_model(self, model: BaseModel):
        their_fields = model.api.structure.field_map
        my_fields = self.structure.field_map
        keys = [k for k in their_fields if k in my_fields]

        # Assume we have a model, and are not the class-based `MyModel.api....` version.
        # todo: have `self.model` raise an exception if called on the class api version
        #   (which does not have a related model, just knows about model-type.).
        my_model = self.model
        for k in keys:
            their_value = getattr(model, k)
            if their_value is not None:
                setattr(my_model, k, their_value)

    def update_from_json(self, json: Union[JsonDict, Mapping]):
        """ REQUIRES associated model object [see self.model].

        todo: Needs more documentation

        We update the dict per-key, with what we got passed in [via 'json' parameter]
        overriding anything we got previously. This also makes a copy of the dict, which is
        want we want [no change to modify the incoming dict parameter].
        """

        structure = self.structure
        model = self.model
        api_state = self._api_state

        if not isinstance(json, Mapping):
            raise XModelError(
                f"update_from_json(...) was given a non-mapping parameter ({json})."
            )

        # Merge the old values with the new values.
        api_state.last_original_update_json = {
            **(api_state.last_original_update_json or {}),
            **json
        }

        fields = structure.fields

        values = {}
        for field_obj in fields:
            path_list = field_obj.json_path.split(field_obj.json_path_separator)
            v = json
            got_value = True
            for name in path_list:
                if name not in v:
                    # We don't even have a 'None' value so we assume we just did not get the value
                    # from the api, and therefore we just skip doing anything with it.
                    got_value = False
                    break

                v = v.get(name)
                if v is None:
                    break

            # We map the value we got from JSON into a flat-dict with the BaseModel name as the
            # key...
            if got_value:
                values[field_obj.name] = v if v is not None else Null

        def set_attr_on_model(field, value, model=model):
            """ Closure to set attr on self unless value is None.
            """
            if value is None:
                return
            setattr(model, field, value)

        # Merge in the outer json, keeping the values we mapped [via Field.json_path] for conflicts
        values = {**json, **values}

        # todo: If the json does not have a value [not even a 'None' value], don't update?
        #       We may have gotten a partial update?  For now, always update [even to None]
        #       all defined fields regardless if they are inside the json or not.

        for field_obj in fields:
            # We deal with related types later....
            if field_obj.related_type:
                continue

            f = field_obj.name
            v = values.get(f, Default)

            # A None from JSON means a Null for us.
            # If JSON does not include anything, that's a None for us.
            if v is None:
                v = Null
            elif v is Default:
                v = None

            # Run the converter if needed.
            # If we have a None value, we don't need to convert that, there was no value to
            # convert.
            if field_obj.converter and v is not None:
                v = field_obj.converter(
                    self,
                    Converter.Direction.from_json,
                    field_obj,
                    v
                )

            set_attr_on_model(f, v)

        for field_obj in fields:
            # Ok, now we deal with related types...
            related_type = field_obj.related_type
            if not related_type:
                continue

            f = field_obj.name

            # todo: at some point, allow customization of this via Field class
            #   Also, s tore the id
            f_id_name = f"{f}_id"
            if typing_inspect.get_origin(field_obj.type_hint) is list:
                # todo: This code is not complete [Kaden never finished it up]
                #   for now, just comment out.

                raise NotImplementedError(
                    "Type-hints for xmodel models in this format: `attr: List[SomeType]` "
                    "are not currently supported. We want to support it someday. For now you "
                    "must use lower-cased non-generic `list`. At some point the idea is to "
                    "allow one to do `List[ChildModel]` and then we know it's a list of "
                    "other BaseModel objects and automatically handle that in some way."
                )

                # child_type: 'Type[M]'
                # child_type = typing_inspect.get_args(obj_type)
                # # __args__ returns a tuple of all arguments passed into List[] so we need to
                # # pull the class out of the tuple
                # if child_type:
                #     child_type = child_type[0]
                #
                # child_api: BaseApi
                # child_api = child_type.api
                # if not child_api and child_api.structure.has_id_field:
                #     # TODO: add a non generic Exception for this
                #     raise XModelError(
                #         f"{model} has an attribute with name ({f}) with type-hint List that "
                #         f"doesn't contain an API BaseModel Type as the only argument"
                #     )
                # parent_name = model.__class__.__name__.lower()
                # state.set_related_field_id(f, parent_name)
                # continue

            v = None
            if f in values:
                v = values.get(f, Null)
                if v is not Null:
                    v = related_type(v)

            # Check to see if we have an api/json field for object relation name with "_id" on
            # end.
            if v is None and related_type.api.structure.has_id_field():
                # If we don't have a defined field for this value, check JSON for it and store it.
                #
                # If we have a defined None value for the id field, meaning the field exists
                # in the json, and is set directly to None, then we have a Null relationship.
                # We set that as the value, since there is no need to 'lookup' a null value.
                f_id_value = json.get(f_id_name)
                id_field = structure.get_field(f_id_name)

                if not id_field:
                    id_field = field_obj.related_type.api.structure.get_field('id')

                # Run the converter if needed.
                # If we have a None value, we don't need to convert that, there was no value to
                # convert.
                if id_field and id_field.converter and f_id_value is not None:
                    f_id_value = id_field.converter(
                        self,
                        Converter.Direction.from_json,
                        id_field,
                        f_id_value
                    )

                if f_id_value is None and f_id_name in json:
                    # We have a Null situation.
                    f_id_value = Null

                if f_id_value is not None:
                    # We have an id!
                    # Set the value to support automatic lookup of value, lazily.
                    # This method also takes care to set child object to Null or delete it
                    # as needed depending on the f_id_value and what the child's id field value is.
                    api_state.set_related_field_id(f, f_id_value)
            else:
                # 'v' is either going to be None, Null or an BaseModel object.
                set_attr_on_model(f, v)

    def list_of_attrs_to_repr(self) -> List[str]:
        """" REQUIRES associated model object [see self.model].

        A list of attribute names to put into the __repr__/string representation
        of the associated model object. This is consulted when the BaseModel has __repr__
        called on it.
        """
        names = set()
        model = self.model

        # todo: Move this into pres-club override of list_of_attrs_to_repr in an BaseApi subclass.
        if hasattr(model, 'account_id'):
            names.add('account_id')

        # todo: Consider adding others here, perhaps all defined fields on model that have
        # todo: a non-None value?

        for f in self.structure.fields:
            if f.include_in_repr:
                names.add(f.name)
        return list(names)

    def forget_original_json_state(self):
        """ If called, we forget/reset the orginal json state, which is a combination
            of all the json that this object has been updated with over it's lifetime.

            The json state is what allows the object to decide what has changed,
            when it's requested to only include changes via the `BaseApi.json` method.

            If forgotten, it's as-if we never got the json in the first place to compare against.
            Therefore, all attributes that have values will be returned for this object
            when it's only requested to include changes
            (the RestClient in xmodel-rest can request it to do this, as an example).

            Resetting the state here only effects this object, not any child objects.
            You'll have to ask child objects directly to forget t heir original json, if desired.
        """
        self._api_state.last_original_update_json = None

    # ----------------------------
    # --------- Private ----------
    #
    # I want to make the state and structure private for now, because it might change a bit later.
    # Want to give this some opportunity to be used for a while to see where the areas for
    # improvement are before potentially opening it up publicly to things outside of the sdk.

    _api_state: PrivateApiState[M] = None
    """ This object will vary from BaseModel class instance-to-instance, and is the area we keep
        api state that is Private for the BaseModel instance.

        Will be None if we are directly associated with BaseModel class, otherwise this will be the
        BaseModel's instance state, methods in this object need the BaseModel instance.
    """

    @property
    def context(self) -> XContext:
        """ BaseApi context to use when asking this object to send/delete/etc its self to/from
            service.

            This is an old hold-over from when we used to keep a XContext reference.
            This is the same as calling `xinject.context.XContext.current`.
        """
        return XContext.grab()

Ancestors

  • typing.Generic

Subclasses

Class variables

var default_converters : Dict[Type[Any], Converter]

For an overview of type-converts, see Type Converters Overview.

The class attribute defaults to None, but an instance/object will always have some sort of dict in place (happens during init call).

Notice the todo note in the overview. I want it to work that way in the future (so via BaseApi.set_default_converter and BaseApi.get_default_converter). It's something coming in the future. For now you'll need to override default_converters and/or change it directly.

You can provide your own values for this directly in a sub-class, when an BaseApi or subclass is created, we will merge converters in this order, with things later in the order taking precedence and override it:

  1. xmodel.converters.DEFAULT_CONVERTERS
  2. BaseApi.default_converters from BaseModel.api from parent model. The parent model is the one the model is directly inheriting from.
  3. Finally, BaseApi.default_converters from the BaseApi subclass's class attribute (only looks on type/class directly for default_converters).

It takes this final mapping and sets it on self.default_converters, and will be inherited as explained on on line number 2 above in the future.

Default converters we have defined at the moment:

See xmodel.converters.DEFAULT_CONVERTERS to see the default converters map/dict.

Maps type-hint to a default converter. This converter will be used for TypeValue.convert when the model BaseStructure is create if none is provided for it at field definition time for a particular type-hint. If a type-hint is not in this converter, no convert is called for it.

You don't need to provide one of these for a BaseModel type-hint, as the system knows to call json/update_from_json on those types of objects.

The default value provides a way to convert to/from a dt.date/dt.datetime and a string.

Static methods

def resolved_type_hints() ‑> Dict[str, Type]
Expand source code
@classmethod
def resolved_type_hints(cls) -> Dict[str, Type]:
    if hints := BaseApi._CACHED_TYPE_HINTS.get(cls):
        return hints

    hints = get_type_hints(cls)
    BaseApi._CACHED_TYPE_HINTS[cls] = hints
    return hints

Instance variables

var contextXContext

BaseApi context to use when asking this object to send/delete/etc its self to/from service.

This is an old hold-over from when we used to keep a XContext reference. This is the same as calling XContext.current().

Expand source code
@property
def context(self) -> XContext:
    """ BaseApi context to use when asking this object to send/delete/etc its self to/from
        service.

        This is an old hold-over from when we used to keep a XContext reference.
        This is the same as calling `xinject.context.XContext.current`.
    """
    return XContext.grab()
var have_changes : bool

Is True if self.json(only_include_changes=True) is not None; see json() method for more details.

Expand source code
@property
def have_changes(self) -> bool:
    """ Is True if `self.json(only_include_changes=True)` is not None;
        see json() method for more details.
    """
    log.debug(f"Checking Obj {self.model} to see if I have any changes [have_changes]")
    return self.json(only_include_changes=True) is not None
var model : ~M

REQUIRES associated model object [see doc text below].

Gives you back the model associated with this api. If this BaseApi obj is associated directly with the BaseModel class type and so there is no associated model, I will raise an exception.

Some BaseApi methods are dependant on having an associated model, and when they ask for it and there is None, this will raise an exception for them. The first line of the doc comment tells you if it needs one. Normally, it's pretty obvious if the method will need the model, due to what it will return to you (ie: if it would need model attrs).

The methods that are dependant on a model are ones, like 'json', where it returns the JSON for a model. It needs a model to get this data.

If you access an object api via a BaseModel object, that will be the associated model. If you access it via a BaseModel type/class, it will be directly associated with the model class.

Examples:

>>> # Grab Account model from some_lib (as an example).
>>> from some_lib.account import Account
>>>
>>> # api object is associated with MyModelClass class, not model obj.
>>> Account.api
>>>
>>> account_obj = Account.api.get_via_id(3)
>>> # api is associated with the account_obj model object.
>>> account_obj.api
>>>
>>> # This sends object attributes to API, so it needs an associated
>>> # BaseModel object, so this works:
>>> account_obj.api.send()
>>>
>>> # This would produce an exception, since it would try to get BaseModel
>>> # attributes to send. But there is no associated model.
>>> Account.api.send()
Expand source code
@property
def model(self) -> M:
    """ REQUIRES associated model object [see doc text below].

    Gives you back the model associated with this api. If this BaseApi obj is associated
    directly with the BaseModel class type and so there is no associated model, I will
    raise an exception.

    Some BaseApi methods are dependant on having an associated model, and when they ask for it
    and there is None, this will raise an exception for them. The first line of the doc
    comment tells you if it needs one.  Normally, it's pretty obvious if the method
    will need the model, due to what it will return to you (ie: if it would need model attrs).

    The methods that are dependant on a model are ones, like 'json', where it returns the
    JSON for a model.  It needs a model to get this data.

    If you access an object api via a BaseModel object, that will be the associated model.
    If you access it via a BaseModel type/class, it will be directly associated with the model
    class.

    Examples:
    >>> # Grab Account model from some_lib (as an example).
    >>> from some_lib.account import Account
    >>>
    >>> # api object is associated with MyModelClass class, not model obj.
    >>> Account.api
    >>>
    >>> account_obj = Account.api.get_via_id(3)
    >>> # api is associated with the account_obj model object.
    >>> account_obj.api
    >>>
    >>> # This sends object attributes to API, so it needs an associated
    >>> # BaseModel object, so this works:
    >>> account_obj.api.send()
    >>>
    >>> # This would produce an exception, since it would try to get BaseModel
    >>> # attributes to send. But there is no associated model.
    >>> Account.api.send()

    """
    api_state = self._api_state
    assert api_state, "BaseApi needs an attached model obj and there is no associated " \
                      "model api state."
    model = api_state.model
    assert model, "BaseApi needs an attached model obj and there is none."
    return model
var model_type : Type[~M]

The same BaseApi class is meant to be re-used for any number of Models, and so a BaseModel specifies it's BaseApi type as generic BaseApi[M]. In this case is the BaseModel it's self. That way we can have the type-system aware that different instances of the same BaseApi class can specify different associated BaseModel classes.

This property will return the BaseModel type/class associated with this BaseApi instance.

Expand source code
@property
def model_type(self) -> Type[M]:
    """ The same BaseApi class is meant to be re-used for any number of Models,
        and so a BaseModel specifies it's BaseApi type as generic `BaseApi[M]`. In this case
        is the BaseModel it's self.  That way we can have the type-system aware that different
        instances of the same BaseApi class can specify different associated BaseModel classes.

        This property will return the BaseModel type/class associated with this BaseApi
        instance.
    """
    # noinspection PyTypeChecker
    return self.structure.model_cls
var structureBaseStructure[Field]

Contain things that don't vary among the model instances; ie: This is the same object and applies to all instances of a particular BaseModel class.

This object has a list of xmodel.fields.Field that apply to the BaseModel you can get via xmodel.base.structure.Structure.fields; for example.

This is currently created in BaseApi.

BaseApi instance for a BaseModel is only created when first asked for via BaseModel.api.

Returns

BaseStructure
Structure with correct field and model type in it.
Expand source code
@property
def _structure(self):
    """
    Contain things that don't vary among the model instances;
    ie: This is the same object and applies to all instances of a particular BaseModel class.

    This object has a list of `xmodel.fields.Field` that apply to the
    `xmodel.base.model.BaseModel` you can get via
    `xmodel.base.structure.Structure.fields`; for example.

    This is currently created in `BaseApi.__init__`.

    BaseApi instance for a BaseModel is only created when first asked for via
    `xmodel.base.model.BaseModel.api`.

    Returns:
        BaseStructure: Structure with correct field and model type in it.
    """
    return self._structure

Methods

def copy_from_model(self, model: BaseModel)
Expand source code
def copy_from_model(self, model: BaseModel):
    their_fields = model.api.structure.field_map
    my_fields = self.structure.field_map
    keys = [k for k in their_fields if k in my_fields]

    # Assume we have a model, and are not the class-based `MyModel.api....` version.
    # todo: have `self.model` raise an exception if called on the class api version
    #   (which does not have a related model, just knows about model-type.).
    my_model = self.model
    for k in keys:
        their_value = getattr(model, k)
        if their_value is not None:
            setattr(my_model, k, their_value)
def fields_to_pop_for_json(self, json: dict, field_objs: List[Field], log_output: bool) ‑> Set[Any]

Goes through the list of fields (field_objs) to determine which ones have not changed in order to pop them out of the json representation. This method is used when we only want to include the changes in the json.

:param json: dict representation of a model's fields and field values as they are currently set on the model. :param field_objs: List of fields and their values for a model :param log_output: boolean to determine if we should log the output or not :return: The field keys to remove from the json representation of the model.

Expand source code
def fields_to_pop_for_json(
        self, json: dict, field_objs: List[Field], log_output: bool
) -> Set[Any]:
    """
    Goes through the list of fields (field_objs) to determine which ones have not changed in
    order to pop them out of the json representation. This method is used when we only want to
    include the changes in the json.

    :param json: dict representation of a model's fields and field values as they are currently
        set on the model.
    :param field_objs: List of fields and their values for a model
    :param log_output: boolean to determine if we should log the output or not
    :return: The field keys to remove from the json representation of the model.
    """
    fields_to_pop = set()
    for field, new_value in json.items():
        # json has simple strings, numbers, lists, dict;
        # so makes general comparison simpler.
        old_value = self._get_old_json_value(field=field, as_type=type(new_value))

        if old_value is Default:
            if log_output:
                log.debug(
                    f"   Included field ({field}) with value "
                    f"({new_value}) because there is no original json value for it."
                )
        elif self.should_include_field_in_json(
                new_value=new_value,
                old_value=old_value,
                field=field
        ):
            if log_output:
                log.debug(
                    f"   Included field ({field}) due to new value "
                    f"({new_value}) != old value ({old_value})."
                )
        else:
            # We don't want to mutate dict while traversing it, remember this for later.
            fields_to_pop.add(field)

    # Map a field-key to what other fields should be included if field-key value is used.
    # For now we are NOT supporting `Field.json_path` to keep things simpler
    # when used in conjunction with `Field.include_with_fields`.
    # `Field` will raise an exception if json_path != field name and include_with_fields
    # is used at the same time.
    # It's something I would like to support in the future, but for now it's not needed.
    # We can assume that `field_obj.name == field_obj.json_path`
    for field_obj in field_objs:
        if not field_obj.include_with_fields:
            continue
        if field_obj.name not in fields_to_pop:
            continue
        if not (field_obj.include_with_fields <= fields_to_pop):
            fields_to_pop.remove(field_obj.name)

    return fields_to_pop
def fields_to_remove_for_json(self, json: dict, field_objs: List[Field]) ‑> Set[str]

Returns set of fields that should be considered 'changed' because they were removed when compared to the original JSON values used to originally update this object.

The names will be the fields json_path.

Expand source code
def fields_to_remove_for_json(self, json: dict, field_objs: List[Field]) -> Set[str]:
    """
    Returns set of fields that should be considered 'changed' because they were removed
    when compared to the original JSON values used to originally update this object.

    The names will be the fields json_path.
    """
    fields_to_remove = set()
    for field in field_objs:
        # A `None` in the `json` means a null, so we use `Default` as our sentinel type.
        new_value = json.get(field.json_path, Default)
        old_value = self._get_old_json_value(field=field.json_path, as_type=type(new_value))
        if new_value is Default and old_value is not Default:
            fields_to_remove.add(field.json_path)
    return fields_to_remove
def forget_original_json_state(self)

If called, we forget/reset the orginal json state, which is a combination of all the json that this object has been updated with over it's lifetime.

The json state is what allows the object to decide what has changed, when it's requested to only include changes via the BaseApi.json() method.

If forgotten, it's as-if we never got the json in the first place to compare against. Therefore, all attributes that have values will be returned for this object when it's only requested to include changes (the RestClient in xmodel-rest can request it to do this, as an example).

Resetting the state here only effects this object, not any child objects. You'll have to ask child objects directly to forget t heir original json, if desired.

Expand source code
def forget_original_json_state(self):
    """ If called, we forget/reset the orginal json state, which is a combination
        of all the json that this object has been updated with over it's lifetime.

        The json state is what allows the object to decide what has changed,
        when it's requested to only include changes via the `BaseApi.json` method.

        If forgotten, it's as-if we never got the json in the first place to compare against.
        Therefore, all attributes that have values will be returned for this object
        when it's only requested to include changes
        (the RestClient in xmodel-rest can request it to do this, as an example).

        Resetting the state here only effects this object, not any child objects.
        You'll have to ask child objects directly to forget t heir original json, if desired.
    """
    self._api_state.last_original_update_json = None
def get_child_without_lazy_lookup(self, child_field_name, *, false_if_not_set=False) ‑> Union[~M, ForwardRef(None), bool, NullType]

REQUIRES associated model object [see self.model].

If the child is current set to Null, or an object, returns that value. Will NOT lazily lookup child, even if its possible to do so.

:param child_field_name: The field name of the child object. :param false_if_not_set: Possible Values [Default: False]: * False: Return None if nothing is currently set. * True: Return False if nothing is currently set. This lets you distinguish between having a None value set on field vs nothing set at all. Normally this distinction is only useful internally in this class, external users probably don't need this option.

Expand source code
def get_child_without_lazy_lookup(
        self,
        child_field_name,
        *,
        false_if_not_set=False,
) -> Union[M, None, bool, NullType]:
    """ REQUIRES associated model object [see self.model].

    If the child is current set to Null, or an object, returns that value.
    Will NOT lazily lookup child, even if its possible to do so.

    :param child_field_name: The field name of the child object.
    :param false_if_not_set:
        Possible Values [Default: False]:
            * False: Return None if nothing is currently set.
            * True:  Return False if nothing is currently set. This lets you distinguish
              between having a None value set on field vs nothing set at all.
              Normally this distinction is only useful internally in this class,
              external users probably don't need this option.
    """

    model = self.model

    if not self.structure.is_field_a_child(child_field_name):
        raise XModelError(
            f"Called get_child_without_lazy_lookup('{child_field_name}') but "
            f"field ({child_field_name}) is NOT a child field on model ({model}).")

    if child_field_name in model.__dict__:
        return getattr(model, child_field_name)

    if false_if_not_set:
        return False

    return None
def json(self, only_include_changes: bool = False, log_output: bool = False, include_removals: bool = False) ‑> Optional[Dict[str, Any]]

REQUIRES associated model object (see BaseApi.model for details on this).

Return associated model object as a JsonDict (str keys, any value), ready to be encoded via JSON encoder and sent to the API.

Args

only_include_changes: If True, will only include what changed in the JsonDict result. Defaults to False. This is normally set to True if system is sending this object via PATCH, which is the normal way the system sends objects to API.

 If only_include_changes is False (default), we always include everything that
 is not 'None'.
 When a <code>xmodel.base.client.BaseClient</code> subclass
 (such as <code>xmodel.rest.RestClient</code>)
 calls this method, it will pass in a value based on it's own
 <code>xmodel.rest.RestClient.enable\_send\_changes\_only</code> is set to
 (defaults to False there too).
 You can override the RestClient.enable_send_changes_only at the BaseModel class
 level by making a RestClient subclass and setting <code>enable\_send\_changes\_only</code> to
 default to <code>True</code>.

 There is a situations where we have to include all attributes, regardless:
     1. If the 'id' field is set to a 'None' value. This indicates we need to create
        a new object, and we are not partially updating an existing one, even if we
        got updated via json at some point in the past.

 As always, properties set to None will *NOT* be included in returned JsonDict,
 regardless of what options have been set.

log_output (bool): If False (default): won't log anything. If True: Logs what method returns at debug level.

include_removals : bool
If False (default): won't include in response any fields that have been removed (vs when compared to the original JSON that updated this object). The value will be the special sentinel object Remove (see top of this module/file for Remove object, and it's RemoveType class).

Returns

JsonDict

Will the needed attributes that should be sent to API. If returned value is None, that means only_include_changes is True and there were no changes.

The returned dict is a copy and so can be mutated be the caller.

Expand source code
def json(
    self,
    only_include_changes: bool = False,
    log_output: bool = False,
    include_removals: bool = False
) -> Optional[JsonDict]:
    """ REQUIRES associated model object (see `BaseApi.model` for details on this).

    Return associated model object as a JsonDict (str keys, any value), ready to be encoded
    via JSON encoder and sent to the API.

    Args:
        only_include_changes: If True, will only include what changed in the JsonDict result.
            Defaults to False.
            This is normally set to True if system is sending this object via PATCH, which is
            the  normal way the system sends objects to API.

            If only_include_changes is False (default), we always include everything that
            is not 'None'.
            When a `xmodel.base.client.BaseClient` subclass
            (such as `xmodel.rest.RestClient`)
            calls this method, it will pass in a value based on it's own
            `xmodel.rest.RestClient.enable_send_changes_only` is set to
            (defaults to False there too).
            You can override the RestClient.enable_send_changes_only at the BaseModel class
            level by making a RestClient subclass and setting `enable_send_changes_only` to
            default to `True`.

            There is a situations where we have to include all attributes, regardless:
                1. If the 'id' field is set to a 'None' value. This indicates we need to create
                   a new object, and we are not partially updating an existing one, even if we
                   got updated via json at some point in the past.

            As always, properties set to None will *NOT* be included in returned JsonDict,
            regardless of what options have been set.

        log_output (bool): If False (default): won't log anything.
            If True: Logs what method returns at debug level.

       include_removals (bool): If False (default): won't include in response any fields
            that have been removed
            (vs when compared to the original JSON that updated this object).
            The value will be the special sentinel object `Remove`
            (see top of this module/file for `Remove` object, and it's `RemoveType` class).

    Returns:
        JsonDict: Will the needed attributes that should be sent to API.
            If returned value is None, that means only_include_changes is True
            and there were no changes.

            The returned dict is a copy and so can be mutated be the caller.
    """

    # todo: Refactor _get_fields() to return getter/setter closures for each field, and we
    #       can make this whole method more generic that way. We also can 'cache' the logic
    #       needed that way instead of having to figure it out each time, every time.

    structure = self.structure
    model = self.model
    api_state = self._api_state

    json: JsonDict = {}

    field_objs = structure.fields

    # Negate only_include_changes if we don't have any original update json to compare against.
    if only_include_changes and api_state.last_original_update_json is None:
        only_include_changes = False

    # noinspection PyDefaultArgument
    def set_value_into_json_dict(value, field_name, *, json=json):
        # Sets field value directly on json dict or passed in dict...
        if value is not None:
            # Convert Null into None (that's how JSON converter represents a Null).
            json[field_name] = value if value is not Null else None

    for field_obj in field_objs:
        # If we are read-only, no need to do anything more.
        if field_obj.read_only:
            continue

        # We deal with non-related types later.
        related_type = field_obj.related_type
        if not related_type:
            continue

        f = field_obj.name
        if field_obj.read_only:
            continue

        # todo: For now, the 'api-field-path' option can't be used at the same time as obj-r.
        if field_obj.json_path != field_obj.name:
            # I've put in some initial support for this below, but it's has not been tested
            # for now, keep raising an exception for this like we have been.
            # There is a work-around, see bottom part of the message in the below error:
            raise NotImplementedError(
                f"Can't have xmodel.Field on BaseModel with related-type and a json_path "
                f"that differ at the moment, for field ({field_obj}). "
                f"It is something I want to support someday; the support is mostly in place "
                f"already, but it needs some more careful thought, attention and testing "
                f"before we should allow it. "
                "Workaround:  Make an `{field.name}_id` field next to related field on the "
                "model. Then, set `json_path` for that `{field.name}_id` field, set it to "
                "what you want it to be. Finally, set the `{related_field.name}` to "
                "read_only=True. This allows you to rename the `_id` field used to/from api "
                "in the JSON input/output, but the Model can have an alternate name for the "
                "related field. You can see a real-example of this at "
                "`bigcommerce.api.orders._BcCommonOrderMetafield.order"
            )

        obj_type_structure = related_type.api.structure
        obj_type_has_id = obj_type_structure.has_id_field()

        if obj_type_has_id:
            # If the obj uses an 'id', then we have a {field_name}_id we want to
            # send instead of the full object as a json dict.
            #
            # This will grab the id from child obj if it exists, or from a defined field
            # of f"{f}_id" or finally from related id storage.

            # todo: If there is an object with no 'id' value, do we ignore it?
            #   or should we embed full object anyway?

            child_obj_id = api_state.get_related_field_id(f)

            # Method below should deal with None vs Null.
            set_value_into_json_dict(child_obj_id, f"{f}_id")
        else:
            obj: 'M' = getattr(model, f)

            # Related-object has no 'id', so get it's json dict and set that into the output.
            v = obj
            if obj is not Null and obj is not None:
                # todo: a Field option to override this and always provide all
                #   values (if object always needs to be fully embedded).
                v = obj.api.json(only_include_changes=only_include_changes)

            # if it returns None (ie: no changes) and only_include_changes is enabled,
            # don't include the sub-object as a change.
            if v is not None or not only_include_changes:
                # Method below should deal with None vs Null.
                set_value_into_json_dict(v, f)

    for field_obj in field_objs:
        # If we are read-only, no need to do anything more.
        if field_obj.read_only:
            continue

        # We don't deal with related-types here.
        if field_obj.related_type:
            continue

        f = field_obj.name
        v = getattr(model, f)
        if v is not None and field_obj.converter:
            # Convert the value....
            v = field_obj.converter(
                api=self,
                direction=Converter.Direction.to_json,
                field=field_obj,
                value=v
            )

        path = field_obj.json_path
        if not path:
            set_value_into_json_dict(v, f)
            continue

        path_list = path.split(field_obj.json_path_separator)
        d = json
        for name in path_list[:-1]:
            d = d.setdefault(name, {})
        name = path_list[-1]

        # Sets field value into a sub-dictionary of the original `json` dict.
        set_value_into_json_dict(v, name, json=d)

    if include_removals:
        removals = self.fields_to_remove_for_json(json, field_objs)
        for f in removals:
            if f in json:
                raise XynModelError(
                    f"Sanity check, we were about to overwrite real value with `Remove` "
                    f"in json field ({f}) for model ({self.model})."
                )

            json[f] = Remove

    # If the `last_original_update_json` is None, then we never got update via JSON
    # so there is nothing to compare, include everything!
    if only_include_changes:
        log.debug(f"Checking Obj {model} for changes to include.")
        fields_to_pop = self.fields_to_pop_for_json(json, field_objs, log_output)

        for f in fields_to_pop:
            del json[f]

        if not json:
            # There were no changes, return None.
            return None
    else:
        due_to_msg = "unknown"
        if not only_include_changes:
            due_to_msg = "only_include_changes is False"
        if api_state.last_original_update_json is None:
            due_to_msg = "no original json value"

        if log_output:
            log.debug(f"Including everything for obj {model} due to {due_to_msg}.")

            # Log out at debug level what we are including in the JSON.
            for field, new_value in json.items():
                log.debug(
                    f"   Included field ({field}) value ({new_value})"
                )

    for k, v in json.items():
        # Must use list of JSON, convert any sets to a list.
        if type(v) is set:
            v = list(v)
            json[k] = v

    return json
def list_of_attrs_to_repr(self) ‑> List[str]

" REQUIRES associated model object [see self.model].

A list of attribute names to put into the repr/string representation of the associated model object. This is consulted when the BaseModel has repr called on it.

Expand source code
def list_of_attrs_to_repr(self) -> List[str]:
    """" REQUIRES associated model object [see self.model].

    A list of attribute names to put into the __repr__/string representation
    of the associated model object. This is consulted when the BaseModel has __repr__
    called on it.
    """
    names = set()
    model = self.model

    # todo: Move this into pres-club override of list_of_attrs_to_repr in an BaseApi subclass.
    if hasattr(model, 'account_id'):
        names.add('account_id')

    # todo: Consider adding others here, perhaps all defined fields on model that have
    # todo: a non-None value?

    for f in self.structure.fields:
        if f.include_in_repr:
            names.add(f.name)
    return list(names)
def should_include_field_in_json(self, new_value: Any, old_value: Any, field: str) ‑> bool

Returns True if the value for field should be included in the JSON. This only gets called if only_include_changes is True when passed to self.json::

# Passed in like so:
self.json(only_include_changes=True)

This method is an easy way to change the comparison logic.

new_value could be xyn_types.default.Default, to indicate that a value's absence is significant (ie: to remove an attribute from destination).

Most of the time, a value's absence does not affect the destination when object is sent to API/Service because whatever value is currently there for the attribute is left intact/alone.

But sometimes a service will remove the attribute if it does not exist. When this is the case, the absence of the value is significant for comparison purposes; ie: when deciding if a value has changed.

:param new_value: New value that will be put into JSON. :param old_value: Old value originals in original JSON [normalized if possible to the same type as new_value. :param field: Field name. :return: If True: Will include the fields value in an update. If False: Won't include the fields value in an update.

Expand source code
def should_include_field_in_json(self, new_value: Any, old_value: Any, field: str) -> bool:
    """
    Returns True if the value for field should be included in the JSON.
    This only gets called if only_include_changes is True when passed to self.json::

        # Passed in like so:
        self.json(only_include_changes=True)

    This method is an easy way to change the comparison logic.

    `new_value` could be `xyn_types.default.Default`, to indicate that a value's
    absence is significant (ie: to remove an attribute from destination).

    Most of the time, a value's absence does not affect the destination when object
    is sent to API/Service because whatever value is currently there for the attribute
    is left intact/alone.

    But sometimes a service will remove the attribute if it does not exist.
    When this is the case, the absence of the value is significant for comparison purposes;
    ie: when deciding if a value has changed.


    :param new_value: New value that will be put into JSON.
    :param old_value:
        Old value originals in original JSON [normalized if possible to the same type as
        new_value.
    :param field: Field name.
    :return:
        If True: Will include the fields value in an update.
        If False: Won't include the fields value in an update.
    """
    # Convert old value to set if new value is set and old value is list (from original JSON).
    # If I was really cool :)... I would find out the inner type in case of int/str
    # and to a conversion to compare Apples to Apples.....
    # But trying to minimize changes so I don't conflict as much with soon to be
    # xdynamo feature.
    if type(new_value) is set and type(old_value) is list:
        old_value = set(old_value)

    return new_value != old_value
def update_from_json(self, json: Union[Dict[str, Any], collections.abc.Mapping])

REQUIRES associated model object [see self.model].

todo: Needs more documentation

We update the dict per-key, with what we got passed in [via 'json' parameter] overriding anything we got previously. This also makes a copy of the dict, which is want we want [no change to modify the incoming dict parameter].

Expand source code
def update_from_json(self, json: Union[JsonDict, Mapping]):
    """ REQUIRES associated model object [see self.model].

    todo: Needs more documentation

    We update the dict per-key, with what we got passed in [via 'json' parameter]
    overriding anything we got previously. This also makes a copy of the dict, which is
    want we want [no change to modify the incoming dict parameter].
    """

    structure = self.structure
    model = self.model
    api_state = self._api_state

    if not isinstance(json, Mapping):
        raise XModelError(
            f"update_from_json(...) was given a non-mapping parameter ({json})."
        )

    # Merge the old values with the new values.
    api_state.last_original_update_json = {
        **(api_state.last_original_update_json or {}),
        **json
    }

    fields = structure.fields

    values = {}
    for field_obj in fields:
        path_list = field_obj.json_path.split(field_obj.json_path_separator)
        v = json
        got_value = True
        for name in path_list:
            if name not in v:
                # We don't even have a 'None' value so we assume we just did not get the value
                # from the api, and therefore we just skip doing anything with it.
                got_value = False
                break

            v = v.get(name)
            if v is None:
                break

        # We map the value we got from JSON into a flat-dict with the BaseModel name as the
        # key...
        if got_value:
            values[field_obj.name] = v if v is not None else Null

    def set_attr_on_model(field, value, model=model):
        """ Closure to set attr on self unless value is None.
        """
        if value is None:
            return
        setattr(model, field, value)

    # Merge in the outer json, keeping the values we mapped [via Field.json_path] for conflicts
    values = {**json, **values}

    # todo: If the json does not have a value [not even a 'None' value], don't update?
    #       We may have gotten a partial update?  For now, always update [even to None]
    #       all defined fields regardless if they are inside the json or not.

    for field_obj in fields:
        # We deal with related types later....
        if field_obj.related_type:
            continue

        f = field_obj.name
        v = values.get(f, Default)

        # A None from JSON means a Null for us.
        # If JSON does not include anything, that's a None for us.
        if v is None:
            v = Null
        elif v is Default:
            v = None

        # Run the converter if needed.
        # If we have a None value, we don't need to convert that, there was no value to
        # convert.
        if field_obj.converter and v is not None:
            v = field_obj.converter(
                self,
                Converter.Direction.from_json,
                field_obj,
                v
            )

        set_attr_on_model(f, v)

    for field_obj in fields:
        # Ok, now we deal with related types...
        related_type = field_obj.related_type
        if not related_type:
            continue

        f = field_obj.name

        # todo: at some point, allow customization of this via Field class
        #   Also, s tore the id
        f_id_name = f"{f}_id"
        if typing_inspect.get_origin(field_obj.type_hint) is list:
            # todo: This code is not complete [Kaden never finished it up]
            #   for now, just comment out.

            raise NotImplementedError(
                "Type-hints for xmodel models in this format: `attr: List[SomeType]` "
                "are not currently supported. We want to support it someday. For now you "
                "must use lower-cased non-generic `list`. At some point the idea is to "
                "allow one to do `List[ChildModel]` and then we know it's a list of "
                "other BaseModel objects and automatically handle that in some way."
            )

            # child_type: 'Type[M]'
            # child_type = typing_inspect.get_args(obj_type)
            # # __args__ returns a tuple of all arguments passed into List[] so we need to
            # # pull the class out of the tuple
            # if child_type:
            #     child_type = child_type[0]
            #
            # child_api: BaseApi
            # child_api = child_type.api
            # if not child_api and child_api.structure.has_id_field:
            #     # TODO: add a non generic Exception for this
            #     raise XModelError(
            #         f"{model} has an attribute with name ({f}) with type-hint List that "
            #         f"doesn't contain an API BaseModel Type as the only argument"
            #     )
            # parent_name = model.__class__.__name__.lower()
            # state.set_related_field_id(f, parent_name)
            # continue

        v = None
        if f in values:
            v = values.get(f, Null)
            if v is not Null:
                v = related_type(v)

        # Check to see if we have an api/json field for object relation name with "_id" on
        # end.
        if v is None and related_type.api.structure.has_id_field():
            # If we don't have a defined field for this value, check JSON for it and store it.
            #
            # If we have a defined None value for the id field, meaning the field exists
            # in the json, and is set directly to None, then we have a Null relationship.
            # We set that as the value, since there is no need to 'lookup' a null value.
            f_id_value = json.get(f_id_name)
            id_field = structure.get_field(f_id_name)

            if not id_field:
                id_field = field_obj.related_type.api.structure.get_field('id')

            # Run the converter if needed.
            # If we have a None value, we don't need to convert that, there was no value to
            # convert.
            if id_field and id_field.converter and f_id_value is not None:
                f_id_value = id_field.converter(
                    self,
                    Converter.Direction.from_json,
                    id_field,
                    f_id_value
                )

            if f_id_value is None and f_id_name in json:
                # We have a Null situation.
                f_id_value = Null

            if f_id_value is not None:
                # We have an id!
                # Set the value to support automatic lookup of value, lazily.
                # This method also takes care to set child object to Null or delete it
                # as needed depending on the f_id_value and what the child's id field value is.
                api_state.set_related_field_id(f, f_id_value)
        else:
            # 'v' is either going to be None, Null or an BaseModel object.
            set_attr_on_model(f, v)
class BaseModel (*args, **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
@model_auto_init()  # noqa - This will be defined.
class BaseModel(ABC):
    """
    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](./#orm-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](./#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](./#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
    `xmodel.base.structure.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 `xurls.url.Url` is constructed.

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

    - `xmodel.base.api.BaseApi.json`
    - `xmodel.base.api.BaseApi.update_from_json`
    - Or passing a JSON dict as the first arrument to `BaseModel.__init__`.

    Other important related classes are listed below.

    - `xmodel.base.api.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`.
    - `xmodel.base.structure.BaseStructure`: Accessable via
        `xmodel.base.api.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.

    """

    # -------------------------------------
    # --------- Public Properties ---------

    api: "BaseApi[Self]" = None
    """ Used to access the api class, which is used to retrieve/send objects to/from api.

        You can specify this as a type-hint in subclasses to change the class we use for this
        automatically, like so::
            from xmodel import BaseModel, BaseApi
            from xmodel.base.model import Self
            from typing import TypeVar

            M = TypeVar("M")

            class MyCoolApi(BaseApi[M]):
                pass

            class MyCoolModel(BaseModel):
                # The `Self` here is imported from xmodel
                # (to maintain backwards compatability with Python < 3.11)
                # It allows us to communicate our subclass-type to the `api` object,
                # allowing IDE to type-complete/hint better.
                api: MyCoolApi[Self] 

        The generic ``T`` type-var in this case refers to whatever model class that your using.
        In the example just above, ``T`` would be referring to ``MyCoolModel`` if you did this
        somewhere to get the BaseModel's api: ``MyCoolModel.api``.
    """

    # --------------------------------------------
    # --------- Config/Option Properties ---------

    def __init_subclass__(
        cls: Type[M],
        *,
        lazy_loader: Callable[[Type[M]], None] = None,
        **kwargs
    ):
        """
        We take all arguments (except `lazy_loader`) passed into here and send them to the method
        on our structure:
        `xmodel.base.structure.BaseStructure.configure_for_model_type`.
        This allows you to easily configure the BaseStructure via class arguments.

        For a list of class-arguments, see method parameters for
        `xmodel.base.structure.BaseStructure.configure_for_model_type`.

        See [Basic BaseModel Example](./#basic-model-example) for an example of what class
        arguments are for `BaseModel` classes and how to use them.

        We lazily configure BaseModel sub-classes. They are configured the first time that
        `BaseModel.api` is accessed under that subclass. At that point all parent + that specific
        subclass are configured and an `xmodel.base.api.BaseApi` object is created and set
        on the `BaseModel.api` attribute. From that point forward, that object is what is used.
        This only happens the first time that `BaseModel.api` is accessed.

        If you want to add support for additional BaseModel class arguments,
        you can do it by modifying the base-implementation
        `xmodel.base.structure.BaseStructure`.
        Or if you want it only for a specific sub-set of Models, you can make a custom
        `xmodel.base.structure.BaseStructure` subclass. You can configure your BaseModel to use
        this BaseStructure subclass via a type-hint on `xmodel.base.api.BaseApi.structure`.

        See `xdynamo.dynamo.DynStructure.configure_for_model_type` for a complete example of a
        custom BaseStructure subclass that adds extra class arguments that are specific to Dynamo.

        Args:
            lazy_loader: This is a callable where the first argument is `cls/self`.
                This is an optional param. If provided, we will call when we need to lazy-load
                but before we do our normal lazy-loading ourselves here.

                Most of the time, you'll want to import into the global/module space of where
                your class lives any imports you need to do lazily, such as circular imports.

                Right after we call your lazy_loader callable, we will be ourselves calling
                the method `get_type_hints` to get all of our type-hints.
                You'll want to be sure all of your forward-referenced type-hints on your
                model sub-class are resolvable.

                Forward-ref type hints are the ones that are string based type-hints,
                they get resolved lazily after your lazy_loader (if provided) is called.

                You can see in the code in our method below, look at the check for:

                >>> if "BaseApi" not in globals():

                Look at that for a real-world example of what I am talking about.
                This little piece of code lazily resolves the `BaseApi` type.
        """

        # We are taking all args and sending them to a xmodel.base.structure.BaseStructure
        # class object.
        super().__init_subclass__()

        def lazy_setup_api(cls_or_self):
            # If requested, before we do our own lazy-loading below, call passed in lazy-loader.
            if lazy_loader:
                lazy_loader(cls)

            for parent in cls.mro():
                if parent is not cls and issubclass(parent, BaseModel):
                    # Ensure that parent-class has a chance to lazy-load it's self
                    # before we try to examine our type-hints.
                    getattr(parent, 'api')

            # We potentially get called a lot (for every sub-class)
            # so check to see if we already loaded BaseApi type or not.
            if 'BaseApi' not in globals():
                # Lazy import BaseApi into module, helps resolve BaseApi forward-refs;
                # ie: `api: "BaseApi[T]"`
                # We need to resolve these due to use of `get_type_hints()` 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.
                #
                # todo: figure out why dynamic model attribute getter is having an issue with this.
                #   (see that near start of this file at top ^^^)
                from xmodel import BaseApi
                globals()['BaseApi'] = BaseApi
            try:
                all_type_hints = get_type_hints(cls)
            except (NameError, AttributeError) as e:
                from xmodel import XModelError
                raise XModelError(
                    f"Unable to construct model subclass ({cls}) due to error resolving "
                    f"type-hints on model class. They must be visible at the module-level that "
                    f"the class is defined in. Original error: ({e})."
                ) from None

            api_cls: Type["BaseApi"] = all_type_hints['api']

            base_cls = None
            for b in cls.__bases__:
                if b is BaseModel:
                    break
                if issubclass(b, BaseModel):
                    base_cls = b
                    break

            base_api = None
            if base_cls:
                base_api = base_cls.api

            api = api_cls(api=base_api)
            cls.api = api

            # Configure structure for our model type with the user supplied options + type-hints.
            structure = api.structure
            try:
                structure.configure_for_model_type(
                    model_type=cls,
                    type_hints=all_type_hints,
                    **kwargs
                )
            except TypeError as e:
                from xmodel import XModelError
                # Adding some more information to the exception.
                raise XModelError(
                    f"Unable to configure model structure for ({cls}) due to error ({e}) "
                    f"while calling ({structure}.configure_for_model_type)."
                )
            return api

        # The LazyClassAttr will turn into the proper type automatically when it's first accessed.
        lazy_api = LazyClassAttr(lazy_setup_api, name="api")

        # Avoids IDE from using this as type-hint for `self.api`, we want it to use the type-hint
        # defined on attribute.
        # Otherwise it will try to be too cleaver by trying to use the type in `lazy_api` instead.
        # The object in `lazy_api` will transform into what has been type-hinted
        # when it's first accessed by something.
        setattr(cls, "api", lazy_api)

    # -------------------------------
    # --------- Init Method ---------

    # todo: Python 3.8 has support for positional-arguments only, do that when we start using it.
    # See Doc-Comment for what *args is.
    def __init__(self, *args, **initial_values):
        """
        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)`.
        """
        args_len = len(args)
        if args_len > 1:
            raise NotImplementedError(
                "Passing XContext via second positional argument is no longer supported."
            )

        cls_api_type = type(type(self).api)
        api = cls_api_type(model=self)
        setattr(self, "api", api)  # Avoids IDE from using this as type-hint for `self.api`.

        first_arg = args[0] if args_len > 0 else None

        if isinstance(first_arg, str):
            # We assume `str` is a json-string, parse json and import.
            json_objs = json.loads(first_arg)
            api.update_from_json(json_objs)
        elif isinstance(first_arg, BaseModel):
            # todo: Probably make this recursive, in that we copy sub-base-models as well???
            api.copy_from_model(first_arg)
        elif isinstance(first_arg, Mapping):
            api.update_from_json(first_arg)
        elif first_arg is not None:
            raise XModelError(
                f"When a first argument to BaseModel.__init__ is provided, it needs to be a "
                f"mapping/dict with the json values in it "
                f"OR a BaseModel instance to copy from "
                f"OR a str with a json dict/obj to parse inside of string; "
                f"I was given a type ({type(first_arg)}) with value ({first_arg}) instead."
            )

        for k, v in initial_values.items():
            if not self.api.structure.get_field(k):
                raise XModelError(
                    f"While constructing {self}, init method got a value for an "
                    f"unknown field ({k})."
                )

            setattr(self, k, v)

    def __repr__(self):
        msgs = []
        for attr in self.api.list_of_attrs_to_repr():
            msgs.append(f'{attr}={getattr(self, attr, None)}')

        full_message = ", ".join(msgs)
        return f"{self.__class__.__name__}({full_message})"

    def __setattr__(self, name, value):
        # This gets called for every attribute set.

        # DO NOT use hasattr() in here, because you could make every lazily loaded object load up
        # [ie: an API request to grab lazily loaded object properties] when the lazy object is set.

        api = self.api
        structure = api.structure
        field = structure.get_field(name)
        type_hint = None

        # By default, we go through the special set/converter logic.
        do_default_attr_set = False

        if inspect.isclass(self):
            # If we are a class, just pass it along
            do_default_attr_set = True
        elif name == "api":
            # Don't do anything special with the 'api' var.
            do_default_attr_set = True
        elif name.startswith("_"):
            # don't do anything with private vars
            do_default_attr_set = True
        elif name.endswith("_id") and structure.is_field_a_child(name[:-3], and_has_id=True):
            # We have a virtual field for a related field id, redirect to special setter.
            state = _private.api.get_api_state(api)
            state.set_related_field_id(name[:-3], value)
            return

        if do_default_attr_set:
            # We don't do anything more if it's a special attribute/field/etc.
            super().__setattr__(name, value)
            return

        field = structure.get_field(name)
        if not field:
            # We don't do anything more without a field object
            # (ie: just a normal python attribute of some sort, not tied with API).
            super().__setattr__(name, value)
            return

        try:
            # We have a value going to an attributed that has a type-hint, checking the type...
            # We will also support auto-converting to correct type if needed and possible,
            # otherwise an error will be thrown if we can't verify type or auto-convert it.
            type_hint = field.type_hint
            value_type = type(value)

            # todo: idea: We could cache some of these details [perhaps even using closures]
            #       or use dict/set's someday for a speed improvement, if we ever need to.

            hint_union_sub_types = ()
            if typing_inspect.is_union_type(type_hint):
                # Gets all args in a union type, to see if one of them will match type_hint.
                hint_union_sub_types = typing_inspect.get_args(type_hint)
                # Get first type hint in untion, Field object (where we just got type-hint)
                # already unwraps the type hint, removing any Null/None types. It's a Union
                # only if there are other non-Null/None types in a union. For right now
                # lets only worry about the first one.
                type_hint = hint_union_sub_types[0]

            state = _private.api.get_api_state(api)
            if (
                # If we have a blank string, but field is not of type str,
                # and field is also nullable; we then we convert the value into a Null.
                # (ie: user is setting a blank-string on a non-string field)
                field.nullable and
                type_hint not in [str, None] and
                value_type is str and
                not value
            ):
                value = Null
            elif value is None:
                # By default, this is None [unless user specified something].
                value = _get_default_value_from_field(self, field)
            elif (
                value_type is type_hint
                or value_type in hint_union_sub_types
                or Optional[value_type] is type_hint
                or type_hint is NullType and field.nullable
            ):
                # Type is the same as type hint, no need to do anything else.
                # We check to reset any related field id info, just in case it exists,
                # since this field is either being set to Null or an actual object.
                state.reset_related_field_id_if_exists(name)
                pass
            elif value is Null:
                # If type_hint supported the Null type, then it would have been dealt with in
                # the previous if statement.
                if not field.nullable:
                    raise AttributeError(
                        f"Setting a Null value for field ({name}) when typehint ({type_hint}) "
                        f"does not support NullType, for object ({self})."
                    )
            elif field.converter:
                # todo: Someday map str/int/bool (basic conversions) to standard converter methods;
                #   kind of like I we do it for date/time... have some default converter methods.
                #
                # This handles datetime, date, etc...
                value = field.converter(api, Converter.Direction.to_model, field, value)
            elif type_hint in (dict, JsonDict) and value_type in (dict, JsonDict):
                # this is fine for now, keep it as-is!
                #
                # For now, we just assume things in the dict are ok.
                # in the future, we will support `Dict[str, int]` or some such and we will
                # check/convert/ensure the types as needed.
                pass
            elif type_hint in (dict, JsonDict) and value_type in (int, bool, str):
                # just passively convert bool/int/str into empty dict if type-hint is a dict.
                log.warning(
                    f"Converted a int/bool/str into an empty dict. Attr name ({name}),"
                    f"value ({value}) type-hint ({type_hint}) object ({self}). If you don't want"
                    f"to do this, then don't put a type-hint on the attribute."
                )
                value = {}
            elif typing_inspect.get_origin(type_hint) in (list, set):
                # See if we have a converter for this type in our default-converters....
                inside_type_hint = typing_inspect.get_args(type_hint)[0]
                basic_type_converter = self.api.default_converters.get(inside_type_hint)
                if basic_type_converter:
                    converted_values = [
                        basic_type_converter(
                            api,
                            Converter.Direction.to_model,
                            field,
                            x
                        ) for x in loop(value)
                    ]

                    container_type = typing_inspect.get_origin(type_hint)
                    value = container_type(converted_values)
                # Else/Otherwise we just leave things as-is for now, no error and no conversion
                pass
            # Python 3.7 does not have GenericMeta anymore, not sure if we need it, we just need
            # to try using this for a bit and see what happens.
            #
            # If needed in Python 3.7, we can see if we can remove this loop-hole with the new
            # typing_inspect.* methods.
            #
            # elif type(type_hint) is GenericMeta:
            #     # This is a complex type (probably a Parameterized generic), not going to try and
            #     # check it out, don't want to throw and error as well, just pass it though.
            #     #
            #
            #     pass

            else:
                raise AttributeError(
                    f"Setting name ({name}) with value ({value}) with type ({value_type}) on "
                    f"API object ({self}) but type-hint is ({type_hint}), and I don't know how"
                    f" to auto-convert type ({value_type}) into ({type_hint})."
                )
        except ValueError:
            # We want to raise a more informative error than the base ValueError when there
            # is a problem parsing a value
            raise AttributeError(
                f"Parsing value ({value}) with type-hint ({type_hint}) resulted in an error "
                f"for attribute ({name}) on object ({self})"
            )

        if isinstance(value, str):
            # This value has caused me a lot of problems, it's time to ALWAYS treat them
            # as blank strings, exactly what they should have been set to in the first place.
            if value.startswith('#########'):
                value = ''

        if field.post_filter:
            value = field.post_filter(api=api, name=name, value=value)

        if field.fset:
            field.fset(self, value)
        elif field.fget:
            raise XModelError(
                f"We have a field ({field}) that does not have a Field.fset (setter function),"
                f"but has a Field.fget ({field.fget}) and someone is attempting to set a "
                f"value on the Model object ({self})... this is unsupported. "
                f"If you want to allow setting the value, you must provider a setter when a "
                f"getter is present/provided."
            )
        else:
            super().__setattr__(name, value)

    def __getattr__(self, name: str):
        # Reminder: This method only gets called if attribute is not currently defined in self.
        structure = self.api.structure
        state = _private.api.get_api_state(self.api)

        field = structure.get_field(name)

        if name.startswith("_"):
            return object.__getattribute__(self, name)

        if field and field.fget:
            # Use getter to get value, if we get a non-None value return it.
            # If we get a None back, then do the default thing we normally do
            # (ie: look for default value, related object, etc).
            value = field.fget(self)
            if value is not None:
                return value

        if name.endswith("_id") and structure.is_field_a_child(name[:-3], and_has_id=True):
            # We have a field that ends with _id, that when taken off is a child field that
            # uses and id. This means we should treat this field as virtually related field id.
            value = state.get_related_field_id(name[:-3])
            if value is not None:
                if field and field.fset:
                    field.fset(self, value)
                return value

        if not field:
            raise AttributeError(
                f"Getting ({name}) on ({self.__class__.__name__}), which does not exist on object "
                f"or in API. For API objects, you need to use already defined fields/attributes."
            )

        if (
            field.related_type is not None and
            field.related_type.api.structure.has_id_field()
        ):
            name_id_value = state.get_related_field_id(name)

            # RemoteModel is an abstract interface,
            # Let's us know how to lazily lookup remote objects by their id value.
            name_type: "RemoteModel" = field.related_type
            obj = None

            if name_id_value is Null:
                # Don't attempt to lookup object, we have it's primary id:
                obj = Null
            elif name_id_value is not None:
                # Attempt to lookup remote object, we have it's primary id:
                obj = name_type.api.get_via_id(name_id_value)
                # todo: consider raising an exception if we have an `id` but no related object?
                #   I'll think about it.

                # if we have an object, set it and return it
                if obj is not None:
                    if field.fset:
                        field.fset(self, obj)
                    else:
                        super().__setattr__(name, obj)
                    return obj
                # Otherwise, we continue, next thing to do is look for any default value.

        # We next look for a default value, if any set/return that.
        default = _get_default_value_from_field(self, field)

        # We set to default value and return it if we have a non-None value.
        if default is not None:
            # We have a value of some sort, call setter:
            if field.fset:
                field.fset(self, default)
            else:
                super().__setattr__(name, default)
            return default

        # We found no default value, return None.
        return None

    def __eq__(self, other):
        """ For BaseModel, by default our identity is based on object instance ID, not any values
            in our attributes.  Makes things simpler when trying to find object/self in a Set;
            which is useful when traversing relationships.
        """
        return self is other

    def __hash__(self):
        """ For BaseModel, by default our identity is based on object instance ID, not any values
            in our attributes.  Makes things simpler when trying to find object/self in a Set;
            which is useful when traversing relationships.
        """
        return id(self)

Ancestors

  • abc.ABC

Subclasses

Class variables

var apiBaseApi[BaseModel]

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

You can specify this as a type-hint in subclasses to change the class we use for this automatically, like so:: from xmodel import BaseModel, BaseApi from xmodel.base.model import Self from typing import TypeVar

M = TypeVar("M")

class MyCoolApi(BaseApi[M]):
    pass

class MyCoolModel(BaseModel):
    # The <code>Self</code> here is imported from xmodel
    # (to maintain backwards compatability with Python < 3.11)
    # It allows us to communicate our subclass-type to the <code>api</code> object,
    # allowing IDE to type-complete/hint better.
    api: MyCoolApi[Self]

The generic T type-var in this case refers to whatever model class that your using. In the example just above, T would be referring to MyCoolModel if you did this somewhere to get the BaseModel's api: MyCoolModel.api.

class BaseStructure (*, parent: Optional[ForwardRef('BaseStructure')], field_type: Type[~F])

BaseStructure class is meant to keep track of things that apply for all BaseModel's at the class-level.

You can use BaseStructure.fields to get all fields for a particular BaseModel as an example of the sort of information on the BaseStructure object.

BaseStructure is lazily configured for a particular BaseModel the first time something attempts to get BaseModel.api off the particular BaseModel subclass.

You can get it via first getting api attribute for BaseModel via BaseModel.api and then getting the structure attribute on that via BaseApi.structure.

Example getting the structure object for the Account model/api:

>>> from some_lib.account import Account
>>> structure = Account.api.structure
Expand source code
class BaseStructure(Generic[F]):

    """
    BaseStructure class is meant to keep track of things that apply for all
    `xmodel.base.model.BaseModel`'s at the class-level.

    You can use `BaseStructure.fields` to get all fields for a particular
    `xmodel.base.model.BaseModel`
    as an example of the sort of information on the `BaseStructure` object.

    BaseStructure is lazily configured for a particular BaseModel the first time something
    attempts to get `xmodel.base.model.BaseModel.api` off the particular BaseModel subclass.

    You can get it via first getting api attribute for BaseModel via
    `xmodel.base.model.BaseModel.api` and then getting the structure attribute on that via
    `xmodel.base.api.BaseApi.structure`.

    Example getting the structure object for the Account model/api:

    >>> from some_lib.account import Account
    >>> structure = Account.api.structure
    """

    def __init__(
            self,
            *,
            parent: Optional['BaseStructure'],
            field_type: Type[F]
    ):
        super().__init__()

        # Set specific ones so I have my own 'instance' of them.
        self._name_to_type_hint_map = {}
        self._get_fields_cache = None

        # Copy all my attributes over from parent, for use as 'default' values.
        if parent:
            self.__dict__.update(parent.__dict__)
            # noinspection PyProtectedMember
            # This parent is my own type/class, so I am fine accessing it's private member.
            self._name_to_type_hint_map = parent._name_to_type_hint_map.copy()

        self._get_fields_cache = None
        self.field_type = field_type
        self.internal_shared_api_values = {}

    def configure_for_model_type(
            self,
            *,  # <-- means we don't support positional arguments
            model_type: Type['BaseModel'],
            type_hints: Dict[str, Any],
    ):
        """
        This EXPECTS to have passed-in the type-hints for my `BaseStructure.;
        see code inside `xmodel.base.model.BaseModel.__init_subclass__` for more details.
        There is no need to get the type-hints twice [it can be a bit expensive, trying to
        limit how may times I grab them]....

        See `xmodel.base.model.BaseModel` for more details on how Models work...
        This describes the options you
        can pass into a `xmodel.base.model.BaseModel` subclass at class-construction time.
        It allows you to customize how the Model class will work.

        This method will remember the options passed to it, but won't finish constructing the class
        until someone asks for it's `xmodel.base.model.BaseModel.api` attribute for the first
        time. This allows you
        to dynamically add more Field classes if needed. It also makes things import faster as
        we won't have to fully setup the class unless something tries to use it.

        Args:
            model_type (Type[xmodel.base.model.BaseModel]): The model we are associated with,
                this is what we are configuring ourselves against.
            type_hints (Dict[str, Any]): List of typehints via Python's `get_type_hints` method;
                Be aware that `get_type_hints` will try and resolve all type-hints, including
                ones that are forward references. Make sure these types are available at
                the module-level by the time `get_type_hints` runs.
        """
        # Prep model class, remove any class Field objects...
        # These objects have been "moved" into me via `self.fields`.
        self._name_to_type_hint_map = type_hints
        self.model_cls = model_type
        for field_obj in self.fields:
            field_name = field_obj.name

            # The default values are inside `field_obj.default` now.
            # We delete the class-vars, so that `__getattr__` is called when someone attempts
            # to grab a value from a BaseModel for an attribute that does not directly exist
            # on the BaseModel subclass so we can do our normal field_obj.default resolution.
            # If the class keeps the value, it prevents `__getattr__` from being called for
            # attributes that don't exist directly on the model instance/object;
            # Python will instead grab and return the value set on the class for that attribute.
            #
            # todo/thoughts/brain-storm:
            #    Consider just using __getattribute__ for BaseModel instead of __getattr_...
            #    It's slightly slower but then I could have more flexablity around this...
            #    Thinking of returning the associated field-object if you do
            #    `BaseModelSubClass.some_attr_field` for various purposes....
            #    Using `__getattribute__` would allow for this....
            #    just something I have been thinking about...
            #    For example: you could use that field object as a query-key instead of a string
            #    with the field-name...
            #    might be nicer, and get auto-completion that way... not sure, thinking about it.
            #
            if field_name in self.model_cls.__dict__:
                delattr(self.model_cls, field_name)

    # --------------------------------------
    # --------- Environmental Properties ---------

    model_cls: "Type[BaseModel]"
    """
    The model's class we are defining the structure for.
    This is typed as some sort of `xmodel.base.model.BaseModel`
    .
    This is NOT generically typed anymore, to get much better generically typed
    version you should use `xmodel.base.api.BaseApi.model_type` to get the BaseModel outside
    of the `xmodel.structure` module.
    Using that will give the IDE the correctly typed BaseModel class!
     """

    # --------------------------------------
    # --------- General Properties ---------
    #
    # Most of these will be set inside __init_subclass__() via associated BaseModel Class.

    field_type: Type[F]
    """
    Field type that this structure will use when auto-generating `xmodel.fields.Field`'s.
    User defined Fields on a model-class will keep whatever type the user used.
    When `xmodel.base.model.BaseModel` class is constructed, and the `BaseStructure` is
    created, we will check to ensure all user-defined fields inherit from this field_type.

    That way you can assume any fields you get off this structure object inherit from
    field_type.
    """

    internal_shared_api_values: Dict[Any, Any] = None
    """
    A place an `xmodel.base.api.BaseApi` object can use to share values BaseModel-class wide
    (ie: for all BaseModel's of a specific type).

    This should NOT be used outside of the BaseApi class.
    For example, ``xmodel.base.api.BaseApi.client` stores it's object lazily here.
    Users outside of BaseApi class should simply ask it for the client and not try
    to go behind it's back and get it here.

    Code/Users outside of `xmodel.base.api.BaseApi` and it's subclasses can't assume
    anything about what's in this dictionary.  This exists for pure-convenience of the
    `xmodel.base.api.BaseApi` class.
    """

    _name_to_type_hint_map: Dict[str, Any]
    """
        .. deprecated:: v0.2.33 Use `BaseStructure.fields` instead to get a list of
            the real fields to use. And `xmodel.fields.Field.type_hint` to get the type-hint
            [don't get it here, keeping this temporary for backwards compatibility].

        A map of  attribute-name to type-hint type.

        .. important:: This WILL NOT take into account field-names where the `Field.name` is
            different then the name of the field on BaseModel the type-hint was assigned to.
    """

    _get_fields_cache: Dict[str, F] = None

    @property
    def have_api_endpoint(self) -> bool:
        """ Right now, a ready-only property that tells you if this BaseModel has an API endpoint.
            That's determined right now via seeing if `BaseStructure.has_id_field_set()` is True.
        """
        if not self.has_id_field():
            return False
        else:
            return True

    def __copy__(self):
        obj = type(self)(parent=self, field_type=self.field_type)
        obj.__dict__.update(self.__dict__)
        obj._name_to_type_hint_map = self._name_to_type_hint_map.copy()
        obj._get_fields_cache = None
        return obj

    def field_exists(self, name: str) -> bool:
        """ Return `True` if the field with `name` exists on the model, otherwise `False`. """
        return name in self.field_map

    def has_id_field(self):
        """ Defaults to False, returns True for RemoteStructure,
            What this property is really saying is if you can do a foreign-key to the related
            object/model.

            It may be better at some point in the long-run to rename this field to more indicate
            that; perhaps the next time we have a breaking-change we need to do for xmodel.

            For now, we are leaving the name along and hard-coding this to
            return False in BaseStructure, and to return True in RemoteStructure.
        """
        return False

    def get_field(self, name: str) -> Optional[F]:
        """
        Args:
            name (str): Field name to query on.
        Returns:
            xmodel.fields.Field: If field object exists with `name`.

            None: If not field with `name` exists
        """
        if name is None:
            return None
        return self.field_map.get(name)

    @property
    def fields(self) -> List[F]:
        """ Returns:
                List[xmodel.fields.Field]: list of field objects.
        """
        return list(self.field_map.values())

    @property
    def field_map(self) -> Mapping[str, F]:
        """

        Returns:
           Dict[str, xmodel.fields.Field]: Map of `xmodel.fields.Field.name` to
                `xmodel.fields.Field` objects.
        """
        cached_content = self._get_fields_cache
        if cached_content is not None:
            # Mapping proxy is a read-only view of the passed in dict.
            # This will LIVE update the mapping if underlying dict changed.
            return MappingProxyType(cached_content)

        generated_fields = self._generate_fields()
        self._get_fields_cache = generated_fields
        return MappingProxyType(generated_fields)

    def excluded_field_map(self) -> Dict[str, F]:
        """
        Returns:
            Dict[str, xmodel.fields.Field]: Mapping of `xmodel.fields.Field.name` to
                field objects that are excluded (`xmodel.fields.Field.exclude` == `True`).
        """
        return {f.name: f for f in self.fields if f.exclude}

    def _generate_fields(self) -> Dict[str, F]:
        """ Goes though object and grabs/generated Field objects and caches them in self.
            Gives back the definitive list of Field objects.

            For now keeping this private, but may open it up in the future if sub-classes
            need to customize how fields are generated.
        """
        full_field_map = {}

        default_field_type: Type[Field] = self.field_type
        type_hint_map = self._name_to_type_hint_map
        model_cls = self.model_cls

        # todo: Figure out how to put this into/consolidate into
        #  `xmodel.base.api.BaseApi`; and simplify stuff!!!
        default_converters = getattr(self.model_cls.api, 'default_converters')

        # todo:  default_con ^^^^ make sure we are using it!!!!

        # Lazy-import BaseModel, we need to check to see if we have a sub-class or not...
        from xmodel import BaseModel

        # This will be a collection of any Fields that exist on the parent(s), merged together...
        base_fields: Dict[str, Field] = {}

        # go though parent and find any Field objects, grab latest version
        # which is the one closest to child on a per-field basis...
        # we exclude it's self [the model we are currently working with].
        for base in reversed(model_cls.__mro__[1:]):
            base: Type[BaseModel]
            if not inspect.isclass(base):
                continue
            if not issubclass(base, BaseModel):
                continue
            if not base.api:
                # `base` is likely xmodel.base.model.BaseModel; and that has no API allocated
                # to it
                # at the moment [mostly because the __init_subclasses is only executed on sub's].
                # todo: BaseModel is an abstract class... do we really need structure/fields on it?
                continue
            # todo:  ensure we later on use these and make a new field if needed...
            base_fields.update(base.api.structure.field_map)

        for name, type_hint in type_hint_map.items():
            # Ignore the 'api' attribute, it's special.
            if name == 'api':
                continue

            # Ignore anything the starts with '_'.
            if name.startswith("_"):
                continue

            # todo:
            #   1. Get Parent Field's, merge values.
            #   2. Map all type's and if not map then raise error.

            # noinspection PyArgumentList
            field_obj: Field
            field_value: Field = getattr(model_cls, name, Default)
            if isinstance(field_value, Field):
                field_obj = field_value
                field_value = Default
            elif field_value is not Default:
                if not inspect.isclass(field_value) and isinstance(field_value, property):
                    field_obj = default_field_type(fget=field_value.fget, fset=field_value.fset)
                else:
                    # noinspection PyArgumentList
                    field_obj = default_field_type(default=field_value)
            else:
                # noinspection PyArgumentList
                field_obj = default_field_type()

            # Name can be overridden, we want to use it to lookup parent field name....
            if field_obj.name:
                name = field_obj.name

            field_obj.resolve_defaults(
                name=name,
                type_hint=type_hint_map.get(name, None),
                default_converter_map=default_converters,
                parent_field=base_fields.get(name)
            )

            # Ensure all fields that still have `Default` as their value are resolved to None.
            field_obj.resolve_remaining_defaults_to_none()

            # field-object will unwrap the type-hint for us.
            type_hint = field_obj.type_hint

            # Name can be overridden, we want to use whatever it says we should be using.
            name = field_obj.name
            full_field_map[field_obj.name] = field_obj

            # If we have a converter, we can assume that will take care of things correctly
            # for whatever type we have.  If we don't have a converter, we only support specific
            # types; We check here for type-compatibility.
            from xmodel import BaseModel
            if (
                not field_obj.converter and
                type_hint not in supported_basic_types and
                (not inspect.isclass(type_hint) or not issubclass(type_hint, BaseModel)) and
                typing_inspect.get_origin(type_hint) not in (list, set)
            ):
                raise XModelError(
                    f"Unsupported type ({type_hint}) with field-name ({name}) "
                    f"for model-class ({model_cls}) in field-obj ({field_obj})."
                )

            if (
                field_obj.json_path and
                field_obj.json_path != field_obj.name and
                field_obj.related_type
            ):
                XModelError(
                    "Right now obj-relationships can't use the 'json_path' option "
                    "while at the same time being obj-relationships. Must use basic field "
                    "with api_path. "

                    # Copy/Paste from `BaseApi.json`:
                    f"Can't have xmodel.Field on BaseModel with related-type and a json_path "
                    f"that differ at the moment, for field ({field_obj}). "
                    f"It is something I want to support someday; the support is mostly in place "
                    f"already, but it needs some more careful thought, attention and testing "
                    f"before we should allow it. "
                    "Workaround:  Make an `{field.name}_id` field next to related field on the "
                    "model. Then, set `json_path` for that `{field.name}_id` field, set it to "
                    "what you want it to be. Finally, set the `{related_field.name}` to "
                    "read_only=True. This allows you to rename the `_id` field used to/from api "
                    "in the JSON input/output, but the Model can have an alternate name for the "
                    "related field. You can see a real-example of this at "
                    "`bigcommerce.api.orders._BcCommonOrderMetafield.order"
                )

        # todo: Provide a 'remove' option in the Field config class.
        if 'id' not in full_field_map:

            # Go though and populate the `Field.field_for_foreign_key_related_field` as needed...
            for k, f in full_field_map.items():
                # If there is a relate field name, and we have a field defined for it...
                # Set it's field_for_foreign_key_related_field so the correct field...
                # Otherwise generate a field object for this key-field.
                #
                # FYI: The `resolve_defaults` call above will always set
                #      field_for_foreign_key_related_field to None.
                #      We then set it to something here if needed.
                if f.related_field_name_for_id:
                    related_field = full_field_map.get(f.related_field_name_for_id)
                    if related_field:
                        related_field.field_for_foreign_key_related_field = f

        return full_field_map

    def id_cache_key(self, _id):
        """ Returns a proper key to use for `xmodel.base.client.BaseClient.cache_get`
            and other caching methods for id-based lookup of an object.
        """
        if type(_id) is dict:
            # todo: Put module name in this key.
            key = f"{self.model_cls.__name__}"
            try:
                sorted_keys = sorted(_id.keys())
            except TypeError:
                sorted_keys = _id.keys()
            for key_name in sorted_keys:
                key += f"-{key_name}-{_id[key_name]}"
            return key
        else:
            return f"{self.model_cls.__name__}-id-{_id}"

    # todo: Get rid of this [only used by Dynamo right now]. Need to use Field instead...
    def get_unwraped_typehint(self, field_name: str):
        """
        This is now done for you on `xmodel.fields.Field.type_hint`, so you can just grab it
        directly your self now.

        Gets typehint for field_name and calls `xmodel.types.unwrap_optional_type`
        on it to try and get the plain type-hint as best as we can.
        """
        field = self.get_field(field_name)
        if field is None:
            return None

        return field.type_hint

    def is_field_a_child(self, child_field_name, *, and_has_id=False):
        """
        True if the field is a child, otherwise False.  Will still return `False` if
        `and_has_id` argument is `True` and the related type is configured to not use id via class
        argument `has_id_field=False` (see `BaseStructure.configure_for_model_type` for more
        details on class arguments).

        Won't raise an exception if field does not exist.

        Args:
            child_field_name (str): Name of field to check.
            and_has_id (bool): If True, then return False if related type is not configured to
                use id.
        Returns:
            bool: `True` if this field is a child field, otherwise `False`.
        """
        field = self.get_field(child_field_name)
        if not field:
            return False

        related_type = field.related_type
        if not related_type:
            return False

        related_structure = related_type.api.structure
        if and_has_id and not related_structure.has_id_field():
            return False

        return True

    @property
    def endpoint_description(self):
        """ Gives some sort of basic descriptive string that contains the path/table-name/etc
            that basically indicates the api endpoint being used.

            This is meant for logging and other human readability/debugging purposes.
            Feel free to change the string to whatever is most useful to know.

            I expect this to be overridden by the concrete implementation, see examples here:

            - `xmodel.rest.RestStructure.endpoint_description`
            - `xmodel.dynamo.DynStructure.endpoint_description`
        """
        return "?"

Ancestors

  • typing.Generic

Subclasses

Class variables

var field_type : Type[~F]

Field type that this structure will use when auto-generating xmodel.fields.Field's. User defined Fields on a model-class will keep whatever type the user used. When BaseModel class is constructed, and the BaseStructure is created, we will check to ensure all user-defined fields inherit from this field_type.

That way you can assume any fields you get off this structure object inherit from field_type.

var internal_shared_api_values : Dict[Any, Any]

A place an BaseApi object can use to share values BaseModel-class wide (ie: for all BaseModel's of a specific type).

This should NOT be used outside of the BaseApi class. For example, `xmodel.base.api.BaseApi.client stores it's object lazily here. Users outside of BaseApi class should simply ask it for the client and not try to go behind it's back and get it here.

Code/Users outside of BaseApi and it's subclasses can't assume anything about what's in this dictionary. This exists for pure-convenience of the BaseApi class.

var model_cls : Type[BaseModel]

The model's class we are defining the structure for. This is typed as some sort of BaseModel . This is NOT generically typed anymore, to get much better generically typed version you should use BaseApi.model_type to get the BaseModel outside of the xmodel.structure module. Using that will give the IDE the correctly typed BaseModel class!

Instance variables

var endpoint_description

Gives some sort of basic descriptive string that contains the path/table-name/etc that basically indicates the api endpoint being used.

This is meant for logging and other human readability/debugging purposes. Feel free to change the string to whatever is most useful to know.

I expect this to be overridden by the concrete implementation, see examples here:

  • xmodel.rest.RestStructure.endpoint_description
  • xmodel.dynamo.DynStructure.endpoint_description
Expand source code
@property
def endpoint_description(self):
    """ Gives some sort of basic descriptive string that contains the path/table-name/etc
        that basically indicates the api endpoint being used.

        This is meant for logging and other human readability/debugging purposes.
        Feel free to change the string to whatever is most useful to know.

        I expect this to be overridden by the concrete implementation, see examples here:

        - `xmodel.rest.RestStructure.endpoint_description`
        - `xmodel.dynamo.DynStructure.endpoint_description`
    """
    return "?"
var field_map : Mapping[str, ~F]

Returns

Dict[str, xmodel.fields.Field]
Map of xmodel.fields.Field.name to xmodel.fields.Field objects.
Expand source code
@property
def field_map(self) -> Mapping[str, F]:
    """

    Returns:
       Dict[str, xmodel.fields.Field]: Map of `xmodel.fields.Field.name` to
            `xmodel.fields.Field` objects.
    """
    cached_content = self._get_fields_cache
    if cached_content is not None:
        # Mapping proxy is a read-only view of the passed in dict.
        # This will LIVE update the mapping if underlying dict changed.
        return MappingProxyType(cached_content)

    generated_fields = self._generate_fields()
    self._get_fields_cache = generated_fields
    return MappingProxyType(generated_fields)
var fields : List[~F]

Returns: List[xmodel.fields.Field]: list of field objects.

Expand source code
@property
def fields(self) -> List[F]:
    """ Returns:
            List[xmodel.fields.Field]: list of field objects.
    """
    return list(self.field_map.values())
var have_api_endpoint : bool

Right now, a ready-only property that tells you if this BaseModel has an API endpoint. That's determined right now via seeing if BaseStructure.has_id_field_set() is True.

Expand source code
@property
def have_api_endpoint(self) -> bool:
    """ Right now, a ready-only property that tells you if this BaseModel has an API endpoint.
        That's determined right now via seeing if `BaseStructure.has_id_field_set()` is True.
    """
    if not self.has_id_field():
        return False
    else:
        return True

Methods

def configure_for_model_type(self, *, model_type: Type[ForwardRef('BaseModel')], type_hints: Dict[str, Any])

This EXPECTS to have passed-in the type-hints for my `BaseStructure.; see code inside BaseModel.__init_subclass__() for more details. There is no need to get the type-hints twice [it can be a bit expensive, trying to limit how may times I grab them]....

See BaseModel for more details on how Models work… This describes the options you can pass into a BaseModel subclass at class-construction time. It allows you to customize how the Model class will work.

This method will remember the options passed to it, but won't finish constructing the class until someone asks for it's BaseModel.api attribute for the first time. This allows you to dynamically add more Field classes if needed. It also makes things import faster as we won't have to fully setup the class unless something tries to use it.

Args

model_type : Type[BaseModel]
The model we are associated with, this is what we are configuring ourselves against.
type_hints : Dict[str, Any]
List of typehints via Python's get_type_hints method; Be aware that get_type_hints will try and resolve all type-hints, including ones that are forward references. Make sure these types are available at the module-level by the time get_type_hints runs.
Expand source code
def configure_for_model_type(
        self,
        *,  # <-- means we don't support positional arguments
        model_type: Type['BaseModel'],
        type_hints: Dict[str, Any],
):
    """
    This EXPECTS to have passed-in the type-hints for my `BaseStructure.;
    see code inside `xmodel.base.model.BaseModel.__init_subclass__` for more details.
    There is no need to get the type-hints twice [it can be a bit expensive, trying to
    limit how may times I grab them]....

    See `xmodel.base.model.BaseModel` for more details on how Models work...
    This describes the options you
    can pass into a `xmodel.base.model.BaseModel` subclass at class-construction time.
    It allows you to customize how the Model class will work.

    This method will remember the options passed to it, but won't finish constructing the class
    until someone asks for it's `xmodel.base.model.BaseModel.api` attribute for the first
    time. This allows you
    to dynamically add more Field classes if needed. It also makes things import faster as
    we won't have to fully setup the class unless something tries to use it.

    Args:
        model_type (Type[xmodel.base.model.BaseModel]): The model we are associated with,
            this is what we are configuring ourselves against.
        type_hints (Dict[str, Any]): List of typehints via Python's `get_type_hints` method;
            Be aware that `get_type_hints` will try and resolve all type-hints, including
            ones that are forward references. Make sure these types are available at
            the module-level by the time `get_type_hints` runs.
    """
    # Prep model class, remove any class Field objects...
    # These objects have been "moved" into me via `self.fields`.
    self._name_to_type_hint_map = type_hints
    self.model_cls = model_type
    for field_obj in self.fields:
        field_name = field_obj.name

        # The default values are inside `field_obj.default` now.
        # We delete the class-vars, so that `__getattr__` is called when someone attempts
        # to grab a value from a BaseModel for an attribute that does not directly exist
        # on the BaseModel subclass so we can do our normal field_obj.default resolution.
        # If the class keeps the value, it prevents `__getattr__` from being called for
        # attributes that don't exist directly on the model instance/object;
        # Python will instead grab and return the value set on the class for that attribute.
        #
        # todo/thoughts/brain-storm:
        #    Consider just using __getattribute__ for BaseModel instead of __getattr_...
        #    It's slightly slower but then I could have more flexablity around this...
        #    Thinking of returning the associated field-object if you do
        #    `BaseModelSubClass.some_attr_field` for various purposes....
        #    Using `__getattribute__` would allow for this....
        #    just something I have been thinking about...
        #    For example: you could use that field object as a query-key instead of a string
        #    with the field-name...
        #    might be nicer, and get auto-completion that way... not sure, thinking about it.
        #
        if field_name in self.model_cls.__dict__:
            delattr(self.model_cls, field_name)
def excluded_field_map(self) ‑> Dict[str, ~F]

Returns

Dict[str, xmodel.fields.Field]
Mapping of xmodel.fields.Field.name to field objects that are excluded (xmodel.fields.Field.exclude == True).
Expand source code
def excluded_field_map(self) -> Dict[str, F]:
    """
    Returns:
        Dict[str, xmodel.fields.Field]: Mapping of `xmodel.fields.Field.name` to
            field objects that are excluded (`xmodel.fields.Field.exclude` == `True`).
    """
    return {f.name: f for f in self.fields if f.exclude}
def field_exists(self, name: str) ‑> bool

Return True if the field with name exists on the model, otherwise False.

Expand source code
def field_exists(self, name: str) -> bool:
    """ Return `True` if the field with `name` exists on the model, otherwise `False`. """
    return name in self.field_map
def get_field(self, name: str) ‑> Optional[~F]

Args

name : str
Field name to query on.

Returns

xmodel.fields.Field
If field object exists with name.
None
If not field with name exists
Expand source code
def get_field(self, name: str) -> Optional[F]:
    """
    Args:
        name (str): Field name to query on.
    Returns:
        xmodel.fields.Field: If field object exists with `name`.

        None: If not field with `name` exists
    """
    if name is None:
        return None
    return self.field_map.get(name)
def get_unwraped_typehint(self, field_name: str)

This is now done for you on xmodel.fields.Field.type_hint, so you can just grab it directly your self now.

Gets typehint for field_name and calls xmodel.types.unwrap_optional_type on it to try and get the plain type-hint as best as we can.

Expand source code
def get_unwraped_typehint(self, field_name: str):
    """
    This is now done for you on `xmodel.fields.Field.type_hint`, so you can just grab it
    directly your self now.

    Gets typehint for field_name and calls `xmodel.types.unwrap_optional_type`
    on it to try and get the plain type-hint as best as we can.
    """
    field = self.get_field(field_name)
    if field is None:
        return None

    return field.type_hint
def has_id_field(self)

Defaults to False, returns True for RemoteStructure, What this property is really saying is if you can do a foreign-key to the related object/model.

It may be better at some point in the long-run to rename this field to more indicate that; perhaps the next time we have a breaking-change we need to do for xmodel.

For now, we are leaving the name along and hard-coding this to return False in BaseStructure, and to return True in RemoteStructure.

Expand source code
def has_id_field(self):
    """ Defaults to False, returns True for RemoteStructure,
        What this property is really saying is if you can do a foreign-key to the related
        object/model.

        It may be better at some point in the long-run to rename this field to more indicate
        that; perhaps the next time we have a breaking-change we need to do for xmodel.

        For now, we are leaving the name along and hard-coding this to
        return False in BaseStructure, and to return True in RemoteStructure.
    """
    return False
def id_cache_key(self, _id)

Returns a proper key to use for xmodel.base.client.BaseClient.cache_get and other caching methods for id-based lookup of an object.

Expand source code
def id_cache_key(self, _id):
    """ Returns a proper key to use for `xmodel.base.client.BaseClient.cache_get`
        and other caching methods for id-based lookup of an object.
    """
    if type(_id) is dict:
        # todo: Put module name in this key.
        key = f"{self.model_cls.__name__}"
        try:
            sorted_keys = sorted(_id.keys())
        except TypeError:
            sorted_keys = _id.keys()
        for key_name in sorted_keys:
            key += f"-{key_name}-{_id[key_name]}"
        return key
    else:
        return f"{self.model_cls.__name__}-id-{_id}"
def is_field_a_child(self, child_field_name, *, and_has_id=False)

True if the field is a child, otherwise False. Will still return False if and_has_id argument is True and the related type is configured to not use id via class argument has_id_field=False (see BaseStructure.configure_for_model_type() for more details on class arguments).

Won't raise an exception if field does not exist.

Args

child_field_name : str
Name of field to check.
and_has_id : bool
If True, then return False if related type is not configured to use id.

Returns

bool
True if this field is a child field, otherwise False.
Expand source code
def is_field_a_child(self, child_field_name, *, and_has_id=False):
    """
    True if the field is a child, otherwise False.  Will still return `False` if
    `and_has_id` argument is `True` and the related type is configured to not use id via class
    argument `has_id_field=False` (see `BaseStructure.configure_for_model_type` for more
    details on class arguments).

    Won't raise an exception if field does not exist.

    Args:
        child_field_name (str): Name of field to check.
        and_has_id (bool): If True, then return False if related type is not configured to
            use id.
    Returns:
        bool: `True` if this field is a child field, otherwise `False`.
    """
    field = self.get_field(child_field_name)
    if not field:
        return False

    related_type = field.related_type
    if not related_type:
        return False

    related_structure = related_type.api.structure
    if and_has_id and not related_structure.has_id_field():
        return False

    return True
class Converter

This is meant to be a Callable that converts to/from a type when a value is assigned to a BaseModel.

See Converter.__call__ for the calling interface.

You can set these on Field.converter or BaseApi.default_converters.

Expand source code
class Converter:
    """ This is meant to be a Callable that converts to/from a type when a value is assigned
        to a `xmodel.base.model.BaseModel`.

        See `Converter.__call__` for the calling interface.

        You can set these on `Field.converter` or `xmodel.base.api.BaseApi.default_converters`.
    """
    class Direction(Enum):
        """ Possible values for field option keys. """
        to_json = EnumAuto()
        """ We are converting from BaseModel into JSON. """
        from_json = EnumAuto()
        """ We are converting from JSON and need value to set on BaseModel. """
        to_model = EnumAuto()
        """ We are setting a value on the BaseModel [could be coming from anywhere]. """

    def __call__(
            self,
            api: "BaseApi",
            direction: Direction,  # todo: Used to be 'to_json', need to fix it everywhere...
            field: "Field",  # todo: this is a new param, use to be 'field_name'...
            value: Any,
    ) -> Any:
        """
        Gets called when something needs to be converted.

        By default, this will call one of these depending on the direction:

        - `Converter.to_json`
        - `Converter.from_json`
        - `Converter.to_model`

        Args:
            api (xmodel.base.api.BaseApi): This has the associated
                `xmodel.base.model.BaseModel.api` object, from which we need a value converted.
            direction (Converter.Direction): Look at `Converter.Direction` for details.
            field (str): Field information, this contains the name, types, etc...
            value (Any): The value that needs to be converted.
        """
        Direction = Converter.Direction  # noqa
        if direction == Direction.to_json:
            return self.to_json(api, field, value)

        if direction == Direction.from_json:
            return self.from_json(api, field, value)

        if direction == Direction.to_model:
            return self.to_model(api, field, value)

    # Instead of implementing `__call__`, you can implement of these instead if that's easier.
    def to_json(self, api: 'BaseApi', field: 'Field', value: Any):
        """ todo """
        raise NotImplementedError(
            f"Converter ({self}) has no __call__ or to_json method which does the conversion."
        )

    def from_json(self, api: 'BaseApi', field: 'Field', value: Any):
        """ todo """
        raise NotImplementedError(
            f"Converter ({self}) has no __call__ or from_json method which does the conversion."
        )

    def to_model(self, api: 'BaseApi', field: 'Field', value: Any):
        """ todo """
        raise NotImplementedError(
            f"Converter ({self}) has no __call__ or to_model method which does the conversion."
        )

Subclasses

Class variables

var Direction

Possible values for field option keys.

Methods

def from_json(self, api: BaseApi, field: Field, value: Any)

todo

Expand source code
def from_json(self, api: 'BaseApi', field: 'Field', value: Any):
    """ todo """
    raise NotImplementedError(
        f"Converter ({self}) has no __call__ or from_json method which does the conversion."
    )
def to_json(self, api: BaseApi, field: Field, value: Any)

todo

Expand source code
def to_json(self, api: 'BaseApi', field: 'Field', value: Any):
    """ todo """
    raise NotImplementedError(
        f"Converter ({self}) has no __call__ or to_json method which does the conversion."
    )
def to_model(self, api: BaseApi, field: Field, value: Any)

todo

Expand source code
def to_model(self, api: 'BaseApi', field: 'Field', value: Any):
    """ todo """
    raise NotImplementedError(
        f"Converter ({self}) has no __call__ or to_model method which does the conversion."
    )
class Field (name: str = Default, type_hint: Type = <property object>, nullable: bool = Default, read_only: bool = Default, exclude: bool = Default, default: Any = Default, post_filter: Optional[Filter] = Default, converter: Optional[Converter] = Default, fget: Optional[Callable[[M], Any]] = Default, fset: Optional[Callable[[BaseModel, Any], None]] = Default, include_with_fields: Set[str] = Default, json_path: str = Default, json_path_separator: str = Default, include_in_repr: bool = Default, related_type: Optional[Type[BaseModel]] = Default, related_field_name_for_id: Optional[str] = Default, related_to_many: bool = Default, model: BaseModel = Default)

If this is not used on a model field/attribute, the field will get the default set of options automatically if the field has a type-hint; see topic BaseModel Fields.

Preferred way going forward to provide additional options/configuration to BaseModel fields.

If you don't specify a value for a particular attribute, it will have the Default value. When a Default value is encountered while constructing a BaseModel, it will resolve these Default values and assign the final value for the field.

To resolve these Defaults, it will look at field on the parent BaseModel class. If a non-Default value is defined there, it will use that for the child. If not, then it looks at the next parent. If no non-Default value is found we then use a value that makes sense. You can see what this is in the first line of each doc-comment. In the future, when we start using Python 3.9 we can use type annotations (typing.Annotated) to annotate a specific value to the Default type generically. For now it's hard-coded.

Side Notes

Keep in mind that after the .api is accessed for the first time on a particular model class, the sdk will construct the rest of the class (lazily)… it will read and then remove/delete from the BaseModel class any type-hinted json fields with a Field object assigned to the class. It moves these Field objects into a special internal structure. The class gets None values set on all fields after this is done.

Details on why we remove them:

Doing this helps with getattr, as it will still be executed for fields without a value when we create an object instance. getattr is used to support lazy lookups [via API] of related objects. Using getattr is much faster than using the getattribute version. So I want to keep using the getattr version if possible.

Expand source code
@dataclasses.dataclass(eq=False)
class Field:
    """
    If this is not used on a model field/attribute, the field will get the default set of
    options automatically if the field has a type-hint; see topic
    [BaseModel Fields](./#model-fields).

    Preferred way going forward to provide additional options/configuration to BaseModel fields.

    If you don't specify a value for a particular attribute, it will have the
    `xsentinels.default.Default` value. When a Default value is encountered while constructing a
    `xmodel.base.model.BaseModel`, it will resolve these Default values and assign the final
    value for the field.

    To resolve these Defaults, it will look at field on the parent BaseModel class.
    If a non-Default value is defined there, it will use that for the child.
    If not, then it looks at the next parent. If no non-Default value is found we then use
    a value that makes sense. You can see what this is in the first line of each doc-comment.
    In the future, when we start using Python 3.9 we can use type annotations (typing.Annotated)
    to annotate a specific value to the Default type generically. For now it's hard-coded.

    ## Side Notes

    Keep in mind that after the `.api` is accessed for the first time on a particular model
    class, the sdk will construct the rest of the class (lazily)...
    it will read and then remove/delete from the BaseModel class any type-hinted json fields
    with a Field object assigned to the class. It moves these Field objects into a special
    internal structure.  The class gets `None` values set on all fields after this is done.

    ## Details on why we remove them:

    Doing this helps with __getattr__, as it will still be executed for fields without a value
    when we create an object instance. __getattr__ is used to support lazy lookups [via API] of
    related objects. Using __getattr__ is much faster than using the __getattribute__ version.
    So I want to keep using the __getattr__ version if possible.
    """

    _options_explicitly_set_by_user: Set[str] = dataclasses.field(default=None, repr=False)

    def was_option_explicitly_set_by_user(self, option_name: str) -> bool:
        """ Given an option / field-attribute name, if the option was explicitly set by
            the user then we return True.

            If not we return False.

            We determine this while `Field.resolve_defaults` is called, it checks to see
            what is still set to `Default`.

            If an option is not `Default` anymore when `resolve_defaults` is first called, we
            consider it set by the user.

            This is important, as it informs subclasses of BaseModel if their parent model's
            field's value was resolved automatically or if it was set by user.

            Generally, if it was resolved automatically, we continue to resolve it automatically.

            If it was set by the user we tend to use what the user set it to and not resolve
            it automatically.
        """
        return option_name in self._options_explicitly_set_by_user

    def resolve_defaults(
            self,
            *,  # Keyword args only after this point
            name,
            type_hint: Type,
            default_converter_map: Optional[Dict[Type, Converter]] = None,
            parent_field: "Field" = None
    ):
        """
        Resolves all dataclass attributes/fields on self that are still set to `Default`.
        The only exception is `type_hint`. We will always use what is passed in, regardless
        of if there is a parent-field with one set. This allows one on a BaseModel to easily
        override the type-hint without having to create a field with an explicitly set
        type_hint set on it (ie: let normal python annotated type-hint override any parent type).

        This includes ones on subclasses [dataclass will generically tell us about all of them].
        System calls this when a BaseModel class is being lazily constructed
        [ie: when gets the `xmodel.base.model.BaseModel.api` attribute for the first time or
        attempts to create an instance of the BaseModel for the fist time].

        When the BaseModel class is being constructed, this method is called to resolve all
        the Default values still on the instance. We do this by:

        1. We first look at parent_field object if one has been given.
            - If ask that parent field which options where explicitly set by user and which
                ones were set by resolving a `xsentinels.default.Default`. Field objects have an
                internal/private var that keeps track of this.
        2. Next, figure out standard default value for option if option's current value is
            current at `xsentinels.default.Default` (a default sentential value, used to detect
            which values were left unset by user).


        ## More Details

        I have Field objects keep track of which fields were not at
        Default when they are resolved. This allows child Field objects
        to know which values to copy into themselves and which ones
        should be resolved normally via Default.

        The goal here is to avoid copying value from Parent that
        were originally resolved via Default mechanism
        (and were not set explicitly by user).

        An example of why this is handy:

        If we have a parent model with a field of a different type vs the one on the child.
        Unless the converter was explicitly set by the user we want to just use the default
        converter for the different type on the child (and not use the wrong converter by default).
        """

        # Get pycharm to go to class-level var/typehint with the attribute docs we have written
        # instead of going into this method where it gets assigned.
        # Using different var-name for self seems to be able to do that.
        _self = self

        if parent_field:
            options_explicitly_set_by_user = parent_field._options_explicitly_set_by_user
        else:
            options_explicitly_set_by_user = set()

        # Keep track of what was Default before resolving with parent
        # [ie: was not explicitly set by user].
        was_default_before_parent = set()

        for data_field in dataclasses.fields(self):
            data_field_name = data_field.name
            child_value = getattr(self, data_field_name)
            if child_value is Default:
                was_default_before_parent.add(data_field_name)
            else:
                options_explicitly_set_by_user.add(data_field_name)

        # Store for future child-fields.
        self._options_explicitly_set_by_user = options_explicitly_set_by_user

        if parent_field:
            if not isinstance(self, type(parent_field)):
                raise XModelError(
                    f"Child field {self} must be same or subclass of parent ({parent_field})."
                )

            # Go though each dataclass Field in parent, take it's value and copy it to child if:
            #   1. The child still has it set to `Default`.
            #   2. The parent's value is not `Default`.
            #   3. The parent's value was set by the user (options_explicitly_set_by_user).
            #       - If the value was not set by user, we just leave us at `Default` and resolve
            #           them normally.
            for parent_data_field in dataclasses.fields(parent_field):
                p_attr_field: dataclasses.Field
                data_field_name = parent_data_field.name
                parent_value = getattr(parent_field, data_field_name)
                child_value = getattr(self, data_field_name)

                if parent_value is Default:
                    continue

                if data_field_name not in options_explicitly_set_by_user:
                    continue

                if child_value is Default:
                    # Child has Default and parent is not-Default, copy value onto child
                    setattr(self, data_field_name, copy(parent_value))

        # We always set the type-hint, Python will automatically surface the most recent
        # type-hint for us. We want to have it easily overridable without having to use a
        # Field class explicitly.
        _self.type_hint = type_hint

        # Resolve the special-case non-None Default's...
        if self.name is Default:
            # todo: figure out if we should always set name...
            #   ...i'm inclined to not do that.
            _self.name = name

        if self.json_path is Default:
            _self.json_path = self.name

        if self.include_with_fields is Default:
            _self.include_with_fields = set()
        else:
            # Ensure it's a set, not a list or some other thing the user provided.
            _self.include_with_fields = set(loop(self.include_with_fields))

        if self.include_with_fields and self.name != self.json_path:
            raise XModelError(
                f"Can't have a Field with `name != json_path` "
                f"('{self.name}' != '{self.json_path}')"
                f"and that also uses include_with_fields "
                f"({self.include_with_fields})"
            )

        if self.json_path_separator is Default:
            _self.json_path_separator = '.'

        if self.include_in_repr is Default:
            _self.include_in_repr = False

        if self.exclude is Default:
            _self.exclude = False

        if self.read_only is Default:
            _self.read_only = False

        # If converter is None, but we do have a default one, use it...
        if (
            default_converter_map and
            self.type_hint in default_converter_map and
            'converter' in was_default_before_parent and
            self.converter in (None, Default)
        ):
            _self.converter = default_converter_map.get(self.type_hint)

        if (
            self.converter is Default and
            inspect.isclass(self.type_hint) and
            issubclass(self.type_hint, Enum)
        ):
            from xmodel.converters import EnumConverter
            _self.converter = EnumConverter()

        if self.related_type is Default:
            # By Default, we look at type-hint to see if it had a related-type or not...
            type_hint = self.type_hint
            related_type = type_hint
            if typing_inspect.get_origin(type_hint) is list:
                # Check to see if related_type is from typing
                # list and pull out first argument for List[]...
                related_type = typing_inspect.get_args(type_hint)[0]

            # Check if related type is a BaseModel or some other thing....
            from xmodel import BaseModel
            if inspect.isclass(related_type) and issubclass(related_type, BaseModel):
                _self.related_type = related_type

        # If we have a related type, and that related type has a usable id then we generate
        # a default related_field_name_for_id value if needed.
        if (
            self.related_field_name_for_id is Default
            and self.related_type
            and self.related_type.api.structure.has_id_field()
        ):
            _self.related_field_name_for_id = f'{self.name}_id'

        # Always base-line this field to None, we set a value for this if needed
        # in `xmodel.base.structure.BaseStructure._generate_fields`.
        # Because we need to cross-examine fields to set this correctly...
        # This field should never be set manually, it's always set automatically
        # as part of the BaseModel class setup process.
        # See `field_for_foreign_key_related_field` doc-comment for more details.
        _self.field_for_foreign_key_related_field = None

    def resolve_remaining_defaults_to_none(self):
        """ Called by `xmodel.base.structure.BaseStructure` after it calls
            `Field.resolve_defaults`.

            It used to be part of `Field.resolve_defaults`, but it was nicer to seperate
            it so that `Field` subclasses could call `super().resolve_defaults()` and
            still see what fields have defaults needing to be resolved, in case they wanted
            to do some special logic after the super/base classes default's were resolve but
            before they get set to None by Default.
        """
        # Resolve all other fields still at Default to None
        for attr_field in dataclasses.fields(self):
            name = attr_field.name
            child_value = getattr(self, name)
            if child_value is Default:
                setattr(self, name, None)

    def __post_init__(self):
        # Ensure we unwrap the type-hint from any optional.
        type = self.type_hint
        if type is Default:
            return
        unwraped = unwrap_optional_type(type)
        object.__setattr__(self, 'type_hint', unwraped)

    name: str = Default
    """ (Default: Parent, Name of field on BaseModel)

        This is set automatically after the BaseModel class associated with Field is constructed.
        This construction is lazy and happens the first time the
        `xmodel.base.model.BaseModel.api` property is accessed by something.
    """

    # See documentation under type_hint setter, this is only here to give type-hint to dataclass.
    # We have value set on it so IDE knows it's not required in __init__ and won't give warning.
    type_hint: Type = Default

    original_type_hint: Type = dataclasses.field(init=False, default=None, repr=False)
    """ This is set to whatever type_hint was originally set with, un-modified.
        `Field.type_hint` modifies what it's set with by filtering out None/Null types
        so the type is simpler.  It then sets `Field.nullable` to True/False if it's value is
        currently still at `xsentinels.default.Default` based on if NullType was seen or not as one
        of the types.

        In case something wants access to the original unmodified type, it's stored here.
    """

    _type_hint = Default  # No type-hint means data-class ignores it.

    # noinspection PyRedeclaration
    @property
    def type_hint(self) -> Type:
        """ (Default: Parent, The type-hint of the field)

            This is set automatically after the BaseModel class associated with Field is
            constructed. This construction is lazy and happens the first time the
            `xmodel.base.model.BaseModel.api` property is accessed by something.
        """
        return self._type_hint

    @type_hint.setter
    def type_hint(self, value: Type):
        if value is Field.type_hint:
            # This means we were not initialized with a value, so just continue to use Default.
            # When data-class is not given an attr-value in __init__, it does a GET on the class
            # and passes that to us here, so we just ignore it since it's the property setter it's
            # self.
            return
        self.original_type_hint = value
        result = unwrap_optional_type(value, return_saw_null=True)
        self._type_hint = result[0]
        if self.nullable is Default:
            self.nullable = bool(result[1])

    nullable: bool = Default
    """ (Default: Nullable in type-hint, ie: `some_var: Union[int, NullType]`; `False`)

        If `True`, we are a nullable field and can have `xmodel.null.Null` set on us.

        When left as Default, when the type-hint is set on us we will examine to see if it
        is a Union type with NullType in it.  If it does have that, this will be set to True
        otherwise to False.
    """

    read_only: bool = Default
    """ (Default: Parent, False)

        If `True`, we will NEVER send any values for this field to API.
    """

    exclude: bool = Default
    """ (Default: `Parent`, `False`)

        If `True`, by default will will try and exclude this field if the api supports doing this.
        This means that we will request API not send it to us by default.

        This could make the API return results in a more efficient manner if it does not have
        to output fields when most of the time we don't care about it's value.
    """

    default: Any = Default
    """ (Default: `Parent`, `None`)

        Default value for a field that we don't currently or did not previously retrieve a value
        for.

        If this default value is callable, like a function, type or an object that has a
        `__call__()` function defined on it; the system will call it without arguments to get
        a value back for the default value whenever a default value is needed for the model field.

        If you set a Non-Field value on a `xmodel.base.model.BaseModel`, it will be used as the
        value for this the `Field` object is created automatically (ie: if you don't set a
        `Field` object the the BaseModel class attribute/field, but something else, then it gets
        set here for you automatically).
    """

    post_filter: Optional[Filter] = Default
    """ (Default: `Parent`, `None`)

        Called when something set the field after it's been type verified and changed if needed.
        You can use this to alter the value if needed.

        An example would be lower-casing all strings set on property.

        You can also return None to indicate the value is unset, or Null to indicate null value.
        Be sure to only do this with fields that expect a Null value, since whatever the
        post_filter returns is used without verifying it's type against what the field expects.
    """

    converter: Optional[Converter] = Default
    """ (Default: `Parent` if set explicit by user;
        otherwise default converter for `Field.type_hint`)

        .. todo:: Implement this in BaseStructure/BaseApi.

        If set, this is used to convert value to/from api/json.
        You can see a real example of a converter at
        `xmodel.base.api.BaseApi.default_converters`.
    """

    # def __call__(self, fget_func: Callable[[], T]) -> T:
    #     if callable(fget_func):
    #         self.fget = fget_func
    #         return self
    #
    #     raise XModelError(
    #         f"Attempt to calling a Field ({self}) as a callable function without "
    #         f"providing a function as the first parameter, "
    #         f"I got this parameter instead: ({func})... "
    #         f"When a Field is used as a decorator (ie: `@Field()`), it needs to be "
    #         f"places right before a function. This function will be used as the fields "
    #         f"property getter function. "
    #     )

    @property
    def getter(self):
        """
        Like the built-in `@property` of python, except you can also place a Field and set
        any field-options you like, so it lets you make a field that will read/write to JSON
        out of a propety function.

        Basically, used to easily set a `fget` (getter) function on self via the standard
        property decorator syntax.

        See `Field.fget` for more details. But in summary, it works like normal python properties
        except that when a value is set on you, `BaseModel` will first convert it if needed
        before invoking your property setter (if you provide a property setter).

        If you don't provide a property setter, then you can only grab values from the property
        and it will be an error to attempt to set a value on one.

        >>> class MyModel(BaseModel):
        ...
        ...    # You can easily setup a field like normal, and then use getter/setter to setup
        ...    # the getter/setter for the field. Note: You MUST allocate a Field object of some
        ...    # sort your-self, otherwise there would be no object (yet) to use for the decorator.
        ...
        ...    my_field: str = Field()
        ...
        ...    @my_field.getter
        ...    def my_field(self):
        ...         return self._my_field_backing_store
        ...
        ...    # In either case, you can do the setter just like how normal properties work:
        ...    @my_field.setter
        ...    def my_field(self, value):
        ...        self._my_field_backing_store = value
        ...
        ...    _my_field_backing_store = None
        """

        def set_setter_on_field_with(func):
            self.fget = func
            return self

        return set_setter_on_field_with

    @property
    def setter(self):
        """
        Used to easily set a `set_func` setter function on self via the standard
        property decorator syntax, ie:

        >>> class MyModel(BaseModel):
        ...    _my_field_backing_store = None
        ...    my_field: str = Field()
        ...    def my_field(self):
        ...         return self._my_field_backing_store
        ...    @my_field.setter
        ...    def my_field(self, value):
        ...        self._my_field_backing_store = value

        """

        def set_setter_on_field_with(func):
            self.fset = func
            return self

        return set_setter_on_field_with

    fget: 'Optional[Callable[[M], Any]]' = Default
    """ (Default: `Parent`; otherwise `None`)

        Function to use to get the value of a property, instead of getting it directly from object,
        BaseModel will use this.

        Callable Args:

        1. The model (ie: `self`)
        2. Is associated Field object
    """

    fset: 'Optional[Callable[[BaseModel, Any], None]]' = Default
    """ (Default: `Parent`; otherwise `None`)

        Function to use to set the value of a property, instead of setting it directly on object,
        BaseModel will use this.

        Callable Args:

        1. The model (ie: `self`)
        2. Is associated Field object
        3. Finally, the value to set.

        The value will be passed into function AFTER it's been verified, and converted if needed.
        If you need to adjust how the converter aspect works, look at `Field.converter`.

        Also, if someone attempts get the value, and the value is None...
        And if there is a `Field.default` set, the BaseModel needs to create a default value
        and return it.

        The created value will be set onto object before the getter returns.
        because no value is there... then it will be created and this function will be called.
    """

    include_with_fields: Set[str] = Default
    """
    (Default: `Parent`, `[]`)

    List of field names that, if they are included in the JSON, this one should too;
    even if our value has not changed.

    Defaults to blank set (ie: nothing).

    .. important:: Can use `include_with_fields` only when `Field.name` and `Field.json_path`
        are the same value (ie: have not customized `field.json_path` to be different.
        It's something that we have chosen not  to support to keep the implementation of this
        simpler. It's something that could be support in the future if the need ever arises.

    .. todo:: in the future, consider also allowing to pass in field-object,
        (which we would convert to the fields name, for the user as a convenience).
    """

    json_path: str = Default
    """
    (Default: `Field.name` at time of BaseModel-class construction [when defaults are resolved])

    Key/name used when mapping field to/from json/api request.

    If you include a `.`, it will go one level deeper in the JSON. That way you can
    map from/to a sub-property....

    Defaults to the Field.name.
    """

    json_path_separator: str = Default
    """ (Default: `Parent`, ".")

        Path separator to use in json_path.  Defaults to a period (".").
    """

    # todo: Would like to rename this to just `repr`, just like in dataclasses.
    include_in_repr: bool = Default
    """ (Default: `Parent`, `False`)

        .. todo:: Would like to rename this to just `repr`, just like in dataclasses.

        Used in `xmodel.base.api.BaseApi.list_of_attrs_to_repr` to return a list of field-names
        that `xmodel.base.model.BaseModel.__repr__` uses to determine if the field should be
        included in the string it returns.

        This string is what get's used when the `xmodel.base.model.BaseModel` gets converted to
        a string, such as when logging the object out or printing it via the debugger.
    """

    related_type: 'Optional[Type[BaseModel]]' = Default
    """
    (Default: `Parent`, `Field.type_hint` if subclass of `xmodel.base.model.BaseModel`, None)

    If not None, this is a type that is as subclass of `xmodel.base.model.BaseModel`.

    If `xsentinels.default.Default`, and we have not Parent field,
    we grab this from type-hints, examples:

    >>> from xmodel.base.model. import BaseModel
    >>> class MyModel(BaseModel):
    ...     # Needs 'my_attr_id' via JSON, will do lazy lookup:
    ...     my_attr: SomeOtherModel
    ...
    ...     # 'List' Not fully supported yet:
    ...     my_attr_list: List[SomeOtherModel]
    ...
    ...     # This works (for basic types, inside list/set)
    ...     # BaseModel-types inside list will come in future.
    ...     my_attr_list: Set[int]

    .. todo:: Right now we only support a one-to-one. In the future, we will support
        one-to-many via the generic `List[SomeModel]` type-hint syntax.

    Generally, when you ask for the value of a field with this set you get back an Instance
    of the type set into this field (as a value in this field).

    By convention, the primary-key value for this is the field name from the api when a
    "_id" appended to the end of the name; ie: "`Field.json_key`_id"

    .. todo:: At some point, I would like to make the `_id` customizable, perhaps with
        a `Field.related_type_id_key` or some such....
    """

    related_field_name_for_id: Optional[str] = Default
    """
    .. important:: Not currently used, will be used when one-to-many support is fully
        added. However, this should still be populated and return correct information.


    (Default: `Parent`;
              If `Field.related_type` is set to something with
              `xmodel.base.structure.BaseStructure.have_usable_id` is True,
              then `_id` is appended on end of`Field.json_path`.

              If the related_field uses no id field, then the object should be a sub-object
              and fully embedded into tje JSON instead of only embedding it's id value.
    )

    When getting Default value (if parent does not have this set) we use `self.json_path` and
    append an `_id` to the end. You can override this if you need to via the usual way:
    `Field(related_field_name_for_id='...xyz...')`.

    When resolve the Default value, we will only do so if the `Field.related_type` has it's
    `api.structure.have_usable_id` set to True (meaning that the related-type uses an `id` field).

    If a related type does not use an `id` field, by default the related type will be an
    embedded object (ie: fully embedded into the produced JSON, as needed).

    .. note:: The below statement is for when one_to_many is supported, someday...

    ~~if related_is_one_to_many is False, otherwise we find the a one-to-one link back to us
    from related_type, and use that field's `Field.json_path`.~~
    """

    field_for_foreign_key_related_field: 'Optional[Field]' = dataclasses.field(
        default=Default, init=False
    )
    """
    .. important:: Not currently used, will be used when one-to-many support is fully
        added. However, this should still be populated and return correct information.


    (Default: If another field on Model has a `Field.related_field_name_for_id` that is equal
              to self.name, then we set this attribute with that other field object.

              Otherwise this is None
    )


    .. important:: this is always automatically generated, and should not be set manually.
        Keep reading for details.

    By Default, if this field represents the value of an 'id' or key, for a one-to-one related
    foreign-key field  then this will be set to that related field.

    This is the other field on the same Model that is the related object for this key field.
    In other-words, the field this points to the field that represents the object for the value
    of this id/key field IF the related field is a one-to-one relationship.

    This can't be set via the `__init__` method for Field, it's always set when
    the `xmodel.base.structure.BaseStructure` generates fields via
    it's `_generate_fields` method.
    """

    @property
    def is_foreign_key(self):
        """
            .. important:: Not currently used, will be used when one-to-many support is fully
                added. However, this should still be populated and return correct information.

            If we have a `field_for_foreign_key_related_field`, then we are a foreign key field.

            This checks `Field.field_for_foreign_key_related_field` and returns True or False
            depending on if that has a field value or not.

            This property just makes it clear and documents on how one knows if we are a
            foreign key field or not.
        """
        return bool(self.field_for_foreign_key_related_field)

    related_to_many: bool = Default
    """
    .. important:: Not currently used, will be used when one-to-many support is fully added.
        Right now this will by Default always be `None`.

    (Default: `Parent`, If type-hint is `List[Model]` and other model has a one-to-one type-hint
    back to myself)

    If True, this field is a one-to-many relationship with another model.

    We use `Field.related_field_name_for_id` a the key for a query on the relationship via
    `BaseApi.get`. We will query `Field.related_type`'s `api`, call get on it and use our model's
    `xmodel.base.model.BaseModel.id` as the query value.

    We will do our best to weak-cache the result, if weak-cache is currently enabled;
    see `xmodel.weak_cache_pool.WeakCachePool` for weak-cache details.
    """

    model: 'BaseModel' = Default

    @property
    def related_field(self) -> 'Field':
        """ Set to the Field for the `Field.related_field_name_for_id`. """
        api = self.related_type.api if self.related_to_many else self.model.api
        return api.structure.get_field(self.related_field_name_for_id)

Class variables

var converter : Optional[Converter]

(Default: Parent if set explicit by user; otherwise default converter for Field.type_hint)

TODO

Implement this in BaseStructure/BaseApi.

If set, this is used to convert value to/from api/json. You can see a real example of a converter at BaseApi.default_converters.

var default : Any

(Default: Parent, None)

Default value for a field that we don't currently or did not previously retrieve a value for.

If this default value is callable, like a function, type or an object that has a __call__() function defined on it; the system will call it without arguments to get a value back for the default value whenever a default value is needed for the model field.

If you set a Non-Field value on a BaseModel, it will be used as the value for this the Field object is created automatically (ie: if you don't set a Field object the the BaseModel class attribute/field, but something else, then it gets set here for you automatically).

var exclude : bool

(Default: Parent, False)

If True, by default will will try and exclude this field if the api supports doing this. This means that we will request API not send it to us by default.

This could make the API return results in a more efficient manner if it does not have to output fields when most of the time we don't care about it's value.

var fget : Optional[Callable[[M], Any]]

(Default: Parent; otherwise None)

Function to use to get the value of a property, instead of getting it directly from object, BaseModel will use this.

Callable Args:

  1. The model (ie: self)
  2. Is associated Field object

Important: Not currently used, will be used when one-to-many support is fully

added. However, this should still be populated and return correct information.

(Default: If another field on Model has a Field.related_field_name_for_id that is equal to self.name, then we set this attribute with that other field object.

      Otherwise this is None

)

Important: this is always automatically generated, and should not be set manually.

Keep reading for details.

By Default, if this field represents the value of an 'id' or key, for a one-to-one related foreign-key field then this will be set to that related field.

This is the other field on the same Model that is the related object for this key field. In other-words, the field this points to the field that represents the object for the value of this id/key field IF the related field is a one-to-one relationship.

This can't be set via the __init__ method for Field, it's always set when the BaseStructure generates fields via it's _generate_fields method.

var fset : Optional[Callable[[BaseModel, Any], None]]

(Default: Parent; otherwise None)

Function to use to set the value of a property, instead of setting it directly on object, BaseModel will use this.

Callable Args:

  1. The model (ie: self)
  2. Is associated Field object
  3. Finally, the value to set.

The value will be passed into function AFTER it's been verified, and converted if needed. If you need to adjust how the converter aspect works, look at Field.converter.

Also, if someone attempts get the value, and the value is None… And if there is a Field.default set, the BaseModel needs to create a default value and return it.

The created value will be set onto object before the getter returns. because no value is there… then it will be created and this function will be called.

var include_in_repr : bool

(Default: Parent, False)

TODO

Would like to rename this to just repr, just like in dataclasses.

Used in BaseApi.list_of_attrs_to_repr() to return a list of field-names that xmodel.base.model.BaseModel.__repr__ uses to determine if the field should be included in the string it returns.

This string is what get's used when the BaseModel gets converted to a string, such as when logging the object out or printing it via the debugger.

var include_with_fields : Set[str]

(Default: Parent, [])

List of field names that, if they are included in the JSON, this one should too; even if our value has not changed.

Defaults to blank set (ie: nothing).

Important: Can use include_with_fields only when Field.name and Field.json_path

are the same value (ie: have not customized field.json_path to be different. It's something that we have chosen not to support to keep the implementation of this simpler. It's something that could be support in the future if the need ever arises.

TODO

in the future, consider also allowing to pass in field-object, (which we would convert to the fields name, for the user as a convenience).

var json_path : str

(Default: Field.name at time of BaseModel-class construction [when defaults are resolved])

Key/name used when mapping field to/from json/api request.

If you include a ., it will go one level deeper in the JSON. That way you can map from/to a sub-property....

Defaults to the Field.name.

var json_path_separator : str

(Default: Parent, ".")

Path separator to use in json_path. Defaults to a period (".").

var modelBaseModel
var name : str

(Default: Parent, Name of field on BaseModel)

This is set automatically after the BaseModel class associated with Field is constructed. This construction is lazy and happens the first time the BaseModel.api property is accessed by something.

var nullable : bool

(Default: Nullable in type-hint, ie: some_var: Union[int, NullType]; False)

If True, we are a nullable field and can have xmodel.null.Null set on us.

When left as Default, when the type-hint is set on us we will examine to see if it is a Union type with NullType in it. If it does have that, this will be set to True otherwise to False.

var original_type_hint : Type

This is set to whatever type_hint was originally set with, un-modified. Field.type_hint modifies what it's set with by filtering out None/Null types so the type is simpler. It then sets Field.nullable to True/False if it's value is currently still at Default based on if NullType was seen or not as one of the types.

In case something wants access to the original unmodified type, it's stored here.

var post_filter : Optional[Filter]

(Default: Parent, None)

Called when something set the field after it's been type verified and changed if needed. You can use this to alter the value if needed.

An example would be lower-casing all strings set on property.

You can also return None to indicate the value is unset, or Null to indicate null value. Be sure to only do this with fields that expect a Null value, since whatever the post_filter returns is used without verifying it's type against what the field expects.

var read_only : bool

(Default: Parent, False)

If True, we will NEVER send any values for this field to API.

var related_field_name_for_id : Optional[str]

Important: Not currently used, will be used when one-to-many support is fully

added. However, this should still be populated and return correct information.

(Default: Parent; If Field.related_type is set to something with xmodel.base.structure.BaseStructure.have_usable_id is True, then _id is appended on end ofField.json_path.

      If the related_field uses no id field, then the object should be a sub-object
      and fully embedded into tje JSON instead of only embedding it's id value.

)

When getting Default value (if parent does not have this set) we use self.json_path and append an _id to the end. You can override this if you need to via the usual way: Field(related_field_name_for_id='...xyz...').

When resolve the Default value, we will only do so if the Field.related_type has it's api.structure.have_usable_id set to True (meaning that the related-type uses an id field).

If a related type does not use an id field, by default the related type will be an embedded object (ie: fully embedded into the produced JSON, as needed).

Note: The below statement is for when one_to_many is supported, someday…

~~if related_is_one_to_many is False, otherwise we find the a one-to-one link back to us from related_type, and use that field's Field.json_path.~~

var related_to_many : bool

Important: Not currently used, will be used when one-to-many support is fully added.

Right now this will by Default always be None.

(Default: Parent, If type-hint is List[Model] and other model has a one-to-one type-hint back to myself)

If True, this field is a one-to-many relationship with another model.

We use Field.related_field_name_for_id a the key for a query on the relationship via BaseApi.get. We will query Field.related_type's api, call get on it and use our model's xmodel.base.model.BaseModel.id as the query value.

We will do our best to weak-cache the result, if weak-cache is currently enabled; see xmodel.weak_cache_pool.WeakCachePool for weak-cache details.

var related_type : Optional[Type[BaseModel]]

(Default: Parent, Field.type_hint if subclass of BaseModel, None)

If not None, this is a type that is as subclass of BaseModel.

If Default, and we have not Parent field, we grab this from type-hints, examples:

>>> from xmodel.base.model. import BaseModel
>>> class MyModel(BaseModel):
...     # Needs 'my_attr_id' via JSON, will do lazy lookup:
...     my_attr: SomeOtherModel
...
...     # 'List' Not fully supported yet:
...     my_attr_list: List[SomeOtherModel]
...
...     # This works (for basic types, inside list/set)
...     # BaseModel-types inside list will come in future.
...     my_attr_list: Set[int]

TODO

Right now we only support a one-to-one. In the future, we will support one-to-many via the generic List[SomeModel] type-hint syntax.

Generally, when you ask for the value of a field with this set you get back an Instance of the type set into this field (as a value in this field).

By convention, the primary-key value for this is the field name from the api when a "_id" appended to the end of the name; ie: "Field.json_key_id"

TODO

At some point, I would like to make the _id customizable, perhaps with a Field.related_type_id_key or some such....

Instance variables

var getter

Like the built-in @property of python, except you can also place a Field and set any field-options you like, so it lets you make a field that will read/write to JSON out of a propety function.

Basically, used to easily set a fget (getter) function on self via the standard property decorator syntax.

See Field.fget for more details. But in summary, it works like normal python properties except that when a value is set on you, BaseModel will first convert it if needed before invoking your property setter (if you provide a property setter).

If you don't provide a property setter, then you can only grab values from the property and it will be an error to attempt to set a value on one.

>>> class MyModel(BaseModel):
...
...    # You can easily setup a field like normal, and then use getter/setter to setup
...    # the getter/setter for the field. Note: You MUST allocate a Field object of some
...    # sort your-self, otherwise there would be no object (yet) to use for the decorator.
...
...    my_field: str = Field()
...
...    @my_field.getter
...    def my_field(self):
...         return self._my_field_backing_store
...
...    # In either case, you can do the setter just like how normal properties work:
...    @my_field.setter
...    def my_field(self, value):
...        self._my_field_backing_store = value
...
...    _my_field_backing_store = None
Expand source code
@property
def getter(self):
    """
    Like the built-in `@property` of python, except you can also place a Field and set
    any field-options you like, so it lets you make a field that will read/write to JSON
    out of a propety function.

    Basically, used to easily set a `fget` (getter) function on self via the standard
    property decorator syntax.

    See `Field.fget` for more details. But in summary, it works like normal python properties
    except that when a value is set on you, `BaseModel` will first convert it if needed
    before invoking your property setter (if you provide a property setter).

    If you don't provide a property setter, then you can only grab values from the property
    and it will be an error to attempt to set a value on one.

    >>> class MyModel(BaseModel):
    ...
    ...    # You can easily setup a field like normal, and then use getter/setter to setup
    ...    # the getter/setter for the field. Note: You MUST allocate a Field object of some
    ...    # sort your-self, otherwise there would be no object (yet) to use for the decorator.
    ...
    ...    my_field: str = Field()
    ...
    ...    @my_field.getter
    ...    def my_field(self):
    ...         return self._my_field_backing_store
    ...
    ...    # In either case, you can do the setter just like how normal properties work:
    ...    @my_field.setter
    ...    def my_field(self, value):
    ...        self._my_field_backing_store = value
    ...
    ...    _my_field_backing_store = None
    """

    def set_setter_on_field_with(func):
        self.fget = func
        return self

    return set_setter_on_field_with
var is_foreign_key

Important: Not currently used, will be used when one-to-many support is fully

added. However, this should still be populated and return correct information.

If we have a field_for_foreign_key_related_field, then we are a foreign key field.

This checks Field.field_for_foreign_key_related_field and returns True or False depending on if that has a field value or not.

This property just makes it clear and documents on how one knows if we are a foreign key field or not.

Expand source code
@property
def is_foreign_key(self):
    """
        .. important:: Not currently used, will be used when one-to-many support is fully
            added. However, this should still be populated and return correct information.

        If we have a `field_for_foreign_key_related_field`, then we are a foreign key field.

        This checks `Field.field_for_foreign_key_related_field` and returns True or False
        depending on if that has a field value or not.

        This property just makes it clear and documents on how one knows if we are a
        foreign key field or not.
    """
    return bool(self.field_for_foreign_key_related_field)
var related_fieldField

Set to the Field for the Field.related_field_name_for_id.

Expand source code
@property
def related_field(self) -> 'Field':
    """ Set to the Field for the `Field.related_field_name_for_id`. """
    api = self.related_type.api if self.related_to_many else self.model.api
    return api.structure.get_field(self.related_field_name_for_id)
var setter

Used to easily set a set_func setter function on self via the standard property decorator syntax, ie:

>>> class MyModel(BaseModel):
...    _my_field_backing_store = None
...    my_field: str = Field()
...    def my_field(self):
...         return self._my_field_backing_store
...    @my_field.setter
...    def my_field(self, value):
...        self._my_field_backing_store = value
Expand source code
@property
def setter(self):
    """
    Used to easily set a `set_func` setter function on self via the standard
    property decorator syntax, ie:

    >>> class MyModel(BaseModel):
    ...    _my_field_backing_store = None
    ...    my_field: str = Field()
    ...    def my_field(self):
    ...         return self._my_field_backing_store
    ...    @my_field.setter
    ...    def my_field(self, value):
    ...        self._my_field_backing_store = value

    """

    def set_setter_on_field_with(func):
        self.fset = func
        return self

    return set_setter_on_field_with
var type_hint : Type

(Default: Parent, The type-hint of the field)

This is set automatically after the BaseModel class associated with Field is constructed. This construction is lazy and happens the first time the BaseModel.api property is accessed by something.

Expand source code
@property
def type_hint(self) -> Type:
    """ (Default: Parent, The type-hint of the field)

        This is set automatically after the BaseModel class associated with Field is
        constructed. This construction is lazy and happens the first time the
        `xmodel.base.model.BaseModel.api` property is accessed by something.
    """
    return self._type_hint

Methods

def resolve_defaults(self, *, name, type_hint: Type, default_converter_map: Optional[Dict[Type, Converter]] = None, parent_field: Field = None)

Resolves all dataclass attributes/fields on self that are still set to Default. The only exception is type_hint. We will always use what is passed in, regardless of if there is a parent-field with one set. This allows one on a BaseModel to easily override the type-hint without having to create a field with an explicitly set type_hint set on it (ie: let normal python annotated type-hint override any parent type).

This includes ones on subclasses [dataclass will generically tell us about all of them]. System calls this when a BaseModel class is being lazily constructed [ie: when gets the xmodel.base.model.BaseModel.api attribute for the first time or attempts to create an instance of the BaseModel for the fist time].

When the BaseModel class is being constructed, this method is called to resolve all the Default values still on the instance. We do this by:

  1. We first look at parent_field object if one has been given.
    • If ask that parent field which options where explicitly set by user and which ones were set by resolving a Default. Field objects have an internal/private var that keeps track of this.
  2. Next, figure out standard default value for option if option's current value is current at Default (a default sentential value, used to detect which values were left unset by user).

More Details

I have Field objects keep track of which fields were not at Default when they are resolved. This allows child Field objects to know which values to copy into themselves and which ones should be resolved normally via Default.

The goal here is to avoid copying value from Parent that were originally resolved via Default mechanism (and were not set explicitly by user).

An example of why this is handy:

If we have a parent model with a field of a different type vs the one on the child. Unless the converter was explicitly set by the user we want to just use the default converter for the different type on the child (and not use the wrong converter by default).

Expand source code
def resolve_defaults(
        self,
        *,  # Keyword args only after this point
        name,
        type_hint: Type,
        default_converter_map: Optional[Dict[Type, Converter]] = None,
        parent_field: "Field" = None
):
    """
    Resolves all dataclass attributes/fields on self that are still set to `Default`.
    The only exception is `type_hint`. We will always use what is passed in, regardless
    of if there is a parent-field with one set. This allows one on a BaseModel to easily
    override the type-hint without having to create a field with an explicitly set
    type_hint set on it (ie: let normal python annotated type-hint override any parent type).

    This includes ones on subclasses [dataclass will generically tell us about all of them].
    System calls this when a BaseModel class is being lazily constructed
    [ie: when gets the `xmodel.base.model.BaseModel.api` attribute for the first time or
    attempts to create an instance of the BaseModel for the fist time].

    When the BaseModel class is being constructed, this method is called to resolve all
    the Default values still on the instance. We do this by:

    1. We first look at parent_field object if one has been given.
        - If ask that parent field which options where explicitly set by user and which
            ones were set by resolving a `xsentinels.default.Default`. Field objects have an
            internal/private var that keeps track of this.
    2. Next, figure out standard default value for option if option's current value is
        current at `xsentinels.default.Default` (a default sentential value, used to detect
        which values were left unset by user).


    ## More Details

    I have Field objects keep track of which fields were not at
    Default when they are resolved. This allows child Field objects
    to know which values to copy into themselves and which ones
    should be resolved normally via Default.

    The goal here is to avoid copying value from Parent that
    were originally resolved via Default mechanism
    (and were not set explicitly by user).

    An example of why this is handy:

    If we have a parent model with a field of a different type vs the one on the child.
    Unless the converter was explicitly set by the user we want to just use the default
    converter for the different type on the child (and not use the wrong converter by default).
    """

    # Get pycharm to go to class-level var/typehint with the attribute docs we have written
    # instead of going into this method where it gets assigned.
    # Using different var-name for self seems to be able to do that.
    _self = self

    if parent_field:
        options_explicitly_set_by_user = parent_field._options_explicitly_set_by_user
    else:
        options_explicitly_set_by_user = set()

    # Keep track of what was Default before resolving with parent
    # [ie: was not explicitly set by user].
    was_default_before_parent = set()

    for data_field in dataclasses.fields(self):
        data_field_name = data_field.name
        child_value = getattr(self, data_field_name)
        if child_value is Default:
            was_default_before_parent.add(data_field_name)
        else:
            options_explicitly_set_by_user.add(data_field_name)

    # Store for future child-fields.
    self._options_explicitly_set_by_user = options_explicitly_set_by_user

    if parent_field:
        if not isinstance(self, type(parent_field)):
            raise XModelError(
                f"Child field {self} must be same or subclass of parent ({parent_field})."
            )

        # Go though each dataclass Field in parent, take it's value and copy it to child if:
        #   1. The child still has it set to `Default`.
        #   2. The parent's value is not `Default`.
        #   3. The parent's value was set by the user (options_explicitly_set_by_user).
        #       - If the value was not set by user, we just leave us at `Default` and resolve
        #           them normally.
        for parent_data_field in dataclasses.fields(parent_field):
            p_attr_field: dataclasses.Field
            data_field_name = parent_data_field.name
            parent_value = getattr(parent_field, data_field_name)
            child_value = getattr(self, data_field_name)

            if parent_value is Default:
                continue

            if data_field_name not in options_explicitly_set_by_user:
                continue

            if child_value is Default:
                # Child has Default and parent is not-Default, copy value onto child
                setattr(self, data_field_name, copy(parent_value))

    # We always set the type-hint, Python will automatically surface the most recent
    # type-hint for us. We want to have it easily overridable without having to use a
    # Field class explicitly.
    _self.type_hint = type_hint

    # Resolve the special-case non-None Default's...
    if self.name is Default:
        # todo: figure out if we should always set name...
        #   ...i'm inclined to not do that.
        _self.name = name

    if self.json_path is Default:
        _self.json_path = self.name

    if self.include_with_fields is Default:
        _self.include_with_fields = set()
    else:
        # Ensure it's a set, not a list or some other thing the user provided.
        _self.include_with_fields = set(loop(self.include_with_fields))

    if self.include_with_fields and self.name != self.json_path:
        raise XModelError(
            f"Can't have a Field with `name != json_path` "
            f"('{self.name}' != '{self.json_path}')"
            f"and that also uses include_with_fields "
            f"({self.include_with_fields})"
        )

    if self.json_path_separator is Default:
        _self.json_path_separator = '.'

    if self.include_in_repr is Default:
        _self.include_in_repr = False

    if self.exclude is Default:
        _self.exclude = False

    if self.read_only is Default:
        _self.read_only = False

    # If converter is None, but we do have a default one, use it...
    if (
        default_converter_map and
        self.type_hint in default_converter_map and
        'converter' in was_default_before_parent and
        self.converter in (None, Default)
    ):
        _self.converter = default_converter_map.get(self.type_hint)

    if (
        self.converter is Default and
        inspect.isclass(self.type_hint) and
        issubclass(self.type_hint, Enum)
    ):
        from xmodel.converters import EnumConverter
        _self.converter = EnumConverter()

    if self.related_type is Default:
        # By Default, we look at type-hint to see if it had a related-type or not...
        type_hint = self.type_hint
        related_type = type_hint
        if typing_inspect.get_origin(type_hint) is list:
            # Check to see if related_type is from typing
            # list and pull out first argument for List[]...
            related_type = typing_inspect.get_args(type_hint)[0]

        # Check if related type is a BaseModel or some other thing....
        from xmodel import BaseModel
        if inspect.isclass(related_type) and issubclass(related_type, BaseModel):
            _self.related_type = related_type

    # If we have a related type, and that related type has a usable id then we generate
    # a default related_field_name_for_id value if needed.
    if (
        self.related_field_name_for_id is Default
        and self.related_type
        and self.related_type.api.structure.has_id_field()
    ):
        _self.related_field_name_for_id = f'{self.name}_id'

    # Always base-line this field to None, we set a value for this if needed
    # in `xmodel.base.structure.BaseStructure._generate_fields`.
    # Because we need to cross-examine fields to set this correctly...
    # This field should never be set manually, it's always set automatically
    # as part of the BaseModel class setup process.
    # See `field_for_foreign_key_related_field` doc-comment for more details.
    _self.field_for_foreign_key_related_field = None
def resolve_remaining_defaults_to_none(self)

Called by BaseStructure after it calls Field.resolve_defaults().

It used to be part of Field.resolve_defaults(), but it was nicer to seperate it so that Field subclasses could call super().resolve_defaults() and still see what fields have defaults needing to be resolved, in case they wanted to do some special logic after the super/base classes default's were resolve but before they get set to None by Default.

Expand source code
def resolve_remaining_defaults_to_none(self):
    """ Called by `xmodel.base.structure.BaseStructure` after it calls
        `Field.resolve_defaults`.

        It used to be part of `Field.resolve_defaults`, but it was nicer to seperate
        it so that `Field` subclasses could call `super().resolve_defaults()` and
        still see what fields have defaults needing to be resolved, in case they wanted
        to do some special logic after the super/base classes default's were resolve but
        before they get set to None by Default.
    """
    # Resolve all other fields still at Default to None
    for attr_field in dataclasses.fields(self):
        name = attr_field.name
        child_value = getattr(self, name)
        if child_value is Default:
            setattr(self, name, None)
def was_option_explicitly_set_by_user(self, option_name: str) ‑> bool

Given an option / field-attribute name, if the option was explicitly set by the user then we return True.

If not we return False.

We determine this while Field.resolve_defaults() is called, it checks to see what is still set to Default.

If an option is not Default anymore when resolve_defaults is first called, we consider it set by the user.

This is important, as it informs subclasses of BaseModel if their parent model's field's value was resolved automatically or if it was set by user.

Generally, if it was resolved automatically, we continue to resolve it automatically.

If it was set by the user we tend to use what the user set it to and not resolve it automatically.

Expand source code
def was_option_explicitly_set_by_user(self, option_name: str) -> bool:
    """ Given an option / field-attribute name, if the option was explicitly set by
        the user then we return True.

        If not we return False.

        We determine this while `Field.resolve_defaults` is called, it checks to see
        what is still set to `Default`.

        If an option is not `Default` anymore when `resolve_defaults` is first called, we
        consider it set by the user.

        This is important, as it informs subclasses of BaseModel if their parent model's
        field's value was resolved automatically or if it was set by user.

        Generally, if it was resolved automatically, we continue to resolve it automatically.

        If it was set by the user we tend to use what the user set it to and not resolve
        it automatically.
    """
    return option_name in self._options_explicitly_set_by_user
class JsonModel (*args, **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 JsonModel(BaseModel):
    pass

Ancestors

Class variables

var apiBaseApi[BaseModel]

Inherited from: BaseModel.api

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

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: …

class XModelError (*args, **kwargs)

Base-class for all xmodel exceptions.

Expand source code
class XModelError(Exception):
    """ Base-class for all xmodel exceptions. """
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException

Subclasses