Source code for imednet.utils.url
"""URL manipulation and sanitization utilities."""
from __future__ import annotations
import re
from typing import Any
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
import httpx
__all__ = ["build_safe_path", "redact_sensitive_text", "redact_url_query", "sanitize_base_url"]
_DUMMY_BASE_URL = "http://dummy/"
[docs]def redact_sensitive_text(text: Any) -> str:
"""Scan and redact sensitive URIs, connection strings, and query parameters in text."""
if not isinstance(text, str):
try:
text_str = str(text)
except Exception:
return "<unprintable object>"
else:
text_str = text
# Redact passwords in connection strings: e.g., mongodb://user:password@host # pragma: allowlist secret
text_str = re.sub(r"(://[^:]+:)[^@]+(@)", r"\g<1>***\g<2>", text_str)
# Redact query parameters in any URLs
def replace_url(match: re.Match[str]) -> str:
"""Redact query parameters in the matched URL string."""
return redact_url_query(match.group(0))
text_str = re.sub(
r'''[a-zA-Z][a-zA-Z0-9+.-]*://[^\s"'>]*[^\s"'>.,;:!?\)\]]''', replace_url, text_str
)
return str(text_str)
[docs]def sanitize_base_url(url: str) -> str:
"""Return base URL without trailing slashes or ``/api`` suffix."""
url = url.rstrip("/")
return str(re.sub(r"/api$", "", url))
[docs]def redact_url_query(url: str, sensitive_params: set[str] | None = None) -> str:
"""Return URL with sensitive query parameters redacted."""
if sensitive_params is None:
sensitive_params = {"api_key", "security_key", "token", "secret", "password"}
parsed = urlparse(url)
query_params = parse_qsl(parsed.query, keep_blank_values=True)
new_query_params = []
for key, value in query_params:
if key in sensitive_params:
new_query_params.append((key, "***"))
else:
new_query_params.append((key, value))
new_query = urlencode(new_query_params, safe="*")
return urlunparse(parsed._replace(query=new_query))
[docs]def build_safe_path(base_path: str, *segments: Any) -> str:
"""Build a normalized relative path using HTTPX URL joining.
Args:
base_path: Base path segment to start from.
*segments: Additional path segments. Non-string values are stringified.
Returns:
A slash-normalized relative path with URL-encoded segments decoded
(for example, ``"a%2Fb"`` becomes ``"a/b"``).
"""
parts: list[str] = []
for segment in (base_path, *segments):
for part in str(segment).split("/"):
if part:
parts.append(part)
if not parts:
return ""
normalized = "/".join(parts)
return str(httpx.URL(_DUMMY_BASE_URL).join(normalized).path.strip("/"))