Skip to content
Merged
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
36 changes: 28 additions & 8 deletions py/src/braintrust/api/_transport.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@
BraintrustTransportError,
BraintrustTransportRetryExhaustedError,
)
from .policies import RetryMode, RetryPolicy
from .policies import RetryMode, RetryPolicy, is_retryable_request_exception


logger = logging.getLogger(__name__)
Expand DownExpand Up@@ -102,6 +102,9 @@ def __init__(self, base_url: str, adapter: HTTPAdapter | None = None):
self.base_url = base_url
self.token = None
self.adapter = adapter
# An adapter handed to us belongs to the caller. `set_http_adapter` installs
# one instance across every connection, so we must not close it.
self._injected_adapter = adapter

self._reset(total=0)

Expand All@@ -120,6 +123,10 @@ def make_long_lived(self) -> None:
)
self._reset()

def close(self) -> None:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

@staticmethod
def sanitize_token(token: str) -> str:
return token.rstrip("\n")
Expand All@@ -131,6 +138,7 @@ def set_token(self, token: str) -> None:

def _set_adapter(self, adapter: HTTPAdapter | None) -> None:
self.adapter = adapter
self._injected_adapter = adapter

def _reset(self, **retry_kwargs: Any) -> None:
self.session = requests.Session()
Expand DownExpand Up@@ -202,6 +210,7 @@ def __init__(
):
custom_transport = session is not None or adapter is not None
self._owns_session = session is None
self._injected_adapter = adapter
self.session = session if session is not None else requests.Session()
if not persist_cookies and self._owns_session:
self.session.cookies.set_policy(_RejectCookiesPolicy())
Expand All@@ -215,6 +224,7 @@ def __init__(

def close(self) -> None:
if self._owns_session:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

def __enter__(self) -> "Transport":
Expand DownExpand Up@@ -272,7 +282,7 @@ def request(
**kwargs,
)
except requests.exceptions.RequestException as exc:
if not _is_retryable_request_exception(exc):
if not is_retryable_request_exception(exc):
error = BraintrustTransportError(method=method, url=url, attempts=attempt, retryable=False)
raise error from exc
if attempt >= max_attempts:
Expand DownExpand Up@@ -392,14 +402,24 @@ def _retry_delay(policy: RetryPolicy, attempt: int, retry_after: float | None) -
return min(policy.max_backoff, policy.backoff_factor * (2 ** (attempt - 1)))


def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))
def _unmount_adapter(session: requests.Session, adapter: HTTPAdapter | None) -> None:
"""Detach a caller-owned adapter so ``Session.close()`` leaves it open.

``requests.Session.close()`` closes every mounted adapter. A single adapter
installed via ``set_http_adapter`` is mounted on many sessions at once, so
closing one session would otherwise clear the connection pools that the
other sessions are still using.
"""

if adapter is None:
return
for prefix, mounted in list(session.adapters.items()):
if mounted is adapter:
del session.adapters[prefix]

def _is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)

def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))


def _parse_retry_after(value: str | None, wall_time: float) -> float | None:
Expand Down
9 changes: 9 additions & 0 deletions py/src/braintrust/api/policies.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@
import enum
from dataclasses import dataclass

import requests


DEFAULT_RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
DEFAULT_MAX_ATTEMPTS = 4
Expand All@@ -11,6 +13,13 @@
DEFAULT_MAX_BACKOFF = 10.0


def is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
"""Return whether a requests transport failure is safe to retry."""
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)


class RetryMode(enum.Enum):
"""The replay safety classification for an API operation."""

Expand Down
62 changes: 57 additions & 5 deletions py/src/braintrust/api/test_transport.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import datetime
import io
from email.utils import format_datetime
from unittest import mock

import pytest
import requests
Expand All@@ -14,7 +15,7 @@
RetryPolicy,
)
from braintrust.api._test_server import scripted_server
from braintrust.api._transport import Transport
from braintrust.api._transport import HTTPConnection, Transport
from braintrust.util import AugmentedHTTPError
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Expand DownExpand Up@@ -68,13 +69,17 @@ def close(self):
super().close()


def test_transport_closes_owned_session():
def test_transport_closes_owned_session_without_closing_injected_adapter():
adapter = TrackingAdapter()
transport = Transport(adapter=adapter)
session = transport.session

with Transport(adapter=adapter) as transport:
assert transport.session is not None
with mock.patch.object(session, "close", wraps=session.close) as close_spy:
transport.close()

assert adapter.close_count > 0
close_spy.assert_called_once()
# The adapter belongs to the caller and may be mounted on other sessions.
assert adapter.close_count == 0


def test_transport_does_not_close_injected_session():
Expand DownExpand Up@@ -351,3 +356,50 @@ def test_non_retrying_custom_adapter_can_delegate_retries_to_sdk():

assert response.status_code == 200
assert handler.request_count == 2


def test_http_connection_close_does_not_close_shared_adapter():
adapter = TrackingAdapter()
first = HTTPConnection("http://localhost", adapter=adapter)
second = HTTPConnection("http://localhost", adapter=adapter)

first.close()

assert adapter.close_count == 0
assert second.session.get_adapter("http://localhost") is adapter


def test_http_connection_close_closes_self_created_long_lived_adapter():
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
adapter = conn.adapter
assert adapter is not None

with mock.patch.object(adapter, "close", wraps=adapter.close) as close_spy:
conn.close()

# Mounted on both the http:// and https:// prefixes, so closed once per mount.
assert close_spy.call_count > 0


def test_http_connection_close_closes_long_lived_adapter_replaced_by_set_adapter():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0


