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
6 changes: 3 additions & 3 deletions UnleashClient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# ruff: noqa: F401
from typing import TYPE_CHECKING

from ._requests_transport import RequestsTransport
from .clients import (
INSTANCES,
UnleashClient,
Expand All @@ -14,10 +13,11 @@
StreamingOptions,
)
from .transport import AsyncTransport, Method, Request, Response, Transport
from .transport._requests import RequestsTransport

if TYPE_CHECKING:
from ._aiohttp_transport import AiohttpTransport
from .clients.async_unleash_client import AsyncUnleashClient
from .transport._aiohttp import AiohttpTransport


def __getattr__(name: str):
Expand All @@ -26,7 +26,7 @@ def __getattr__(name: str):

return AsyncUnleashClient
if name == "AiohttpTransport":
from ._aiohttp_transport import AiohttpTransport # noqa: PLC0415
from .transport._aiohttp import AiohttpTransport # noqa: PLC0415

return AiohttpTransport
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
2 changes: 1 addition & 1 deletion UnleashClient/_async_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from yggdrasil_engine.engine import UnleashEngine

from UnleashClient._async_scheduler import _AsyncJob, _AsyncScheduler
from UnleashClient._async_unleash_api_client import _AsyncUnleashApiClient
from UnleashClient._payloads import _build_metrics_payload
from UnleashClient.api_client._async import _AsyncUnleashApiClient
from UnleashClient.config import UnleashConfig
from UnleashClient.impact_metrics import ImpactMetrics
from UnleashClient.utils import LOGGER
Expand Down
2 changes: 1 addition & 1 deletion UnleashClient/_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from UnleashClient._payloads import _build_metrics_payload
from UnleashClient._scheduler import _ScheduledJob, _Scheduler
from UnleashClient._unleash_api_client import _UnleashApiClient
from UnleashClient.api_client._sync import _UnleashApiClient
from UnleashClient.config import UnleashConfig
from UnleashClient.impact_metrics import ImpactMetrics
from UnleashClient.utils import LOGGER
Expand Down
1 change: 1 addition & 0 deletions UnleashClient/api_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Talks to the Unleash API: feature fetches, registration and metrics."""
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
import threading
from typing import Any, Dict, Optional, Tuple

from UnleashClient._aiohttp_transport import AiohttpTransport
from UnleashClient._event_dispatcher import _EventDispatcher
from UnleashClient._headers import _HeaderFactory
from UnleashClient._retry import _send_with_retries_async
from UnleashClient._unleash_api_client import (
from UnleashClient.api_client._shared import (
FetchResult,
_emit_fetch_completed,
_failure_status,
Expand All @@ -24,11 +23,12 @@
from UnleashClient.errors import AlreadyClosedError
from UnleashClient.events import FetchStatus
from UnleashClient.transport import AsyncTransport, Method, Request
from UnleashClient.transport._aiohttp import AiohttpTransport
from UnleashClient.utils import LOGGER


class _AsyncUnleashApiClient:
"""The asyncio twin of :class:`UnleashClient._unleash_api_client._UnleashApiClient`."""
"""The asyncio twin of :class:`UnleashClient.api_client._sync._UnleashApiClient`."""

def __init__(
self,
Expand Down
92 changes: 92 additions & 0 deletions UnleashClient/api_client/_shared.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
What both API clients share: the fetch result, how a fetch outcome is
classified and reported, and reading a response. Names no HTTP library.
"""

import uuid
from typing import NamedTuple, Optional

from UnleashClient._event_dispatcher import _EventDispatcher
from UnleashClient.events import (
FetchStatus,
UnleashEventType,
UnleashFetchCompletedEvent,
)
from UnleashClient.transport import Response
from UnleashClient.utils import LOGGER


class FetchResult(NamedTuple):
"""
The outcome of one feature fetch.

:param raw_state: Feature state body, or None on a 304 and on failure.
:param etag: ETag returned by the server, or "" when the response had none.
:param not_modified: True when the server answered 304.
"""

raw_state: Optional[str]
etag: str
not_modified: bool = False


def _failure_status(http_status_code: Optional[int]) -> FetchStatus:
"""Classifies a failed fetch by the response it got, if any."""
if http_status_code is None:
return FetchStatus.UNAVAILABLE
if http_status_code in (401, 403):
return FetchStatus.UNAUTHORIZED
if http_status_code == 429:
return FetchStatus.RATE_LIMITED
if http_status_code >= 500:
return FetchStatus.UNAVAILABLE
return FetchStatus.INVALID_RESPONSE


def _fetched_status(raw_state: str) -> FetchStatus:
"""An empty 200 is not usable: there is no state to apply."""
return FetchStatus.CHANGED if raw_state else FetchStatus.INVALID_RESPONSE


def _emit_fetch_completed(
events: Optional[_EventDispatcher], status: FetchStatus
) -> None:
"""
Reports how a fetch ended. Never raises: a broken listener must not break
the fetch.
"""
if events is None:
return

