Skip to content

Commit e35655a

Browse files
authored
Merge pull request #1434 from guardrails-ai/hub-import-tlc
Migrate to Dynamic Hub Imports
2 parents 6d97e0e + 685c9c1 commit e35655a

File tree

16 files changed

+1114
-227
lines changed

16 files changed

+1114
-227
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ mlartifacts
4848
.idea
4949
.pytest_cache
5050
.ruff_cache
51-
.vscode
51+
.vscode
52+
docs/examples/.guardrails/hub_registry.json

docs/examples/input_validation.ipynb

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
"output_type": "stream",
1111
"text": [
1212
"Installing hub:\u001b[35m/\u001b[0m\u001b[35m/guardrails/\u001b[0m\u001b[95mtwo_words...\u001b[0m\n",
13-
"✅Successfully installed guardrails/two_words!\n",
13+
"✅Successfully installed guardrails/two_words version \u001b[1;36m0.0\u001b[0m.\u001b[1;36m0\u001b[0m!\n",
1414
"\n",
1515
"\n"
1616
]
1717
}
1818
],
1919
"source": [
20-
"!guardrails hub install hub://guardrails/two_words --quiet"
20+
"! guardrails hub install hub://guardrails/two_words --quiet"
2121
]
2222
},
2323
{
@@ -38,7 +38,7 @@
3838
},
3939
{
4040
"cell_type": "code",
41-
"execution_count": 1,
41+
"execution_count": 2,
4242
"metadata": {
4343
"is_executing": true
4444
},
@@ -76,7 +76,7 @@
7676
},
7777
{
7878
"cell_type": "code",
79-
"execution_count": 2,
79+
"execution_count": 3,
8080
"metadata": {
8181
"is_executing": true
8282
},
@@ -85,9 +85,7 @@
8585
"name": "stderr",
8686
"output_type": "stream",
8787
"text": [
88-
"/Users/dtam/dev/guardrails/guardrails/validator_service/__init__.py:85: UserWarning: Could not obtain an event loop. Falling back to synchronous validation.\n",
89-
" warnings.warn(\n",
90-
"/Users/dtam/dev/guardrails/guardrails/validator_service/__init__.py:85: UserWarning: Could not obtain an event loop. Falling back to synchronous validation.\n",
88+
"/Users/calebcourier/Projects/guardrails/docs/.venv/lib/python3.10/site-packages/guardrails/validator_service/__init__.py:75: UserWarning: Could not obtain an event loop. Falling back to synchronous validation.\n",
9189
" warnings.warn(\n"
9290
]
9391
}
@@ -114,7 +112,7 @@
114112
},
115113
{
116114
"cell_type": "code",
117-
"execution_count": 5,
115+
"execution_count": 4,
118116
"metadata": {},
119117
"outputs": [
120118
{
@@ -123,14 +121,6 @@
123121
"text": [
124122
"Validation failed for field with errors: Value must be exactly two words\n"
125123
]
126-
},
127-
{
128-
"name": "stderr",
129-
"output_type": "stream",
130-
"text": [
131-
"/Users/dtam/dev/guardrails/guardrails/validator_service/__init__.py:85: UserWarning: Could not obtain an event loop. Falling back to synchronous validation.\n",
132-
" warnings.warn(\n"
133-
]
134124
}
135125
],
136126
"source": [
@@ -158,7 +148,7 @@
158148
],
159149
"metadata": {
160150
"kernelspec": {
161-
"display_name": "litellm",
151+
"display_name": ".venv (3.10.16)",
162152
"language": "python",
163153
"name": "python3"
164154
},
@@ -172,7 +162,7 @@
172162
"name": "python",
173163
"nbconvert_exporter": "python",
174164
"pygments_lexer": "ipython3",
175-
"version": "3.12.3"
165+
"version": "3.10.16"
176166
}
177167
},
178168
"nbformat": 4,

guardrails/cli/hub/list.py

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import os
2-
import re
3-
41
from guardrails.cli.hub.hub import hub_command
2+
from guardrails.hub.registry import get_registry
53
from guardrails.hub_telemetry.hub_tracing import trace
64
from .console import console
75

@@ -10,22 +8,14 @@
108
@trace(name="guardrails-cli/hub/list")
119
def list():
1210
"""List all installed validators."""
13-
from guardrails.hub.validator_package_service import ValidatorPackageService
14-
15-
site_packages = ValidatorPackageService.get_site_packages_location()
16-
hub_init_file = os.path.join(site_packages, "guardrails", "hub", "__init__.py")
17-
18-
installed_validators = []
11+
registry = get_registry()
1912

