diff --git a/hyperglass/api/models/cert_import.py b/hyperglass/api/models/cert_import.py index 6107556..d82ea42 100644 --- a/hyperglass/api/models/cert_import.py +++ b/hyperglass/api/models/cert_import.py @@ -6,7 +6,7 @@ from typing import Union from pydantic import BaseModel, StrictStr # Project -from hyperglass.configuration.models._utils import StrictBytes +from hyperglass.models import StrictBytes class EncodedRequest(BaseModel): diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index de2553e..e7e92fb 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -33,7 +33,7 @@ from hyperglass.configuration.models import routers as _routers from hyperglass.configuration.models import commands as _commands from hyperglass.configuration.markdown import get_markdown -set_app_path() +set_app_path(required=True) CONFIG_PATH = Path(os.environ["hyperglass_directory"]) log.info("Configuration directory: {d}", d=str(CONFIG_PATH)) diff --git a/hyperglass/configuration/models/_utils.py b/hyperglass/configuration/models/_utils.py index 5a80684..ccd6505 100644 --- a/hyperglass/configuration/models/_utils.py +++ b/hyperglass/configuration/models/_utils.py @@ -2,140 +2,10 @@ # Standard Library import os -import re -from typing import TypeVar from pathlib import Path -# Third Party -from pydantic import HttpUrl, BaseModel, StrictInt, StrictFloat - # Project -from hyperglass.util import log, clean_name - -IntFloat = TypeVar("IntFloat", StrictInt, StrictFloat) - - -class HyperglassModel(BaseModel): - """Base model for all hyperglass configuration models.""" - - class Config: - """Default Pydantic configuration. - - See https://pydantic-docs.helpmanual.io/usage/model_config - """ - - validate_all = True - extra = "forbid" - validate_assignment = True - alias_generator = clean_name - json_encoders = {HttpUrl: lambda v: str(v)} - - def export_json(self, *args, **kwargs): - """Return instance as JSON. - - Returns: - {str} -- Stringified JSON. - """ - return self.json(by_alias=True, exclude_unset=False, *args, **kwargs) - - def export_dict(self, *args, **kwargs): - """Return instance as dictionary. - - Returns: - {dict} -- Python dictionary. - """ - return self.dict(by_alias=True, exclude_unset=False, *args, **kwargs) - - def export_yaml(self, *args, **kwargs): - """Return instance as YAML. - - Returns: - {str} -- Stringified YAML. - """ - import json - import yaml - - return yaml.safe_dump(json.loads(self.export_json()), *args, **kwargs) - - -class HyperglassModelExtra(HyperglassModel): - """Model for hyperglass configuration models with dynamic fields.""" - - pass - - class Config: - """Default pydantic configuration.""" - - extra = "allow" - - -class AnyUri(str): - """Custom field type for HTTP URI, e.g. /example.""" - - @classmethod - def __get_validators__(cls): - """Pydantic custim field method.""" - yield cls.validate - - @classmethod - def validate(cls, value): - """Ensure URI string contains a leading forward-slash.""" - uri_regex = re.compile(r"^(\/.*)$") - if not isinstance(value, str): - raise TypeError("AnyUri type must be a string") - match = uri_regex.fullmatch(value) - if not match: - raise ValueError( - "Invalid format. A URI must begin with a forward slash, e.g. '/example'" - ) - return cls(match.group()) - - def __repr__(self): - """Stringify custom field representation.""" - return f"AnyUri({super().__repr__()})" - - -class StrictBytes(bytes): - """Custom data type for a strict byte string. - - Used for validating the encoded JWT request payload. - """ - - @classmethod - def __get_validators__(cls): - """Yield Pydantic validator function. - - See: https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types - - Yields: - {function} -- Validator - """ - yield cls.validate - - @classmethod - def validate(cls, value): - """Validate type. - - Arguments: - value {Any} -- Pre-validated input - - Raises: - TypeError: Raised if value is not bytes - - Returns: - {object} -- Instantiated class - """ - if not isinstance(value, bytes): - raise TypeError("bytes required") - return cls() - - def __repr__(self): - """Return representation of object. - - Returns: - {str} -- Representation - """ - return f"StrictBytes({super().__repr__()})" +from hyperglass.log import log def validate_image(value): diff --git a/hyperglass/configuration/models/cache.py b/hyperglass/configuration/models/cache.py index 429823f..7f372a8 100644 --- a/hyperglass/configuration/models/cache.py +++ b/hyperglass/configuration/models/cache.py @@ -7,7 +7,7 @@ from typing import Union from pydantic import Field, StrictInt, StrictStr, StrictBool, IPvAnyAddress # Project -from hyperglass.configuration.models._utils import HyperglassModel +from hyperglass.models import HyperglassModel class Cache(HyperglassModel): diff --git a/hyperglass/configuration/models/commands.py b/hyperglass/configuration/models/commands.py index 1b798fa..2b5c1cd 100644 --- a/hyperglass/configuration/models/commands.py +++ b/hyperglass/configuration/models/commands.py @@ -4,7 +4,7 @@ from pydantic import StrictStr # Project -from hyperglass.configuration.models._utils import HyperglassModel +from hyperglass.models import HyperglassModel class Command(HyperglassModel): diff --git a/hyperglass/configuration/models/credentials.py b/hyperglass/configuration/models/credentials.py index 1ca7269..1c1ad2d 100644 --- a/hyperglass/configuration/models/credentials.py +++ b/hyperglass/configuration/models/credentials.py @@ -5,7 +5,7 @@ from pydantic import SecretStr, StrictStr # Project from hyperglass.util import clean_name -from hyperglass.configuration.models._utils import HyperglassModel +from hyperglass.models import HyperglassModel class Credential(HyperglassModel): diff --git a/hyperglass/configuration/models/docs.py b/hyperglass/configuration/models/docs.py index 379e428..45bedd2 100644 --- a/hyperglass/configuration/models/docs.py +++ b/hyperglass/configuration/models/docs.py @@ -3,19 +3,10 @@ from pydantic import Field, HttpUrl, StrictStr, StrictBool, constr # Project -from hyperglass.configuration.models._utils import AnyUri, HyperglassModel +from hyperglass.models import AnyUri, HyperglassModel -class HyperglassLevel3(HyperglassModel): - """Automatic docs sorting subclass.""" - - class Config: - """Pydantic model configuration.""" - - schema_extra = {"level": 3} - - -class EndpointConfig(HyperglassLevel3): +class EndpointConfig(HyperglassModel): """Validation model for per API endpoint documentation.""" title: StrictStr = Field( @@ -106,4 +97,3 @@ class Docs(HyperglassModel): "description": "`/api/devices` API documentation options.", }, } - schema_extra = {"level": 2} diff --git a/hyperglass/configuration/models/logging.py b/hyperglass/configuration/models/logging.py index 2be6d92..2b842a9 100644 --- a/hyperglass/configuration/models/logging.py +++ b/hyperglass/configuration/models/logging.py @@ -22,7 +22,7 @@ from pydantic import ( # Project from hyperglass.constants import __version__ -from hyperglass.configuration.models._utils import HyperglassModel, HyperglassModelExtra +from hyperglass.models import HyperglassModel, HyperglassModelExtra class Syslog(HyperglassModel): diff --git a/hyperglass/configuration/models/messages.py b/hyperglass/configuration/models/messages.py index f4a1b02..716693b 100644 --- a/hyperglass/configuration/models/messages.py +++ b/hyperglass/configuration/models/messages.py @@ -4,7 +4,7 @@ from pydantic import Field, StrictStr # Project -from hyperglass.configuration.models._utils import HyperglassModel +from hyperglass.models import HyperglassModel class Messages(HyperglassModel): diff --git a/hyperglass/configuration/models/networks.py b/hyperglass/configuration/models/networks.py index b1368f5..592d492 100644 --- a/hyperglass/configuration/models/networks.py +++ b/hyperglass/configuration/models/networks.py @@ -5,7 +5,7 @@ from pydantic import Field, StrictStr # Project from hyperglass.util import clean_name -from hyperglass.configuration.models._utils import HyperglassModel +from hyperglass.models import HyperglassModel class Network(HyperglassModel): diff --git a/hyperglass/configuration/models/opengraph.py b/hyperglass/configuration/models/opengraph.py index f420d50..0555f2f 100644 --- a/hyperglass/configuration/models/opengraph.py +++ b/hyperglass/configuration/models/opengraph.py @@ -10,7 +10,8 @@ import PIL.Image as PilImage from pydantic import StrictInt, StrictStr, root_validator # Project -from hyperglass.configuration.models._utils import HyperglassModel, validate_image +from hyperglass.models import HyperglassModel +from hyperglass.configuration.models._utils import validate_image CONFIG_PATH = Path(os.environ["hyperglass_directory"]) diff --git a/hyperglass/configuration/models/params.py b/hyperglass/configuration/models/params.py index f92a6ba..96687a6 100644 --- a/hyperglass/configuration/models/params.py +++ b/hyperglass/configuration/models/params.py @@ -16,10 +16,10 @@ from pydantic import ( ) # Project +from hyperglass.models import IntFloat, HyperglassModel from hyperglass.configuration.models.web import Web from hyperglass.configuration.models.docs import Docs from hyperglass.configuration.models.cache import Cache -from hyperglass.configuration.models._utils import IntFloat, HyperglassModel from hyperglass.configuration.models.logging import Logging from hyperglass.configuration.models.queries import Queries from hyperglass.configuration.models.messages import Messages diff --git a/hyperglass/configuration/models/proxies.py b/hyperglass/configuration/models/proxies.py index 9f93dc3..1569785 100644 --- a/hyperglass/configuration/models/proxies.py +++ b/hyperglass/configuration/models/proxies.py @@ -5,8 +5,8 @@ from pydantic import StrictInt, StrictStr, validator # Project from hyperglass.util import clean_name +from hyperglass.models import HyperglassModel from hyperglass.exceptions import UnsupportedDevice -from hyperglass.configuration.models._utils import HyperglassModel from hyperglass.configuration.models.credentials import Credential diff --git a/hyperglass/configuration/models/queries.py b/hyperglass/configuration/models/queries.py index 922a5ab..ea11b8b 100644 --- a/hyperglass/configuration/models/queries.py +++ b/hyperglass/configuration/models/queries.py @@ -4,8 +4,8 @@ from pydantic import Field, StrictStr, StrictBool, constr # Project +from hyperglass.models import HyperglassModel from hyperglass.constants import SUPPORTED_QUERY_TYPES -from hyperglass.configuration.models._utils import HyperglassModel class HyperglassLevel3(HyperglassModel): diff --git a/hyperglass/configuration/models/routers.py b/hyperglass/configuration/models/routers.py index e68bff6..e99334e 100644 --- a/hyperglass/configuration/models/routers.py +++ b/hyperglass/configuration/models/routers.py @@ -12,11 +12,11 @@ from pydantic import StrictInt, StrictStr, validator # Project from hyperglass.log import log from hyperglass.util import clean_name +from hyperglass.models import HyperglassModel, HyperglassModelExtra from hyperglass.constants import SCRAPE_HELPERS, TRANSPORT_REST, TRANSPORT_SCRAPE from hyperglass.exceptions import ConfigError, UnsupportedDevice from hyperglass.configuration.models.ssl import Ssl from hyperglass.configuration.models.vrfs import Vrf, Info -from hyperglass.configuration.models._utils import HyperglassModel, HyperglassModelExtra from hyperglass.configuration.models.proxies import Proxy from hyperglass.configuration.models.commands import Command from hyperglass.configuration.models.networks import Network diff --git a/hyperglass/configuration/models/ssl.py b/hyperglass/configuration/models/ssl.py index 75ba8b4..c9cfaf2 100644 --- a/hyperglass/configuration/models/ssl.py +++ b/hyperglass/configuration/models/ssl.py @@ -7,7 +7,7 @@ from typing import Optional from pydantic import Field, FilePath, StrictBool # Project -from hyperglass.configuration.models._utils import HyperglassModel +from hyperglass.models import HyperglassModel class Ssl(HyperglassModel): diff --git a/hyperglass/configuration/models/vrfs.py b/hyperglass/configuration/models/vrfs.py index 46a50af..b2efe44 100644 --- a/hyperglass/configuration/models/vrfs.py +++ b/hyperglass/configuration/models/vrfs.py @@ -17,7 +17,7 @@ from pydantic import ( ) # Project -from hyperglass.configuration.models._utils import HyperglassModel, HyperglassModelExtra +from hyperglass.models import HyperglassModel, HyperglassModelExtra class AccessList4(HyperglassModel): diff --git a/hyperglass/configuration/models/web.py b/hyperglass/configuration/models/web.py index d81ae79..1d1f08b 100644 --- a/hyperglass/configuration/models/web.py +++ b/hyperglass/configuration/models/web.py @@ -17,8 +17,9 @@ from pydantic import ( from pydantic.color import Color # Project +from hyperglass.models import HyperglassModel from hyperglass.constants import DNS_OVER_HTTPS, FUNC_COLOR_MAP -from hyperglass.configuration.models._utils import HyperglassModel, validate_image +from hyperglass.configuration.models._utils import validate_image from hyperglass.configuration.models.opengraph import OpenGraph diff --git a/hyperglass/models.py b/hyperglass/models.py new file mode 100644 index 0000000..bfdaa54 --- /dev/null +++ b/hyperglass/models.py @@ -0,0 +1,134 @@ +# Standard Library +import re +from typing import TypeVar + +# Third Party +from pydantic import HttpUrl, BaseModel, StrictInt, StrictFloat + +# Project +from hyperglass.util import clean_name + +IntFloat = TypeVar("IntFloat", StrictInt, StrictFloat) + + +class HyperglassModel(BaseModel): + """Base model for all hyperglass configuration models.""" + + class Config: + """Default Pydantic configuration. + + See https://pydantic-docs.helpmanual.io/usage/model_config + """ + + validate_all = True + extra = "forbid" + validate_assignment = True + alias_generator = clean_name + json_encoders = {HttpUrl: lambda v: str(v)} + + def export_json(self, *args, **kwargs): + """Return instance as JSON. + + Returns: + {str} -- Stringified JSON. + """ + return self.json(by_alias=True, exclude_unset=False, *args, **kwargs) + + def export_dict(self, *args, **kwargs): + """Return instance as dictionary. + + Returns: + {dict} -- Python dictionary. + """ + return self.dict(by_alias=True, exclude_unset=False, *args, **kwargs) + + def export_yaml(self, *args, **kwargs): + """Return instance as YAML. + + Returns: + {str} -- Stringified YAML. + """ + import json + import yaml + + return yaml.safe_dump(json.loads(self.export_json()), *args, **kwargs) + + +class HyperglassModelExtra(HyperglassModel): + """Model for hyperglass configuration models with dynamic fields.""" + + pass + + class Config: + """Default pydantic configuration.""" + + extra = "allow" + + +class AnyUri(str): + """Custom field type for HTTP URI, e.g. /example.""" + + @classmethod + def __get_validators__(cls): + """Pydantic custim field method.""" + yield cls.validate + + @classmethod + def validate(cls, value): + """Ensure URI string contains a leading forward-slash.""" + uri_regex = re.compile(r"^(\/.*)$") + if not isinstance(value, str): + raise TypeError("AnyUri type must be a string") + match = uri_regex.fullmatch(value) + if not match: + raise ValueError( + "Invalid format. A URI must begin with a forward slash, e.g. '/example'" + ) + return cls(match.group()) + + def __repr__(self): + """Stringify custom field representation.""" + return f"AnyUri({super().__repr__()})" + + +class StrictBytes(bytes): + """Custom data type for a strict byte string. + + Used for validating the encoded JWT request payload. + """ + + @classmethod + def __get_validators__(cls): + """Yield Pydantic validator function. + + See: https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types + + Yields: + {function} -- Validator + """ + yield cls.validate + + @classmethod + def validate(cls, value): + """Validate type. + + Arguments: + value {Any} -- Pre-validated input + + Raises: + TypeError: Raised if value is not bytes + + Returns: + {object} -- Instantiated class + """ + if not isinstance(value, bytes): + raise TypeError("bytes required") + return cls() + + def __repr__(self): + """Return representation of object. + + Returns: + {str} -- Representation + """ + return f"StrictBytes({super().__repr__()})" diff --git a/validate_examples.py b/validate_examples.py index 37cb575..a0001ed 100644 --- a/validate_examples.py +++ b/validate_examples.py @@ -1,4 +1,5 @@ """Validate example files.""" + # Standard Library import re import sys @@ -9,9 +10,6 @@ import yaml # Project from hyperglass.util import set_app_path -from hyperglass.configuration.models.params import Params -from hyperglass.configuration.models.routers import Routers -from hyperglass.configuration.models.commands import Commands EXAMPLES = Path(__file__).parent.parent / "hyperglass" / "examples" @@ -52,6 +50,8 @@ def _comment_optional_files(): def _validate_devices(): + from hyperglass.configuration.models.routers import Routers + with DEVICES.open() as raw: devices_dict = yaml.safe_load(raw.read()) or {} try: @@ -62,6 +62,8 @@ def _validate_devices(): def _validate_commands(): + from hyperglass.configuration.models.commands import Commands + with COMMANDS.open() as raw: commands_dict = yaml.safe_load(raw.read()) or {} try: @@ -72,6 +74,8 @@ def _validate_commands(): def _validate_main(): + from hyperglass.configuration.models.params import Params + with MAIN.open() as raw: main_dict = yaml.safe_load(raw.read()) or {} try: