Skip to content

Commit 9e001ca

Browse files
authored
Merge pull request #446 from plotly/cam/445/add-headers
feat: Allow for request headers to be passed in
2 parents 8c86e4c + 4063040 commit 9e001ca

6 files changed

Lines changed: 98 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## Unreleased
22

3+
### Added
4+
- Allow for request headers to be added to Choreographer calls [[#443](https://github.com/plotly/Kaleido/issues/443)]
5+
36
### Fixed
47
- Fix issue where exporting large figures could cause hang [[#442](https://github.com/plotly/Kaleido/pull/442)], with thanks to @EliasTalcott for the contribution!
58

‎src/py/kaleido/_kaleido_tab/_tab.py‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,20 @@ class _KaleidoTab:
5353
js_logger: _js_logger.JavascriptLogger
5454
"""A log for recording javascript."""
5555

56-
def __init__(self, tab):
56+
def __init__(self, tab, *, headers: dict[str, str] | None = None):
5757
"""
5858
Create a new _KaleidoTab.
5959
6060
Args:
6161
tab: the choreographer tab to wrap.
6262
63+
headers (dict[str, str] | None, optional):
64+
Extra HTTP headers to send with every request made by the
65+
browser tab. Defaults to None.
66+
6367
"""
6468
self.tab = tab
69+
self._headers = headers
6570
self.js_logger = _js_logger.JavascriptLogger(self.tab)
6671

6772
async def navigate(self, url: str | Path = ""):
@@ -79,6 +84,9 @@ async def navigate(self, url: str | Path = ""):
7984
# Subscribe to event indicating page ready.
8085
page_ready = _subscribe_new(self.tab, "Page.loadEventFired")
8186

87+
# Apply headers if they exist
88+
await self._apply_headers()
89+
8290
# Navigating page. This will trigger the above events.
8391
_logger.debug2(f"Calling Page.navigate on {self.tab}")
8492
_raise_error(await self.tab.send_command("Page.navigate", params={"url": url}))
@@ -118,6 +126,19 @@ async def reload(self):
118126

119127
self.js_logger.reset()
120128

129+
async def _apply_headers(self):
130+
"""Apply extra HTTP headers to the tab if configured."""
131+
if self._headers:
132+
_logger.debug(f"Setting extra HTTP headers on {self.tab}")
133+
_logger.debug2(f"Extra headers are: {self._headers}")
134+
_raise_error(await self.tab.send_command("Network.enable"))
135+
_raise_error(
136+
await self.tab.send_command(
137+
"Network.setExtraHTTPHeaders",
138+
params={"headers": self._headers},
139+
)
140+
)
141+
121142
async def _calc_fig(
122143
self,
123144
spec: fig_tools.Spec,

‎src/py/kaleido/kaleido.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,15 @@ class Kaleido(choreo.Browser):
105105

106106
### KALEIDO LIFECYCLE FUNCTIONS ###
107107

108-
def __init__(
108+
def __init__( # noqa: PLR0913
109109
self,
110110
# *args: Any, force named vars for all choreographer passthrough
111111
n: int = 1,
112112
timeout: float | None = 90,
113113
page_generator: None | PageGenerator | str | Path = None,
114114
plotlyjs: str | Path | None = None,
115115
mathjax: str | Path | Literal[False] | None = None,
116+
headers: dict[str, str] | None = None,
116117
**kwargs: Any,
117118
) -> None:
118119
"""
@@ -145,6 +146,12 @@ def __init__(
145146
disabled. Defaults to None- which means to use version 2.35 via
146147
CDN.
147148
149+
headers (dict[str, str] | None, optional):
150+
A dictionary of extra HTTP headers to send with every request
151+
made by the browser (e.g. {"Referer": "https://example.com/"}).
152+
Uses the Chrome DevTools Protocol Network.setExtraHTTPHeaders.
153+
Defaults to None.
154+
148155
**kwargs (Any):
149156
Additional keyword arguments passed through to the underlying
150157
Choreographer.browser constructor. Notable options include
@@ -172,6 +179,7 @@ def __init__(
172179
self._n = n
173180
self._plotlyjs = plotlyjs
174181
self._mathjax = mathjax
182+
self._headers = headers
175183

176184
# Diagnostic
177185
_logger.debug(f"Timeout: {self._timeout}")
@@ -229,7 +237,7 @@ async def _conform_tabs(self, tabs: Listish[choreo.Tab] | None = None) -> None:
229237
_logger.debug2(f"Subscribing * to tab: {tab}.")
230238
tab.subscribe("*", _utils.event_printer(f"tab-{i!s}: Event Dump:"))
231239

232-
kaleido_tabs = [_KaleidoTab(tab) for tab in tabs]
240+
kaleido_tabs = [_KaleidoTab(tab, headers=self._headers) for tab in tabs]
233241

234242
await asyncio.gather(*(tab.navigate(self._index) for tab in kaleido_tabs))
235243

‎src/py/pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ dev = [
5555
"typing-extensions>=4.12.2",
5656
"hypothesis>=6.113.0",
5757
"pyright>=1.1.406",
58+
"ruff",
5859
]
5960

6061
pickles = [

‎src/py/tests/test_kaleido.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,3 +509,38 @@ async def test_plotlyjs_mathjax_injection(plotlyjs, mathjax):
509509
finally:
510510
# Put the tab back in the queue
511511
await k.tabs_ready.put(tab)
512+
513+
514+
async def test_headers_stored_on_tabs():
515+
"""Test that custom headers are passed through to _KaleidoTab instances."""
516+
test_headers = {"Referer": "https://example.com/", "X-Custom": "value"}
517+
518+
async with Kaleido(headers=test_headers, n=1) as k:
519+
tab = await k.tabs_ready.get()
520+
try:
521+
assert tab._headers == test_headers # noqa: SLF001
522+
finally:
523+
await k.tabs_ready.put(tab)
524+
525+
526+
async def test_headers_none_by_default():
527+
"""Test that headers default to None when not specified."""
528+
async with Kaleido(n=1) as k:
529+
tab = await k.tabs_ready.get()
530+
try:
531+
assert tab._headers is None # noqa: SLF001
532+
finally:
533+
await k.tabs_ready.put(tab)
534+
535+
536+
async def test_headers_rendering_works(simple_figure_with_bytes):
537+
"""Test that rendering still works correctly when headers are set."""
538+
test_headers = {"Referer": "https://example.com/"}
539+
540+
async with Kaleido(headers=test_headers) as k:
541+
result = await k.calc_fig(
542+
simple_figure_with_bytes["fig"],
543+
opts=simple_figure_with_bytes["opts"],
544+
)
545+
546+
assert result[:8] == b"\x89PNG\r\n\x1a\n", "Generated data is not a valid PNG"

‎src/py/uv.lock‎

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)