Skip to content

Commit 595ccf5

Browse files
Installs from private pypi with validator versioning
1 parent b114ef6 commit 595ccf5

File tree

3 files changed

+127
-95
lines changed

3 files changed

+127
-95
lines changed

guardrails/cli/hub/utils.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@
33
import re
44
import subprocess
55
import sys
6+
7+
from typing import Literal
8+
import logging
9+
610
from email.parser import BytesHeaderParser
7-
from typing import List, Literal, Union
11+
from typing import List, Union
812
from pydash.strings import snake_case
913

1014
from guardrails_hub_types import Manifest
11-
from guardrails.cli.logger import logger
12-
1315

1416
json_format: Literal["json"] = "json"
1517
string_format: Literal["string"] = "string"
1618

19+
logger = logging.getLogger(__name__)
20+
21+
json_format = "json"
22+
string_format = "string"
23+
1724

1825
def pip_process(
1926
action: str,
@@ -34,47 +41,50 @@ def pip_process(
3441
env = dict(os.environ)
3542
if no_color:
3643
env["NO_COLOR"] = "true"
37-
if not quiet:
38-
logger.debug(f"decoding output from pip {action} {package}")
39-
output = subprocess.check_output(command, env=env)
40-
else:
41-
output = subprocess.check_output(
42-
command, stderr=subprocess.DEVNULL, env=env
43-
)
44+
45+
result = subprocess.run(
46+
command,
47+
env=env,
48+
capture_output=True, # Capture both stdout and stderr
49+
text=True, # Automatically decode to strings
50+
check=True, # Automatically raise error on non-zero exit code
51+
)
4452

4553
if format == json_format:
46-
parsed = BytesHeaderParser().parsebytes(output)
4754
try:
4855
remove_color_codes = re.compile(r"\x1b\[[0-9;]*m")
49-
parsed_as_string = re.sub(
50-
remove_color_codes, "", parsed.as_string().strip()
51-
)
56+
parsed_as_string = re.sub(remove_color_codes, "", result.stdout.strip())
5257
return json.loads(parsed_as_string)
5358
except Exception:
5459
logger.debug(
55-
f"JSON parse exception in decoding output from pip \
56-
{action} {package}. Falling back to accumulating the byte stream",
60+
f"JSON parse exception in decoding output from pip {action}"
61+
"{package}. Falling back to accumulating the byte stream",
5762
)
5863
accumulator = {}
64+
parsed = BytesHeaderParser().parsebytes(result.stdout.encode())
5965
for key, value in parsed.items():
6066
accumulator[key] = value
6167
return accumulator
62-
return str(output.decode())
68+
69+
return result.stdout
70+
6371
except subprocess.CalledProcessError as exc:
6472
logger.error(
6573
(
6674
f"Failed to {action} {package}\n"
6775
f"Exit code: {exc.returncode}\n"
68-
f"stdout: {exc.output}"
76+
f"stderr: {(exc.stderr or "").strip()}\n"
77+
f"stdout: {(exc.stdout or "").strip()}"
6978
)
7079
)
71-
sys.exit(1)
80+
# Re-raise the error or handle it accordingly
81+
raise
7282
except Exception as e:
7383
logger.error(
74-
f"An unexpected exception occurred while try to {action} {package}!",
84+
f"An unexpected exception occurred while trying to {action} {package}!",
7585
e,
7686
)
77-
sys.exit(1)
87+
raise
7888

7989

8090
def get_site_packages_location() -> str:

guardrails/hub/install.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from contextlib import contextmanager
2+
import contextlib
23
from string import Template
34
from typing import Callable, cast, List
45

6+
import pkg_resources
7+
58
from guardrails.hub.validator_package_service import (
69
ValidatorPackageService,
710
ValidatorModuleType,
@@ -52,17 +55,21 @@ def install(
5255
5356
Examples:
5457
>>> RegexMatch = install("hub://guardrails/regex_match").RegexMatch
58+
>>> RegexMatch = install("hub://guardrails/regex_match~=1.4").RegexMatch
59+
5560
56-
>>> install("hub://guardrails/regex_match")
57-
>>> import guardrails.hub.regex_match as regex_match
61+
>>> install("hub://guardrails/regex_match>=1.4,==1.*.")
62+
>>> from guardrails.hub.regex_match import RegexMatch
5863
"""
5964

6065
verbose_printer = console.print
6166
quiet_printer = console.print if not quiet else lambda x: None
6267

6368
# 1. Validation
6469
rc_file_exists = RC.exists()
65-
module_name = ValidatorPackageService.get_module_name(package_uri)
70+
validator_id, validator_version = ValidatorPackageService.get_validator_id(
71+
package_uri
72+
)
6673

6774
installing_msg = f"Installing {package_uri}..."
6875
cli_logger.log(
@@ -78,15 +85,15 @@ def install(
7885
fetch_manifest_msg = "Fetching manifest"
7986
with loader(fetch_manifest_msg, spinner="bouncingBar"):
8087
(module_manifest, site_packages) = (
81-
ValidatorPackageService.get_manifest_and_site_packages(module_name)
88+
ValidatorPackageService.get_manifest_and_site_packages(validator_id)
8289
)
8390

8491
# 3. Install - Pip Installation of git module
8592
dl_deps_msg = "Downloading dependencies"
8693
with loader(dl_deps_msg, spinner="bouncingBar"):
8794
ValidatorPackageService.install_hub_module(
88-
module_manifest,
89-
site_packages,
95+
validator_id,
96+
validator_version=validator_version,
9097
quiet=quiet,
9198
upgrade=upgrade,
9299
logger=cli_logger,
@@ -139,7 +146,16 @@ def install(
139146
# Print success messages
140147
cli_logger.info("Installation complete")
141148

142-
verbose_printer(f"✅Successfully installed {module_name}!\n\n")
149+
installed_version_message = ""
150+
with contextlib.suppress(Exception):
151+
package_name = ValidatorPackageService.get_normalized_package_name(validator_id)
152+
installed_version = pkg_resources.get_distribution(package_name).version
153+
if installed_version:
154+
installed_version_message = f" version {installed_version}"
155+
156+
verbose_printer(
157+
f"✅Successfully installed {validator_id}{installed_version_message}!\n\n"
158+
)
143159
success_message_cli = Template(
144160
"[bold]Import validator:[/bold]\n"
145161
"from guardrails.hub import ${export}\n\n"

guardrails/hub/validator_package_service.py

Lines changed: 73 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import importlib
22
import os
33
from pathlib import Path
4+
import re
45
import subprocess
56
import sys
67

7-
from typing import List, Literal
8+
from typing import List, Literal, Optional
89
from types import ModuleType
910
from pydash.strings import snake_case
11+
from packaging.utils import canonicalize_name # PEP 503
1012

11-
from guardrails.classes.generic.stack import Stack
1213
from guardrails.logger import logger as guardrails_logger
1314

1415

1516
from guardrails.cli.hub.utils import pip_process
1617
from guardrails_hub_types import Manifest
1718
from guardrails.cli.server.hub_client import get_validator_manifest
19+
from guardrails.settings import settings
1820

1921

2022
json_format: Literal["json"] = "json"
@@ -91,11 +93,13 @@ def get_validator_from_manifest(manifest: Manifest) -> ModuleType:
9193
Returns:
9294
Any: The Validator class from the installed module
9395
"""
94-
org_package = ValidatorPackageService.get_org_and_package_dirs(manifest)
95-
module_name = manifest.module_name
9696

97-
_relative_path = ".".join([*org_package, module_name])
98-
import_line = f"guardrails.hub.{_relative_path}"
97+
validator_id = manifest.id
98+
import_path = ValidatorPackageService.get_import_path_from_validator_id(
99+
validator_id
100+
)
101+
102+
import_line = f"{import_path}"
99103

100104
# Reload or import the module
101105
return ValidatorPackageService.reload_module(import_line)
@@ -112,14 +116,15 @@ def get_org_and_package_dirs(
112116

113117
@staticmethod
114118
def add_to_hub_inits(manifest: Manifest, site_packages: str):
119+
validator_id = manifest.id
115120
org_package = ValidatorPackageService.get_org_and_package_dirs(manifest)
116121
exports: List[str] = manifest.exports or []
117122
sorted_exports = sorted(exports, reverse=True)
118-
module_name = manifest.module_name
119-
relative_path = ".".join([*org_package, module_name])
120-
import_line = (
121-
f"from guardrails.hub.{relative_path} import {', '.join(sorted_exports)}"
123+
124+
import_path = ValidatorPackageService.get_import_path_from_validator_id(
125+
validator_id
122126
)
127+
import_line = f"from {import_path} import {', '.join(sorted_exports)}"
123128

124129
hub_init_location = os.path.join(
125130
site_packages, "guardrails", "hub", "__init__.py"
@@ -179,14 +184,29 @@ def get_module_path(package_name):
179184
return package_path
180185

181186
@staticmethod
182-
def get_module_name(package_uri: str):
183-
if not package_uri.startswith("hub://"):
187+
def get_validator_id(validator_uri: str):
188+
if not validator_uri.startswith("hub://"):
184189
raise InvalidHubInstallURL(
185190
"Invalid URI! The package URI must start with 'hub://'"
186191
)
187192

188-
module_name = package_uri.replace("hub://", "")
189-
return module_name
193+
validator_uri_with_version = validator_uri.replace("hub://", "")
194+
195+
validator_id_version_regex = (
196+
r"(?P<validator_id>[\/a-zA-Z0-9\-_]+)(?P<version>.*)"
197+
)
198+
match = re.match(validator_id_version_regex, validator_uri_with_version)
199+
validator_version = None
200+
201+
if match:
202+
validator_id = match.group("validator_id")
203+
validator_version = (
204+
match.group("version").strip() if match.group("version") else None
205+
)
206+
else:
207+
validator_id = validator_uri_with_version
208+
209+
return (validator_id, validator_version)
190210

191211
@staticmethod
192212
def get_install_url(manifest: Manifest) -> str:
@@ -207,18 +227,19 @@ def get_install_url(manifest: Manifest) -> str:
207227
def run_post_install(
208228
manifest: Manifest, site_packages: str, logger=guardrails_logger
209229
):
210-
org_package = ValidatorPackageService.get_org_and_package_dirs(manifest)
230+
validator_id = manifest.id
211231
post_install_script = manifest.post_install
232+
212233
if not post_install_script:
213234
return
214235

215-
module_name = manifest.module_name
236+
import_path = ValidatorPackageService.get_import_path_from_validator_id(
237+
validator_id
238+
)
239+
216240
relative_path = os.path.join(
217241
site_packages,
218-
"guardrails",
219-
"hub",
220-
*org_package,
221-
module_name,
242+
import_path,
222243
post_install_script,
223244
)
224245

@@ -255,67 +276,52 @@ def get_hub_directory(manifest: Manifest, site_packages: str) -> str:
255276
org_package = ValidatorPackageService.get_org_and_package_dirs(manifest)
256277
return os.path.join(site_packages, "guardrails", "hub", *org_package)
257278

279+
@staticmethod
280+
def get_normalized_package_name(validator_id: str):
281+
validator_id_parts = validator_id.split("/")
282+
concatanated_package_name = (
283+
f"{validator_id_parts[0]}-grhub-{validator_id_parts[1]}"
284+
)
285+
pep_503_package_name = canonicalize_name(concatanated_package_name)
286+
return pep_503_package_name
287+
288+
@staticmethod
289+
def get_import_path_from_validator_id(validator_id):
290+
pep_503_package_name = ValidatorPackageService.get_normalized_package_name(
291+
validator_id
292+
)
293+
return pep_503_package_name.replace("-", "_")
294+
258295
@staticmethod
259296
def install_hub_module(
260-
module_manifest: Manifest,
261-
site_packages: str,
297+
validator_id: str,
298+
validator_version: Optional[str] = "",
262299
quiet: bool = False,
263300
upgrade: bool = False,
264301
logger=guardrails_logger,
265302
):
266-
install_url = ValidatorPackageService.get_install_url(module_manifest)
267-
install_directory = ValidatorPackageService.get_hub_directory(
268-
module_manifest, site_packages
303+
pep_503_package_name = ValidatorPackageService.get_normalized_package_name(
304+
validator_id
269305
)
306+
validator_version = validator_version if validator_version else ""
307+
full_package_name = f"{pep_503_package_name}{validator_version}"
308+
309+
guardrails_token = settings.rc.token
270310

271-
pip_flags = [f"--target={install_directory}", "--no-deps"]
311+
pip_flags = [
312+
f"--index-url=https://__token__:{guardrails_token}@e4c4zula06.execute-api.us-east-1.amazonaws.com/simple",
313+
"--extra-index-url=https://pypi.org/simple",
314+
]
272315

273316
if upgrade:
274317
pip_flags.append("--upgrade")
275318

276319
if quiet:
277320
pip_flags.append("-q")
278321

279-
# Install validator module in namespaced directory under guardrails.hub
280-
download_output = pip_process("install", install_url, pip_flags, quiet=quiet)
322+
# Install from guardrails hub pypi server with public pypi index as fallback
323+
download_output = pip_process(
324+
"install", full_package_name, pip_flags, quiet=quiet
325+
)
281326
if not quiet:
282327
logger.info(download_output)
283-
284-
# Install validator module's dependencies in normal site-packages directory
285-
inspect_output = pip_process(
286-
"inspect",
287-
flags=[f"--path={install_directory}"],
288-
format=json_format,
289-
quiet=quiet,
290-
no_color=True,
291-
)
292-
293-
# throw if inspect_output is a string. Mostly for pyright
294-
if isinstance(inspect_output, str):
295-
logger.error("Failed to inspect the installed package!")
296-
raise FailedPackageInspection
297-
298-
dependencies = (
299-
Stack(*inspect_output.get("installed", []))
300-
.at(0, {})
301-
.get("metadata", {}) # type: ignore
302-
.get("requires_dist", []) # type: ignore
303-
)
304-
requirements = list(filter(lambda dep: "extra" not in dep, dependencies))
305-
for req in requirements:
306-
if "git+" in req:
307-
install_spec = req.replace(" ", "")
308-
dep_install_output = pip_process("install", install_spec, quiet=quiet)
309-
if not quiet:
310-
logger.info(dep_install_output)
311-
else:
312-
req_info = Stack(*req.split(" "))
313-
name = req_info.at(0, "").strip() # type: ignore
314-
versions = req_info.at(1, "").strip("()") # type: ignore
315-
if name:
316-
install_spec = name if not versions else f"{name}{versions}"
317-
dep_install_output = pip_process(
318-
"install", install_spec, quiet=quiet
319-
)
320-
if not quiet:
321-
logger.info(dep_install_output)

0 commit comments

Comments
 (0)