def test_http_connection_close_does_not_close_adapter_set_after_construction():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix: Use apiKey in loadPrompt by AbhiPrasad · Pull Request #718 · braintrustdata/braintrust-sdk-python · GitHub
Skip to content
Merged
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
36 changes: 28 additions & 8 deletions py/src/braintrust/api/_transport.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@
BraintrustTransportError,
BraintrustTransportRetryExhaustedError,
)
from .policies import RetryMode, RetryPolicy
from .policies import RetryMode, RetryPolicy, is_retryable_request_exception


logger = logging.getLogger(__name__)
Expand DownExpand Up@@ -102,6 +102,9 @@ def __init__(self, base_url: str, adapter: HTTPAdapter | None = None):
self.base_url = base_url
self.token = None
self.adapter = adapter
# An adapter handed to us belongs to the caller. `set_http_adapter` installs
# one instance across every connection, so we must not close it.
self._injected_adapter = adapter

self._reset(total=0)

Expand All@@ -120,6 +123,10 @@ def make_long_lived(self) -> None:
)
self._reset()

def close(self) -> None:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

@staticmethod
def sanitize_token(token: str) -> str:
return token.rstrip("\n")
Expand All@@ -131,6 +138,7 @@ def set_token(self, token: str) -> None:

def _set_adapter(self, adapter: HTTPAdapter | None) -> None:
self.adapter = adapter
self._injected_adapter = adapter

def _reset(self, **retry_kwargs: Any) -> None:
self.session = requests.Session()
Expand DownExpand Up@@ -202,6 +210,7 @@ def __init__(
):
custom_transport = session is not None or adapter is not None
self._owns_session = session is None
self._injected_adapter = adapter
self.session = session if session is not None else requests.Session()
if not persist_cookies and self._owns_session:
self.session.cookies.set_policy(_RejectCookiesPolicy())
Expand All@@ -215,6 +224,7 @@ def __init__(

def close(self) -> None:
if self._owns_session:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

def __enter__(self) -> "Transport":
Expand DownExpand Up@@ -272,7 +282,7 @@ def request(
**kwargs,
)
except requests.exceptions.RequestException as exc:
if not _is_retryable_request_exception(exc):
if not is_retryable_request_exception(exc):
error = BraintrustTransportError(method=method, url=url, attempts=attempt, retryable=False)
raise error from exc
if attempt >= max_attempts:
Expand DownExpand Up@@ -392,14 +402,24 @@ def _retry_delay(policy: RetryPolicy, attempt: int, retry_after: float | None) -
return min(policy.max_backoff, policy.backoff_factor * (2 ** (attempt - 1)))


def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))
def _unmount_adapter(session: requests.Session, adapter: HTTPAdapter | None) -> None:
"""Detach a caller-owned adapter so ``Session.close()`` leaves it open.

``requests.Session.close()`` closes every mounted adapter. A single adapter
installed via ``set_http_adapter`` is mounted on many sessions at once, so
closing one session would otherwise clear the connection pools that the
other sessions are still using.
"""

if adapter is None:
return
for prefix, mounted in list(session.adapters.items()):
if mounted is adapter:
del session.adapters[prefix]

def _is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)

def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))


def _parse_retry_after(value: str | None, wall_time: float) -> float | None:
Expand Down
9 changes: 9 additions & 0 deletions py/src/braintrust/api/policies.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@
import enum
from dataclasses import dataclass

import requests


DEFAULT_RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
DEFAULT_MAX_ATTEMPTS = 4
Expand All@@ -11,6 +13,13 @@
DEFAULT_MAX_BACKOFF = 10.0


def is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
"""Return whether a requests transport failure is safe to retry."""
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)


class RetryMode(enum.Enum):
"""The replay safety classification for an API operation."""

Expand Down
62 changes: 57 additions & 5 deletions py/src/braintrust/api/test_transport.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import datetime
import io
from email.utils import format_datetime
from unittest import mock

import pytest
import requests
Expand All@@ -14,7 +15,7 @@
RetryPolicy,
)
from braintrust.api._test_server import scripted_server
from braintrust.api._transport import Transport
from braintrust.api._transport import HTTPConnection, Transport
from braintrust.util import AugmentedHTTPError
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Expand DownExpand Up@@ -68,13 +69,17 @@ def close(self):
super().close()


def test_transport_closes_owned_session():
def test_transport_closes_owned_session_without_closing_injected_adapter():
adapter = TrackingAdapter()
transport = Transport(adapter=adapter)
session = transport.session

with Transport(adapter=adapter) as transport:
assert transport.session is not None
with mock.patch.object(session, "close", wraps=session.close) as close_spy:
transport.close()

assert adapter.close_count > 0
close_spy.assert_called_once()
# The adapter belongs to the caller and may be mounted on other sessions.
assert adapter.close_count == 0


def test_transport_does_not_close_injected_session():
Expand DownExpand Up@@ -351,3 +356,50 @@ def test_non_retrying_custom_adapter_can_delegate_retries_to_sdk():

assert response.status_code == 200
assert handler.request_count == 2


def test_http_connection_close_does_not_close_shared_adapter():
adapter = TrackingAdapter()
first = HTTPConnection("http://localhost", adapter=adapter)
second = HTTPConnection("http://localhost", adapter=adapter)

first.close()

assert adapter.close_count == 0
assert second.session.get_adapter("http://localhost") is adapter


def test_http_connection_close_closes_self_created_long_lived_adapter():
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
adapter = conn.adapter
assert adapter is not None

with mock.patch.object(adapter, "close", wraps=adapter.close) as close_spy:
conn.close()

# Mounted on both the http:// and https:// prefixes, so closed once per mount.
assert close_spy.call_count > 0


def test_http_connection_close_closes_long_lived_adapter_replaced_by_set_adapter():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0


def test_http_connection_close_does_not_close_adapter_set_after_construction():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: Use apiKey in loadPrompt by AbhiPrasad · Pull Request #718 · braintrustdata/braintrust-sdk-python · GitHub
Skip to content
Merged
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
36 changes: 28 additions & 8 deletions py/src/braintrust/api/_transport.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@
BraintrustTransportError,
BraintrustTransportRetryExhaustedError,
)
from .policies import RetryMode, RetryPolicy
from .policies import RetryMode, RetryPolicy, is_retryable_request_exception


