2019-12-29 23:57:39 -07:00
|
|
|
"""Utility Functions for Pydantic Models."""
|
2019-09-11 01:35:31 -07:00
|
|
|
|
2020-02-03 02:35:11 -07:00
|
|
|
# Standard Library
|
2020-02-15 11:00:51 -07:00
|
|
|
import os
|
2019-09-11 01:35:31 -07:00
|
|
|
import re
|
2020-02-15 11:00:51 -07:00
|
|
|
from pathlib import Path
|
2019-10-09 03:10:52 -07:00
|
|
|
|
2020-02-03 02:35:11 -07:00
|
|
|
# Third Party
|
2020-03-19 17:33:29 -07:00
|
|
|
from pydantic import HttpUrl, BaseModel
|
2019-09-11 01:35:31 -07:00
|
|
|
|
2020-03-21 01:44:38 -07:00
|
|
|
# Project
|
|
|
|
from hyperglass.util import clean_name
|
2019-10-04 16:54:32 -07:00
|
|
|
|
|
|
|
|
2020-01-20 10:17:52 -07:00
|
|
|
class HyperglassModel(BaseModel):
|
2019-12-29 23:57:39 -07:00
|
|
|
"""Base model for all hyperglass configuration models."""
|
2019-10-04 16:54:32 -07:00
|
|
|
|
|
|
|
class Config:
|
2019-12-29 23:57:39 -07:00
|
|
|
"""Default Pydantic configuration.
|
|
|
|
|
|
|
|
See https://pydantic-docs.helpmanual.io/usage/model_config
|
|
|
|
"""
|
2019-10-04 16:54:32 -07:00
|
|
|
|
|
|
|
validate_all = True
|
|
|
|
extra = "forbid"
|
|
|
|
validate_assignment = True
|
2019-10-09 03:10:52 -07:00
|
|
|
alias_generator = clean_name
|
2020-03-19 17:33:29 -07:00
|
|
|
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)))
|
2019-10-09 03:10:52 -07:00
|
|
|
|
|
|
|
|
|
|
|
class HyperglassModelExtra(HyperglassModel):
|
2019-12-29 23:57:39 -07:00
|
|
|
"""Model for hyperglass configuration models with dynamic fields."""
|
2019-10-09 03:10:52 -07:00
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
class Config:
|
2019-12-29 23:57:39 -07:00
|
|
|
"""Default pydantic configuration."""
|
2019-10-09 03:10:52 -07:00
|
|
|
|
|
|
|
extra = "allow"
|
2020-01-20 10:17:52 -07:00
|
|
|
|
|
|
|
|
|
|
|
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__()})"
|
2020-02-15 11:00:51 -07:00
|
|
|
|
|
|
|
|
2020-03-21 01:44:38 -07:00
|
|
|
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__()})"
|
|
|
|
|
|
|
|
|
2020-02-15 11:00:51 -07:00
|
|
|
def validate_image(value):
|
|
|
|
"""Convert file path to URL path.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
value {FilePath} -- Path to logo file.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
{str} -- Formatted logo path
|
|
|
|
"""
|
|
|
|
base_path = value.split("/")
|
|
|
|
|
|
|
|
if base_path[0] == "/":
|
|
|
|
value = "/".join(base_path[1:])
|
|
|
|
|
|
|
|
if base_path[0] not in ("images", "custom"):
|
|
|
|
raise ValueError(
|
|
|
|
"Logo files must be in the 'custom/' directory of your hyperglass directory. Got: {f}",
|
|
|
|
f=value,
|
|
|
|
)
|
|
|
|
|
|
|
|
if base_path[0] == "custom":
|
|
|
|
config_path = Path(os.environ["hyperglass_directory"])
|
|
|
|
custom_file = config_path / "static" / value
|
|
|
|
|
|
|
|
if not custom_file.exists():
|
|
|
|
raise ValueError("'{f}' does not exist", f=str(custom_file))
|
|
|
|
|
|
|
|
return value
|