# Configuration Group - Creation of First and Second Layer

This README document provides guidelines on creating configuration groups within the `Cisco Catalyst WAN SDK` repository.

## How to create Parcel model

Cellular controller Parcel will be used for example purposes. Please change the parcel according to which one you are implementing.

1. Add new endpoints following the guide: https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/CONTRIBUTING.md
2. Download a schema with new endpoint:
```python
    @versions(supported_versions=(">=20.9"), raises=False)
    @get("/v1/feature-profile/sdwan/transport/cellular-controller/schema", resp_json_key="request")
    def get_sdwan_transport_cellular_controller_parcel_schema(self, params: SchemaTypeQuery) -> JSON:
        ...
```

Usage:
```python
schema = session.endpoints.configuration_feature_profile.get_sdwan_transport_cellular_controller_parcel_schema(
    params=SchemaTypeQuery(schemaType=SchemaType.POST)
)

with open("sdwan_transport_cellular_controller_parcel_schema.json", "w") as f:
    json.dump(schema, f, indent=4)
```

3. Generate `model.py`

`datamodel-codegen --input sdwan_transport_cellular_controller_parcel_schema.json --output model.py --output-model-type pydantic_v2.BaseModel  --enum-field-as-literal all --input-file-type jsonschema --field-constraints --target-python-version 3.8 --snake-case-field`

`model.py`
```python
# generated by datamodel-codegen:
#   filename:  sdwan_transport_cellular_controller_parcel_schema.json
#   timestamp: 2023-12-20T15:25:55+00:00

from __future__ import annotations

from typing import Any, Literal, Optional, Union

from pydantic import BaseModel, ConfigDict, Field


class ConfigType(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['default']
    value: Literal['non-eSim']


class Id(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['global']
    value: str = Field(..., max_length=5, min_length=1)


class Id1(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['variable']
    value: str = Field(
        ...,
        max_length=64,
        min_length=1,
        pattern='^\\{\\{[.\\/\\[\\]a-zA-Z0-9_-]+\\}\\}$',
    )


class Slot(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['global']
    value: int = Field(..., ge=0, le=1)


class Slot1(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['variable']
    value: str = Field(
        ...,
        max_length=64,
        min_length=1,
        pattern='^\\{\\{[.\\/\\[\\]a-zA-Z0-9_-]+\\}\\}$',
    )


class Slot2(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['default']


class MaxRetry(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['global']
    value: int = Field(..., ge=0, le=65535)


class MaxRetry1(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['variable']
    value: str = Field(
        ...,
        max_length=64,
        min_length=1,
        pattern='^\\{\\{[.\\/\\[\\]a-zA-Z0-9_-]+\\}\\}$',
    )


class MaxRetry2(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['default']


class Failovertimer(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['global']
    value: int = Field(..., ge=3, le=7)


class Failovertimer1(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['variable']
    value: str = Field(
        ...,
        max_length=64,
        min_length=1,
        pattern='^\\{\\{[.\\/\\[\\]a-zA-Z0-9_-]+\\}\\}$',
    )


class Failovertimer2(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['default']


class AutoSim(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['global']
    value: bool


class AutoSim1(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['variable']
    value: str = Field(
        ...,
        max_length=64,
        min_length=1,
        pattern='^\\{\\{[.\\/\\[\\]a-zA-Z0-9_-]+\\}\\}$',
    )


class AutoSim2(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    optionType: Literal['default']
    value: Literal[True]


class ControllerConfig(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    id: Union[Id, Id1] = Field(..., description='Cellular ID')
    slot: Optional[Union[Slot, Slot1, Slot2]] = Field(
        None, description='Set primary SIM slot'
    )
    maxRetry: Optional[Union[MaxRetry, MaxRetry1, MaxRetry2]] = Field(
        None, description='Set SIM failover retries'
    )
    failovertimer: Optional[Union[Failovertimer, Failovertimer1, Failovertimer2]] = (
        Field(None, description='Set SIM failover timeout in minutes')
    )
    autoSim: Optional[Union[AutoSim, AutoSim1, AutoSim2]] = Field(
        None, description='Enable/Disable Firmware Auto Sim'
    )


class Data(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    configType: ConfigType
    controllerConfig: ControllerConfig


class CellularController(BaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    name: str = Field(
        ...,
        description='Set the parcel name',
        max_length=128,
        min_length=1,
        pattern='^[^&<>! "]+$',
    )
    description: Optional[str] = Field(None, description='Set the parcel description')
    data: Data
    metadata: Optional[Any] = None
    documentation: Optional[Any] = Field(
        None,
        description='This is the documentation for POST request schema for Transport CellularController profile parcel',
        examples=[
            {
                'name': 'Cedge_CG1_Transport_CellularController_parcel1',
                'data': {
                    'configType': {'optionType': 'default', 'value': 'non-eSim'},
                    'controllerConfig': {
                        'id': {'optionType': 'global', 'value': '1'},
                        'slot': {'optionType': 'global', 'value': 0},
                        'maxRetry': {'optionType': 'global', 'value': 5},
                        'failovertimer': {'optionType': 'global', 'value': 3},
                        'autoSim': {'optionType': 'default', 'value': True},
                    },
                },
                'description': 'Cedge Transport CellularController Parcel config',
            }
        ],
    )
```