logger = logging.getLogger(__name__)
Expand DownExpand Up@@ -102,6 +102,9 @@ def __init__(self, base_url: str, adapter: HTTPAdapter | None = None):
self.base_url = base_url
self.token = None
self.adapter = adapter
# An adapter handed to us belongs to the caller. `set_http_adapter` installs
# one instance across every connection, so we must not close it.
self._injected_adapter = adapter

self._reset(total=0)

Expand All@@ -120,6 +123,10 @@ def make_long_lived(self) -> None:
)
self._reset()

def close(self) -> None:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

@staticmethod
def sanitize_token(token: str) -> str:
return token.rstrip("\n")
Expand All@@ -131,6 +138,7 @@ def set_token(self, token: str) -> None:

def _set_adapter(self, adapter: HTTPAdapter | None) -> None:
self.adapter = adapter
self._injected_adapter = adapter

def _reset(self, **retry_kwargs: Any) -> None:
self.session = requests.Session()
Expand DownExpand Up@@ -202,6 +210,7 @@ def __init__(
):
custom_transport = session is not None or adapter is not None
self._owns_session = session is None
self._injected_adapter = adapter
self.session = session if session is not None else requests.Session()
if not persist_cookies and self._owns_session:
self.session.cookies.set_policy(_RejectCookiesPolicy())
Expand All@@ -215,6 +224,7 @@ def __init__(

def close(self) -> None:
if self._owns_session:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

def __enter__(self) -> "Transport":
Expand DownExpand Up@@ -272,7 +282,7 @@ def request(
**kwargs,
)
except requests.exceptions.RequestException as exc:
if not _is_retryable_request_exception(exc):
if not is_retryable_request_exception(exc):
error = BraintrustTransportError(method=method, url=url, attempts=attempt, retryable=False)
raise error from exc
if attempt >= max_attempts:
Expand DownExpand Up@@ -392,14 +402,24 @@ def _retry_delay(policy: RetryPolicy, attempt: int, retry_after: float | None) -
return min(policy.max_backoff, policy.backoff_factor * (2 ** (attempt - 1)))


def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))
def _unmount_adapter(session: requests.Session, adapter: HTTPAdapter | None) -> None:
"""Detach a caller-owned adapter so ``Session.close()`` leaves it open.

``requests.Session.close()`` closes every mounted adapter. A single adapter
installed via ``set_http_adapter`` is mounted on many sessions at once, so
closing one session would otherwise clear the connection pools that the
other sessions are still using.
"""

if adapter is None:
return
for prefix, mounted in list(session.adapters.items()):
if mounted is adapter:
del session.adapters[prefix]

def _is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)

def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))


def _parse_retry_after(value: str | None, wall_time: float) -> float | None:
Expand Down
9 changes: 9 additions & 0 deletions py/src/braintrust/api/policies.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@
import enum
from dataclasses import dataclass

import requests


DEFAULT_RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
DEFAULT_MAX_ATTEMPTS = 4
Expand All@@ -11,6 +13,13 @@
DEFAULT_MAX_BACKOFF = 10.0


def is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
"""Return whether a requests transport failure is safe to retry."""
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)


class RetryMode(enum.Enum):
"""The replay safety classification for an API operation."""

Expand Down
62 changes: 57 additions & 5 deletions py/src/braintrust/api/test_transport.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import datetime
import io
from email.utils import format_datetime
from unittest import mock

import pytest
import requests
Expand All@@ -14,7 +15,7 @@
RetryPolicy,
)
from braintrust.api._test_server import scripted_server
from braintrust.api._transport import Transport
from braintrust.api._transport import HTTPConnection, Transport
from braintrust.util import AugmentedHTTPError
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Expand DownExpand Up@@ -68,13 +69,17 @@ def close(self):
super().close()


def test_transport_closes_owned_session():
def test_transport_closes_owned_session_without_closing_injected_adapter():
adapter = TrackingAdapter()
transport = Transport(adapter=adapter)
session = transport.session

with Transport(adapter=adapter) as transport:
assert transport.session is not None
with mock.patch.object(session, "close", wraps=session.close) as close_spy:
transport.close()

assert adapter.close_count > 0
close_spy.assert_called_once()
# The adapter belongs to the caller and may be mounted on other sessions.
assert adapter.close_count == 0


def test_transport_does_not_close_injected_session():
Expand DownExpand Up@@ -351,3 +356,50 @@ def test_non_retrying_custom_adapter_can_delegate_retries_to_sdk():

assert response.status_code == 200
assert handler.request_count == 2


def test_http_connection_close_does_not_close_shared_adapter():
adapter = TrackingAdapter()
first = HTTPConnection("http://localhost", adapter=adapter)
second = HTTPConnection("http://localhost", adapter=adapter)

first.close()

assert adapter.close_count == 0
assert second.session.get_adapter("http://localhost") is adapter


def test_http_connection_close_closes_self_created_long_lived_adapter():
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
adapter = conn.adapter
assert adapter is not None

with mock.patch.object(adapter, "close", wraps=adapter.close) as close_spy:
conn.close()

# Mounted on both the http:// and https:// prefixes, so closed once per mount.
assert close_spy.call_count > 0


def test_http_connection_close_closes_long_lived_adapter_replaced_by_set_adapter():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0


def test_http_connection_close_does_not_close_adapter_set_after_construction():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: Use apiKey in loadPrompt by AbhiPrasad · Pull Request #718 · braintrustdata/braintrust-sdk-python · GitHub
Skip to content
Merged
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
36 changes: 28 additions & 8 deletions py/src/braintrust/api/_transport.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@
BraintrustTransportError,
BraintrustTransportRetryExhaustedError,
)
from .policies import RetryMode, RetryPolicy
from .policies import RetryMode, RetryPolicy, is_retryable_request_exception


