diff --git a/CHANGELOG.md b/CHANGELOG.md index a77f9998..59c8a400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,51 +1,14 @@ ## Unreleased - -> Note: this section of the changelog as-is it's working as a registry of all that -> went on. It needs to be consolidated into what will eventually become the -> final changelog + documentation changes (if any). - -* (Minor): `AsyncUnleashClient` is now exported from the package root: `from UnleashClient import AsyncUnleashClient`. It still requires the `async` extra (`pip install UnleashClient[async]`), and importing it without `aiohttp` raises an `ImportError` asking for it. `import UnleashClient` keeps working without `aiohttp`, so nothing changes for code using `UnleashClient`. -* (Bugfix): `UnleashClient` can be imported again without the optional `aiohttp` dependency. Importing the package used to fail with an `ImportError` asking for `UnleashClient[async]`, even for code that never touches the asynchronous client. Only the asynchronous client requires `aiohttp` now, and importing it without `aiohttp` still raises that `ImportError`. -* (Minor): The in-progress asynchronous client now exposes `feature_definitions()`. It returns the same dict as `UnleashClient`, keyed by feature name with each toggle's `type` and `project`. It is a plain method, not a coroutine, and it does not wait for the server: before the first fetch it answers from the cached state, and without any state it returns an empty dict. Every method on the client is now implemented. The client is still unexported. Nothing changes for code using `UnleashClient`. -* (Minor): The in-progress asynchronous client can now resolve variants. `get_variant()` returns the same variant dict as `UnleashClient` and emits the same impression events. It is a plain method, not a coroutine, and it does not wait for the server: before the first fetch it answers from the cached state, and a toggle the client does not know resolves to the disabled variant. When an initialized client is asked for a toggle it does not know, it logs at `verbose_log_level` that the client does not know the toggle. `feature_definitions()` still raises `NotImplementedError`. The client is still unexported. Nothing changes for code using `UnleashClient`. -* (Minor): The in-progress asynchronous client can now evaluate feature toggles. `is_enabled()` resolves a toggle against the feature state the client holds, with the same results, `fallback_function` handling and impression events as `UnleashClient`. It is a plain method, not a coroutine, and it does not wait for the server: before the first fetch it answers from the cached state, and a toggle the client does not know resolves to the fallback's answer, or to false without one. `get_variant()` and `feature_definitions()` still raise `NotImplementedError`. The client is still unexported. Nothing changes for code using `UnleashClient`. -* (Minor): The in-progress asynchronous client can now be initialized and shut down. `await initialize_client()` registers with the server, loads the cached feature state, polls every `refresh_interval` seconds and reports metrics every `metrics_interval` seconds, all on the running event loop. It returns without waiting for the server: the first fetch runs one refresh interval later, and until then the client holds the cached state. Like `UnleashClient`, it clears the cached ETag first, so the first fetch is unconditional. Polling is the only mode: `fetch_toggles` is accepted for parity but has no effect, `experimental_mode` is ignored, and there is no bootstrapping. `await destroy()` stops polling, sends the remaining metrics, closes the connection pool and is safe to call more than once. A `destroy()` that runs while registration is still in flight leaves nothing polling. The client also works as an async context manager (`async with AsyncUnleashClient(...) as client:`). `is_enabled()`, `get_variant()` and `feature_definitions()` still raise `NotImplementedError`. The client is still unexported. Nothing changes for code using `UnleashClient`. - -* (Minor): The in-progress asynchronous client gains an internal `_AsyncPollingConnector`, in the private `UnleashClient.connectors._async_connector` module. The module is not part of the public API and may change or disappear without notice. It fetches feature state over the asynchronous transport on its own scheduler. Starting it loads the cached state and returns without waiting for the server; the first fetch runs one refresh interval later. With a non-empty cache, READY is therefore emitted when the connector starts rather than after the first fetch. The asynchronous client does not use it yet. Nothing changes for code using `UnleashClient`. -* (Minor): New `UnleashClient.errors` module with a hierarchy of SDK errors. `UnleashClientError` is the base of every error in the hierarchy, so catching it catches all of them without catching `Exception`. Each module gets one error grouping that module's errors, such as `InstanceRegistryError`, and specific errors derive from those, such as `MultipleInstancesNotAllowedError`. A client rejected under `InstanceAllowType.BLOCK` now raises `MultipleInstancesNotAllowedError` instead of a plain `Exception`, with the same message. It still derives from `Exception`, so existing `except Exception` handlers keep catching it. `TransportError` groups the errors raised while talking to the Unleash server. Other errors raised by the SDK are not part of the hierarchy yet. -* (Bugfix): The in-progress asynchronous metrics reporter keeps impact metrics whose send timed out or was cancelled, and sends them with the next flush. A timed-out send now counts as a failed send, like any status other than 202. Cancelling the reporter mid-send, as `stop()` does, used to lose the impact metrics of that send; they now go out with the final flush. Feature metrics from a failed send are still dropped, as before. `UnleashClient` is unaffected. -* (Minor): The in-progress asynchronous client gains an internal `_AsyncScheduler`, in the private `UnleashClient._async_scheduler` module, which runs recurring jobs as tasks on the event loop with the same interval, first-run delay and one-sided jitter as the synchronous `_Scheduler`. The module is not part of the public API and may change or disappear without notice. The asynchronous metrics reporter schedules its sends through it. Nothing changes for code using `UnleashClient`. -* (Minor): The in-progress async transport can no longer be used once it has been closed. Closing it used to only drop its connection pool, so a later request quietly opened a fresh session that nobody would ever close. Closing now takes the transport out of service, and feature fetches, registration and metrics submissions each raise `AlreadyClosedError`, importable from `UnleashClient.errors`. It is part of the SDK error hierarchy, under the new `TransportError`, so catching `UnleashClientError` catches it too. Closing a transport that is already closed still does nothing. `UnleashClient` is unaffected. -* (Minor): The Sphinx documentation site is no longer built or published. It was served from a GitHub Pages custom domain that stopped resolving, so nothing published there had been reachable for some time. Reference documentation for this SDK is at https://docs.getunleash.io/reference/sdks/python, and `README.md` covers installation, usage, configuration options, custom strategies, custom caches, event callbacks and the WSGI and Celery notes. Contributor setup and the release checklist are in `DEVELOPMENT.md`. The `Documentation` URL in the package metadata now points at the docs site rather than a page that returned 404. -* (Minor): `AsyncUnleashClient` ignores `custom_options`. The constructor still accepts it and it still reaches the shared configuration, but the async request path never passes it to the HTTP library, so the async client's surface does not depend on aiohttp's own keyword arguments. `UnleashClient` is unaffected and keeps passing `custom_options` to `requests`. -* (Minor): Duplicate-instance detection now happens through one internal `_InstanceRegistry`, in the private `UnleashClient._instance_registry` module, instead of an `InstanceCounter` and a private method on the client. The identifier a client is keyed by, the message, the error logged under `InstanceAllowType.WARN` and the counting are exactly what they were, and a client rejected under `BLOCK` is still not counted. `INSTANCES` keeps its name, both its import paths and all of its methods, and remains the supported way to reach the registry. The module is not part of the public API and may change or disappear without notice. `multiple_instance_mode` is unchanged. `UnleashClient.utils.InstanceCounter` is gone, so this affects code importing that class directly. Nothing changes for code using `UnleashClient`. -* (Minor): The in-progress asynchronous client now performs the same duplicate-instance check and accepts `multiple_instance_mode`. Both flavors register into one process-wide registry, so a synchronous and an asynchronous client sharing an app name and instance id are reported as duplicates of each other. This is accurate, since both register with the server and both report metrics. Nothing changes for code using `UnleashClient`. -* (Minor): The in-progress asynchronous client now builds an internal `_AsyncMetricsReporter`, in the private `UnleashClient._metrics` module, over an internal `_AsyncTransport`, in the private `UnleashClient._async_transport` module, which is not part of the public API and may change or disappear without notice, and exposes `impact_metrics` like the synchronous client does. Nothing starts it yet, as `initialize_client()` still raises `NotImplementedError`, so no metrics are sent, and nothing changes for code using `UnleashClient`. The request body, the impact-metrics collection and the restore-after-a-failed-send are shared with the synchronous reporter; only the request itself and the recurring schedule are separate. -* (Minor): The asynchronous reporter schedules its sends through the asynchronous client's `_AsyncScheduler` rather than the APScheduler-backed scheduler, because the send has to be awaited and APScheduler runs jobs on worker threads that cannot await. Like the synchronous reporter, it reads `unleash_metrics_interval` and `unleash_metrics_jitter` once, when reporting starts, and the interval and the one-sided jitter are identical. `UnleashClient` and its scheduler are unaffected, and the `scheduler` and `scheduler_executor` constructor arguments still work exactly as before. -* (Minor): The asynchronous client does not carry over `metrics_headers` or `metric_job`. `metrics_headers` has been informational since the `_Transport` change below (set `unleash_custom_headers` instead), and the metrics job handle is internal. Both are unchanged on `UnleashClient`. -* (Minor): Metrics reporting now happens through one internal `_MetricsReporter` object, in the private `UnleashClient._metrics` module, which is not part of the public API and may change or disappear without notice, instead of being spread across the module-level `aggregate_and_send_metrics`, the job registration in `initialize_client()` and a second, slightly different call in `destroy()`. The request body is now assembled by `_build_metrics_payload` in the private `UnleashClient._payloads` module, alongside the registration payload. The interval, the jitter, the fields sent and the impact-metrics restore-on-failure are exactly what they were, apart from the two entries below. `UnleashClient.periodic_tasks` and its `aggregate_and_send_metrics` are gone, so this affects code importing from `UnleashClient.periodic_tasks` directly. `metric_job`, `metrics_headers` and `impact_metrics` are unchanged on `UnleashClient`. Nothing changes for code using `UnleashClient`. -* (Minor): The metrics request body is now read from the configuration on every send. As a result, reassigning `unleash_app_name`, `unleash_instance_id`, `unleash_sdk_flavor` or `unleash_sdk_flavor_version` after `initialize_client()` now takes effect on the next metrics send, where those used to be captured when the job was registered and changes to them afterwards were silently ignored. This mirrors the read-through `_Transport` already does for urls and headers. `unleash_metrics_interval` and `unleash_metrics_jitter` are still read once, when the job is registered, as before. -* (Bugfix): The metrics flush on `destroy()` now sends `sdkFlavor` and `sdkFlavorVersion` like every other metrics send. The final send of a client's life used to omit them. Only affects clients configured with `sdk_flavor`. -* (Minor): `ImpactMetrics` gains `collect()` and `restore()`, so draining impact metrics for a send and handing them back after a failed one go through the object that owns them rather than reaching into the engine directly. -* (Minor): Flag evaluation now happens through one internal `_Evaluator` object instead of being implemented on the client. `is_enabled()`, `get_variant()` and `feature_definitions()` keep their signatures, their return shapes and the impression events they emit. Nothing changes for code using `UnleashClient`. The in-progress asynchronous client is wired with the same object but does not expose it yet: its `is_enabled()`, `get_variant()` and `feature_definitions()` raise `NotImplementedError` alongside the rest of its surface until it can initialize. -* (Minor): Custom strategies are now registered on the engine by `initialize_client()` rather than by the constructor. The `ValueError` raised for a strategy that has no `apply` method, or whose `apply` does not take exactly two parameters, therefore surfaces from `initialize_client()` instead of from `UnleashClient(...)`. Code that passes valid strategies and calls `initialize_client()` before evaluating anything is unaffected; a bootstrapped client that evaluates a custom-strategy flag before initializing now gets `False` for that strategy. -* (Minor): New optional `async` extra: `pip install UnleashClient[async]` installs `aiohttp` for the in-progress asynchronous client. The default install is unchanged and pulls in no new packages. The asynchronous client itself is still unfinished and unexported. -* (Minor): Outbound HTTP now happens through one internal `_Transport` object, in the private `UnleashClient._transport` module, instead of the three module-level functions in `UnleashClient.api`. The module is not part of the public API and may change or disappear without notice. The requests on the wire (urls, methods, headers, bodies, status handling, the retry adapter on feature fetches, and the fatal-URL exceptions that registration re-raises) are exactly what they were. `UnleashClient.api` and its `get_feature_toggles`, `send_metrics`, `register_client` and `build_normalized_url` are gone, as is `UnleashClient.utils.log_resp_info`; `PollingConnector` now takes a `transport` instead of `url`, `app_name`, `instance_id`, `headers`, `custom_options`, `request_timeout`, `request_retries` and `project`, and `aggregate_and_send_metrics` takes one in first position instead of `url`, `headers`, `custom_options` and `request_timeout`. This only affects code importing from `UnleashClient.api`, `UnleashClient.connectors` or `UnleashClient.periodic_tasks` directly. Nothing changes for code using `UnleashClient`. -* (Minor): `_Transport` asks the internal `_HeaderFactory`, in the private `UnleashClient._headers` module, which is not part of the public API and may change or disappear without notice, for the header set each request needs, rather than being handed a dict built once at startup. As a result, reassigning `unleash_url`, `unleash_custom_headers`, `unleash_custom_options`, `unleash_request_timeout`, `unleash_request_retries`, `unleash_project_name`, `unleash_app_name` or `unleash_instance_id` after `initialize_client()` now takes effect on the next poll and the next metrics send. Those used to be captured when the connector and the metrics job were created, and changes to them afterwards were silently ignored. Registration always read them at call time and is unaffected. The headers on the wire are otherwise unchanged. -* (Minor): The `metrics_headers` attribute has been removed from `UnleashClient`. It used to hold the header dict handed to the metrics job at `initialize_client()`, so reassigning it changed what the metrics request sent; the `_Transport` now builds that header set per request. Set `unleash_custom_headers` instead, as it is read on every send. -* (Minor): The registration request body is now assembled by `_build_register_payload` in the new private `UnleashClient._payloads` module rather than inline in `register_client`. The module is not part of the public API and may change or disappear without notice. The fields sent are unchanged, `started` is still stamped at the moment of the request, and nothing changes for code using `UnleashClient`. -* (Minor): Job scheduling now happens through one internal `_Scheduler` object, in the private `UnleashClient._scheduler` module, instead of being re-implemented by the client and each connector. The module is not part of the public API and may change or disappear without notice. The jobs, intervals, jitter and executors are exactly what they were. `PollingConnector` and `OfflineConnector` now take that `_Scheduler` rather than an APScheduler instance, and no longer take `scheduler_executor`, so code importing from `UnleashClient.connectors` directly must build one. The `scheduler` and `scheduler_executor` constructor arguments, and the `unleash_scheduler` and `unleash_executor_name` attributes, are unchanged. -* (Minor): Passing a `scheduler` that is already running no longer raises `SchedulerAlreadyRunningError`. Starting an already-started scheduler is now a no-op. -* (Minor): The unused `fl_job` attribute has been removed from `UnleashClient`. It was always `None`, since the connectors own their own jobs, and nothing read it. -* (Minor): Upgraded to yggdrasil-engine 2.0. Counting toggle and variant evaluations, and deciding whether an impression event is due, now happen inside the engine rather than in the SDK. `is_enabled()` and `get_variant()` return the same types as before, so no calling code needs to change. -* (Minor): A `fallback_function` that raises now results in `False` and a logged warning, instead of the exception propagating out of `is_enabled()`. The toggle is not counted in that case. -* (Minor): Event callbacks are now invoked on a dedicated background thread instead of on whichever thread produced the event. `is_enabled()` and `get_variant()` no longer wait for your callback, so a slow callback can't hold up flag evaluation. Three consequences worth knowing about: callbacks can no longer read thread local state from the caller (Flask `g`, the current Django request, contextvars); they return before the callback has run, so tests asserting straight after the call now need to wait; and reassigning `unleash_event_callback` after construction is no longer honoured. -* (Minor): Connectors take an internal `_EventDispatcher`, in the private `UnleashClient._event_dispatcher` module, which is not part of the public API and may change or disappear without notice, instead of `ready_callback`/`event_callback`. Connectors aren't part of the documented API, so this only affects code importing from `UnleashClient.connectors` directly. -* (Minor): Request headers are now assembled once, by an internal `_HeaderFactory`, in the private `UnleashClient._headers` module, which is not part of the public API and may change or disappear without notice, and passed to each collaborator complete. The headers on the wire are unchanged. `PollingConnector` and `StreamingConnector` no longer add `unleash-interval` and `Accept`/`Content-Type`/`Unleash-Client-Spec` themselves, so code importing from `UnleashClient.connectors` directly must now supply complete headers. Nothing changes for code using `UnleashClient`. -* (Minor): Applying feature state (the cache write, the handover to the engine, and the READY and FETCHED events that follow) now happens in one internal `_FeatureStore`, in the private `UnleashClient._feature_store` module, instead of being re-implemented by each connector. The module is not part of the public API and may change or disappear without notice. The cache writes, engine updates and events are exactly what they were. Connectors now take a `store` instead of `engine`, `cache` and `events`, and `BaseConnector.load_features()` is gone, so code importing from `UnleashClient.connectors` directly must build a `_FeatureStore` and call `store.load_from_cache()`. Nothing changes for code using `UnleashClient`. -* (Minor): The `engine` and `cache` attributes are gone from `UnleashClient`. Both were always configured through the constructor (pass `cache=` to supply your own), and neither appears in the documented API. -* (Bugfix): `refresh_jitter` now reaches the polling job. It was accepted by the constructor, documented, and applied to the offline refresh job, but never passed to the polling connector, so jitter was silently dropped in the default polling mode. -* (Minor): Constructor arguments are now normalized once into an internal `UnleashConfig` object rather than being copied onto the client attribute by attribute. The public `unleash_*` attributes keep their exact values and stay writable, reading and writing through that object, so nothing in calling code needs to change. This is groundwork for an asynchronous client that shares the same configuration handling. +* (Minor): New `AsyncUnleashClient`. +* (Minor): New `UnleashClient.errors` module. A duplicate client under `InstanceAllowType.BLOCK` now raises `MultipleInstancesNotAllowedError`. +* (Minor): A `fallback_function` that raises now makes `is_enabled()` return `False` instead of raising. +* (Minor): Invalid custom strategies now raise from `initialize_client()` instead of the constructor. +* (Minor): Upgraded to yggdrasil-engine 2.0. +* (Bugfix): Event callbacks run on a background thread. +* (Bugfix): `refresh_jitter` now applies in polling mode. +* (Bugfix): Changing `unleash_*` attributes after `initialize_client()` now takes effect. +* (Bugfix): The final metrics send on `destroy()` now includes `sdkFlavor` and `sdkFlavorVersion`. +* (Patch): Removed modules and attributes that aren't part of the public API. ## v6.7.0 * (Minor): Support for CIDR, Semver GTE and LTE constraints