How to create a programmatic IDS¶
ts-ids-core allows you to export your programmatic IDS to JSON schema.
The exported IDS JSON schema is the interface between an IDS definition and the TetraScience platform.
If you want to convert an existing JSON schema to a programmatic IDS, see Generate programmatic IDS from JSON schema.
Model definition¶
IDSs are built using components. Components are individual object definitions that conform to the TetraScience platform requirements of IDSs.
To define IDS schema components, inherit from the ts_ids_core.base.ids_element.IdsElement class and define fields similar to how pydantic.BaseModel or dataclasses.dataclass() are defined.
Fields can be specified in the following three ways:
With a type annotation. For example,
from ts_ids_core.base.ids_element import IdsElement
class Example(IdsElement):
field_1: str
This is the equivalent of:
{
"additionalProperties": false,
"properties": {
"field_1": {
"type": "string"
}
},
"type": "object"
}
With a type annotation and assignment to a default value via the equals operator. For example, if the default value is to be “value”, do
from ts_ids_core.base.ids_element import IdsElement
class Example(IdsElement):
field_1: str = "value"
Via the
ts_ids_core.base.ids_element.IdsField()function. For example,
from ts_ids_core.base.ids_element import IdsElement
from ts_ids_core.base.ids_field import IdsField
class Example(IdsElement):
field_1: str = IdsField()
field_2: str = IdsField(default="value")
Specifying const fields¶
Defining a field as a constant, or const, is accomplished the same way as Pydantic does, using the Literal type hint.
from typing import Literal
from ts_ids_core.base.ids_element import IdsElement
class Example(IdsElement):
field_1: Literal["value"]
This is the equivalent of:
{
"additionalProperties": false,
"properties": {
"field_1": {
"const": "value",
"type": "string"
}
},
"type": "object"
}
Disallowed field types¶
Note that fields may only be atomic types or subclasses of ts_ids_core.base.ids_element.IdsElement.
They cannot be containers, e.g. dict or tuple, or other generics, e.g. typing.TypeVar.
Additionally, fields may not have multiple types, with the exception that atomic types may be “nullable” (see below).
For example:
Allowed:
from ts_ids_core.base.ids_element import IdsElement
from typing import Union
class Foo(IdsElement):
foo: str
class Bar(IdsElement):
bar: Foo
baz: Union[str, None]
Disallowed:
from ts_ids_core.base.ids_element import IdsElement
from typing import Union, Tuple
class Foo(IdsElement):
foo: str
class Bar(IdsElement):
bar: Union[Foo, None]
baz: Tuple[Foo, str]
Nullable fields¶
Nullable fields can be defined using the ts_ids_core.annotations.Nullable type hint.
Defining a field as nullable is equivalent to defining the field as Union[T, None] where T is any atomic type.
Thus, a nullable field may take on the value of None in Python (null in JSON) or a value of type T.
This is the only case in which a field can be defined as a union of two types, see Disallowed field types above.
Non-nullable fields are fields that cannot take on the value None.
from ts_ids_core.base.ids_element import IdsElement
from ts_ids_core.annotations import Nullable
class Example(IdsElement):
field: Nullable[str]
Required Fields¶
There is a difference between how Pydantic defines fields as required vs. how ts-ids-core does.
Designating a field as required in Pydantic can be done by either defining a field with no default value or defining it with an ellipsis as the default.
For more see here.
from pydantic import BaseModel, Field
class Model(BaseModel):
a: int
b: int = ...
c: int = Field(...)
In this case you cannot instantiate the Model class without passing a value for each field.
Fields that are defined with a default value that is not an ellipsis are considered non-required.
from pydantic import BaseModel, Field
class Model(BaseModel):
a: int = 1
b: int = Field(default=1)
c: int = Field(default_factory=lambda: 1)
In this case the Model class can be instantiated without passing a value for any given field.
This model of required is conducive to Python’s behavior vs. the behavior of JSON schema validation.
In the case of Python, all fields must have a value for the class to be instantiated whether they are required or not.
Values for required fields need to be explicitly passed and values for non-required fields can be explicitly passed or will fall back to their defined default values.
In the case of JSON schema validation, a JSON instance is valid when you have a non-required field and do not provide value for it (i.e. the field is non-existent in the JSON instance).
“Required” in ts-ids-core is equivalent to that in JSON Schema; required fields must have a value in the JSON instance and non-required fields can either have a value or be missing from the JSON instance.
The Pydantic way of defining a field as required is not supported in ts-ids-core.
To define a field as required using ts-ids-core, use the ts_ids_core.annotations.Required type annotation.
For example:
from ts_ids_core.annotations import Required
from ts_ids_core.base.ids_element import IdsElement
class Example(IdsElement):
required_field: Required[str] # required
non_required_no_default: str # not required
non_required_with_default: str = "foo" # not required
This is the equivalent of:
{
"additionalProperties": false,
"properties": {
"required_field": {
"type": "string"
},
"non_required_no_default": {
"type": "string"
},
"non_required_with_default": {
"type": "string"
}
},
"required": [
"required_field"
],
"type": "object"
}
In the example above, unlike Pydantic’s default behavior, the Example class can be instantiated without passing a value for the field non_required_no_default.
Under the hood, ts-ids-core will assign the field with a default of ts_ids_core.base.ids_undefined_type.IDS_UNDEFINED.
Once you serialize this model’s instance to JSON, non_required_no_default will be omitted from the JSON instance.