logger = logging.getLogger(__name__)
Expand DownExpand Up@@ -102,6 +102,9 @@ def __init__(self, base_url: str, adapter: HTTPAdapter | None = None):
self.base_url = base_url
self.token = None
self.adapter = adapter
# An adapter handed to us belongs to the caller. `set_http_adapter` installs
# one instance across every connection, so we must not close it.
self._injected_adapter = adapter

self._reset(total=0)

Expand All@@ -120,6 +123,10 @@ def make_long_lived(self) -> None:
)
self._reset()

def close(self) -> None:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

@staticmethod
def sanitize_token(token: str) -> str:
return token.rstrip("\n")
Expand All@@ -131,6 +138,7 @@ def set_token(self, token: str) -> None:

def _set_adapter(self, adapter: HTTPAdapter | None) -> None:
self.adapter = adapter
self._injected_adapter = adapter

def _reset(self, **retry_kwargs: Any) -> None:
self.session = requests.Session()
Expand DownExpand Up@@ -202,6 +210,7 @@ def __init__(
):
custom_transport = session is not None or adapter is not None
self._owns_session = session is None
self._injected_adapter = adapter
self.session = session if session is not None else requests.Session()
if not persist_cookies and self._owns_session:
self.session.cookies.set_policy(_RejectCookiesPolicy())
Expand All@@ -215,6 +224,7 @@ def __init__(

def close(self) -> None:
if self._owns_session:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

def __enter__(self) -> "Transport":
Expand DownExpand Up@@ -272,7 +282,7 @@ def request(
**kwargs,
)
except requests.exceptions.RequestException as exc:
if not _is_retryable_request_exception(exc):
if not is_retryable_request_exception(exc):
error = BraintrustTransportError(method=method, url=url, attempts=attempt, retryable=False)
raise error from exc
if attempt >= max_attempts:
Expand DownExpand Up@@ -392,14 +402,24 @@ def _retry_delay(policy: RetryPolicy, attempt: int, retry_after: float | None) -
return min(policy.max_backoff, policy.backoff_factor * (2 ** (attempt - 1)))


def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))
def _unmount_adapter(session: requests.Session, adapter: HTTPAdapter | None) -> None:
"""Detach a caller-owned adapter so ``Session.close()`` leaves it open.

``requests.Session.close()`` closes every mounted adapter. A single adapter
installed via ``set_http_adapter`` is mounted on many sessions at once, so
closing one session would otherwise clear the connection pools that the
other sessions are still using.
"""

if adapter is None:
return
for prefix, mounted in list(session.adapters.items()):
if mounted is adapter:
del session.adapters[prefix]

def _is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)

def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))


def _parse_retry_after(value: str | None, wall_time: float) -> float | None:
Expand Down
9 changes: 9 additions & 0 deletions py/src/braintrust/api/policies.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@
import enum
from dataclasses import dataclass

import requests


DEFAULT_RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
DEFAULT_MAX_ATTEMPTS = 4
Expand All@@ -11,6 +13,13 @@
DEFAULT_MAX_BACKOFF = 10.0


def is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
"""Return whether a requests transport failure is safe to retry."""
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)


class RetryMode(enum.Enum):
"""The replay safety classification for an API operation."""

Expand Down
62 changes: 57 additions & 5 deletions py/src/braintrust/api/test_transport.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import datetime
import io
from email.utils import format_datetime
from unittest import mock

import pytest
import requests
Expand All@@ -14,7 +15,7 @@
RetryPolicy,
)
from braintrust.api._test_server import scripted_server
from braintrust.api._transport import Transport
from braintrust.api._transport import HTTPConnection, Transport
from braintrust.util import AugmentedHTTPError
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Expand DownExpand Up@@ -68,13 +69,17 @@ def close(self):
super().close()


def test_transport_closes_owned_session():
def test_transport_closes_owned_session_without_closing_injected_adapter():
adapter = TrackingAdapter()
transport = Transport(adapter=adapter)
session = transport.session

with Transport(adapter=adapter) as transport:
assert transport.session is not None
with mock.patch.object(session, "close", wraps=session.close) as close_spy:
transport.close()

assert adapter.close_count > 0
close_spy.assert_called_once()
# The adapter belongs to the caller and may be mounted on other sessions.
assert adapter.close_count == 0


def test_transport_does_not_close_injected_session():
Expand DownExpand Up@@ -351,3 +356,50 @@ def test_non_retrying_custom_adapter_can_delegate_retries_to_sdk():

assert response.status_code == 200
assert handler.request_count == 2


def test_http_connection_close_does_not_close_shared_adapter():
adapter = TrackingAdapter()
first = HTTPConnection("http://localhost", adapter=adapter)
second = HTTPConnection("http://localhost", adapter=adapter)

first.close()

assert adapter.close_count == 0
assert second.session.get_adapter("http://localhost") is adapter


def test_http_connection_close_closes_self_created_long_lived_adapter():
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
adapter = conn.adapter
assert adapter is not None

with mock.patch.object(adapter, "close", wraps=adapter.close) as close_spy:
conn.close()

# Mounted on both the http:// and https:// prefixes, so closed once per mount.
assert close_spy.call_count > 0


def test_http_connection_close_closes_long_lived_adapter_replaced_by_set_adapter():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0


def test_http_connection_close_does_not_close_adapter_set_after_construction():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix: Use apiKey in loadPrompt by AbhiPrasad · Pull Request #718 · braintrustdata/braintrust-sdk-python · GitHub
Skip to content
Merged
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
36 changes: 28 additions & 8 deletions py/src/braintrust/api/_transport.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@
BraintrustTransportError,
BraintrustTransportRetryExhaustedError,
)
from .policies import RetryMode, RetryPolicy
from .policies import RetryMode, RetryPolicy, is_retryable_request_exception


