Hello World
YANG Model
Let's take a simple yang model for a "Hello World" example. This model allows specifying a simple endpoint containing an address and port. An optional description can be defined.
module my-endpoint {
namespace
"http://pydantify.github.io/ns/yang/pydantify-endpoint";
prefix "ep";
description 'Example demonstarating leafref nodes';
typedef port {
type uint16 {
range "1 .. 65535";
}
}
container endpoint {
description "Definition of a endpoint";
leaf address {
type string;
description "Endpoint address. IP or FQDN";
mandatory true;
}
leaf port {
type port;
description "Port number between 1 and 65535";
mandatory true;
}
leaf description {
type string;
description "Endpoint description";
}
}
}
Using pyang the model can be validated and displayed as a tree.
module: my-endpoint
+--rw endpoint
+--rw address string
+--rw port port
+--rw description? string
Create pydantic model
Pydantify can now convert the YANG model into a pydantic one.
$ pydantify examples/hello_world/my-endpoint.yang
[INFO] /workspaces/pydantify/pydantify/plugins/pydantic_plugin.py:41 (emit): Output model generated in 0.049s.
The generated module will be in the file out/out.py
. We can move and rename it to endpoint.py
.
from __future__ import annotations
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field, RootModel
from typing_extensions import Annotated
class EndpointContainer(BaseModel):
"""
Definition of a endpoint
"""
model_config = ConfigDict(
populate_by_name=True,
regex_engine="python-re",
)
address: Annotated[str, Field(alias='my-endpoint:address', title='AddressLeaf')]
"""
Endpoint address. IP or FQDN
"""
port: Annotated[
int, Field(alias='my-endpoint:port', ge=1, le=65535, title='PortLeaf')
]
"""
Port number between 1 and 65535
"""
description: Annotated[
Optional[str], Field(alias='my-endpoint:description', title='DescriptionLeaf')
] = None
"""
Endpoint description
"""
class Model(BaseModel):
"""
Initialize an instance of this class and serialize it to JSON; this results in a RESTCONF payload.
## Tips
Initialization:
- all values have to be set via keyword arguments
- if a class contains only a `root` field, it can be initialized as follows:
- `member=MyNode(root=<value>)`
- `member=<value>`
Serialziation:
- `exclude_defaults=True` omits fields set to their default value (recommended)
- `by_alias=True` ensures qualified names are used (necessary)
"""
model_config = ConfigDict(
populate_by_name=True,
regex_engine="python-re",
)
endpoint: Annotated[
Optional[EndpointContainer], Field(alias='my-endpoint:endpoint')
] = None
if __name__ == "__main__":
model = Model(
# <Initialize model here>
)
restconf_payload = model.model_dump_json(
exclude_defaults=True, by_alias=True, indent=2
)
print(f"Generated output: {restconf_payload}")
# Send config to network device:
# from pydantify.utility import restconf_patch_request
# restconf_patch_request(url='...', user_pw_auth=('usr', 'pw'), data=restconf_payload)
Using the model
The model can now be used as any other pydantic python model, and pydantify is not required if we don't use helper functions from pydantify.
The model can be imported, and Python objects can be created. The IDE (like Visual Studio Code) will offer code completion.
create_json.py | |
---|---|
After creating the objects port
and host
, the model is instantiated with the passed objects. The pydantic model object can generate JSON directly by calling .json()
.
By default, the JSON output will include all values. Using the argument exlude_defaults=True
will not show these values. In this example, the description
leaf is optional and has, therefore, a default value of Null
.
create_json.py | |
---|---|
With the option by_alias
the JSON includes the YANG module name in the keys.
{
"my-endpoint:endpoint": {
"my-endpoint:address": "localhost",
"my-endpoint:port": 8080
}
}
Model objects containing only a root
field can be created automatically. So, instead of creating a port object, specify the value in the argument of the EndpointContainer
creation. Pydantic creates objects automatically in the background. More information can be found in the pydantic documentation for Custom Root Types
create_json.py | |
---|---|
{
"my-endpoint:endpoint": {
"my-endpoint:address": "::1",
"my-endpoint:port": 2222,
"my-endpoint:description": "Port 2222 on localhost"
}
}
A Python dictionary can also be used to create nested model objects:
create_json.py | |
---|---|
{
"my-endpoint:endpoint": {
"my-endpoint:address": "172.0.0.1",
"my-endpoint:port": 9100
}
}
Using dictionary unpacking, the first level of the dictionary needs to match the field names of the model. Inside the model, also the alias name can be used.
create_json.py | |
---|---|
{
"my-endpoint:endpoint": {
"my-endpoint:address": "localhost",
"my-endpoint:port": 8000
}
}
To be able to use the alias also on the top level, the static class functions parse_obj
, parse_file
, or parse_raw
can be used:
create_json.py | |
---|---|