diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index 5e8b6db1..d8f93c26 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -15,7 +15,7 @@ from apscheduler.triggers.interval import IntervalTrigger from yggdrasil_engine.engine import UnleashEngine -from UnleashClient.api import register_client +from UnleashClient.api.sync_api import register_client from UnleashClient.connectors import ( BaseConnector, BootstrapConnector, diff --git a/UnleashClient/api/__init__.py b/UnleashClient/api/__init__.py index d1fc3dd9..e69de29b 100644 --- a/UnleashClient/api/__init__.py +++ b/UnleashClient/api/__init__.py @@ -1,4 +0,0 @@ -# ruff: noqa: F401 -from .features import get_feature_toggles -from .metrics import send_metrics -from .register import register_client diff --git a/UnleashClient/api/features.py b/UnleashClient/api/features.py deleted file mode 100644 index 0cf97e43..00000000 --- a/UnleashClient/api/features.py +++ /dev/null @@ -1,95 +0,0 @@ -from typing import Optional, Tuple - -import requests -from requests.adapters import HTTPAdapter -from urllib3 import Retry - -from UnleashClient.constants import FEATURES_URL -from UnleashClient.utils import LOGGER, log_resp_info - - -# pylint: disable=broad-except -def get_feature_toggles( - url: str, - app_name: str, - instance_id: str, - headers: dict, - custom_options: dict, - request_timeout: int, - request_retries: int, - project: Optional[str] = None, - cached_etag: str = "", -) -> Tuple[str, str]: - """ - Retrieves feature flags from unleash central server. - - Notes: - * If unsuccessful (i.e. not HTTP status code 200), exception will be caught and logged. - This is to allow "safe" error handling if unleash server goes down. - - :param url: - :param app_name: - :param instance_id: - :param headers: - :param custom_options: - :param request_timeout: - :param request_retries: - :param project: - :param cached_etag: - :return: (Feature flags, etag) if successful, ({},'') if not - """ - try: - LOGGER.info("Getting feature flag.") - - request_specific_headers = { - "UNLEASH-APPNAME": app_name, - "UNLEASH-INSTANCEID": instance_id, - } - - if cached_etag: - request_specific_headers["If-None-Match"] = cached_etag - - base_url = f"{url}{FEATURES_URL}" - base_params = {} - - if project: - base_params = {"project": project} - - adapter = HTTPAdapter( - max_retries=Retry(total=request_retries, status_forcelist=[500, 502, 504]) - ) - with requests.Session() as session: - session.mount("https://", adapter) - session.mount("http://", adapter) - resp = session.get( - base_url, - headers={**headers, **request_specific_headers}, - params=base_params, - timeout=request_timeout, - **custom_options, - ) - - if resp.status_code not in [200, 304]: - log_resp_info(resp) - LOGGER.warning( - "Unleash Client feature fetch failed due to unexpected HTTP status code: %s", - resp.status_code, - ) - raise Exception( - "Unleash Client feature fetch failed!" - ) # pylint: disable=broad-exception-raised - - etag = "" - if "etag" in resp.headers.keys(): - etag = resp.headers["etag"] - - if resp.status_code == 304: - return None, etag - - return resp.text, etag - except Exception as exc: - LOGGER.exception( - "Unleash Client feature fetch failed due to exception: %s", exc - ) - - return None, "" diff --git a/UnleashClient/api/metrics.py b/UnleashClient/api/metrics.py deleted file mode 100644 index 4a94c4d2..00000000 --- a/UnleashClient/api/metrics.py +++ /dev/null @@ -1,58 +0,0 @@ -import json - -import requests - -from UnleashClient.constants import APPLICATION_HEADERS, METRICS_URL -from UnleashClient.utils import LOGGER, log_resp_info - - -# pylint: disable=broad-except -def send_metrics( - url: str, - request_body: dict, - headers: dict, - custom_options: dict, - request_timeout: int, -) -> bool: - """ - Attempts to send metrics to Unleash server - - Notes: - * If unsuccessful (i.e. not HTTP status code 200), message will be logged - - :param url: - :param request_body: - :param headers: - :param custom_options: - :param request_timeout: - :return: true if registration successful, false if registration unsuccessful or exception. - """ - try: - LOGGER.info("Sending messages to with unleash @ %s", url) - LOGGER.info("unleash metrics information: %s", request_body) - - resp = requests.post( - url + METRICS_URL, - data=json.dumps(request_body), - headers={**headers, **APPLICATION_HEADERS}, - timeout=request_timeout, - **custom_options, - ) - - if resp.status_code != 202: - log_resp_info(resp) - LOGGER.warning( - "Unleash Client metrics submission due to unexpected HTTP status code: %s", - resp.status_code, - ) - return False - - LOGGER.info("Unleash Client metrics successfully sent!") - - return True - except requests.RequestException as exc: - LOGGER.warning( - "Unleash Client metrics submission failed due to exception: %s", exc - ) - - return False diff --git a/UnleashClient/api/register.py b/UnleashClient/api/register.py deleted file mode 100644 index 219fc225..00000000 --- a/UnleashClient/api/register.py +++ /dev/null @@ -1,93 +0,0 @@ -import json -from datetime import datetime, timezone -from platform import python_implementation, python_version - -import requests -import yggdrasil_engine -from requests.exceptions import InvalidHeader, InvalidSchema, InvalidURL, MissingSchema - -from UnleashClient.constants import ( - APPLICATION_HEADERS, - CLIENT_SPEC_VERSION, - REGISTER_URL, - SDK_NAME, - SDK_VERSION, -) -from UnleashClient.utils import LOGGER, log_resp_info - - -# pylint: disable=broad-except -def register_client( - url: str, - app_name: str, - instance_id: str, - connection_id: str, - metrics_interval: int, - headers: dict, - custom_options: dict, - supported_strategies: dict, - request_timeout: int, -) -> bool: - """ - Attempts to register client with unleash server. - - Notes: - * If unsuccessful (i.e. not HTTP status code 202), exception will be caught and logged. - This is to allow "safe" error handling if unleash server goes down. - - :param url: - :param app_name: - :param instance_id: - :param metrics_interval: - :param headers: - :param custom_options: - :param supported_strategies: - :param request_timeout: - :return: true if registration successful, false if registration unsuccessful or exception. - """ - registration_request = { - "appName": app_name, - "instanceId": instance_id, - "connectionId": connection_id, - "sdkVersion": f"{SDK_NAME}:{SDK_VERSION}", - "strategies": [*supported_strategies], - "started": datetime.now(timezone.utc).isoformat(), - "interval": metrics_interval, - "platformName": python_implementation(), - "platformVersion": python_version(), - "yggdrasilVersion": yggdrasil_engine.__yggdrasil_core_version__, - "specVersion": CLIENT_SPEC_VERSION, - } - - try: - LOGGER.info("Registering unleash client with unleash @ %s", url) - LOGGER.info("Registration request information: %s", registration_request) - - resp = requests.post( - url + REGISTER_URL, - data=json.dumps(registration_request), - headers={**headers, **APPLICATION_HEADERS}, - timeout=request_timeout, - **custom_options, - ) - - if resp.status_code not in {200, 202}: - log_resp_info(resp) - LOGGER.warning( - "Unleash Client registration failed due to unexpected HTTP status code: %s", - resp.status_code, - ) - return False - - LOGGER.info("Unleash Client successfully registered!") - - return True - except (MissingSchema, InvalidSchema, InvalidHeader, InvalidURL) as exc: - LOGGER.exception( - "Unleash Client registration failed fatally due to exception: %s", exc - ) - raise exc - except requests.RequestException as exc: - LOGGER.exception("Unleash Client registration failed due to exception: %s", exc) - - return False diff --git a/UnleashClient/api/sync_api.py b/UnleashClient/api/sync_api.py new file mode 100644 index 00000000..3b5361c5 --- /dev/null +++ b/UnleashClient/api/sync_api.py @@ -0,0 +1,237 @@ +import json +from datetime import datetime, timezone +from platform import python_implementation, python_version +from typing import Optional, Tuple + +import requests +import yggdrasil_engine +from requests.adapters import HTTPAdapter +from requests.exceptions import InvalidHeader, InvalidSchema, InvalidURL, MissingSchema +from urllib3 import Retry + +from UnleashClient.constants import ( + APPLICATION_HEADERS, + CLIENT_SPEC_VERSION, + FEATURES_URL, + METRICS_URL, + REGISTER_URL, + SDK_NAME, + SDK_VERSION, +) +from UnleashClient.utils import LOGGER, log_resp_info + + +# pylint: disable=broad-except +def register_client( + url: str, + app_name: str, + instance_id: str, + connection_id: str, + metrics_interval: int, + headers: dict, + custom_options: dict, + supported_strategies: dict, + request_timeout: int, +) -> bool: + """ + Attempts to register client with unleash server. + + Notes: + * If unsuccessful (i.e. not HTTP status code 202), exception will be caught and logged. + This is to allow "safe" error handling if unleash server goes down. + + :param url: + :param app_name: + :param instance_id: + :param metrics_interval: + :param headers: + :param custom_options: + :param supported_strategies: + :param request_timeout: + :return: true if registration successful, false if registration unsuccessful or exception. + """ + registration_request = { + "appName": app_name, + "instanceId": instance_id, + "connectionId": connection_id, + "sdkVersion": f"{SDK_NAME}:{SDK_VERSION}", + "strategies": [*supported_strategies], + "started": datetime.now(timezone.utc).isoformat(), + "interval": metrics_interval, + "platformName": python_implementation(), + "platformVersion": python_version(), + "yggdrasilVersion": yggdrasil_engine.__yggdrasil_core_version__, + "specVersion": CLIENT_SPEC_VERSION, + } + + try: + LOGGER.info("Registering unleash client with unleash @ %s", url) + LOGGER.info("Registration request information: %s", registration_request) + + resp = requests.post( + url + REGISTER_URL, + data=json.dumps(registration_request), + headers={**headers, **APPLICATION_HEADERS}, + timeout=request_timeout, + **custom_options, + ) + + if resp.status_code not in {200, 202}: + log_resp_info(resp) + LOGGER.warning( + "Unleash Client registration failed due to unexpected HTTP status code: %s", + resp.status_code, + ) + return False + + LOGGER.info("Unleash Client successfully registered!") + + return True + except (MissingSchema, InvalidSchema, InvalidHeader, InvalidURL) as exc: + LOGGER.exception( + "Unleash Client registration failed fatally due to exception: %s", exc + ) + raise exc + except requests.RequestException as exc: + LOGGER.exception("Unleash Client registration failed due to exception: %s", exc) + + return False + + +# pylint: disable=broad-except +def send_metrics( + url: str, + request_body: dict, + headers: dict, + custom_options: dict, + request_timeout: int, +) -> bool: + """ + Attempts to send metrics to Unleash server + + Notes: + * If unsuccessful (i.e. not HTTP status code 200), message will be logged + + :param url: + :param request_body: + :param headers: + :param custom_options: + :param request_timeout: + :return: true if registration successful, false if registration unsuccessful or exception. + """ + try: + LOGGER.info("Sending messages to with unleash @ %s", url) + LOGGER.info("unleash metrics information: %s", request_body) + + resp = requests.post( + url + METRICS_URL, + data=json.dumps(request_body), + headers={**headers, **APPLICATION_HEADERS}, + timeout=request_timeout, + **custom_options, + ) + + if resp.status_code != 202: + log_resp_info(resp) + LOGGER.warning( + "Unleash Client metrics submission due to unexpected HTTP status code: %s", + resp.status_code, + ) + return False + + LOGGER.info("Unleash Client metrics successfully sent!") + + return True + except requests.RequestException as exc: + LOGGER.warning( + "Unleash Client metrics submission failed due to exception: %s", exc + ) + + return False + + +# pylint: disable=broad-except +def get_feature_toggles( + url: str, + app_name: str, + instance_id: str, + headers: dict, + custom_options: dict, + request_timeout: int, + request_retries: int, + project: Optional[str] = None, + cached_etag: str = "", +) -> Tuple[str, str]: + """ + Retrieves feature flags from unleash central server. + + Notes: + * If unsuccessful (i.e. not HTTP status code 200), exception will be caught and logged. + This is to allow "safe" error handling if unleash server goes down. + + :param url: + :param app_name: + :param instance_id: + :param headers: + :param custom_options: + :param request_timeout: + :param request_retries: + :param project: + :param cached_etag: + :return: (Feature flags, etag) if successful, ({},'') if not + """ + try: + LOGGER.info("Getting feature flag.") + + request_specific_headers = { + "UNLEASH-APPNAME": app_name, + "UNLEASH-INSTANCEID": instance_id, + } + + if cached_etag: + request_specific_headers["If-None-Match"] = cached_etag + + base_url = f"{url}{FEATURES_URL}" + base_params = {} + + if project: + base_params = {"project": project} + + adapter = HTTPAdapter( + max_retries=Retry(total=request_retries, status_forcelist=[500, 502, 504]) + ) + with requests.Session() as session: + session.mount("https://", adapter) + session.mount("http://", adapter) + resp = session.get( + base_url, + headers={**headers, **request_specific_headers}, + params=base_params, + timeout=request_timeout, + **custom_options, + ) + + if resp.status_code not in [200, 304]: + log_resp_info(resp) + LOGGER.warning( + "Unleash Client feature fetch failed due to unexpected HTTP status code: %s", + resp.status_code, + ) + raise Exception( + "Unleash Client feature fetch failed!" + ) # pylint: disable=broad-exception-raised + + etag = "" + if "etag" in resp.headers.keys(): + etag = resp.headers["etag"] + + if resp.status_code == 304: + return None, etag + + return resp.text, etag + except Exception as exc: + LOGGER.exception( + "Unleash Client feature fetch failed due to exception: %s", exc + ) + + return None, "" diff --git a/UnleashClient/connectors/polling_connector.py b/UnleashClient/connectors/polling_connector.py index 7e62cbd4..b33bfe4a 100644 --- a/UnleashClient/connectors/polling_connector.py +++ b/UnleashClient/connectors/polling_connector.py @@ -5,7 +5,7 @@ from apscheduler.triggers.interval import IntervalTrigger from yggdrasil_engine.engine import UnleashEngine -from UnleashClient.api import get_feature_toggles +from UnleashClient.api.sync_api import get_feature_toggles from UnleashClient.cache import BaseCache from UnleashClient.constants import ETAG, FEATURES_URL from UnleashClient.events import UnleashEventType, UnleashFetchedEvent diff --git a/UnleashClient/core/client.py b/UnleashClient/core/client.py index a729320d..1c3d7752 100644 --- a/UnleashClient/core/client.py +++ b/UnleashClient/core/client.py @@ -2,7 +2,7 @@ from dataclasses import asdict from datetime import datetime, timezone from enum import IntEnum -from typing import Any, Dict, Optional, Callable +from typing import Any, Callable, Dict, Optional from UnleashClient.events import ( BaseEvent, diff --git a/UnleashClient/periodic_tasks/send_metrics.py b/UnleashClient/periodic_tasks/send_metrics.py index 55fed20f..8d86756f 100644 --- a/UnleashClient/periodic_tasks/send_metrics.py +++ b/UnleashClient/periodic_tasks/send_metrics.py @@ -3,7 +3,7 @@ import yggdrasil_engine from yggdrasil_engine.engine import UnleashEngine -from UnleashClient.api import send_metrics +from UnleashClient.api.sync_api import send_metrics from UnleashClient.constants import CLIENT_SPEC_VERSION from UnleashClient.utils import LOGGER diff --git a/tests/unit_tests/api/test_feature.py b/tests/unit_tests/api/test_feature.py index a10bd237..d791942a 100644 --- a/tests/unit_tests/api/test_feature.py +++ b/tests/unit_tests/api/test_feature.py @@ -19,7 +19,7 @@ REQUEST_TIMEOUT, URL, ) -from UnleashClient.api import get_feature_toggles +from UnleashClient.api.sync_api import get_feature_toggles from UnleashClient.constants import FEATURES_URL FULL_FEATURE_URL = URL + FEATURES_URL diff --git a/tests/unit_tests/api/test_metrics.py b/tests/unit_tests/api/test_metrics.py index 35ff98d1..d6d941ce 100644 --- a/tests/unit_tests/api/test_metrics.py +++ b/tests/unit_tests/api/test_metrics.py @@ -11,7 +11,7 @@ REQUEST_TIMEOUT, URL, ) -from UnleashClient.api import send_metrics +from UnleashClient.api.sync_api import send_metrics from UnleashClient.constants import METRICS_URL FULL_METRICS_URL = URL + METRICS_URL diff --git a/tests/unit_tests/api/test_register.py b/tests/unit_tests/api/test_register.py index 972f56a7..f17e84d0 100644 --- a/tests/unit_tests/api/test_register.py +++ b/tests/unit_tests/api/test_register.py @@ -14,7 +14,7 @@ REQUEST_TIMEOUT, URL, ) -from UnleashClient.api import register_client +from UnleashClient.api.sync_api import register_client from UnleashClient.constants import CLIENT_SPEC_VERSION, REGISTER_URL FULL_REGISTER_URL = URL + REGISTER_URL