logger = logging.getLogger(__name__)
Expand DownExpand Up@@ -102,6 +102,9 @@ def __init__(self, base_url: str, adapter: HTTPAdapter | None = None):
self.base_url = base_url
self.token = None
self.adapter = adapter
# An adapter handed to us belongs to the caller. `set_http_adapter` installs
# one instance across every connection, so we must not close it.
self._injected_adapter = adapter

self._reset(total=0)

Expand All@@ -120,6 +123,10 @@ def make_long_lived(self) -> None:
)
self._reset()

def close(self) -> None:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

@staticmethod
def sanitize_token(token: str) -> str:
return token.rstrip("\n")
Expand All@@ -131,6 +138,7 @@ def set_token(self, token: str) -> None:

def _set_adapter(self, adapter: HTTPAdapter | None) -> None:
self.adapter = adapter
self._injected_adapter = adapter

def _reset(self, **retry_kwargs: Any) -> None:
self.session = requests.Session()
Expand DownExpand Up@@ -202,6 +210,7 @@ def __init__(
):
custom_transport = session is not None or adapter is not None
self._owns_session = session is None
self._injected_adapter = adapter
self.session = session if session is not None else requests.Session()
if not persist_cookies and self._owns_session:
self.session.cookies.set_policy(_RejectCookiesPolicy())
Expand All@@ -215,6 +224,7 @@ def __init__(

def close(self) -> None:
if self._owns_session:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

def __enter__(self) -> "Transport":
Expand DownExpand Up@@ -272,7 +282,7 @@ def request(
**kwargs,
)
except requests.exceptions.RequestException as exc:
if not _is_retryable_request_exception(exc):
if not is_retryable_request_exception(exc):
error = BraintrustTransportError(method=method, url=url, attempts=attempt, retryable=False)
raise error from exc
if attempt >= max_attempts:
Expand DownExpand Up@@ -392,14 +402,24 @@ def _retry_delay(policy: RetryPolicy, attempt: int, retry_after: float | None) -
return min(policy.max_backoff, policy.backoff_factor * (2 ** (attempt - 1)))


def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))
def _unmount_adapter(session: requests.Session, adapter: HTTPAdapter | None) -> None:
"""Detach a caller-owned adapter so ``Session.close()`` leaves it open.

``requests.Session.close()`` closes every mounted adapter. A single adapter
installed via ``set_http_adapter`` is mounted on many sessions at once, so
closing one session would otherwise clear the connection pools that the
other sessions are still using.
"""

if adapter is None:
return
for prefix, mounted in list(session.adapters.items()):
if mounted is adapter:
del session.adapters[prefix]

def _is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)

def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))


def _parse_retry_after(value: str | None, wall_time: float) -> float | None:
Expand Down
9 changes: 9 additions & 0 deletions py/src/braintrust/api/policies.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@
import enum
from dataclasses import dataclass

import requests


DEFAULT_RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
DEFAULT_MAX_ATTEMPTS = 4
Expand All@@ -11,6 +13,13 @@
DEFAULT_MAX_BACKOFF = 10.0


def is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
"""Return whether a requests transport failure is safe to retry."""
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)


class RetryMode(enum.Enum):
"""The replay safety classification for an API operation."""

Expand Down
62 changes: 57 additions & 5 deletions py/src/braintrust/api/test_transport.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import datetime
import io
from email.utils import format_datetime
from unittest import mock

import pytest
import requests
Expand All@@ -14,7 +15,7 @@
RetryPolicy,
)
from braintrust.api._test_server import scripted_server
from braintrust.api._transport import Transport
from braintrust.api._transport import HTTPConnection, Transport
from braintrust.util import AugmentedHTTPError
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Expand DownExpand Up@@ -68,13 +69,17 @@ def close(self):
super().close()


def test_transport_closes_owned_session():
def test_transport_closes_owned_session_without_closing_injected_adapter():
adapter = TrackingAdapter()
transport = Transport(adapter=adapter)
session = transport.session

with Transport(adapter=adapter) as transport:
assert transport.session is not None
with mock.patch.object(session, "close", wraps=session.close) as close_spy:
transport.close()

assert adapter.close_count > 0
close_spy.assert_called_once()
# The adapter belongs to the caller and may be mounted on other sessions.
assert adapter.close_count == 0


def test_transport_does_not_close_injected_session():
Expand DownExpand Up@@ -351,3 +356,50 @@ def test_non_retrying_custom_adapter_can_delegate_retries_to_sdk():

assert response.status_code == 200
assert handler.request_count == 2


def test_http_connection_close_does_not_close_shared_adapter():
adapter = TrackingAdapter()
first = HTTPConnection("http://localhost", adapter=adapter)
second = HTTPConnection("http://localhost", adapter=adapter)

first.close()

assert adapter.close_count == 0
assert second.session.get_adapter("http://localhost") is adapter


def test_http_connection_close_closes_self_created_long_lived_adapter():
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
adapter = conn.adapter
assert adapter is not None

with mock.patch.object(adapter, "close", wraps=adapter.close) as close_spy:
conn.close()

# Mounted on both the http:// and https:// prefixes, so closed once per mount.
assert close_spy.call_count > 0


def test_http_connection_close_closes_long_lived_adapter_replaced_by_set_adapter():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0


def test_http_connection_close_does_not_close_adapter_set_after_construction():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: Use apiKey in loadPrompt by AbhiPrasad · Pull Request #718 · braintrustdata/braintrust-sdk-python · GitHub
Skip to content
Merged
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
36 changes: 28 additions & 8 deletions py/src/braintrust/api/_transport.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@
BraintrustTransportError,
BraintrustTransportRetryExhaustedError,
)
from .policies import RetryMode, RetryPolicy
from .policies import RetryMode, RetryPolicy, is_retryable_request_exception


