From d32a599f7d7926235c108eb82f60d791982ea8c3 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 27 Aug 2024 16:12:55 +0100 Subject: [PATCH] FFM-11935 Add `with_httpx_args` option (#106) * {client,config}: allow passing through additional httpx arguments (#105) * FFM-11935 Fix line length for flake8 / Update docs and add sample. * FFM-11935 Update docstring and flake8 formatting change --------- Co-authored-by: Zane van Iperen <162071496+zane-zeroflucs@users.noreply.github.com> --- docs/further_reading.md | 32 +++++++++++++++++ .../with_httpx_args.py | 35 +++++++++++++++++++ featureflags/__init__.py | 2 +- featureflags/analytics.py | 2 +- featureflags/client.py | 8 +++-- featureflags/config.py | 23 ++++++++++-- featureflags/streaming.py | 3 +- setup.cfg | 2 +- setup.py | 2 +- 9 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 examples/with_httpx_args_example/with_httpx_args.py diff --git a/docs/further_reading.md b/docs/further_reading.md index 24496ab..7849287 100644 --- a/docs/further_reading.md +++ b/docs/further_reading.md @@ -109,3 +109,35 @@ Then pass them with the new URLs when creating your client. [Example](../examples/url_change_example/url_change.py) +## HTTPX Configuration Options +The `httpx` client allows you to apply various configurations to all outgoing requests by passing parameters to the Client constructor. + +Here are some of the options you can configure: + +* `proxies`: Configure proxy settings to route requests through a specific proxy server. +* `headers`: Add custom headers to all outgoing requests. +* `timeout`: Set request and connection timeouts. +* `transport`: Use a custom transport layer for advanced scenarios. + +**Important**: ensure you supply a valid httpx option. if you supply an option that doesn't exist in `httpx` the SDK will fail to initialize with `got an unexpected keyword argument` + +Further Reading: + +* [HTTPX Advanced Client Configuration](https://www.python-httpx.org/advanced/clients/) + +Example of Custom HTTPX Client Configuration + +Here's an example demonstrating how to use httpx client arguments in the SDK: + + +```python +from featureflags.config import with_httpx_args + +client = CfClient(api_key, + with_base_url("https://config.ff.harness.io/api/1.0"), + with_httpx_args({ + 'proxies': 'http://localhost:8888'} + })) +``` + +[Example](../examples/with_httpx_args_example/with_httpx_args.py) diff --git a/examples/with_httpx_args_example/with_httpx_args.py b/examples/with_httpx_args_example/with_httpx_args.py new file mode 100644 index 0000000..1aaf3b5 --- /dev/null +++ b/examples/with_httpx_args_example/with_httpx_args.py @@ -0,0 +1,35 @@ +import logging +import time + +from featureflags.config import with_httpx_args, with_base_url +from featureflags.evaluations.auth_target import Target +from featureflags.client import CfClient +from featureflags.util import log + + +def main(): + log.setLevel(logging.INFO) + log.info("Starting example") + api_key = "Your API key" + + # Using the httpx proxies option. + # Ensure you supply a valid httpx option. if you supply an option that + # doesn't exist in `httpx` the SDK will fail to initialize with `got an + # unexpected keyword argument` + client = CfClient(api_key, + with_httpx_args({'proxies': 'http://localhost:8888'})) + + client.wait_for_initialization() + + target = Target(identifier='HT_1', name="Harness_Target_1", + attributes={"location": "emea"}) + + while True: + result = client.bool_variation('identifier_of_your_bool_flag', target, + False) + log.info("Result %s", result) + time.sleep(10) + + +if __name__ == "__main__": + main() diff --git a/featureflags/__init__.py b/featureflags/__init__.py index 87311e3..ae7a79f 100644 --- a/featureflags/__init__.py +++ b/featureflags/__init__.py @@ -2,4 +2,4 @@ __author__ = """Harness""" __email__ = "support@harness.io" -__version__ = '1.6.4' +__version__ = '1.7.0' diff --git a/featureflags/analytics.py b/featureflags/analytics.py index 4c8d0a8..84cb2db 100644 --- a/featureflags/analytics.py +++ b/featureflags/analytics.py @@ -39,7 +39,7 @@ VARIATION_VALUE_ATTRIBUTE = 'variationValue' TARGET_ATTRIBUTE = 'target' SDK_VERSION_ATTRIBUTE = 'SDK_VERSION' -SDK_VERSION = '1.6.4' +SDK_VERSION = '1.7.0' SDK_TYPE_ATTRIBUTE = 'SDK_TYPE' SDK_TYPE = 'server' SDK_LANGUAGE_ATTRIBUTE = 'SDK_LANGUAGE' diff --git a/featureflags/client.py b/featureflags/client.py index 3cc845a..617819d 100644 --- a/featureflags/client.py +++ b/featureflags/client.py @@ -23,7 +23,7 @@ from .streaming import StreamProcessor from .util import log -VERSION: str = "1.6.4" +VERSION: str = "1.7.0" class MissingOrEmptyAPIKeyException(Exception): @@ -177,7 +177,8 @@ def authenticate(self): verify = self._config.tls_trusted_cas_file client = Client(base_url=self._config.base_url, verify_ssl=verify, - raise_on_unexpected_status=True) + raise_on_unexpected_status=True, + httpx_args=self._config.httpx_args) body = AuthenticationRequest(api_key=self._sdk_key) response = retryable_authenticate(client=client, body=body).parsed self._auth_token = response.auth_token @@ -206,7 +207,8 @@ def make_client(self, url, token, account_id, config): client = AuthenticatedClient( base_url=url, token=token, - verify_ssl=verify + verify_ssl=verify, + httpx_args=self._config.httpx_args, ) # Additional headers used to track usage additional_headers = { diff --git a/featureflags/config.py b/featureflags/config.py index 43ccb46..85d55a0 100644 --- a/featureflags/config.py +++ b/featureflags/config.py @@ -1,7 +1,7 @@ """Configuration is a base class that has default values that you can change during the instance of the client class""" -from typing import Callable +from typing import Any, Callable, Dict from .interface import Cache from .lru_cache import LRUCache @@ -28,7 +28,8 @@ def __init__( enable_stream: bool = True, enable_analytics: bool = True, max_auth_retries: int = 10, - tls_trusted_cas_file: str = None + tls_trusted_cas_file: str = None, + httpx_args: Dict[str, Any] = None, ): self.base_url = base_url self.events_url = events_url @@ -49,6 +50,9 @@ def __init__( self.enable_analytics = enable_analytics self.max_auth_retries = max_auth_retries self.tls_trusted_cas_file = tls_trusted_cas_file + self.httpx_args = httpx_args + if self.httpx_args is None: + self.httpx_args = {} default_config = Config() @@ -102,7 +106,22 @@ def with_tls_trusted_cas_file(value: str) -> Callable: It takes a filename of a CA bundle. It should include all intermediate CAs and the root CA (concatenated in PEM format). """ + def func(config: Config) -> None: config.tls_trusted_cas_file = value return func + + +""" + Allows the user to pass additional arguments to the HTTPx client + configuration, such as proxies, timeouts, or custom headers. See + https://www.python-httpx.org/advanced/clients/ for further information. +""" + + +def with_httpx_args(args: Dict[str, Any]) -> Callable: + def func(config: Config) -> None: + config.httpx_args.update(args) + + return func diff --git a/featureflags/streaming.py b/featureflags/streaming.py index 567aea8..11e037b 100644 --- a/featureflags/streaming.py +++ b/featureflags/streaming.py @@ -168,7 +168,8 @@ def run(self): cluster=self._cluster).parsed if fc is None: - log.debug("Feature config '%s' not loaded", self._msg.identifier) + log.debug("Feature config '%s' not loaded", + self._msg.identifier) else: log.debug("Feature config '%s' loaded", fc.feature) self._repository.set_flag(fc) diff --git a/setup.cfg b/setup.cfg index e581259..26d1744 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.6.4 +current_version = 1.7.0 commit = True tag = True diff --git a/setup.py b/setup.py index 4be0a48..5ce28f6 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/harness/ff-python-server-sdk", - version='1.6.4', + version='1.7.0', zip_safe=False, )