try:
events.emit_event(
UnleashFetchCompletedEvent(
event_type=UnleashEventType.FETCH_COMPLETED,
event_id=uuid.uuid4(),
status=status,
)
)
except Exception: # pylint: disable=broad-except
LOGGER.debug("Failed to emit the fetch completed event", exc_info=True)


def _normalized_url(url: str, path: str) -> str:
return f"{url}{path}"


def _header(resp: Response, name: str) -> str:
name = name.lower()
for key, value in resp.headers.items():
if key.lower() == name:
return value
return ""


def _text(resp: Response) -> str:
return resp.body.decode("utf-8", errors="replace")


def _log_resp_info(resp: Response) -> None:
LOGGER.debug("HTTP status code: %s", resp.status)
LOGGER.debug("HTTP headers: %s", resp.headers)
LOGGER.debug("HTTP content: %s", _text(resp))
Original file line number Diff line number Diff line change
@@ -1,98 +1,27 @@
import json
import uuid
from typing import Any, Dict, NamedTuple, Optional, Tuple
from typing import Any, Dict, Optional, Tuple

from UnleashClient._event_dispatcher import _EventDispatcher
from UnleashClient._headers import _HeaderFactory
from UnleashClient._requests_transport import RequestsTransport
from UnleashClient._retry import _send_with_retries
from UnleashClient.api_client._shared import (
FetchResult,
_emit_fetch_completed,
_failure_status,
_fetched_status,
_header,
_log_resp_info,
_normalized_url,
_text,
)
from UnleashClient.config import PollingOptions, UnleashConfig
from UnleashClient.constants import FEATURES_URL, METRICS_URL, REGISTER_URL
from UnleashClient.events import (
FetchStatus,
UnleashEventType,
UnleashFetchCompletedEvent,
)
from UnleashClient.transport import Method, Request, Response, Transport
from UnleashClient.events import FetchStatus
from UnleashClient.transport import Method, Request, Transport
from UnleashClient.transport._requests import RequestsTransport
from UnleashClient.utils import LOGGER


class FetchResult(NamedTuple):
"""
The outcome of one feature fetch.

:param raw_state: Feature state body, or None on a 304 and on failure.
:param etag: ETag returned by the server, or "" when the response had none.
:param not_modified: True when the server answered 304.
"""

raw_state: Optional[str]
etag: str
not_modified: bool = False


def _failure_status(http_status_code: Optional[int]) -> FetchStatus:
"""Classifies a failed fetch by the response it got, if any."""
if http_status_code is None:
return FetchStatus.UNAVAILABLE
if http_status_code in (401, 403):
return FetchStatus.UNAUTHORIZED
if http_status_code == 429:
return FetchStatus.RATE_LIMITED
if http_status_code >= 500:
return FetchStatus.UNAVAILABLE
return FetchStatus.INVALID_RESPONSE


def _fetched_status(raw_state: str) -> FetchStatus:
"""An empty 200 is not usable: there is no state to apply."""
return FetchStatus.CHANGED if raw_state else FetchStatus.INVALID_RESPONSE


def _emit_fetch_completed(
events: Optional[_EventDispatcher], status: FetchStatus
) -> None:
"""
Reports how a fetch ended. Never raises: a broken listener must not break
the fetch.
"""
if events is None:
return

try:
events.emit_event(
UnleashFetchCompletedEvent(
event_type=UnleashEventType.FETCH_COMPLETED,
event_id=uuid.uuid4(),
status=status,
)
)
except Exception: # pylint: disable=broad-except
LOGGER.debug("Failed to emit the fetch completed event", exc_info=True)


def _normalized_url(url: str, path: str) -> str:
return f"{url}{path}"


def _header(resp: Response, name: str) -> str:
name = name.lower()
for key, value in resp.headers.items():
if key.lower() == name:
return value
return ""


def _text(resp: Response) -> str:
return resp.body.decode("utf-8", errors="replace")


def _log_resp_info(resp: Response) -> None:
LOGGER.debug("HTTP status code: %s", resp.status)
LOGGER.debug("HTTP headers: %s", resp.headers)
LOGGER.debug("HTTP content: %s", _text(resp))