logger = logging.getLogger(__name__)
Expand DownExpand Up@@ -102,6 +102,9 @@ def __init__(self, base_url: str, adapter: HTTPAdapter | None = None):
self.base_url = base_url
self.token = None
self.adapter = adapter
# An adapter handed to us belongs to the caller. `set_http_adapter` installs
# one instance across every connection, so we must not close it.
self._injected_adapter = adapter

self._reset(total=0)

Expand All@@ -120,6 +123,10 @@ def make_long_lived(self) -> None:
)
self._reset()

def close(self) -> None:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

@staticmethod
def sanitize_token(token: str) -> str:
return token.rstrip("\n")
Expand All@@ -131,6 +138,7 @@ def set_token(self, token: str) -> None:

def _set_adapter(self, adapter: HTTPAdapter | None) -> None:
self.adapter = adapter
self._injected_adapter = adapter

def _reset(self, **retry_kwargs: Any) -> None:
self.session = requests.Session()
Expand DownExpand Up@@ -202,6 +210,7 @@ def __init__(
):
custom_transport = session is not None or adapter is not None
self._owns_session = session is None
self._injected_adapter = adapter
self.session = session if session is not None else requests.Session()
if not persist_cookies and self._owns_session:
self.session.cookies.set_policy(_RejectCookiesPolicy())
Expand All@@ -215,6 +224,7 @@ def __init__(

def close(self) -> None:
if self._owns_session:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

def __enter__(self) -> "Transport":
Expand DownExpand Up@@ -272,7 +282,7 @@ def request(
**kwargs,
)
except requests.exceptions.RequestException as exc:
if not _is_retryable_request_exception(exc):
if not is_retryable_request_exception(exc):
error = BraintrustTransportError(method=method, url=url, attempts=attempt, retryable=False)
raise error from exc
if attempt >= max_attempts:
Expand DownExpand Up@@ -392,14 +402,24 @@ def _retry_delay(policy: RetryPolicy, attempt: int, retry_after: float | None) -
return min(policy.max_backoff, policy.backoff_factor * (2 ** (attempt - 1)))


def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))
def _unmount_adapter(session: requests.Session, adapter: HTTPAdapter | None) -> None:
"""Detach a caller-owned adapter so ``Session.close()`` leaves it open.

``requests.Session.close()`` closes every mounted adapter. A single adapter
installed via ``set_http_adapter`` is mounted on many sessions at once, so
closing one session would otherwise clear the connection pools that the
other sessions are still using.
"""

if adapter is None:
return
for prefix, mounted in list(session.adapters.items()):
if mounted is adapter:
del session.adapters[prefix]

def _is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)

def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))


def _parse_retry_after(value: str | None, wall_time: float) -> float | None:
Expand Down
9 changes: 9 additions & 0 deletions py/src/braintrust/api/policies.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@
import enum
from dataclasses import dataclass

import requests


DEFAULT_RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
DEFAULT_MAX_ATTEMPTS = 4
Expand All@@ -11,6 +13,13 @@
DEFAULT_MAX_BACKOFF = 10.0


def is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
"""Return whether a requests transport failure is safe to retry."""
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)


class RetryMode(enum.Enum):
"""The replay safety classification for an API operation."""

Expand Down
62 changes: 57 additions & 5 deletions py/src/braintrust/api/test_transport.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import datetime
import io
from email.utils import format_datetime
from unittest import mock

import pytest
import requests
Expand All@@ -14,7 +15,7 @@
RetryPolicy,
)
from braintrust.api._test_server import scripted_server
from braintrust.api._transport import Transport
from braintrust.api._transport import HTTPConnection, Transport
from braintrust.util import AugmentedHTTPError
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Expand DownExpand Up@@ -68,13 +69,17 @@ def close(self):
super().close()


def test_transport_closes_owned_session():
def test_transport_closes_owned_session_without_closing_injected_adapter():
adapter = TrackingAdapter()
transport = Transport(adapter=adapter)
session = transport.session

with Transport(adapter=adapter) as transport:
assert transport.session is not None
with mock.patch.object(session, "close", wraps=session.close) as close_spy:
transport.close()

assert adapter.close_count > 0
close_spy.assert_called_once()
# The adapter belongs to the caller and may be mounted on other sessions.
assert adapter.close_count == 0


def test_transport_does_not_close_injected_session():
Expand DownExpand Up@@ -351,3 +356,50 @@ def test_non_retrying_custom_adapter_can_delegate_retries_to_sdk():

assert response.status_code == 200
assert handler.request_count == 2


def test_http_connection_close_does_not_close_shared_adapter():
adapter = TrackingAdapter()
first = HTTPConnection("http://localhost", adapter=adapter)
second = HTTPConnection("http://localhost", adapter=adapter)

first.close()

assert adapter.close_count == 0
assert second.session.get_adapter("http://localhost") is adapter


def test_http_connection_close_closes_self_created_long_lived_adapter():
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
adapter = conn.adapter
assert adapter is not None

with mock.patch.object(adapter, "close", wraps=adapter.close) as close_spy:
conn.close()

# Mounted on both the http:// and https:// prefixes, so closed once per mount.
assert close_spy.call_count > 0


def test_http_connection_close_closes_long_lived_adapter_replaced_by_set_adapter():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0


def test_http_connection_close_does_not_close_adapter_set_after_construction():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: Use apiKey in loadPrompt by AbhiPrasad · Pull Request #718 · braintrustdata/braintrust-sdk-python · GitHub
Skip to content
Merged
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
36 changes: 28 additions & 8 deletions py/src/braintrust/api/_transport.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@
BraintrustTransportError,
BraintrustTransportRetryExhaustedError,
)
from .policies import RetryMode, RetryPolicy
from .policies import RetryMode, RetryPolicy, is_retryable_request_exception


