Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Unreleased

### Added
- Allow for request headers to be added to Choreographer calls [[#443](https://github.com/plotly/Kaleido/issues/443)]

### Fixed
- Fix issue where exporting large figures could cause hang [[#442](https://github.com/plotly/Kaleido/pull/442)], with thanks to @EliasTalcott for the contribution!

Expand Down
23 changes: 22 additions & 1 deletion src/py/kaleido/_kaleido_tab/_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,20 @@ class _KaleidoTab:
js_logger: _js_logger.JavascriptLogger
"""A log for recording javascript."""

def __init__(self, tab):
def __init__(self, tab, *, headers: dict[str, str] | None = None):
"""
Create a new _KaleidoTab.

Args:
tab: the choreographer tab to wrap.

headers (dict[str, str] | None, optional):
Extra HTTP headers to send with every request made by the
browser tab. Defaults to None.

"""
self.tab = tab
self._headers = headers
self.js_logger = _js_logger.JavascriptLogger(self.tab)

async def navigate(self, url: str | Path = ""):
Expand Down Expand Up @@ -100,6 +105,8 @@ async def navigate(self, url: str | Path = ""):
# requires a couple extra lines
self.js_logger.reset()

await self._apply_headers()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a potential for a race condition here, where some requests might get sent before the headers are applied? Should _apply_headers() be called earlier in this function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discussed, maybe? Moving this call earlier shouldn't hurt anything, so I'll do that.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@camdecoster Are you still OK with moving this call earlier?

Also, should the headers be applied to the initial call to Page.navigate?


# reload is truly so close to navigate
async def reload(self):
"""Reload the tab, and set the javascript runtime id."""
Expand All @@ -118,6 +125,20 @@ async def reload(self):

self.js_logger.reset()

await self._apply_headers()

async def _apply_headers(self):
"""Apply extra HTTP headers to the tab if configured."""
if self._headers:
_logger.debug2(f"Setting extra HTTP headers on {self.tab}")
_raise_error(await self.tab.send_command("Network.enable"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this "Network.enable" needed for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That needs to be called first so that the call to Network.setExtraHTTPHeaders will succeed.

_raise_error(
await self.tab.send_command(
"Network.setExtraHTTPHeaders",
params={"headers": self._headers},
)
)

async def _calc_fig(
self,
spec: fig_tools.Spec,
Expand Down
12 changes: 10 additions & 2 deletions src/py/kaleido/kaleido.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,15 @@ class Kaleido(choreo.Browser):

### KALEIDO LIFECYCLE FUNCTIONS ###

def __init__(
def __init__( # noqa: PLR0913
self,
# *args: Any, force named vars for all choreographer passthrough
n: int = 1,
timeout: float | None = 90,
page_generator: None | PageGenerator | str | Path = None,
plotlyjs: str | Path | None = None,
mathjax: str | Path | Literal[False] | None = None,
headers: dict[str, str] | None = None,
**kwargs: Any,
) -> None:
"""
Expand Down Expand Up @@ -145,6 +146,12 @@ def __init__(
disabled. Defaults to None- which means to use version 2.35 via
CDN.

headers (dict[str, str] | None, optional):
A dictionary of extra HTTP headers to send with every request
made by the browser (e.g. {"Referer": "https://example.com/"}).
Uses the Chrome DevTools Protocol Network.setExtraHTTPHeaders.
Defaults to None.

**kwargs (Any):
Additional keyword arguments passed through to the underlying
Choreographer.browser constructor. Notable options include
Expand Down Expand Up @@ -172,6 +179,7 @@ def __init__(
self._n = n
self._plotlyjs = plotlyjs
self._mathjax = mathjax
self._headers = headers

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

kaleido_tabs = [_KaleidoTab(tab) for tab in tabs]
kaleido_tabs = [_KaleidoTab(tab, headers=self._headers) for tab in tabs]

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

Expand Down
1 change: 1 addition & 0 deletions src/py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ dev = [
"typing-extensions>=4.12.2",
"hypothesis>=6.113.0",
"pyright>=1.1.406",
"ruff",
]

pickles = [
Expand Down
35 changes: 35 additions & 0 deletions src/py/tests/test_kaleido.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,3 +509,38 @@ async def test_plotlyjs_mathjax_injection(plotlyjs, mathjax):
finally:
# Put the tab back in the queue
await k.tabs_ready.put(tab)


async def test_headers_stored_on_tabs():
"""Test that custom headers are passed through to _KaleidoTab instances."""
test_headers = {"Referer": "https://example.com/", "X-Custom": "value"}

async with Kaleido(headers=test_headers, n=1) as k:
tab = await k.tabs_ready.get()
try:
assert tab._headers == test_headers # noqa: SLF001
finally:
await k.tabs_ready.put(tab)


async def test_headers_none_by_default():
"""Test that headers default to None when not specified."""
async with Kaleido(n=1) as k:
tab = await k.tabs_ready.get()
try:
assert tab._headers is None # noqa: SLF001
finally:
await k.tabs_ready.put(tab)


async def test_headers_rendering_works(simple_figure_with_bytes):
"""Test that rendering still works correctly when headers are set."""
test_headers = {"Referer": "https://example.com/"}

async with Kaleido(headers=test_headers) as k:
result = await k.calc_fig(
simple_figure_with_bytes["fig"],
opts=simple_figure_with_bytes["opts"],
)

assert result[:8] == b"\x89PNG\r\n\x1a\n", "Generated data is not a valid PNG"
27 changes: 27 additions & 0 deletions src/py/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading