Skip to content

Commit c5fb14a

Browse files
authored
Docker: update video_graphQLQuery shell script to python (#3002)
1 parent 8efe84e commit c5fb14a

File tree

2 files changed

+258
-85
lines changed

2 files changed

+258
-85
lines changed

Video/video_graphQLQuery.py

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#!/usr/bin/env python3
2+
3+
from __future__ import annotations
4+
5+
import base64
6+
import json
7+
import os
8+
import string
9+
import subprocess
10+
import sys
11+
import time
12+
import urllib.error
13+
import urllib.request
14+
from typing import Tuple
15+
16+
from video_gridUrl import get_grid_url
17+
18+
MAX_TIME_SECONDS = 1
19+
RETRY_TIME = 3
20+
21+
22+
def get_graphql_endpoint() -> str:
23+
"""Derive the GraphQL endpoint from env or helper script.
24+
25+
If SE_NODE_GRID_GRAPHQL_URL is set, use it. Otherwise run /opt/bin/video_gridUrl.py
26+
(same as the bash script). Append '/graphql' if missing and non-empty.
27+
"""
28+
endpoint = os.getenv("SE_NODE_GRID_GRAPHQL_URL")
29+
if not endpoint:
30+
endpoint = get_grid_url()
31+
if endpoint and not endpoint.endswith("/graphql"):
32+
endpoint = f"{endpoint}/graphql"
33+
return endpoint
34+
35+
36+
def build_basic_auth_header() -> str | None:
37+
username = os.getenv("SE_ROUTER_USERNAME")
38+
password = os.getenv("SE_ROUTER_PASSWORD")
39+
if username and password:
40+
token = base64.b64encode(f"{username}:{password}".encode()).decode()
41+
return f"Authorization: Basic {token}"
42+
return None
43+
44+
45+
def poll_session(endpoint: str, session_id: str, poll_interval: float) -> dict | None:
46+
"""Poll the GraphQL endpoint for the session.
47+
48+
Returns full parsed response dict if any request succeeded (HTTP 200) else None.
49+
Saves last successful body to /tmp/graphQL_<session_id>.json (for parity).
50+
"""
51+
if not endpoint:
52+
return None
53+
54+
query_obj = {
55+
"query": (
56+
f"{{ session (id: \"{session_id}\") {{ id, capabilities, startTime, uri, nodeId, nodeUri, "
57+
"sessionDurationMillis, slot { id, stereotype, lastStarted } }} }} "
58+
)
59+
}
60+
headers = {
61+
"Content-Type": "application/json",
62+
}
63+
basic_auth_header = build_basic_auth_header()
64+
if basic_auth_header:
65+
# urllib expects header name:value separately; we split at first space after name for compatibility.
66+
# Our header already includes 'Authorization: Basic <token>' so we parse.
67+
name, value = basic_auth_header.split(": ", 1)
68+
headers[name] = value
69+
70+
response_data: dict | None = None
71+
72+
current_check = 1
73+
while True:
74+
data_bytes = json.dumps(query_obj).encode("utf-8")
75+
req = urllib.request.Request(endpoint, data=data_bytes, headers=headers, method="POST")
76+
status_code = None
77+
body_text = ""
78+
try:
79+
with urllib.request.urlopen(req, timeout=MAX_TIME_SECONDS) as resp:
80+
status_code = resp.getcode()
81+
body_text = resp.read().decode("utf-8", errors="replace")
82+
except urllib.error.HTTPError as e: # HTTPError is also a valid response with body
83+
status_code = e.code
84+
try:
85+
body_text = e.read().decode("utf-8", errors="replace")
86+
except Exception:
87+
body_text = ""
88+
except Exception:
89+
# Any other networking issue; proceed to retry logic
90+
status_code = None
91+
92+
if status_code == 200:
93+
try:
94+
response_data = json.loads(body_text)
95+
# Break early if capabilities has se:vncEnabled key
96+
caps_str = response_data.get("data", {}).get("session", {}).get("capabilities")
97+
if isinstance(caps_str, str):
98+
try:
99+
caps_json = json.loads(caps_str)
100+
if "se:vncEnabled" in caps_json:
101+
# Save the body to file for parity then break
102+
_persist_body(session_id, body_text)
103+
break
104+
except Exception:
105+
pass
106+
# Save after each successful 200 (even if not early break) to emulate bash behavior
107+
_persist_body(session_id, body_text)
108+
except Exception:
109+
# Ignore parse errors; continue polling
110+
pass
111+
112+
current_check += 1
113+
if current_check == RETRY_TIME: # Same off-by-one semantics as bash script
114+
break
115+
time.sleep(poll_interval)
116+
117+
return response_data
118+
119+
120+
def _persist_body(session_id: str, body_text: str) -> None:
121+
try:
122+
path = f"/tmp/graphQL_{session_id}.json"
123+
with open(path, "w", encoding="utf-8") as f:
124+
f.write(body_text)
125+
except Exception:
126+
pass # Non-fatal
127+
128+
129+
def extract_capabilities(
130+
session_id: str, video_cap_name: str, test_name_cap: str, video_name_cap: str
131+
) -> Tuple[str | None, str | None, str | None]:
132+
"""Read persisted JSON file and extract capability values.
133+
134+
Returns (record_video_raw, test_name_raw, video_name_raw) which may be None or 'null'.
135+
"""
136+
path = f"/tmp/graphQL_{session_id}.json"
137+
if not os.path.exists(path):
138+
return None, None, None
139+
try:
140+
with open(path, "r", encoding="utf-8") as f:
141+
data = json.load(f)
142+
caps_str = data.get("data", {}).get("session", {}).get("capabilities")
143+
if not isinstance(caps_str, str):
144+
return None, None, None
145+
caps = json.loads(caps_str)
146+
record_video = caps.get(video_cap_name)
147+
test_name = caps.get(test_name_cap)
148+
video_name = caps.get(video_name_cap)
149+
return record_video, test_name, video_name
150+
except Exception:
151+
return None, None, None
152+
153+
154+
def normalize_filename(raw_name: str, session_id: str, suffix_enabled: bool, trim_pattern: str) -> str:
155+
"""Normalize the video file name.
156+
157+
Steps:
158+
- Replace spaces with underscores.
159+
- Keep only allowed characters defined by trim_pattern (default [:alnum:]-_).
160+
- Truncate to max length 251.
161+
- If raw_name empty, return session_id.
162+
- If suffix_enabled and raw_name non-empty, append _<session_id>.
163+
"""
164+
name = (raw_name or "").strip()
165+
if not name:
166+
name = session_id
167+
suffix_applied = False
168+
else:
169+
suffix_applied = suffix_enabled
170+
171+
if suffix_applied:
172+
name = f"{name}_{session_id}"
173+
174+
# Replace spaces
175+
name = name.replace(" ", "_")
176+
177+
allowed_chars = derive_allowed_chars(trim_pattern)
178+
filtered = "".join(ch for ch in name if ch in allowed_chars)
179+
return filtered[:251]
180+
181+
182+
def derive_allowed_chars(pattern: str) -> set[str]:
183+
"""Translate the tr -dc style pattern (very minimally) into a set of allowed characters.
184+
185+
Only special token recognized: [:alnum:]
186+
Other characters are taken literally except [] which are ignored.
187+
"""
188+
if pattern == ":alnum:" or pattern == "[:alnum:]": # convenience
189+
return set(string.ascii_letters + string.digits)
190+
allowed: set[str] = set()
191+
i = 0
192+
while i < len(pattern):
193+
if pattern.startswith("[:alnum:]", i):
194+
allowed.update(string.ascii_letters + string.digits)
195+
i += len("[:alnum:]")
196+
continue
197+
c = pattern[i]
198+
if c not in "[]":
199+
allowed.add(c)
200+
i += 1
201+
# Fallback: if somehow empty, default safe set
202+
return allowed or set(string.ascii_letters + string.digits + "-_")
203+
204+
205+
def main(argv: list[str]) -> int:
206+
if len(argv) < 2:
207+
print("Usage: video_graphQLQuery.py <SESSION_ID>", file=sys.stderr)
208+
return 2
209+
session_id = argv[1]
210+
211+
graphql_endpoint = get_graphql_endpoint()
212+
213+
# Capability names & settings (environment overrides)
214+
video_cap_name = os.getenv("VIDEO_CAP_NAME", "se:recordVideo")
215+
test_name_cap = os.getenv("TEST_NAME_CAP", "se:name")
216+
video_name_cap = os.getenv("VIDEO_NAME_CAP", "se:videoName")
217+
trim_pattern = os.getenv("SE_VIDEO_FILE_NAME_TRIM_REGEX", "[:alnum:]-_")
218+
suffix_flag_raw = os.getenv("SE_VIDEO_FILE_NAME_SUFFIX", "true")
219+
poll_interval_raw = os.getenv("SE_VIDEO_POLL_INTERVAL", "1")
220+
221+
try:
222+
poll_interval = float(poll_interval_raw)
223+
except ValueError:
224+
poll_interval = 1.0
225+
226+
# Poll endpoint to populate /tmp file
227+
poll_session(graphql_endpoint, session_id, poll_interval)
228+
229+
# Extract capabilities
230+
record_video_raw, test_name_raw, video_name_raw = extract_capabilities(
231+
session_id, video_cap_name, test_name_cap, video_name_cap
232+
)
233+
234+
# Determine RECORD_VIDEO value
235+
record_video = True
236+
if isinstance(record_video_raw, str):
237+
if record_video_raw.lower() == "false":
238+
record_video = False
239+
elif record_video_raw is False:
240+
record_video = False
241+
242+
# Decide TEST_NAME referencing precedence (video_name first, then test_name)
243+
chosen_name: str = ""
244+
if video_name_raw not in (None, "null", ""):
245+
chosen_name = str(video_name_raw)
246+
elif test_name_raw not in (None, "null", ""):
247+
chosen_name = str(test_name_raw)
248+
# suffix logic: if chosen_name empty we will receive session id inside normalize_filename
249+
suffix_enabled = suffix_flag_raw.lower() == "true"
250+
normalized_name = normalize_filename(chosen_name, session_id, suffix_enabled, trim_pattern)
251+
252+
# Output matches bash: RECORD_VIDEO TEST_NAME GRAPHQL_ENDPOINT
253+
print(f"{str(record_video).lower()} {normalized_name} {graphql_endpoint}".strip())
254+
return 0
255+
256+
257+
if __name__ == "__main__": # pragma: no cover
258+
sys.exit(main(sys.argv))

Video/video_graphQLQuery.sh

Lines changed: 0 additions & 85 deletions
This file was deleted.

0 commit comments

Comments
 (0)