logger = logging.getLogger(__name__)
Expand DownExpand Up@@ -102,6 +102,9 @@ def __init__(self, base_url: str, adapter: HTTPAdapter | None = None):
self.base_url = base_url
self.token = None
self.adapter = adapter
# An adapter handed to us belongs to the caller. `set_http_adapter` installs
# one instance across every connection, so we must not close it.
self._injected_adapter = adapter

self._reset(total=0)

Expand All@@ -120,6 +123,10 @@ def make_long_lived(self) -> None:
)
self._reset()

def close(self) -> None:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

@staticmethod
def sanitize_token(token: str) -> str:
return token.rstrip("\n")
Expand All@@ -131,6 +138,7 @@ def set_token(self, token: str) -> None:

def _set_adapter(self, adapter: HTTPAdapter | None) -> None:
self.adapter = adapter
self._injected_adapter = adapter

def _reset(self, **retry_kwargs: Any) -> None:
self.session = requests.Session()
Expand DownExpand Up@@ -202,6 +210,7 @@ def __init__(
):
custom_transport = session is not None or adapter is not None
self._owns_session = session is None
self._injected_adapter = adapter
self.session = session if session is not None else requests.Session()
if not persist_cookies and self._owns_session:
self.session.cookies.set_policy(_RejectCookiesPolicy())
Expand All@@ -215,6 +224,7 @@ def __init__(

def close(self) -> None:
if self._owns_session:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

def __enter__(self) -> "Transport":
Expand DownExpand Up@@ -272,7 +282,7 @@ def request(
**kwargs,
)
except requests.exceptions.RequestException as exc:
if not _is_retryable_request_exception(exc):
if not is_retryable_request_exception(exc):
error = BraintrustTransportError(method=method, url=url, attempts=attempt, retryable=False)
raise error from exc
if attempt >= max_attempts:
Expand DownExpand Up@@ -392,14 +402,24 @@ def _retry_delay(policy: RetryPolicy, attempt: int, retry_after: float | None) -
return min(policy.max_backoff, policy.backoff_factor * (2 ** (attempt - 1)))


def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))
def _unmount_adapter(session: requests.Session, adapter: HTTPAdapter | None) -> None:
"""Detach a caller-owned adapter so ``Session.close()`` leaves it open.

``requests.Session.close()`` closes every mounted adapter. A single adapter
installed via ``set_http_adapter`` is mounted on many sessions at once, so
closing one session would otherwise clear the connection pools that the
other sessions are still using.
"""

if adapter is None:
return
for prefix, mounted in list(session.adapters.items()):
if mounted is adapter:
del session.adapters[prefix]

def _is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)

def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))


def _parse_retry_after(value: str | None, wall_time: float) -> float | None:
Expand Down
9 changes: 9 additions & 0 deletions py/src/braintrust/api/policies.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@
import enum
from dataclasses import dataclass

import requests


DEFAULT_RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
DEFAULT_MAX_ATTEMPTS = 4
Expand All@@ -11,6 +13,13 @@
DEFAULT_MAX_BACKOFF = 10.0


def is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
"""Return whether a requests transport failure is safe to retry."""
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)


class RetryMode(enum.Enum):
"""The replay safety classification for an API operation."""

Expand Down
62 changes: 57 additions & 5 deletions py/src/braintrust/api/test_transport.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import datetime
import io
from email.utils import format_datetime
from unittest import mock

import pytest
import requests
Expand All@@ -14,7 +15,7 @@
RetryPolicy,
)
from braintrust.api._test_server import scripted_server
from braintrust.api._transport import Transport
from braintrust.api._transport import HTTPConnection, Transport
from braintrust.util import AugmentedHTTPError
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Expand DownExpand Up@@ -68,13 +69,17 @@ def close(self):
super().close()


def test_transport_closes_owned_session():
def test_transport_closes_owned_session_without_closing_injected_adapter():
adapter = TrackingAdapter()
transport = Transport(adapter=adapter)
session = transport.session

with Transport(adapter=adapter) as transport:
assert transport.session is not None
with mock.patch.object(session, "close", wraps=session.close) as close_spy:
transport.close()

assert adapter.close_count > 0
close_spy.assert_called_once()
# The adapter belongs to the caller and may be mounted on other sessions.
assert adapter.close_count == 0


def test_transport_does_not_close_injected_session():
Expand DownExpand Up@@ -351,3 +356,50 @@ def test_non_retrying_custom_adapter_can_delegate_retries_to_sdk():

assert response.status_code == 200
assert handler.request_count == 2


def test_http_connection_close_does_not_close_shared_adapter():
adapter = TrackingAdapter()
first = HTTPConnection("http://localhost", adapter=adapter)
second = HTTPConnection("http://localhost", adapter=adapter)

first.close()

assert adapter.close_count == 0
assert second.session.get_adapter("http://localhost") is adapter


def test_http_connection_close_closes_self_created_long_lived_adapter():
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
adapter = conn.adapter
assert adapter is not None

with mock.patch.object(adapter, "close", wraps=adapter.close) as close_spy:
conn.close()

# Mounted on both the http:// and https:// prefixes, so closed once per mount.
assert close_spy.call_count > 0


def test_http_connection_close_closes_long_lived_adapter_replaced_by_set_adapter():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0


def test_http_connection_close_does_not_close_adapter_set_after_construction():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix: Use apiKey in loadPrompt by AbhiPrasad · Pull Request #718 · braintrustdata/braintrust-sdk-python · GitHub
Skip to content
Merged
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
36 changes: 28 additions & 8 deletions py/src/braintrust/api/_transport.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@
BraintrustTransportError,
BraintrustTransportRetryExhaustedError,
)
from .policies import RetryMode, RetryPolicy
from .policies import RetryMode, RetryPolicy, is_retryable_request_exception