class _UnleashApiClient:
"""
Talks to the Unleash API: feature fetches, client registration and metrics
Expand Down
2 changes: 1 addition & 1 deletion UnleashClient/clients/async_unleash_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

from UnleashClient._async_metrics import _AsyncMetricsReporter
from UnleashClient._async_scheduler import _AsyncScheduler
from UnleashClient._async_unleash_api_client import _AsyncUnleashApiClient
from UnleashClient._context import _ContextEnricher
from UnleashClient._evaluator import _Evaluator
from UnleashClient._event_dispatcher import _EventDispatcher
Expand All @@ -24,6 +23,7 @@
_instance_identifier,
)
from UnleashClient._payloads import _build_register_payload
from UnleashClient.api_client._async import _AsyncUnleashApiClient
from UnleashClient.cache import BaseCache, FileCache
from UnleashClient.clients.unleash_client import _RunState
from UnleashClient.config import (
Expand Down
2 changes: 1 addition & 1 deletion UnleashClient/clients/unleash_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from UnleashClient._metrics import _MetricsReporter
from UnleashClient._payloads import _build_register_payload
from UnleashClient._scheduler import _ScheduledJob, _Scheduler
from UnleashClient._unleash_api_client import _UnleashApiClient
from UnleashClient.api_client._sync import _UnleashApiClient
from UnleashClient.cache import BaseCache, FileCache
from UnleashClient.config import (
IdentityOptions,
Expand Down
2 changes: 1 addition & 1 deletion UnleashClient/connectors/_async_connector.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from abc import ABC, abstractmethod

from UnleashClient._async_scheduler import _AsyncScheduler
from UnleashClient._async_unleash_api_client import _AsyncUnleashApiClient
from UnleashClient._feature_store import _FeatureStore
from UnleashClient.api_client._async import _AsyncUnleashApiClient
from UnleashClient.config import PollingOptions


Expand Down
2 changes: 1 addition & 1 deletion UnleashClient/connectors/polling_connector.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from UnleashClient._feature_store import _FeatureStore
from UnleashClient._scheduler import _ScheduledJob, _Scheduler
from UnleashClient._unleash_api_client import _UnleashApiClient
from UnleashClient.api_client._sync import _UnleashApiClient
from UnleashClient.config import PollingOptions

from .base_connector import BaseConnector
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
"""
HTTP layer under the Unleash client: sends one request and returns the
response.

Pass a transport to the client to choose how requests are sent:
``RequestsTransport`` with your own ``requests.Session``,
``AiohttpTransport`` with your own ``aiohttp.ClientSession`` for the async
client, or any object with an ``execute(Request) -> Response`` method.
"""

from dataclasses import dataclass, field
Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/connectors/test_async_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
from tests.utilities.fake_unleash_server import FakeUnleash
from tests.utilities.mocks.mock_features import MOCK_FEATURE_RESPONSE
from tests.utilities.testing_constants import APP_NAME, ETAG_VALUE
from UnleashClient._async_unleash_api_client import _AsyncUnleashApiClient
from UnleashClient._event_dispatcher import _EventDispatcher
from UnleashClient._feature_store import _FeatureStore
from UnleashClient._headers import _HeaderFactory
from UnleashClient.api_client._async import _AsyncUnleashApiClient
from UnleashClient.config import PollingOptions, UnleashConfig
from UnleashClient.connectors._async_connector import _AsyncPollingConnector
from UnleashClient.constants import ETAG, FEATURES_URL
Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/connectors/test_polling_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from UnleashClient._feature_store import _FeatureStore
from UnleashClient._headers import _HeaderFactory
from UnleashClient._scheduler import _Scheduler
from UnleashClient._unleash_api_client import _UnleashApiClient
from UnleashClient.api_client._sync import _UnleashApiClient
from UnleashClient.config import (
IdentityOptions,
PollingOptions,
Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/test_aiohttp_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from pytest import mark

from tests.utilities.fake_unleash_server import FakeUnleash
from UnleashClient._aiohttp_transport import AiohttpTransport
from UnleashClient.errors import AlreadyClosedError
from UnleashClient.transport import AsyncTransport, Method, Request
from UnleashClient.transport._aiohttp import AiohttpTransport

PATH = "/client/features"

Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/test_async_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from tests.utilities.fake_unleash_server import FakeUnleash
from UnleashClient._async_metrics import _AsyncMetricsReporter
from UnleashClient._async_scheduler import _AsyncJob, _AsyncJobFn, _AsyncScheduler
from UnleashClient._async_unleash_api_client import _AsyncUnleashApiClient
from UnleashClient._headers import _HeaderFactory
from UnleashClient.api_client._async import _AsyncUnleashApiClient
from UnleashClient.config import IdentityOptions, MetricsOptions, UnleashConfig
from UnleashClient.constants import CLIENT_SPEC_VERSION, METRICS_URL
from UnleashClient.impact_metrics import ImpactMetrics
Expand Down
4 changes: 2 additions & 2 deletions tests/unit_tests/test_async_unleash_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@
PROJECT_NAME,
URL,
)
from UnleashClient._aiohttp_transport import AiohttpTransport
from UnleashClient._async_unleash_api_client import _AsyncUnleashApiClient
from UnleashClient._headers import _HeaderFactory
from UnleashClient.api_client._async import _AsyncUnleashApiClient
from UnleashClient.config import (
IdentityOptions,
MetricsOptions,
Expand All @@ -38,6 +37,7 @@
from UnleashClient.errors import AlreadyClosedError
from UnleashClient.events import FetchStatus, UnleashEventType
from UnleashClient.transport import Method, Request, Response
from UnleashClient.transport._aiohttp import AiohttpTransport

API_PREFIX = "/api"
FEATURES_PATH = API_PREFIX + FEATURES_URL
Expand Down
Loading
Loading