|
21 | 21 | cast, |
22 | 22 | ) |
23 | 23 |
|
| 24 | +import yaml |
| 25 | +from urllib.parse import urlparse, urlunparse |
| 26 | + |
24 | 27 | import redis.exceptions |
25 | 28 |
|
26 | 29 | # Add missing imports |
|
107 | 110 | _HYBRID_SEARCH_ERROR_MESSAGE = "Hybrid search is not available in this version of redis-py. Please upgrade to redis-py >= 7.1.0." |
108 | 111 |
|
109 | 112 |
|
| 113 | +def _sanitize_redis_url(url: Optional[str]) -> Optional[str]: |
| 114 | + """Remove password from Redis URL for safe serialization. |
| 115 | +
|
| 116 | + Args: |
| 117 | + url: Redis URL that may contain a password. |
| 118 | +
|
| 119 | + Returns: |
| 120 | + URL with password replaced by '***', or None if url is None. |
| 121 | +
|
| 122 | + Example: |
| 123 | + >>> _sanitize_redis_url("redis://:secret@localhost:6379") |
| 124 | + 'redis://***@localhost:6379' |
| 125 | + """ |
| 126 | + if not url: |
| 127 | + return None |
| 128 | + try: |
| 129 | + parsed = urlparse(url) |
| 130 | + if parsed.password: |
| 131 | + # Replace password with *** but keep username if present |
| 132 | + if parsed.username: |
| 133 | + netloc = f"{parsed.username}:***@{parsed.hostname}" |
| 134 | + else: |
| 135 | + netloc = f"***@{parsed.hostname}" |
| 136 | + if parsed.port: |
| 137 | + netloc = f"{netloc}:{parsed.port}" |
| 138 | + return urlunparse(parsed._replace(netloc=netloc)) |
| 139 | + return url |
| 140 | + except Exception: |
| 141 | + # If parsing fails, return a safe placeholder |
| 142 | + return "***" |
| 143 | + |
| 144 | + |
110 | 145 | REQUIRED_MODULES_FOR_INTROSPECTION = [ |
111 | 146 | {"name": "search", "ver": 20810}, |
112 | 147 | {"name": "searchlight", "ver": 20810}, |
@@ -400,6 +435,60 @@ def key(self, id: str) -> str: |
400 | 435 | key_separator=self.schema.index.key_separator, |
401 | 436 | ) |
402 | 437 |
|
| 438 | + def to_dict(self, include_connection: bool = False) -> Dict[str, Any]: |
| 439 | + """Serialize the index configuration to a dictionary. |
| 440 | +
|
| 441 | + Args: |
| 442 | + include_connection (bool, optional): Whether to include connection |
| 443 | + parameters. Defaults to False for security (passwords/URLs |
| 444 | + are excluded by default). |
| 445 | +
|
| 446 | + Returns: |
| 447 | + Dict[str, Any]: Dictionary representation of the index configuration. |
| 448 | +
|
| 449 | + Example: |
| 450 | + >>> config = index.to_dict() |
| 451 | + >>> new_index = SearchIndex.from_dict(config) |
| 452 | + """ |
| 453 | + config = self.schema.to_dict() |
| 454 | + if include_connection: |
| 455 | + # Sanitize the Redis URL to remove password before serialization |
| 456 | + sanitized_url = _sanitize_redis_url(self._redis_url) |
| 457 | + if sanitized_url is not None: |
| 458 | + config["_redis_url"] = sanitized_url |
| 459 | + # Note: connection_kwargs may contain sensitive info |
| 460 | + # Only include non-sensitive keys |
| 461 | + safe_keys = {"decode_responses", "ssl", "socket_timeout", "socket_connect_timeout"} |
| 462 | + connection_kwargs = getattr(self, "_connection_kwargs", {}) or {} |
| 463 | + config["_connection_kwargs"] = { |
| 464 | + k: v for k, v in connection_kwargs.items() |
| 465 | + if k in safe_keys |
| 466 | + } |
| 467 | + return config |
| 468 | + |
| 469 | + def to_yaml(self, path: str, include_connection: bool = False, overwrite: bool = True) -> None: |
| 470 | + """Serialize the index configuration to a YAML file. |
| 471 | +
|
| 472 | + Args: |
| 473 | + path (str): Path to write the YAML file. |
| 474 | + include_connection (bool, optional): Whether to include connection |
| 475 | + parameters. Defaults to False for security. |
| 476 | + overwrite (bool, optional): Whether to overwrite existing file. |
| 477 | + Defaults to True. If False and file exists, raises FileExistsError. |
| 478 | +
|
| 479 | + Example: |
| 480 | + >>> index.to_yaml("schemas/my_index.yaml") |
| 481 | +
|
| 482 | + Raises: |
| 483 | + FileExistsError: If overwrite=False and file already exists. |
| 484 | + """ |
| 485 | + import os |
| 486 | + if not overwrite and os.path.exists(path): |
| 487 | + raise FileExistsError(f"File already exists: {path}") |
| 488 | + config = self.to_dict(include_connection=include_connection) |
| 489 | + with open(path, "w") as f: |
| 490 | + yaml.dump(config, f, default_flow_style=False, sort_keys=False) |
| 491 | + |
403 | 492 |
|
404 | 493 | class SearchIndex(BaseSearchIndex): |
405 | 494 | """A search index class for interacting with Redis as a vector database. |
@@ -1294,49 +1383,6 @@ def info(self, name: Optional[str] = None) -> Dict[str, Any]: |
1294 | 1383 | index_name = name or self.schema.index.name |
1295 | 1384 | return self._info(index_name, self._redis_client) |
1296 | 1385 |
|
1297 | | - def to_dict(self, include_connection: bool = False) -> Dict[str, Any]: |
1298 | | - """Serialize the index configuration to a dictionary. |
1299 | | -
|
1300 | | - Args: |
1301 | | - include_connection (bool, optional): Whether to include connection |
1302 | | - parameters. Defaults to False for security (passwords/URLs |
1303 | | - are excluded by default). |
1304 | | -
|
1305 | | - Returns: |
1306 | | - Dict[str, Any]: Dictionary representation of the index configuration. |
1307 | | -
|
1308 | | - Example: |
1309 | | - >>> config = index.to_dict() |
1310 | | - >>> new_index = SearchIndex.from_dict(config) |
1311 | | - """ |
1312 | | - config = self.schema.to_dict() |
1313 | | - if include_connection: |
1314 | | - config["_redis_url"] = self._redis_url |
1315 | | - # Note: connection_kwargs may contain sensitive info |
1316 | | - # Only include non-sensitive keys |
1317 | | - safe_keys = {"decode_responses", "ssl", "socket_timeout", "socket_connect_timeout"} |
1318 | | - config["_connection_kwargs"] = { |
1319 | | - k: v for k, v in self._connection_kwargs.items() |
1320 | | - if k in safe_keys |
1321 | | - } |
1322 | | - return config |
1323 | | - |
1324 | | - def to_yaml(self, path: str, include_connection: bool = False) -> None: |
1325 | | - """Serialize the index configuration to a YAML file. |
1326 | | -
|
1327 | | - Args: |
1328 | | - path (str): Path to write the YAML file. |
1329 | | - include_connection (bool, optional): Whether to include connection |
1330 | | - parameters. Defaults to False for security. |
1331 | | -
|
1332 | | - Example: |
1333 | | - >>> index.to_yaml("schemas/my_index.yaml") |
1334 | | - """ |
1335 | | - import yaml |
1336 | | - config = self.to_dict(include_connection=include_connection) |
1337 | | - with open(path, "w") as f: |
1338 | | - yaml.dump(config, f, default_flow_style=False, sort_keys=False) |
1339 | | - |
1340 | 1386 | def __enter__(self): |
1341 | 1387 | return self |
1342 | 1388 |
|
@@ -2294,49 +2340,6 @@ def disconnect_sync(self): |
2294 | 2340 | return |
2295 | 2341 | sync_wrapper(self.disconnect)() |
2296 | 2342 |
|
2297 | | - def to_dict(self, include_connection: bool = False) -> Dict[str, Any]: |
2298 | | - """Serialize the index configuration to a dictionary. |
2299 | | -
|
2300 | | - Args: |
2301 | | - include_connection (bool, optional): Whether to include connection |
2302 | | - parameters. Defaults to False for security (passwords/URLs |
2303 | | - are excluded by default). |
2304 | | -
|
2305 | | - Returns: |
2306 | | - Dict[str, Any]: Dictionary representation of the index configuration. |
2307 | | -
|
2308 | | - Example: |
2309 | | - >>> config = index.to_dict() |
2310 | | - >>> new_index = AsyncSearchIndex.from_dict(config) |
2311 | | - """ |
2312 | | - config = self.schema.to_dict() |
2313 | | - if include_connection: |
2314 | | - config["_redis_url"] = self._redis_url |
2315 | | - # Note: connection_kwargs may contain sensitive info |
2316 | | - # Only include non-sensitive keys |
2317 | | - safe_keys = {"decode_responses", "ssl", "socket_timeout", "socket_connect_timeout"} |
2318 | | - config["_connection_kwargs"] = { |
2319 | | - k: v for k, v in self._connection_kwargs.items() |
2320 | | - if k in safe_keys |
2321 | | - } |
2322 | | - return config |
2323 | | - |
2324 | | - def to_yaml(self, path: str, include_connection: bool = False) -> None: |
2325 | | - """Serialize the index configuration to a YAML file. |
2326 | | -
|
2327 | | - Args: |
2328 | | - path (str): Path to write the YAML file. |
2329 | | - include_connection (bool, optional): Whether to include connection |
2330 | | - parameters. Defaults to False for security. |
2331 | | -
|
2332 | | - Example: |
2333 | | - >>> await index.to_yaml("schemas/my_index.yaml") |
2334 | | - """ |
2335 | | - import yaml |
2336 | | - config = self.to_dict(include_connection=include_connection) |
2337 | | - with open(path, "w") as f: |
2338 | | - yaml.dump(config, f, default_flow_style=False, sort_keys=False) |
2339 | | - |
2340 | 2343 | async def __aenter__(self): |
2341 | 2344 | return self |
2342 | 2345 |
|
|
0 commit comments