logger = logging.getLogger(__name__)
Expand DownExpand Up@@ -102,6 +102,9 @@ def __init__(self, base_url: str, adapter: HTTPAdapter | None = None):
self.base_url = base_url
self.token = None
self.adapter = adapter
# An adapter handed to us belongs to the caller. `set_http_adapter` installs
# one instance across every connection, so we must not close it.
self._injected_adapter = adapter

self._reset(total=0)

Expand All@@ -120,6 +123,10 @@ def make_long_lived(self) -> None:
)
self._reset()

def close(self) -> None:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

@staticmethod
def sanitize_token(token: str) -> str:
return token.rstrip("\n")
Expand All@@ -131,6 +138,7 @@ def set_token(self, token: str) -> None:

def _set_adapter(self, adapter: HTTPAdapter | None) -> None:
self.adapter = adapter
self._injected_adapter = adapter

def _reset(self, **retry_kwargs: Any) -> None:
self.session = requests.Session()
Expand DownExpand Up@@ -202,6 +210,7 @@ def __init__(
):
custom_transport = session is not None or adapter is not None
self._owns_session = session is None
self._injected_adapter = adapter
self.session = session if session is not None else requests.Session()
if not persist_cookies and self._owns_session:
self.session.cookies.set_policy(_RejectCookiesPolicy())
Expand All@@ -215,6 +224,7 @@ def __init__(

def close(self) -> None:
if self._owns_session:
_unmount_adapter(self.session, self._injected_adapter)
self.session.close()

def __enter__(self) -> "Transport":
Expand DownExpand Up@@ -272,7 +282,7 @@ def request(
**kwargs,
)
except requests.exceptions.RequestException as exc:
if not _is_retryable_request_exception(exc):
if not is_retryable_request_exception(exc):
error = BraintrustTransportError(method=method, url=url, attempts=attempt, retryable=False)
raise error from exc
if attempt >= max_attempts:
Expand DownExpand Up@@ -392,14 +402,24 @@ def _retry_delay(policy: RetryPolicy, attempt: int, retry_after: float | None) -
return min(policy.max_backoff, policy.backoff_factor * (2 ** (attempt - 1)))


def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))
def _unmount_adapter(session: requests.Session, adapter: HTTPAdapter | None) -> None:
"""Detach a caller-owned adapter so ``Session.close()`` leaves it open.

``requests.Session.close()`` closes every mounted adapter. A single adapter
installed via ``set_http_adapter`` is mounted on many sessions at once, so
closing one session would otherwise clear the connection pools that the
other sessions are still using.
"""

if adapter is None:
return
for prefix, mounted in list(session.adapters.items()):
if mounted is adapter:
del session.adapters[prefix]

def _is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)

def _request_body_is_replayable(data: Any, files: Any) -> bool:
return files is None and (data is None or isinstance(data, (bytes, str)))


def _parse_retry_after(value: str | None, wall_time: float) -> float | None:
Expand Down
9 changes: 9 additions & 0 deletions py/src/braintrust/api/policies.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@
import enum
from dataclasses import dataclass

import requests


DEFAULT_RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
DEFAULT_MAX_ATTEMPTS = 4
Expand All@@ -11,6 +13,13 @@
DEFAULT_MAX_BACKOFF = 10.0


def is_retryable_request_exception(exc: requests.exceptions.RequestException) -> bool:
"""Return whether a requests transport failure is safe to retry."""
return isinstance(exc, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)) and not isinstance(
exc, requests.exceptions.SSLError
)


class RetryMode(enum.Enum):
"""The replay safety classification for an API operation."""

Expand Down
62 changes: 57 additions & 5 deletions py/src/braintrust/api/test_transport.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import datetime
import io
from email.utils import format_datetime
from unittest import mock

import pytest
import requests
Expand All@@ -14,7 +15,7 @@
RetryPolicy,
)
from braintrust.api._test_server import scripted_server
from braintrust.api._transport import Transport
from braintrust.api._transport import HTTPConnection, Transport
from braintrust.util import AugmentedHTTPError
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Expand DownExpand Up@@ -68,13 +69,17 @@ def close(self):
super().close()


def test_transport_closes_owned_session():
def test_transport_closes_owned_session_without_closing_injected_adapter():
adapter = TrackingAdapter()
transport = Transport(adapter=adapter)
session = transport.session

with Transport(adapter=adapter) as transport:
assert transport.session is not None
with mock.patch.object(session, "close", wraps=session.close) as close_spy:
transport.close()

assert adapter.close_count > 0
close_spy.assert_called_once()
# The adapter belongs to the caller and may be mounted on other sessions.
assert adapter.close_count == 0


def test_transport_does_not_close_injected_session():
Expand DownExpand Up@@ -351,3 +356,50 @@ def test_non_retrying_custom_adapter_can_delegate_retries_to_sdk():

assert response.status_code == 200
assert handler.request_count == 2


def test_http_connection_close_does_not_close_shared_adapter():
adapter = TrackingAdapter()
first = HTTPConnection("http://localhost", adapter=adapter)
second = HTTPConnection("http://localhost", adapter=adapter)

first.close()

assert adapter.close_count == 0
assert second.session.get_adapter("http://localhost") is adapter


def test_http_connection_close_closes_self_created_long_lived_adapter():
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
adapter = conn.adapter
assert adapter is not None

with mock.patch.object(adapter, "close", wraps=adapter.close) as close_spy:
conn.close()

# Mounted on both the http:// and https:// prefixes, so closed once per mount.
assert close_spy.call_count > 0


def test_http_connection_close_closes_long_lived_adapter_replaced_by_set_adapter():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn.make_long_lived()
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0


def test_http_connection_close_does_not_close_adapter_set_after_construction():
adapter = TrackingAdapter()
conn = HTTPConnection("http://localhost")
conn._set_adapter(adapter)
conn._reset()

conn.close()

assert adapter.close_count == 0
Loading