Files
AI/참고/guardrails-main/guardrails/hub/validator_package_service.py
2026-05-12 19:40:31 +09:00

426 lines
14 KiB
Python

import importlib
import importlib.util
import json
import os
from datetime import datetime, timezone
from pathlib import Path
import re
import shutil
import subprocess
import sys
import sysconfig
from typing import List, Literal, Optional
from types import ModuleType
from guardrails.hub.registry import get_registry_path
from packaging.utils import canonicalize_name # PEP 503
from guardrails.logger import logger as guardrails_logger
from guardrails.cli.hub.utils import (
PipProcessError,
installer_process,
)
from guardrails_hub_types import Manifest
from guardrails.cli.server.hub_client import get_validator_manifest
from guardrails.settings import settings
from guardrails.types.validator_registry import ValidatorRegistry
json_format: Literal["json"] = "json"
string_format: Literal["string"] = "string"
_GUARDRAILS_INSTALLER_ENV = "GUARDRAILS_INSTALLER"
class ValidatorModuleType(ModuleType):
__validator_exports__: List[str]
class FailedPackageInspection(Exception):
pass
class FailedToLocateModule(Exception):
pass
class FailedPackageInstallation(Exception):
pass
class FailedPackageInstallationPostInstall(FailedPackageInstallation):
pass
class InvalidHubInstallURL(Exception):
pass
class ValidatorPackageService:
@staticmethod
def detect_installer() -> str:
"""Detect preferred package installer.
Precedence:
1. GUARDRAILS_INSTALLER env var (explicit override)
2. uv (if available via shutil.which)
3. pip (fallback)
"""
env_installer = os.environ.get(_GUARDRAILS_INSTALLER_ENV, "").strip().lower()
if env_installer in ("uv", "pip"):
return env_installer
if shutil.which("uv") is not None:
return "uv"
return "pip"
@staticmethod
def get_manifest_and_site_packages(module_name: str) -> tuple[Manifest, str]:
module_manifest = get_validator_manifest(module_name)
site_packages = ValidatorPackageService.get_site_packages_location()
return (module_manifest, site_packages)
@staticmethod
def get_site_packages_location():
return sysconfig.get_paths()["purelib"]
@staticmethod
def reload_module(module_path) -> ModuleType:
try:
reloaded_module = None
# Dynamically import the module based on its path
if "guardrails.hub" in sys.modules:
# Reload the module if it's already imported
importlib.reload(sys.modules["guardrails.hub"])
if module_path not in sys.modules:
# Import the module if it has not been imported yet
importlib.invalidate_caches()
reloaded_module = importlib.import_module(module_path)
sys.modules[module_path] = reloaded_module
else:
reloaded_module = sys.modules[module_path]
return reloaded_module
except ModuleNotFoundError:
raise
except Exception:
raise
@staticmethod
def get_validator_from_manifest(manifest: Manifest) -> ModuleType:
"""
Get Validator class from the installed module based on the manifest.
Note: manifest.exports yields a list of exported Validator classes.
Args:
manifest (Manifest): The manifest of the installed module
Returns:
Any: The Validator class from the installed module
"""
validator_id = manifest.id
import_path = ValidatorPackageService.get_import_path_from_validator_id(
validator_id
)
import_line = f"{import_path}"
# Reload or import the module
return ValidatorPackageService.reload_module(import_line)
@staticmethod
def rewrite_stub_file(registry: ValidatorRegistry):
stub_file = (
Path(ValidatorPackageService.get_site_packages_location())
/ "guardrails"
/ "hub"
/ "__init__.pyi"
)
import_statements = []
for v in registry.validators.values():
if v.exports and v.import_path and importlib.util.find_spec(v.import_path):
import_statements.extend(
[f"from {v.import_path} import {e} as {e}" for e in v.exports]
)
stub_file.write_text("\n".join(import_statements))
@staticmethod
def register_validator(manifest: Manifest):
"""Register a validator in the project-level JSON registry."""
registry_file = get_registry_path()
registry_file.parent.mkdir(parents=True, exist_ok=True)
registry = {"version": 1, "validators": {}}
if registry_file.exists():
try:
registry = json.loads(registry_file.read_text())
except (json.JSONDecodeError, OSError):
guardrails_logger.warning(
"Failed to read hub registry at %s, creating new one",
registry_file,
)
validator_id = manifest.id
if "/" not in validator_id:
guardrails_logger.debug(
"Skipping registry for validator %s: id missing namespace",
validator_id,
)
return
import_path = ValidatorPackageService.get_import_path_from_validator_id(
validator_id
)
package_name = ValidatorPackageService.get_normalized_package_name(validator_id)
registry["validators"][validator_id] = {
"import_path": import_path,
"exports": manifest.exports or [],
"installed_at": datetime.now(timezone.utc).isoformat(),
"package_name": package_name,
}
registry_file.write_text(json.dumps(registry, indent=2))
ValidatorPackageService.rewrite_stub_file(
ValidatorRegistry.model_validate(registry)
)
@staticmethod
def unregister_validator(validator_id: str):
"""Remove a validator from the project-level JSON registry."""
registry_file = get_registry_path()
if not registry_file.exists():
return
try:
registry = json.loads(registry_file.read_text())
except (json.JSONDecodeError, OSError):
guardrails_logger.debug(
"Registry at %s is unreadable; skipping unregister",
registry_file,
)
return
validators = registry.get("validators", {})
if validator_id in validators:
del validators[validator_id]
registry["validators"] = validators
registry_file.write_text(json.dumps(registry, indent=2))
ValidatorPackageService.rewrite_stub_file(
ValidatorRegistry.model_validate(registry)
)
@staticmethod
def add_to_hub_inits(manifest: Manifest, site_packages: str):
validator_id = manifest.id
exports: List[str] = manifest.exports or []
sorted_exports = sorted(exports, reverse=True)
import_path = ValidatorPackageService.get_import_path_from_validator_id(
validator_id
)
import_line = f"from {import_path} import {', '.join(sorted_exports)}"
hub_init_location = os.path.join(
site_packages, "guardrails", "hub", "__init__.py"
)
with open(hub_init_location, "a+") as hub_init:
hub_init.seek(0, 0)
content = hub_init.read()
if import_line in content:
hub_init.close()
else:
hub_init.seek(0, 2)
if len(content) > 0:
hub_init.write("\n")
hub_init.write(import_line)
hub_init.close()
@staticmethod
def get_module_path(package_name):
try:
if package_name not in sys.modules:
module = importlib.import_module(package_name)
sys.modules[package_name] = module
module = sys.modules[package_name]
package_path = module.__path__[0] # Take the first entry if it's a list
except (ModuleNotFoundError, AttributeError, TypeError) as e:
# wasn't able to import the module
raise FailedToLocateModule(
f"""
The module {package_name} could not be found in
the current environment.
"""
) from e
return package_path
@staticmethod
def get_validator_id(validator_uri: str):
if not validator_uri.startswith("hub://"):
raise InvalidHubInstallURL(
"Invalid URI! The package URI must start with 'hub://'"
)
validator_uri_with_version = validator_uri.replace("hub://", "")
validator_id_version_regex = (
r"(?P<validator_id>[\/a-zA-Z0-9\-_]+)(?P<version>.*)"
)
match = re.match(validator_id_version_regex, validator_uri_with_version)
validator_version = None
if match:
validator_id = match.group("validator_id")
validator_version = (
match.group("version").strip() if match.group("version") else None
)
else:
validator_id = validator_uri_with_version
return (validator_id, validator_version)
@staticmethod
def run_post_install(
manifest: Manifest, site_packages: str, logger=guardrails_logger
):
validator_id = manifest.id
post_install_script = manifest.post_install
if not post_install_script:
return
import_path = ValidatorPackageService.get_import_path_from_validator_id(
validator_id
)
relative_path = os.path.join(
site_packages,
import_path,
post_install_script,
)
if os.path.isfile(relative_path):
try:
logger.debug("running post install script...")
command = [sys.executable, relative_path]
subprocess.check_output(command)
except subprocess.CalledProcessError as exc:
logger.error(
(
f"Failed to run post install script for {manifest.id}\n"
f"Exit code: {exc.returncode}\n"
f"stdout: {exc.output}"
)
)
raise FailedPackageInstallationPostInstall(
f"Failed to run post install script for {manifest.id}\n"
)
except Exception as e:
logger.error(
f"An unexpected exception occurred while running the post install script for {manifest.id}!", # noqa
e,
)
raise FailedPackageInstallationPostInstall(
f"""
An unexpected exception occurred while running the post install
script for {manifest.id}!
"""
)
@staticmethod
def get_normalized_package_name(validator_id: str):
validator_id_parts = validator_id.split("/")
concatanated_package_name = (
f"{validator_id_parts[0]}-grhub-{validator_id_parts[1]}"
)
pep_503_package_name = canonicalize_name(concatanated_package_name)
return pep_503_package_name
@staticmethod
def get_import_path_from_validator_id(validator_id):
pep_503_package_name = ValidatorPackageService.get_normalized_package_name(
validator_id
)
return pep_503_package_name.replace("-", "_")
@staticmethod
def install_hub_module(
validator_id: str,
validator_version: Optional[str] = "",
quiet: bool = False,
upgrade: bool = False,
logger=guardrails_logger,
):
pep_503_package_name = ValidatorPackageService.get_normalized_package_name(
validator_id
)
validator_version = validator_version if validator_version else ""
guardrails_token = settings.rc.token
installer = ValidatorPackageService.detect_installer()
install_flags = [
f"--index-url=https://__token__:{guardrails_token}@pypi.guardrailsai.com/simple",
"--extra-index-url=https://pypi.org/simple",
]
if upgrade:
install_flags.append("--upgrade")
if quiet:
install_flags.append("-q")
# Install from guardrails hub pypi server with public pypi index as fallback
try:
full_package_name = f"{pep_503_package_name}[validators]{validator_version}"
download_output = installer_process(
"install",
full_package_name,
install_flags,
quiet=quiet,
installer=installer,
)
if not quiet:
logger.info(download_output)
except PipProcessError:
try:
full_package_name = f"{pep_503_package_name}{validator_version}"
download_output = installer_process(
"install",
full_package_name,
install_flags,
quiet=quiet,
installer=installer,
)
if not quiet:
logger.info(download_output)
except PipProcessError as e:
action = e.action
package = e.package
stderr = e.stderr
stdout = e.stdout
returncode = e.returncode
logger.error(
(
f"Failed to {action} {package}\n"
f"Exit code: {returncode}\n"
f"stderr: {(stderr or '').strip()}\n"
f"stdout: {(stdout or '').strip()}"
)
)
raise
except Exception as e:
logger.error(
"An unexpected exception occurred while "
f"installing {validator_id}: ",
e,
)
raise