Coverage for src/qdrant_loader/connectors/confluence/pagination.py: 74%
38 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
1from __future__ import annotations
3import re
4from typing import Any
6_ALLOWED_TOKEN_RE = re.compile(r"^[A-Za-z0-9_-]+$")
8# Full expand used when the page body/content is needed (streaming, fetch_by_id).
9CONTENT_EXPAND = (
10 "body.storage,version,metadata.labels,history,space,extensions.position,"
11 "children.comment.body.storage,ancestors,children.page"
12)
14# Minimal expand used when only enough metadata to filter by labels is needed
15# (e.g. list_entity_ids), avoiding fetching full page bodies.
16LIGHT_EXPAND = "metadata.labels"
19def _quote_cql_literal(value: str) -> str:
20 # Escape backslashes first, then double quotes
21 escaped = value.replace("\\", "\\\\").replace('"', '\\"')
22 return f'"{escaped}"'
25def _sanitize_space_key(space_key: str) -> str:
26 if not _ALLOWED_TOKEN_RE.fullmatch(space_key):
27 raise ValueError(
28 "Invalid Confluence space key. Only alphanumerics, underscore and hyphen are allowed."
29 )
30 return _quote_cql_literal(space_key)
33def _sanitize_content_types(content_types: list[str]) -> list[str]:
34 sanitized: list[str] = []
35 for content_type in content_types:
36 if not isinstance(content_type, str) or not _ALLOWED_TOKEN_RE.fullmatch(
37 content_type
38 ):
39 raise ValueError(f"Invalid Confluence content type: {content_type!r}")
40 sanitized.append(_quote_cql_literal(content_type))
41 return sanitized
44def build_cloud_search_params(
45 space_key: str,
46 content_types: list[str] | None,
47 cursor: str | None,
48 light: bool = False,
49) -> dict[str, Any]:
50 params: dict[str, Any] = {
51 "expand": LIGHT_EXPAND if light else CONTENT_EXPAND,
52 "limit": 25,
53 }
54 cql = f"space = {_sanitize_space_key(space_key)}"
55 if content_types:
56 safe_types = _sanitize_content_types(content_types)
57 cql += f" and type in ({','.join(safe_types)})"
58 params["cql"] = cql
59 if cursor is not None:
60 params["cursor"] = cursor
61 return params
64def build_dc_search_params(
65 space_key: str,
66 content_types: list[str] | None,
67 start: int,
68 light: bool = False,
69) -> dict[str, Any]:
70 params: dict[str, Any] = {
71 "expand": LIGHT_EXPAND if light else CONTENT_EXPAND,
72 "limit": 25,
73 "start": start,
74 }
75 cql = f"space = {_sanitize_space_key(space_key)}"
76 if content_types:
77 safe_types = _sanitize_content_types(content_types)
78 cql += f" and type in ({','.join(safe_types)})"
79 params["cql"] = cql
80 return params