# pylint: disable=C0413,C0302
"""
Internal validation features. This module contains functions for validating various data types.
Before adding anything to this module, please first check if there is a built-in Python function
or a Django utility that can do the validation.
"""
import logging
import re
import warnings
from typing import Optional
from urllib.parse import urlparse, urlunparse
import validators
from django.apps import apps
from django.core.exceptions import AppRegistryNotReady
from smarter.common.const import SMARTER_API_SUBDOMAIN, SmarterEnvironments
from smarter.common.exceptions import SmarterValueError
from smarter.common.helpers.console_helpers import formatted_text
from smarter.lib import json
from smarter.lib.logging import WaffleSwitchedLoggerWrapper
# guard against Sphinx doc build circular import errors
validator_logging_is_active: bool = False
if apps.ready:
try:
# this resolves an import issue in collect static assets where Django apps are not yet importable
# pylint: disable=import-outside-toplevel,C0412
from smarter.lib.django import waffle
from smarter.lib.django.waffle import SmarterWaffleSwitches
validator_logging_is_active = waffle.switch_is_active(SmarterWaffleSwitches.VALIDATOR_LOGGING)
# pylint: disable=broad-except
except (AppRegistryNotReady, ImportError):
pass
# pylint: disable=W0613
def should_log(level):
"""Check if logging should be done based on the waffle switch."""
return validator_logging_is_active
base_logger = logging.getLogger(__name__)
logger = WaffleSwitchedLoggerWrapper(base_logger, should_log)
logger_prefix = formatted_text(f"{__name__}.SmarterValidator")
# pylint: disable=R0904
[docs]
class SmarterValidator:
"""
Class for validating various data types. Before adding anything to this class, please
first check if there is a built-in Python function or a Django utility that can do the validation.
.. todo::
add `import validators` and study this library to see what can be removed and/or refactored here
see https://python-validators.github.io/validators/
"""
LOCAL_HOSTS = ["localhost", "127.0.0.1"]
"""List of local hosts used for validation purposes."""
LOCAL_HOSTS += [host + ":9357" for host in LOCAL_HOSTS]
LOCAL_HOSTS.append("testserver")
LOCAL_URLS = [f"http://{host}" for host in LOCAL_HOSTS] + [f"https://{host}" for host in LOCAL_HOSTS]
"""List of local URLs used for validation purposes."""
VALID_ALPHNUMERIC_NO_SPACES_PATTERN = r"^[a-zA-Z0-9]+$"
"""Pattern for validating alphanumeric strings with no spaces."""
VALID_ACCOUNT_NUMBER_PATTERN = r"^\d{4}-\d{4}-\d{4}$"
"""Pattern for validating Smarter account numbers."""
VALID_CHATBOT_SLUG_PATTERN = r"^[a-zA-Z0-9-]+$"
VALID_PORT_PATTERN = r"^[0-9]{1,5}$"
"""Pattern for validating port numbers."""
VALID_URL_PATTERN = r"^(http|https)://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}(:[0-9]{1,5})?$"
"""Pattern for validating URLs."""
VALID_HOSTNAME_PATTERN = r"^(?!-)[A-Za-z0-9_-]{1,63}(?<!-)$"
"""Pattern for validating hostnames."""
VALID_UUID_PATTERN = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
"""Pattern for validating UUIDs."""
VALID_SESSION_KEY = r"^[a-fA-F0-9]{64}$"
"""Pattern for validating Smarter session keys."""
VALID_SEMANTIC_VERSION = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$"
"""Pattern for validating semantic version strings."""
VALID_URL_FRIENDLY_STRING = r"^[a-zA-Z0-9._/-]+$"
"""Pattern for validating URL slugs (alphanumeric, hyphens, underscores)."""
VALID_CLEAN_STRING = r"^(?!-)[A-Za-z0-9_-]{1,63}(?<!-)(\.[A-Za-z0-9_-]{1,63})*$"
"""Pattern for validating clean strings."""
VALID_CLEAN_STRING_WITH_SPACES = r"^[\w\-\.~:\/\?#\[\]@!$&'()*+,;= %]+$"
"""Pattern for validating clean strings that may include spaces."""
VALID_EMAIL_PATTERN = r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
"""Pattern for validating email addresses."""
VALID_URL_ENDPOINT = r"^/[a-zA-Z0-9/_\-\{\}]+/$" # NOTE: this allows placeholders like {id} in the url
"""Pattern for validating URL endpoints."""
VALID_CAMEL_CASE = r"^[a-zA-Z0-9]+(?:[A-Z][a-z0-9]+)*$"
"""Pattern for validating camel case strings."""
VALID_SNAKE_CASE = r"^[a-z0-9]+(?:_[a-z0-9]+)*$"
"""Pattern for validating snake case strings."""
VALID_PASCAL_CASE = r"^[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)*$"
"""Pattern for validating pascal case strings."""
SMARTER_ACCOUNT_NUMBER_REGEX = r"\b\d{4}-\d{4}-\d{4}\b"
"""Regex for matching Smarter account numbers in text."""
[docs]
@staticmethod
def validate_camel_case(value: str) -> str:
"""Validate camel case format
Checks if the provided string is in camel case format.
:param value: The string to validate.
:type value: str
:raises SmarterValueError: If the value is not in camel case format.
:returns: The validated camel case string.
:rtype: str
Example::
SmarterValidator.validate_camel_case("myCamelCase") # returns "myCamelCase"
SmarterValidator.validate_camel_case("NotCamelCase") # raises SmarterValueError
"""
logger.debug("%s.validate_camel_case() %s", logger_prefix, value)
if not re.match(SmarterValidator.VALID_CAMEL_CASE, value):
raise SmarterValueError(f"Invalid camel case {value}")
if not value:
raise SmarterValueError("Value cannot be empty")
if not value[0].islower():
raise SmarterValueError(f"Value must start with a lowercase letter: {value}")
if not value[0].isalpha():
raise SmarterValueError(f"Value must start with a letter: {value}")
if not value[1:].isalnum():
raise SmarterValueError(f"Value must be in camel case format: {value}")
if not value[1:].isalpha():
raise SmarterValueError(f"Value must be in camel case format: {value}")
return value
[docs]
@staticmethod
def is_valid_camel_case(value: str) -> bool:
"""Check if the value is valid camel case
Checks whether the provided string is in camel case format.
:param value: The string to check.
:type value: str
:returns: True if the value is valid camel case, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_camel_case("myCamelCase") # returns True
SmarterValidator.is_valid_camel_case("NotCamelCase") # returns False
"""
logger.debug("%s.is_valid_camel_case() %s", logger_prefix, value)
try:
SmarterValidator.validate_camel_case(value)
return True
except SmarterValueError:
logger.debug("%s.is_valid_camel_case() invalid %s", logger_prefix, value)
return False
[docs]
@staticmethod
def validate_snake_case(value: str) -> None:
"""Validate snake case format
Checks if the provided string is in snake case format.
:param value: The string to validate.
:type value: str
:raises SmarterValueError: If the value is not in snake case format.
:returns: None if the value is valid.
:rtype: None
Example::
SmarterValidator.validate_snake_case("my_snake_case") # returns None
SmarterValidator.validate_snake_case("NotSnakeCase") # raises SmarterValueError
"""
logger.debug("%s.validate_snake_case() %s", logger_prefix, value)
if not value:
raise SmarterValueError("Value cannot be empty")
if not re.match(SmarterValidator.VALID_SNAKE_CASE, value):
raise SmarterValueError(f"Invalid snake case {value}")
return
[docs]
@staticmethod
def is_valid_snake_case(value: str) -> bool:
"""Check if the value is valid snake case
Checks whether the provided string is in snake case format.
:param value: The string to check.
:type value: str
:returns: True if the value is valid snake case, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_snake_case("my_snake_case") # returns True
SmarterValidator.is_valid_snake_case("NotSnakeCase") # returns False
"""
logger.debug("%s.is_valid_snake_case() %s", logger_prefix, value)
try:
SmarterValidator.validate_snake_case(value)
return True
except SmarterValueError:
logger.debug("%s.is_valid_snake_case() invalid %s", logger_prefix, value)
return False
[docs]
@staticmethod
def validate_pascal_case(value: str) -> str:
"""Validate pascal case format
Checks if the provided string is in pascal case format.
:param value: The string to validate.
:type value: str
:raises SmarterValueError: If the value is not in pascal case format.
:returns: The validated pascal case string.
:rtype: str
Example::
SmarterValidator.validate_pascal_case("MyPascalCase") # returns "MyPascalCase"
SmarterValidator.validate_pascal_case("notPascalCase") # raises SmarterValueError
"""
logger.debug("%s.validate_pascal_case() %s", logger_prefix, value)
if not re.match(SmarterValidator.VALID_PASCAL_CASE, value):
raise SmarterValueError(f"Invalid pascal case {value}")
if not value:
raise SmarterValueError("Value cannot be empty")
if not value[0].isupper():
raise SmarterValueError(f"Value must start with an uppercase letter: {value}")
if not value[0].isalpha():
raise SmarterValueError(f"Value must start with a letter: {value}")
if not value[1:].islower():
raise SmarterValueError(f"Value must be in pascal case format: {value}")
if not value[1:].isalnum():
raise SmarterValueError(f"Value must be in pascal case format: {value}")
if not value[1:].isalpha():
raise SmarterValueError(f"Value must be in pascal case format: {value}")
return value
[docs]
@staticmethod
def is_valid_pascal_case(value: str) -> bool:
"""Check if the value is valid pascal case
Checks whether the provided string is in pascal case format.
:param value: The string to check.
:type value: str
:returns: True if the value is valid pascal case, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_pascal_case("MyPascalCase") # returns True
SmarterValidator.is_valid_pascal_case("notPascalCase") # returns False
"""
logger.debug("%s.is_valid_pascal_case() %s", logger_prefix, value)
try:
SmarterValidator.validate_pascal_case(value)
return True
except SmarterValueError:
logger.debug("%s.is_valid_pascal_case() invalid %s", logger_prefix, value)
return False
[docs]
@staticmethod
def validate_json(value: str) -> Optional[str]:
"""Validate JSON format
Checks if the provided string is valid JSON.
:param value: The string to validate.
:type value: str
:raises SmarterValueError: If the value is not valid JSON.
:returns: The validated JSON string.
:rtype: str
Example::
SmarterValidator.validate_json('{"key": "value"}') # returns '{"key": "value"}'
SmarterValidator.validate_json('not json') # raises SmarterValueError
"""
logger.debug("%s.validate_json() %s", logger_prefix, value)
try:
if not isinstance(value, str):
raise SmarterValueError("Value must be a string")
if not value.strip():
return
json.loads(value)
except (ValueError, TypeError) as e:
raise SmarterValueError(f"Invalid JSON value {value}") from e
return value
[docs]
@staticmethod
def is_valid_json(value: str) -> bool:
"""Check if the value is valid JSON
Checks whether the provided string is valid JSON.
:param value: The string to check.
:type value: str
:returns: True if the value is valid JSON, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_json('{"key": "value"}') # returns True
SmarterValidator.is_valid_json('not json') # returns False
"""
logger.debug("%s.is_valid_json() %s", logger_prefix, value)
try:
SmarterValidator.validate_json(value)
return True
except SmarterValueError:
logger.debug("%s.is_valid_json() invalid %s", logger_prefix, value)
return False
[docs]
@staticmethod
def validate_semantic_version(version: str) -> str:
"""Validate semantic version format (e.g., 1.12.1)
Checks if the provided string is a valid semantic version.
:param version: The version string to validate.
:type version: str
:raises SmarterValueError: If the version is not a valid semantic version.
:returns: The validated semantic version string.
:rtype: str
Example::
SmarterValidator.validate_semantic_version("1.2.3") # returns "1.2.3"
SmarterValidator.validate_semantic_version("1.2") # raises SmarterValueError
"""
logger.debug("%s.validate_semantic_version() %s", logger_prefix, version)
if not re.match(SmarterValidator.VALID_SEMANTIC_VERSION, version):
raise SmarterValueError(f"Invalid semantic version {version}")
return version
[docs]
@staticmethod
def is_valid_semantic_version(version: str) -> bool:
"""Check if the semantic version is valid
Checks whether the provided string is a valid semantic version.
:param version: The version string to check.
:type version: str
:returns: True if the version is valid semantic version, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_semantic_version("1.2.3") # returns True
SmarterValidator.is_valid_semantic_version("1.2") # returns False
"""
logger.debug("%s.is_valid_semantic_version() %s", logger_prefix, version)
try:
SmarterValidator.validate_semantic_version(version)
return True
except SmarterValueError:
logger.debug("%s.is_valid_semantic_version() invalid %s", logger_prefix, version)
return False
[docs]
@staticmethod
def validate_is_not_none(value: str) -> str:
"""Validate that the value is not None
Checks if the provided value is not None and not empty.
:param value: The value to validate.
:type value: str
:raises SmarterValueError: If the value is None or empty.
:returns: The validated value.
:rtype: str
Example::
SmarterValidator.validate_is_not_none("something") # returns "something"
SmarterValidator.validate_is_not_none(None) # raises SmarterValueError
"""
logger.debug("%s.validate_is_not_none() %s", logger_prefix, value)
if value is None:
raise SmarterValueError("Value cannot be None")
if not value:
raise SmarterValueError("Value cannot be empty")
return value
[docs]
@staticmethod
def is_not_none(value: str) -> bool:
"""Check if the value is not None
Checks whether the provided value is not None and not empty.
:param value: The value to check.
:type value: str
:returns: True if the value is not None and not empty, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_not_none("something") # returns True
SmarterValidator.is_not_none(None) # returns False
"""
logger.debug("%s.is_not_none() %s", logger_prefix, value)
try:
SmarterValidator.validate_is_not_none(value)
return True
except SmarterValueError:
logger.debug("%s.is_not_none() invalid %s", logger_prefix, value)
return False
[docs]
@staticmethod
def validate_session_key(session_key: str) -> str:
"""Validate session key format
Checks if the provided string is a valid session key.
:param session_key: The session key to validate.
:type session_key: str
:raises SmarterValueError: If the session key is not valid.
:returns: The validated session key.
:rtype: str
Example::
SmarterValidator.validate_session_key("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") # returns the session key
SmarterValidator.validate_session_key("invalid") # raises SmarterValueError
"""
logger.debug("%s.validate_session_key() %s", logger_prefix, session_key)
if not re.match(SmarterValidator.VALID_SESSION_KEY, session_key):
raise SmarterValueError(f"Invalid session key {session_key}")
return session_key
[docs]
@staticmethod
def validate_account_number(account_number: str) -> str:
"""Validate account number format
Checks if the provided string is a valid account number in the format XXXX-XXXX-XXXX.
:param account_number: The account number to validate.
:type account_number: str
:raises SmarterValueError: If the account number is not valid.
:returns: The validated account number.
:rtype: str
Example::
SmarterValidator.validate_account_number("1234-5678-9012") # returns "1234-5678-9012"
SmarterValidator.validate_account_number("invalid") # raises SmarterValueError
"""
logger.debug("%s.validate_account_number() %s", logger_prefix, account_number)
if not re.match(SmarterValidator.VALID_ACCOUNT_NUMBER_PATTERN, account_number):
raise SmarterValueError(f"Invalid account number {account_number}")
return account_number
[docs]
@staticmethod
def validate_chatbot_slug(slug: str) -> str:
"""Validate chatbot slug format
Checks if the provided string is a valid chatbot slug.
:param slug: The chatbot slug to validate.
:type slug: str
:raises SmarterValueError: If the chatbot slug is not valid.
:returns: The validated chatbot slug.
:rtype: str
Example::
SmarterValidator.validate_chatbot_slug("example-slug") # returns "example-slug"
SmarterValidator.validate_chatbot_slug("invalid slug") # raises SmarterValueError
"""
logger.debug("%s.validate_chatbot_slug() %s", logger_prefix, slug)
if not re.match(SmarterValidator.VALID_CHATBOT_SLUG_PATTERN, slug):
raise SmarterValueError(f"Invalid chatbot slug {slug}")
return slug
[docs]
@staticmethod
def validate_username(username: str) -> str:
"""Validate username format
Checks if the provided string is a valid username.
:param username: The username to validate.
:type username: str
:raises SmarterValueError: If the username is not valid.
:returns: The validated username.
:rtype: str
Example::
SmarterValidator.validate_username("valid_username") # returns "valid_username"
SmarterValidator.validate_username("invalid username") # raises SmarterValueError
"""
logger.debug("%s.validate_username() %s", logger_prefix, username)
if not re.match(r"^[a-zA-Z0-9_.-]+$", username):
raise SmarterValueError(f"Invalid username {username}")
return username
[docs]
@staticmethod
def validate_domain(domain: Optional[str]) -> Optional[str]:
"""Validate domain format
Checks if the provided string is a valid domain.
:param domain: The domain to validate.
:type domain: Optional[str]
:raises SmarterValueError: If the domain is not valid.
:returns: The validated domain or None.
:rtype: Optional[str]
Example::
SmarterValidator.validate_domain("example.com") # returns "example.com"
SmarterValidator.validate_domain("invalid_domain") # raises SmarterValueError
"""
logger.debug("%s.validate_domain() %s", logger_prefix, domain)
if isinstance(domain, str) and domain not in SmarterValidator.LOCAL_HOSTS + [None, ""]:
SmarterValidator.validate_hostname(domain.split(":")[0])
SmarterValidator.validate_url("http://" + domain)
return domain
[docs]
@staticmethod
def validate_email(email: str) -> str:
"""Validate email format
Checks if the provided string is a valid email address.
:param email: The email address to validate.
:type email: str
:raises SmarterValueError: If the email address is not valid.
:returns: The validated email address.
:rtype: str
Example::
SmarterValidator.validate_email("user@example.com") # returns "user@example.com"
SmarterValidator.validate_email("invalid") # raises SmarterValueError
"""
logger.debug("%s.validate_email() %s", logger_prefix, email)
if not isinstance(email, str) or not re.match(SmarterValidator.VALID_EMAIL_PATTERN, email):
raise SmarterValueError(f"Invalid email {email}")
return email
[docs]
@staticmethod
def validate_ip(ip: str) -> str:
"""Validate IP address format
Checks if the provided string is a valid IP address.
:param ip: The IP address to validate.
:type ip: str
:raises SmarterValueError: If the IP address is not valid.
:returns: The validated IP address.
:rtype: str
Example::
SmarterValidator.validate_ip("192.168.1.1") # returns "192.168.1.1"
SmarterValidator.validate_ip("invalid") # raises SmarterValueError
"""
logger.debug("%s.validate_ip() %s", logger_prefix, ip)
try:
# pylint: disable=import-outside-toplevel
from django.core.exceptions import ValidationError
from django.core.validators import validate_ipv4_address
validate_ipv4_address(ip)
except ValidationError as e:
raise SmarterValueError(f"Invalid IP address {ip}") from e
return ip
[docs]
@staticmethod
def validate_port(port: str) -> str:
"""Validate port format
Checks if the provided string is a valid port number.
:param port: The port to validate.
:type port: str
:raises SmarterValueError: If the port is not valid.
:returns: The validated port.
:rtype: str
Example::
SmarterValidator.validate_port("8080") # returns "8080"
SmarterValidator.validate_port("99999") # raises SmarterValueError
"""
logger.debug("%s.validate_port() %s", logger_prefix, port)
if not re.match(SmarterValidator.VALID_PORT_PATTERN, port):
raise SmarterValueError(f"Invalid port {port}")
if not port.isdigit():
raise SmarterValueError(f"Port must be numeric: {port}")
port_num = int(port)
if not 0 <= port_num <= 65535:
raise SmarterValueError(f"Port out of range (0-65535): {port}")
return port
[docs]
@staticmethod
def validate_url_path(path: str) -> str:
"""Validate URL path format
Checks if the provided string is a valid URL path.
:param path: The URL path to validate.
:type path: str
:raises SmarterValueError: If the URL path is not valid.
:returns: The validated URL path.
:rtype: str
Example::
SmarterValidator.validate_url_path("/api/v1/resource/") # returns "/api/v1/resource/"
SmarterValidator.validate_url_path("invalid_path") # raises SmarterValueError
"""
logger.debug("%s.validate_url_path() %s", logger_prefix, path)
if not path.startswith("/"):
raise SmarterValueError(f"Invalid URL path {path}. Must start with '/'")
if not bool(re.fullmatch(r"/[A-Za-z0-9._~!$&'()*+,;=:@/-]*", path)):
raise SmarterValueError(f"Invalid URL path {path}")
return path
[docs]
@staticmethod
def validate_url(url: str) -> str:
"""Validate URL format
Checks if the provided string is a valid URL.
:param url: The URL to validate.
:type url: str
:raises SmarterValueError: If the URL is not valid.
:returns: The validated URL.
:rtype: str
Example::
SmarterValidator.validate_url("https://example.com") # returns "https://example.com"
SmarterValidator.validate_url("invalid_url") # raises SmarterValueError
"""
logger.debug("%s.validate_url() %s", logger_prefix, url)
valid_protocols = ["http", "https"]
if not url:
raise SmarterValueError(f"Invalid url {url}")
if not isinstance(url, str):
raise SmarterValueError(f"Invalid url {url}. Should be a string")
try:
if any(local_url in url for local_url in SmarterValidator.LOCAL_URLS):
return url
except TypeError:
pass
try:
# pylint: disable=C0415
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
validator = URLValidator(schemes=valid_protocols)
validator(url)
parsed = urlparse(url)
if parsed.hostname:
SmarterValidator.validate_hostname(parsed.hostname)
except ValidationError as e:
parsed = urlparse(url)
if parsed.scheme not in valid_protocols:
raise SmarterValueError(f"Invalid url protocol {parsed.scheme}") from e
if all([parsed.scheme, parsed.netloc]) or url.startswith("localhost"):
return url
if SmarterValidator.is_valid_ip(url):
return url
if validators.url(url):
parsed = urlparse(url)
if parsed.scheme in valid_protocols:
return url
raise SmarterValueError(f"Invalid url {url}") from e
return url
[docs]
@staticmethod
def validate_hostname(hostname: str) -> str:
"""Validate hostname format
Checks if the provided string is a valid hostname.
:param hostname: The hostname to validate.
:type hostname: str
:raises SmarterValueError: If the hostname is not valid.
:returns: The validated hostname.
:rtype: str
Example::
SmarterValidator.validate_hostname("example.com") # returns "example.com"
SmarterValidator.validate_hostname("invalid_hostname!") # raises SmarterValueError
"""
logger.debug("%s.validate_hostname() %s", logger_prefix, hostname)
# Accept Django wildcard hostnames starting with a dot (e.g., .api.localhost)
if hostname.startswith("."):
# Allow leading dot for ALLOWED_HOSTS wildcard, validate the rest
hostname = hostname[1:]
if not hostname:
raise SmarterValueError("Invalid hostname . (dot only)")
if ":" in hostname:
hostname, port = hostname.split(":")
if not port.isdigit() or not 0 <= int(port) <= 65535:
raise SmarterValueError(f"Invalid port {port}")
if len(hostname) > 255:
raise SmarterValueError(f"Invalid hostname {hostname}")
if hostname and hostname[-1] == ".":
hostname = hostname[:-1] # strip exactly one dot from the right, if present
labels = hostname.split(".")
if labels[0] == "*":
# Wildcard only allowed as the leftmost label
labels = labels[1:]
if not labels:
raise SmarterValueError(f"Invalid hostname {hostname}")
allowed = re.compile(SmarterValidator.VALID_HOSTNAME_PATTERN, re.IGNORECASE)
if all(allowed.match(x) for x in labels):
return hostname
raise SmarterValueError(f"Invalid hostname {hostname}")
[docs]
@staticmethod
def validate_uuid(uuid: str) -> str:
"""Validate UUID format
Checks if the provided string is a valid UUID.
:param uuid: The UUID string to validate.
:type uuid: str
:raises SmarterValueError: If the UUID is not valid.
:returns: The validated UUID.
:rtype: str
Example::
SmarterValidator.validate_uuid("123e4567-e89b-12d3-a456-426614174000") # returns the UUID
SmarterValidator.validate_uuid("invalid") # raises SmarterValueError
"""
logger.debug("%s.validate_uuid() %s", logger_prefix, uuid)
if not re.match(SmarterValidator.VALID_UUID_PATTERN, uuid):
raise SmarterValueError(f"Invalid UUID {uuid}")
return uuid
[docs]
@staticmethod
def validate_clean_string(v: str) -> str:
"""Validate clean string format
Checks if the provided string is a valid "clean" string (alphanumeric, underscores, or hyphens, and may include dots).
:param v: The string to validate.
:type v: str
:raises SmarterValueError: If the string is not a valid clean string.
:returns: The validated clean string.
:rtype: str
Example::
SmarterValidator.validate_clean_string("valid_string-123") # returns "valid_string-123"
SmarterValidator.validate_clean_string("invalid string!") # raises SmarterValueError
"""
logger.debug("%s.validate_clean_string() %s", logger_prefix, v)
if not re.match(SmarterValidator.VALID_CLEAN_STRING, v):
raise SmarterValueError(f"Invalid clean string {v}")
return v
# --------------------------------------------------------------------------
# boolean helpers
# --------------------------------------------------------------------------
[docs]
@staticmethod
def is_valid_session_key(session_key: str) -> bool:
"""Check if session key is valid
Checks whether the provided session key is valid.
:param session_key: The session key to check.
:type session_key: str
:returns: True if the session key is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_session_key("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") # returns True
SmarterValidator.is_valid_session_key("invalid") # returns False
"""
logger.debug("%s.is_valid_session_key() %s", logger_prefix, session_key)
try:
SmarterValidator.validate_session_key(session_key)
return True
except SmarterValueError:
logger.debug("%s.is_valid_session_key() invalid %s", logger_prefix, session_key)
return False
[docs]
@staticmethod
def is_valid_account_number(account_number: str) -> bool:
"""Check if account number is valid
Checks whether the provided account number is valid.
:param account_number: The account number to check.
:type account_number: str
:returns: True if the account number is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_account_number("1234-5678-9012") # returns True
SmarterValidator.is_valid_account_number("invalid") # returns False
"""
logger.debug("%s.is_valid_account_number() %s", logger_prefix, account_number)
try:
SmarterValidator.validate_account_number(account_number)
return True
except SmarterValueError:
logger.debug("%s.is_valid_account_number() invalid %s", logger_prefix, account_number)
return False
[docs]
@staticmethod
def is_valid_chatbot_slug(slug: str) -> bool:
"""Check if chatbot slug is valid
Checks whether the provided chatbot slug is valid.
:param slug: The chatbot slug to check.
:type slug: str
:returns: True if the chatbot slug is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_chatbot_slug("example-slug") # returns True
SmarterValidator.is_valid_chatbot_slug("invalid slug") # returns False
"""
logger.debug("%s.is_valid_chatbot_slug() %s", logger_prefix, slug)
try:
SmarterValidator.validate_chatbot_slug(slug)
return True
except SmarterValueError:
logger.debug("%s.is_valid_chatbot_slug() invalid %s", logger_prefix, slug)
return False
[docs]
@staticmethod
def is_valid_username(username: str) -> bool:
"""Check if username is valid
Checks whether the provided username is valid.
:param username: The username to check.
:type username: str
:returns: True if the username is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_username("valid_username") # returns True
SmarterValidator.is_valid_username("invalid username") # returns False
"""
logger.debug("%s.is_valid_username() %s", logger_prefix, username)
try:
SmarterValidator.validate_username(username)
return True
except SmarterValueError:
logger.debug("%s.is_valid_username() invalid %s", logger_prefix, username)
return False
[docs]
@staticmethod
def is_valid_domain(domain: str) -> bool:
"""Check if domain is valid
Checks whether the provided domain is valid.
:param domain: The domain to check.
:type domain: str
:returns: True if the domain is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_domain("example.com") # returns True
SmarterValidator.is_valid_domain("invalid_domain") # returns False
"""
logger.debug("%s.is_valid_domain() %s", logger_prefix, domain)
try:
SmarterValidator.validate_domain(domain)
return True
except SmarterValueError:
logger.debug("%s.is_valid_domain() invalid %s", logger_prefix, domain)
return False
[docs]
@staticmethod
def is_valid_email(email: str) -> bool:
"""Check if email is valid
Checks whether the provided email address is valid.
:param email: The email address to check.
:type email: str
:returns: True if the email address is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_email("user@example.com") # returns True
SmarterValidator.is_valid_email("invalid") # returns False
"""
logger.debug("%s.is_valid_email() %s", logger_prefix, email)
try:
SmarterValidator.validate_email(email)
return True
except (SmarterValueError, ValueError):
logger.debug("%s.is_valid_email() invalid %s", logger_prefix, email)
return False
[docs]
@staticmethod
def is_valid_ip(ip: str) -> bool:
"""Check if IP address is valid
Checks whether the provided IP address is valid.
:param ip: The IP address to check.
:type ip: str
:returns: True if the IP address is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_ip("192.168.1.1") # returns True
SmarterValidator.is_valid_ip("invalid") # returns False
"""
logger.debug("%s.is_valid_ip() %s", logger_prefix, ip)
try:
SmarterValidator.validate_ip(ip)
return True
except SmarterValueError:
logger.debug("%s.is_valid_ip() invalid %s", logger_prefix, ip)
return False
[docs]
@staticmethod
def is_valid_port(port: str) -> bool:
"""Check if port is valid
Checks whether the provided port is valid.
:param port: The port to check.
:type port: str
:returns: True if the port is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_port("8080") # returns True
SmarterValidator.is_valid_port("invalid") # returns False
"""
logger.debug("%s.is_valid_port() %s", logger_prefix, port)
try:
SmarterValidator.validate_port(port)
return True
except SmarterValueError:
logger.debug("%s.is_valid_port() invalid %s", logger_prefix, port)
return False
[docs]
@staticmethod
def is_valid_url(url: str) -> bool:
"""Check if URL is valid
Checks whether the provided URL is valid.
:param url: The URL to check.
:type url: str
:returns: True if the URL is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_url("https://example.com") # returns True
SmarterValidator.is_valid_url("invalid_url") # returns False
"""
logger.debug("%s.is_valid_url() %s", logger_prefix, url)
try:
SmarterValidator.validate_url(url)
return True
except SmarterValueError:
logger.debug("%s.is_valid_url() invalid %s", logger_prefix, url)
return False
[docs]
@staticmethod
def is_valid_url_path(url_path: str) -> bool:
"""Check if URL path is valid
Checks whether the provided URL path is valid.
:param url_path: The URL path to check.
:type url_path: str
:returns: True if the URL path is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_url_path("/api/v1/tests/unauthenticated/list/") # returns True
SmarterValidator.is_valid_url_path("invalid_url_path") # returns False
"""
logger.debug("%s.is_valid_url_path() %s", logger_prefix, url_path)
try:
SmarterValidator.validate_url_path(url_path)
return True
except SmarterValueError:
logger.debug("%s.is_valid_url_path() invalid %s", logger_prefix, url_path)
return False
[docs]
@staticmethod
def is_valid_hostname(hostname: str) -> bool:
"""Check if URL is valid
Checks whether the provided URL is valid.
:param url: The URL to check.
:type url: str
:returns: True if the URL is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_url("https://example.com") # returns True
SmarterValidator.is_valid_url("invalid_url") # returns False
"""
logger.debug("%s.is_valid_hostname() %s", logger_prefix, hostname)
try:
SmarterValidator.validate_hostname(hostname)
return True
except SmarterValueError:
logger.debug("%s.is_valid_hostname() invalid %s", logger_prefix, hostname)
return False
[docs]
@staticmethod
def is_valid_uuid(uuid: str) -> bool:
"""Check if UUID is valid
Checks whether the provided UUID is valid.
:param uuid: The UUID string to check.
:type uuid: str
:returns: True if the UUID is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_uuid("123e4567-e89b-12d3-a456-426614174000") # returns True
SmarterValidator.is_valid_uuid("invalid") # returns False
"""
logger.debug("%s.is_valid_uuid() %s", logger_prefix, uuid)
try:
SmarterValidator.validate_uuid(uuid)
return True
except SmarterValueError:
logger.debug("%s.is_valid_uuid() invalid %s", logger_prefix, uuid)
return False
[docs]
@staticmethod
def is_valid_cleanstring(v: str) -> bool:
"""Check if hostname is valid
Checks whether the provided hostname is valid.
:param hostname: The hostname to check.
:type hostname: str
:returns: True if the hostname is valid, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_hostname("example.com") # returns True
SmarterValidator.is_valid_hostname("invalid_hostname!") # returns False
"""
logger.debug("%s.is_valid_cleanstring() %s", logger_prefix, v)
try:
SmarterValidator.validate_clean_string(v)
return True
except SmarterValueError:
logger.debug("%s.is_valid_cleanstring() invalid %s", logger_prefix, v)
return False
[docs]
@staticmethod
def is_valid_url_endpoint(url: str) -> bool:
"""
Check if the URL is valid and ends with a trailing slash.
Checks whether the provided URL is valid and ends with a trailing slash.
:param url: The URL to check.
:type url: str
:returns: True if the URL is valid and ends with a trailing slash, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_valid_url_endpoint("/api/v1/tests/unauthenticated/list/") # returns True
SmarterValidator.is_valid_url_endpoint("/api/v1/tests/unauthenticated/list") # returns False
"""
logger.debug("%s.is_valid_url_endpoint() %s", logger_prefix, url)
try:
SmarterValidator.validate_url_endpoint(url)
return True
except SmarterValueError:
logger.debug("%s.is_valid_url_endpoint() invalid %s", logger_prefix, url)
return False
[docs]
@staticmethod
def is_api_endpoint(url: str) -> bool:
"""
Check if the URL is an API endpoint.
Checks whether the provided URL contains '/api/'.
:param url: The URL to check.
:type url: str
:returns: True if the URL is an API endpoint, otherwise False.
:rtype: bool
Example::
SmarterValidator.is_api_endpoint("/api/v1/tests/unauthenticated/list/") # returns True
SmarterValidator.is_api_endpoint("/v1/tests/unauthenticated/list/") # returns False
"""
logger.debug("%s.is_api_endpoint() %s", logger_prefix, url)
if not isinstance(url, str):
return False
if "/api/" in url:
# checks for /api/ in the full url: example.com/api/v1/
return True
try:
if SMARTER_API_SUBDOMAIN in str(SmarterValidator.base_url(url)):
# checks for api subdomain in base url: api.example.com
return True
except SmarterValueError:
logger.debug("%s.is_api_endpoint() invalid %s", logger_prefix, url)
return False
return False
# --------------------------------------------------------------------------
# list helpers
# --------------------------------------------------------------------------
[docs]
@staticmethod
def validate_url_endpoint(url: str) -> None:
"""
Validate URL endpoint format
Checks if the provided string is a valid URL endpoint (must start and end with a slash).
:param url: The URL endpoint to validate.
:type url: str
:raises SmarterValueError: If the URL endpoint is not valid.
:returns: None if the URL endpoint is valid.
:rtype: None
Example::
SmarterValidator.validate_url_endpoint("/api/v1/tests/unauthenticated/list/") # returns None
SmarterValidator.validate_url_endpoint("/api/v1/tests/unauthenticated/list") # raises SmarterValueError
"""
logger.debug("%s.validate_url_endpoint() %s", logger_prefix, url)
if not re.match(SmarterValidator.VALID_URL_ENDPOINT, url):
raise SmarterValueError(f"URL endpoint '{url}' contains invalid characters.")
if not url.startswith("/"):
raise SmarterValueError(f"Invalid URL endpoint '{url}'. Should start with a leading slash")
if not url.endswith("/"):
raise SmarterValueError(f"Invalid URL endpoint '{url}'. Should end with a trailing slash")
[docs]
@staticmethod
def validate_list_of_account_numbers(account_numbers: list) -> None:
"""Validate list of account numbers
Checks if each item in the provided list is a valid account number.
:param account_numbers: The list of account numbers to validate.
:type account_numbers: list
:raises SmarterValueError: If any account number in the list is not valid.
:returns: None if all account numbers are valid.
:rtype: None
Example::
SmarterValidator.validate_list_of_account_numbers(["1234-5678-9012", "2345-6789-0123"]) # returns None
SmarterValidator.validate_list_of_account_numbers(["invalid", "2345-6789-0123"]) # raises SmarterValueError
"""
logger.debug("%s.validate_list_of_account_numbers() %s", logger_prefix, account_numbers)
for account_number in account_numbers:
SmarterValidator.validate_account_number(account_number)
[docs]
@staticmethod
def validate_list_of_domains(domains: list) -> None:
"""Validate list of domains
Checks if each item in the provided list is a valid domain.
:param domains: The list of domains to validate.
:type domains: list
:raises SmarterValueError: If any domain in the list is not valid.
:returns: None if all domains are valid.
:rtype: None
Example::
SmarterValidator.validate_list_of_domains(["example.com", "test.com"]) # returns None
SmarterValidator.validate_list_of_domains(["invalid_domain", "test.com"]) # raises SmarterValueError
"""
logger.debug("%s.validate_list_of_domains() %s", logger_prefix, domains)
for domain in domains:
SmarterValidator.validate_domain(domain)
[docs]
@staticmethod
def validate_list_of_emails(emails: list) -> None:
"""Validate list of emails
Checks if each item in the provided list is a valid email address.
:param emails: The list of email addresses to validate.
:type emails: list
:raises SmarterValueError: If any email address in the list is not valid.
:returns: None if all email addresses are valid.
:rtype: None
Example::
SmarterValidator.validate_list_of_emails(["user@example.com", "admin@test.com"]) # returns None
SmarterValidator.validate_list_of_emails(["invalid", "admin@test.com"]) # raises SmarterValueError
"""
logger.debug("%s.validate_list_of_emails() %s", logger_prefix, emails)
for email in emails:
SmarterValidator.validate_email(email)
[docs]
@staticmethod
def validate_list_of_ips(ips: list) -> None:
"""Validate list of IP addresses
Checks if each item in the provided list is a valid IP address.
:param ips: The list of IP addresses to validate.
:type ips: list
:raises SmarterValueError: If any IP address in the list is not valid.
:returns: None if all IP addresses are valid.
:rtype: None
Example::
SmarterValidator.validate_list_of_ips(["192.168.1.1", "10.0.0.1"]) # returns None
SmarterValidator.validate_list_of_ips(["invalid", "10.0.0.1"]) # raises SmarterValueError
"""
logger.debug("%s.validate_list_of_ips() %s", logger_prefix, ips)
for ip in ips:
SmarterValidator.validate_ip(ip)
[docs]
@staticmethod
def validate_list_of_ports(ports: list) -> None:
"""
Validate list of ports
Checks if each item in the provided list is a valid port.
:param ports: The list of ports to validate.
:type ports: list
:raises SmarterValueError: If any port in the list is not valid.
:returns: None if all ports are valid.
:rtype: None
Example::
SmarterValidator.validate_list_of_ports(["8080", "443"]) # returns None
SmarterValidator.validate_list_of_ports(["invalid", "443"]) # raises SmarterValueError
"""
logger.debug("%s.validate_list_of_ports() %s", logger_prefix, ports)
for port in ports:
SmarterValidator.validate_port(port)
[docs]
@staticmethod
def validate_list_of_urls(urls: list) -> None:
"""Validate list of URLs
Checks if each item in the provided list is a valid URL.
:param urls: The list of URLs to validate.
:type urls: list
:raises SmarterValueError: If any URL in the list is not valid.
:returns: None if all URLs are valid.
:rtype: None
Example::
SmarterValidator.validate_list_of_urls(["https://example.com", "https://test.com"]) # returns None
SmarterValidator.validate_list_of_urls(["invalid_url", "https://test.com"]) # raises SmarterValueError
"""
logger.debug("%s.validate_list_of_urls() %s", logger_prefix, urls)
for url in urls:
SmarterValidator.validate_url(url)
[docs]
@staticmethod
def validate_list_of_uuids(uuids: list) -> None:
"""Validate list of UUIDs
Checks if each item in the provided list is a valid UUID.
:param uuids: The list of UUIDs to validate.
:type uuids: list
:raises SmarterValueError: If any UUID in the list is not valid.
:returns: None if all UUIDs are valid.
:rtype: None
Example::
SmarterValidator.validate_list_of_uuids([
"123e4567-e89b-12d3-a456-426614174000",
"987e6543-e21b-12d3-a456-426614174111"
]) # returns None
SmarterValidator.validate_list_of_uuids([
"invalid",
"987e6543-e21b-12d3-a456-426614174111"
]) # raises SmarterValueError
"""
logger.debug("%s.validate_list_of_uuids() %s", logger_prefix, uuids)
for uuid in uuids:
SmarterValidator.validate_uuid(uuid)
# --------------------------------------------------------------------------
# utility helpers
# --------------------------------------------------------------------------
[docs]
@staticmethod
def base_domain(url: str) -> Optional[str]:
"""
Get the base domain from a URL.
Extracts the base domain from the provided URL string.
:param url: The URL string to extract the base domain from.
:type url: str
:returns: The base domain as a string, or None if not found.
:rtype: Optional[str]
Example::
SmarterValidator.base_domain("https://example.com/path/") # returns "example.com"
SmarterValidator.base_domain("") # returns None
"""
logger.debug("%s.base_domain() %s", logger_prefix, url)
if not url:
return None
base_url = SmarterValidator.base_url(url)
if not base_url:
return None
base_url = base_url.replace("http://", "").replace("https://", "")
return base_url.rstrip("/")
[docs]
@staticmethod
def base_url(url: str) -> Optional[str]:
"""
Get the base URL from a URL.
Extracts the base URL from the provided URL string.
:param url: The URL string to extract the base URL from.
:type url: str
:returns: The base URL as a string, or None if not found.
:rtype: Optional[str]
Example::
SmarterValidator.base_url("https://example.com/path/") # returns "https://example.com"
SmarterValidator.base_url("") # returns None
"""
logger.debug("%s.base_url() %s", logger_prefix, url)
if not url:
return None
SmarterValidator.validate_url(url)
parsed_url = urlparse(url)
unparsed_url = urlunparse((parsed_url.scheme, parsed_url.netloc, "", "", "", ""))
return SmarterValidator.trailing_slash(unparsed_url)
[docs]
@staticmethod
def trailing_slash(url: str) -> Optional[str]:
"""
ensure that URL ends with a trailing slash
Appends a trailing slash to the URL if it does not already have one.
:param url: The URL to process.
:type url: str
:returns: The URL with a trailing slash, or None if the input is empty.
:rtype: Optional[str]
Example::
SmarterValidator.trailing_slash("https://example.com") # returns "https://example.com/"
SmarterValidator.trailing_slash("https://example.com/") # returns "https://example.com/"
SmarterValidator.trailing_slash("") # returns None
"""
logger.debug("%s.trailing_slash() %s", logger_prefix, url)
if not url:
return None
return url if url.endswith("/") else url + "/"
[docs]
@staticmethod
def leading_slash(path: str) -> Optional[str]:
"""
ensure that URL path starts with a leading slash
Appends a leading slash to the URL path if it does not already have one.
:param path: The URL path to process.
:type path: str
:returns: The URL path with a leading slash, or None if the input is empty.
:rtype: Optional[str]
Example::
SmarterValidator.leading_slash("api/v1/resource/") # returns "/api/v1/resource/"
SmarterValidator.leading_slash("/api/v1/resource/") # returns "/api/v1/resource/"
SmarterValidator.leading_slash("") # returns None
"""
logger.debug("%s.leading_slash() %s", logger_prefix, path)
if not path:
return None
return path if path.startswith("/") else "/" + path
[docs]
@staticmethod
def urlify(url: str, scheme: Optional[str] = None, environment: str = SmarterEnvironments.LOCAL) -> str:
"""
ensure that URL starts with http:// or https://
and ends with a trailing slash
Ensures the provided URL starts with a valid scheme (http or https) and ends with a trailing slash.
:param url: The URL to process.
:type url: str
:param scheme: (Optional) The scheme to use ("http" or "https"). Deprecated.
:type scheme: Optional[str]
:param environment: The environment to determine the default scheme.
:type environment: str
:returns: The normalized URL with scheme and trailing slash.
:rtype: str
Example::
SmarterValidator.urlify("example.com") # returns "https://example.com/"
SmarterValidator.urlify("example.com", scheme="http") # returns "http://example.com/"
SmarterValidator.urlify("https://example.com") # returns "https://example.com/"
"""
logger.debug("%s.urlify() %s, %s", logger_prefix, url, scheme)
if not url:
raise SmarterValueError("URL cannot be empty")
if scheme:
warnings.warn("scheme is deprecated and will be removed in a future release.", DeprecationWarning)
if scheme and scheme not in ["http", "https"]:
SmarterValidator.raise_error(f"Invalid scheme {scheme}. Should be one of ['http', 'https']")
scheme = "http" if environment == SmarterEnvironments.LOCAL else "https"
if not "://" in url:
url = f"{scheme}://{url}"
parsed_url = urlparse(url)
retval = urlunparse((scheme, parsed_url.netloc, parsed_url.path, "", "", ""))
retval = SmarterValidator.trailing_slash(url)
if not retval:
raise SmarterValueError(f"Invalid URL {url}")
SmarterValidator.validate_url(retval)
if not retval.endswith("/"):
retval += "/"
return retval
[docs]
@staticmethod
def raise_error(msg: str) -> None:
"""Raise a SmarterValueError with the given message"""
raise SmarterValueError(msg)