5. Fix `model.py` file

`cellular_controller.py`
```python
from enum import Enum
from typing import Union

from pydantic import Field

from vmngclient.api.configuration_groups.parcel import Default, Global, Parcel, Variable


class ConfigTypeValue(Enum):
    NON_E_SIM = "non-eSim"


class ControllerConfig(Parcel):
    id: Union[Variable, Global[str]] = Field(min_length=1, max_length=5, description="Cellular ID")
    slot: Union[Variable, Global[int], Default[int], None] = Field(
        default=None, description="Set primary SIM slot", ge=0, le=1
    )
    maxRetry: Union[Variable, Global[int], Default[None], None] = Field(
        default=None, description="Set SIM failover retries", ge=0, le=65535
    )
    failovertimer: Union[Variable, Global[int], Default[None], None] = Field(
        default=None, description="Set SIM failover timeout in minutes", ge=3, le=7
    )
    autoSim: Union[Variable, Global[bool], Default[None], None] = Field(
        default=None, description="Enable/Disable Firmware Auto Sim"
    )


class CellularControllerParcel(Parcel):
    config_type: Default[ConfigTypeValue] = Field(default=Default(value=ConfigTypeValue.NON_E_SIM), alias="configType")
    controller_config: ControllerConfig = Field(alias="controllerConfig")
```

6. Add `cellular_controller.py` file to folder `catalystwan/models/configuration/feature_profile/sdwan/transport`

## Guidelines for Creating Config Groups

### 1. Directory Structure

Place every model in the `models` directory. 

For endpoints, use `endpoints/configuration/feature_profiles/sdwan/*.py`. 

For models, use `models/configuration/feature_profiles/sdwan/*.py`.

### 2. Default Values

Use `Default[None]` for `Defaults` without values.

Example:
```python
auto_sim: Union[Variable, Global[bool], Default[None]]
```

`auto_sim` can be set as `Default` but without the value.

### 3. Literals vs Enums

Use `Literal` over the `Enums`. Justification:
- Both provides static typing safety ant intellisense (type-hints).
- `(str, Enum)` can be problematic as `__str__` != `__format__` for Enum
- When instantiating pydantic Field defined as `Literal` no explicit import of specific `Enum` is needed

### 4. Naming Conventions

- Avoid explicit naming `MyAwesomeEntityParcel`. Use `MyAwesomeEntity` instead.

### 5. Case Sensitivity

When dealing with multiple proper nouns in a class name in Python, it's customary to use CamelCase, where each proper noun is capitalized. If we consider `VANWPN` as an acronym or initialism for multiple proper nouns, you might structure your class name like this:

```python
class VanWpn:
    # Class implementation goes here
```

This adheres to Python's PEP 8 style guide, which recommends using CamelCase for class names. Following this convention makes your code more readable and aligns with the standard practices in the Python community.

https://peps.python.org/pep-0008/#descriptive-naming-styles

### 6. Excluding values

Utilize `model_validator` for two or more excluding values.

If you have any questions or need further clarification, please reach out to the project maintainers. Thank you for your contribution!

### 8. Future Features

- Automate model generation step
- Improve model generator
- Implement a pipeline and integration tests.
