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
2 changes: 1 addition & 1 deletion .sdk_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"languages": [
"Python"
],
"userAgents": ["PythonClient"],
"userAgents": ["PythonClient", "PythonAsyncClient"],
"features": {
"allFlags": { "introduced": "2.0" },
"appMetadata": { "introduced": "7.6" },
Expand Down
4 changes: 2 additions & 2 deletions ldclient/impl/aio/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ld_eventsource.config.retry_delay_strategy import RetryDelayStrategy

from ldclient.impl.aio.transport_types import TransportResponse
from ldclient.impl.http import _base_headers, _get_proxy_url
from ldclient.impl.http import ASYNC_USER_AGENT, _base_headers, _get_proxy_url
from ldclient.impl.util import log

# Allows up to 5 minutes to elapse without any data sent across the stream.
Expand Down Expand Up @@ -123,7 +123,7 @@ def create(self, url: str, initial_retry_delay: float, query_params=None) -> Asy
proxy settings, and the retry/backoff policy come from the SDK config.
``query_params`` is an optional zero-argument callable evaluated on
each (re)connect to produce additional query string parameters."""
base_headers = _base_headers(self._config)
base_headers = _base_headers(self._config, ASYNC_USER_AGENT)
aiohttp_request_options: dict = {
"timeout": aiohttp.ClientTimeout(
total=None,
Expand Down
3 changes: 2 additions & 1 deletion ldclient/impl/datasource/async_feature_requester.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

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

async def get_all_data(self):
uri = self._poll_uri
hdrs = _headers(self._config)
hdrs = _headers(self._config, ASYNC_USER_AGENT)
cache_entry = self._cache.get(uri)
hdrs['Accept-Encoding'] = 'gzip'
if cache_entry is not None:
Expand Down
5 changes: 3 additions & 2 deletions ldclient/impl/datasourcev2/async_polling.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
polling_payload_to_changeset,
polling_result_to_basis
)
from ldclient.impl.http import ASYNC_USER_AGENT
from ldclient.impl.util import (
UnsuccessfulResponseException,
_Fail,
Expand Down Expand Up @@ -192,7 +193,7 @@ async def fetch(self, selector: Optional[Selector]) -> PollingResult:
filter_query = parse.urlencode(query_params)
uri += f"?{filter_query}"

hdrs = _headers(self._config)
hdrs = _headers(self._config, ASYNC_USER_AGENT)
hdrs["Accept-Encoding"] = "gzip"

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

hdrs = _headers(self._config)
hdrs = _headers(self._config, ASYNC_USER_AGENT)
hdrs["Accept-Encoding"] = "gzip"

if self._etag is not None:
Expand Down
3 changes: 2 additions & 1 deletion ldclient/impl/events/async_event_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
EventOutputFormatter
)
from ldclient.impl.events.types import EventInput
from ldclient.impl.http import ASYNC_USER_AGENT
from ldclient.impl.lru_cache import SimpleLRUCache
from ldclient.impl.sampler import Sampler
from ldclient.impl.util import (
Expand Down Expand Up @@ -277,7 +278,7 @@ async def __aexit__(self, type, value, traceback):


async def _post_events_with_retry(http_client: AsyncHTTPTransport, config: AsyncConfig, uri: str, payload_id: Optional[str], body: str, events_description: str):
hdrs = _headers(config)
hdrs = _headers(config, ASYNC_USER_AGENT)
hdrs['Content-Type'] = 'application/json'
if config.enable_event_compression:
hdrs['Content-Encoding'] = 'gzip'
Expand Down
10 changes: 8 additions & 2 deletions ldclient/impl/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

from ldclient.version import VERSION

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


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


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

if config._instance_id is not None:
headers['X-LaunchDarkly-Instance-Id'] = config._instance_id
Expand Down
6 changes: 3 additions & 3 deletions ldclient/impl/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any, Dict, Generic, Mapping, Optional, TypeVar, Union
from urllib.parse import urlparse, urlunparse

from ldclient.impl.http import _base_headers
from ldclient.impl.http import SYNC_USER_AGENT, _base_headers


def current_time_millis() -> int:
Expand Down Expand Up @@ -82,8 +82,8 @@ def validate_sdk_key_format(sdk_key: str, logger: logging.Logger) -> str:
return sdk_key


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

Expand Down
35 changes: 35 additions & 0 deletions ldclient/testing/impl/test_user_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Tests for the sync and async User-Agent tokens.

Sync and async ship in one package, so the User-Agent token is how LaunchDarkly
tells the two clients apart. These tests lock the exact tokens sent on the wire.
"""

from ldclient.config import Config
from ldclient.impl.http import ASYNC_USER_AGENT, SYNC_USER_AGENT, _base_headers
from ldclient.impl.util import _headers
from ldclient.version import VERSION


def _config():
return Config(sdk_key='sdk-key')


def test_user_agent_tokens():
assert SYNC_USER_AGENT == 'PythonClient'
assert ASYNC_USER_AGENT == 'PythonAsyncClient'


def test_base_headers_default_user_agent_is_sync():
assert _base_headers(_config())['User-Agent'] == 'PythonClient/' + VERSION


def test_base_headers_async_user_agent():
assert _base_headers(_config(), ASYNC_USER_AGENT)['User-Agent'] == 'PythonAsyncClient/' + VERSION


def test_headers_default_user_agent_is_sync():
assert _headers(_config())['User-Agent'] == 'PythonClient/' + VERSION


def test_headers_async_user_agent():
assert _headers(_config(), ASYNC_USER_AGENT)['User-Agent'] == 'PythonAsyncClient/' + VERSION
Loading