20-
if os.path.isfile(hub_init_file):
21-
with open(hub_init_file, "r") as file:
22-
content = file.read()
23-
matches = re.findall(r"from .* import (\w+)", content)
24-
installed_validators.extend(matches)
25-
26-
if installed_validators:
27-
console.print("Installed Validators:")
28-
for validator in installed_validators:
29-
console.print(f"- {validator}")
30-
else:
13+
validators = registry.validators
14+
if not validators:
3115
console.print("No validators installed.")
16+
return
17+
18+
console.print("Installed Validators:")
19+
for validator_id, entry in sorted(validators.items()):
20+
exports = ", ".join(entry.exports)
21+
console.print(f"- {validator_id} ({exports})")

guardrails/cli/hub/uninstall.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,14 @@ def uninstall(
7979
# Prep
8080
with console.status("Fetching manifest", spinner="bouncingBar"):
8181
module_manifest = get_validator_manifest(module_name)
82-
site_packages = ValidatorPackageService.get_site_packages_location()
8382

8483
# Uninstall
8584
with console.status("Removing module", spinner="bouncingBar"):
8685
uninstall_hub_module(module_manifest)
8786

8887
# Cleanup
8988
with console.status("Cleaning up", spinner="bouncingBar"):
90-
remove_from_hub_inits(module_manifest, site_packages)
89+
ValidatorPackageService.unregister_validator(module_name)
9190

9291
console.print("✅ Successfully uninstalled!") # type: ignore
9392
logger.log(level=LEVELS.get("SPAM"), msg="✅ Successfully uninstalled!") # type: ignore

guardrails/hub/__init__.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,60 @@
1-
# Should contain imports for all validators
2-
# Will be auto-populated by the installation script
1+
"""guardrails.hub - Dynamic import resolution from hub_registry.json.
2+
3+
Validators registered in .guardrails/hub_registry.json are resolved lazily
4+
on first attribute access and cached for subsequent imports.
5+
"""
6+
7+
import importlib
8+
9+
from guardrails.hub.registry import get_registry
10+
11+
12+
_export_map_cache = None
13+
14+
15+
def _build_export_map() -> dict:
16+
"""Build mapping from export name to import path.
17+
18+
Returns a dict mapping export names (e.g. "DetectPII") to their
19+
module import paths (e.g. "guardrails_grhub_detect_pii").
20+
"""
21+
registry = get_registry()
22+
export_map = {}
23+
for entry in registry.validators.values():
24+
import_path = entry.import_path
25+
for export_name in entry.exports:
26+
export_map[export_name] = import_path
27+
return export_map
28+
29+
30+
def _get_export_map() -> dict:
31+
"""Return cached export map, building it on first access."""
32+
global _export_map_cache
33+
if _export_map_cache is None:
34+
_export_map_cache = _build_export_map()
35+
return _export_map_cache
36+
37+
38+
def __getattr__(name: str):
39+
export_map = _get_export_map()
40+
if name in export_map:
41+
import_path = export_map[name]
42+
try:
43+
module = importlib.import_module(import_path)
44+
attr = getattr(module, name)
45+
globals()[name] = attr
46+
return attr
47+
except (ModuleNotFoundError, AttributeError) as e:
48+
raise ImportError(
49+
f"Cannot import '{name}' from hub registry. "
50+
f"Module '{import_path}' not found. "
51+
f"Try reinstalling: guardrails hub install "
52+
f"hub://<org>/<validator>"
53+
) from e
54+
raise AttributeError(f"module 'guardrails.hub' has no attribute '{name}'")
55+
56+
57+
def __dir__():
58+
base = list(globals().keys())
59+
base.extend(_get_export_map().keys())
60+
return base

guardrails/hub/install.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ def install(
135135
msg="Skipping post install, models will not be "
136136
"downloaded for local inference.",
137137
)
138-
ValidatorPackageService.add_to_hub_inits(module_manifest, site_packages)
139138
ValidatorPackageService.register_validator(module_manifest)
140139

141140
# 5. Get Validator Class for the installed module

guardrails/hub/registry.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import json
2+
import os
3+
from pathlib import Path
4+
5+
from guardrails.types.validator_registry import ValidatorRegistry
6+
from guardrails.logger import logger
7+
8+
9+
def get_registry_path() -> Path:
10+
"""Return the project-level registry path."""
11+
return Path(os.getcwd()) / ".guardrails" / "hub_registry.json"
12+
13+
14+
def get_registry() -> ValidatorRegistry:
15+
registry_file = get_registry_path()
16+
try:
17+
registry_str = registry_file.read_text()
18+
registry_dict = json.loads(registry_str)
19+
registry = ValidatorRegistry.model_validate(registry_dict)
20+
return registry
21+
except (json.JSONDecodeError, OSError):
22+
logger.warning("Failed to read hub registry at %s", registry_file)
23+
return ValidatorRegistry()

guardrails/hub/validator_package_service.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import importlib
2+
import importlib.util
23
import json
34
import os
45
from datetime import datetime, timezone
@@ -11,6 +12,7 @@
1112

1213
from typing import List, Literal, Optional
1314
from types import ModuleType
15+
from guardrails.hub.registry import get_registry_path
1416
from packaging.utils import canonicalize_name # PEP 503
1517

1618
from guardrails.logger import logger as guardrails_logger
@@ -23,6 +25,7 @@
2325
from guardrails_hub_types import Manifest
2426
from guardrails.cli.server.hub_client import get_validator_manifest
2527
from guardrails.settings import settings
28+
from guardrails.types.validator_registry import ValidatorRegistry
2629

2730

2831
json_format: Literal["json"] = "json"
@@ -72,11 +75,6 @@ def detect_installer() -> str:
7275
return "uv"
7376
return "pip"
7477

75-
@staticmethod
76-
def get_registry_path() -> Path:
77-
"""Return the project-level registry path."""
78-
return Path(os.getcwd()) / ".guardrails" / "hub_registry.json"
79-
8078
@staticmethod
8179
def get_manifest_and_site_packages(module_name: str) -> tuple[Manifest, str]:
8280
module_manifest = get_validator_manifest(module_name)
@@ -131,10 +129,28 @@ def get_validator_from_manifest(manifest: Manifest) -> ModuleType:
131129
# Reload or import the module
132130
return ValidatorPackageService.reload_module(import_line)
133131

132+
@staticmethod
133+
def rewrite_stub_file(registry: ValidatorRegistry):
134+
stub_file = (
135+
Path(ValidatorPackageService.get_site_packages_location())
136+
/ "guardrails"
137+
/ "hub"
138+
/ "__init__.pyi"
139+
)
140+
141+
import_statements = []
142+
for v in registry.validators.values():
143+
if v.exports and v.import_path and importlib.util.find_spec(v.import_path):
144+
import_statements.extend(
145+
[f"from {v.import_path} import {e} as {e}" for e in v.exports]
146+
)
147+
148+
stub_file.write_text("\n".join(import_statements))
149+
134150
@staticmethod
135151
def register_validator(manifest: Manifest):
136152
"""Register a validator in the project-level JSON registry."""
137-
registry_file = ValidatorPackageService.get_registry_path()
153+
registry_file = get_registry_path()
138154
registry_file.parent.mkdir(parents=True, exist_ok=True)
139155

140156
registry = {"version": 1, "validators": {}}
@@ -168,6 +184,35 @@ def register_validator(manifest: Manifest):
168184

169185
registry_file.write_text(json.dumps(registry, indent=2))
170186

187+
ValidatorPackageService.rewrite_stub_file(
188+
ValidatorRegistry.model_validate(registry)
189+
)
190+
191+
@staticmethod
192+
def unregister_validator(validator_id: str):
193+
"""Remove a validator from the project-level JSON registry."""
194+
registry_file = get_registry_path()
195+
if not registry_file.exists():
196+
return
197+
198+
try:
199+
registry = json.loads(registry_file.read_text())
200+
except (json.JSONDecodeError, OSError):
201+
guardrails_logger.debug(
202+
"Registry at %s is unreadable; skipping unregister",
203+
registry_file,
204+
)
205+
return
206+
207+
validators = registry.get("validators", {})
208+
if validator_id in validators:
209+
del validators[validator_id]
210+
registry["validators"] = validators
211+
registry_file.write_text(json.dumps(registry, indent=2))
212+
ValidatorPackageService.rewrite_stub_file(
213+
ValidatorRegistry.model_validate(registry)
214+
)
215+
171216
@staticmethod
172217
def add_to_hub_inits(manifest: Manifest, site_packages: str):
173218
validator_id = manifest.id
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Optional
2+
3+
from pydantic import BaseModel, Field
4+
5+
6+
class ValidatorRegistryEntry(BaseModel):
7+
import_path: Optional[str] = Field(default=None)
8+
exports: list[str] = Field(default_factory=list)
9+
installed_at: Optional[str] = Field(default=None)
10+
package_name: Optional[str] = Field(default=None)
11+
12+
13+
class ValidatorRegistry(BaseModel):
14+
version: int = Field(default=1)
15+
validators: dict[str, ValidatorRegistryEntry] = Field(default_factory=dict)

0 commit comments

Comments
 (0)