Skip to content

Commit eaa3d72

Browse files
Merge remote-tracking branch 'origin/main' into devin/1788468576-fdv2-payload-filter-warning
2 parents 7c9d237 + 514c467 commit eaa3d72

8 files changed

Lines changed: 56 additions & 12 deletions

File tree

‎.sdk_metadata.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"languages": [
88
"Python"
99
],
10-
"userAgents": ["PythonClient"],
10+
"userAgents": ["PythonClient", "PythonAsyncClient"],
1111
"features": {
1212
"allFlags": { "introduced": "2.0" },
1313
"appMetadata": { "introduced": "7.6" },

‎ldclient/impl/aio/transport.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from ld_eventsource.config.retry_delay_strategy import RetryDelayStrategy
1616

1717
from ldclient.impl.aio.transport_types import TransportResponse
18-
from ldclient.impl.http import _base_headers, _get_proxy_url
18+
from ldclient.impl.http import ASYNC_USER_AGENT, _base_headers, _get_proxy_url
1919
from ldclient.impl.util import log
2020

2121
# Allows up to 5 minutes to elapse without any data sent across the stream.
@@ -123,7 +123,7 @@ def create(self, url: str, initial_retry_delay: float, query_params=None) -> Asy
123123
proxy settings, and the retry/backoff policy come from the SDK config.
124124
``query_params`` is an optional zero-argument callable evaluated on
125125
each (re)connect to produce additional query string parameters."""
126-
base_headers = _base_headers(self._config)
126+
base_headers = _base_headers(self._config, ASYNC_USER_AGENT)
127127
aiohttp_request_options: dict = {
128128
"timeout": aiohttp.ClientTimeout(
129129
total=None,

‎ldclient/impl/datasource/async_feature_requester.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from ldclient.impl.aio.transport import AsyncHTTPTransport
1111
from ldclient.impl.datasource.datasource_common import FDV1_POLLING_ENDPOINT
12+
from ldclient.impl.http import ASYNC_USER_AGENT
1213
from ldclient.impl.util import _headers, log, throw_if_unsuccessful_response
1314
from ldclient.interfaces import AsyncFeatureRequester
1415
from ldclient.versioned_data_kind import FEATURES, SEGMENTS
@@ -30,7 +31,7 @@ def __init__(self, config, transport: Optional[AsyncHTTPTransport] = None):
3031

3132
async def get_all_data(self):
3233
uri = self._poll_uri
33-
hdrs = _headers(self._config)
34+
hdrs = _headers(self._config, ASYNC_USER_AGENT)
3435
cache_entry = self._cache.get(uri)
3536
hdrs['Accept-Encoding'] = 'gzip'
3637
if cache_entry is not None:

‎ldclient/impl/datasourcev2/async_polling.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
polling_payload_to_changeset,
2727
polling_result_to_basis
2828
)
29+
from ldclient.impl.http import ASYNC_USER_AGENT
2930
from ldclient.impl.util import (
3031
UnsuccessfulResponseException,
3132
_Fail,
@@ -192,7 +193,7 @@ async def fetch(self, selector: Optional[Selector]) -> PollingResult:
192193
filter_query = parse.urlencode(query_params)
193194
uri += f"?{filter_query}"
194195

195-
hdrs = _headers(self._config)
196+
hdrs = _headers(self._config, ASYNC_USER_AGENT)
196197
hdrs["Accept-Encoding"] = "gzip"
197198

198199
if self._etag is not None:
@@ -387,7 +388,7 @@ async def fetch(self, selector: Optional[Selector]) -> PollingResult:
387388
filter_query = parse.urlencode(query_params)
388389
uri += f"?{filter_query}"
389390

390-
hdrs = _headers(self._config)
391+
hdrs = _headers(self._config, ASYNC_USER_AGENT)
391392
hdrs["Accept-Encoding"] = "gzip"
392393

393394
if self._etag is not None:

‎ldclient/impl/events/async_event_processor.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
EventOutputFormatter
3030
)
3131
from ldclient.impl.events.types import EventInput
32+
from ldclient.impl.http import ASYNC_USER_AGENT
3233
from ldclient.impl.lru_cache import SimpleLRUCache
3334
from ldclient.impl.sampler import Sampler
3435
from ldclient.impl.util import (
@@ -277,7 +278,7 @@ async def __aexit__(self, type, value, traceback):
277278

278279

279280
async def _post_events_with_retry(http_client: AsyncHTTPTransport, config: AsyncConfig, uri: str, payload_id: Optional[str], body: str, events_description: str):
280-
hdrs = _headers(config)
281+
hdrs = _headers(config, ASYNC_USER_AGENT)
281282
hdrs['Content-Type'] = 'application/json'
282283
if config.enable_event_compression:
283284
hdrs['Content-Encoding'] = 'gzip'

‎ldclient/impl/http.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77

88
from ldclient.version import VERSION
99

10+
#: User-Agent product token for the synchronous client.
11+
SYNC_USER_AGENT = "PythonClient"
12+
#: User-Agent product token for the asynchronous client. Sync and async ship in
13+
#: one package, so the token is how LaunchDarkly tells the two apart.
14+
ASYNC_USER_AGENT = "PythonAsyncClient"
15+
1016

1117
def _application_header_value(application: dict) -> str:
1218
parts = []
@@ -22,8 +28,8 @@ def _application_header_value(application: dict) -> str:
2228
return " ".join(parts)
2329

2430

25-
def _base_headers(config):
26-
headers = {'Authorization': config.sdk_key or '', 'User-Agent': 'PythonClient/' + VERSION}
31+
def _base_headers(config, user_agent=SYNC_USER_AGENT):
32+
headers = {'Authorization': config.sdk_key or '', 'User-Agent': user_agent + '/' + VERSION}
2733

2834
if config._instance_id is not None:
2935
headers['X-LaunchDarkly-Instance-Id'] = config._instance_id

‎ldclient/impl/util.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Dict, Generic, Mapping, Optional, TypeVar, Union
88
from urllib.parse import urlparse, urlunparse
99

10-
from ldclient.impl.http import _base_headers
10+
from ldclient.impl.http import SYNC_USER_AGENT, _base_headers
1111

1212

1313
def current_time_millis() -> int:
@@ -82,8 +82,8 @@ def validate_sdk_key_format(sdk_key: str, logger: logging.Logger) -> str:
8282
return sdk_key
8383

8484

85-
def _headers(config):
86-
base_headers = _base_headers(config)
85+
def _headers(config, user_agent=SYNC_USER_AGENT):
86+
base_headers = _base_headers(config, user_agent)
8787
base_headers.update({'Content-Type': "application/json"})
8888
return base_headers
8989

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Tests for the sync and async User-Agent tokens.
2+
3+
Sync and async ship in one package, so the User-Agent token is how LaunchDarkly
4+
tells the two clients apart. These tests lock the exact tokens sent on the wire.
5+
"""
6+
7+
from ldclient.config import Config
8+
from ldclient.impl.http import ASYNC_USER_AGENT, SYNC_USER_AGENT, _base_headers
9+
from ldclient.impl.util import _headers
10+
from ldclient.version import VERSION
11+
12+
13+
def _config():
14+
return Config(sdk_key='sdk-key')
15+
16+
17+
def test_user_agent_tokens():
18+
assert SYNC_USER_AGENT == 'PythonClient'
19+
assert ASYNC_USER_AGENT == 'PythonAsyncClient'
20+
21+
22+
def test_base_headers_default_user_agent_is_sync():
23+
assert _base_headers(_config())['User-Agent'] == 'PythonClient/' + VERSION
24+
25+
26+
def test_base_headers_async_user_agent():
27+
assert _base_headers(_config(), ASYNC_USER_AGENT)['User-Agent'] == 'PythonAsyncClient/' + VERSION
28+
29+
30+
def test_headers_default_user_agent_is_sync():
31+
assert _headers(_config())['User-Agent'] == 'PythonClient/' + VERSION
32+
33+
34+
def test_headers_async_user_agent():
35+
assert _headers(_config(), ASYNC_USER_AGENT)['User-Agent'] == 'PythonAsyncClient/' + VERSION

0 commit comments

Comments
 (0)