diff --git a/packages/claude-agents/README.md b/packages/claude-agents/README.md index 41bde8a0..670742be 100644 --- a/packages/claude-agents/README.md +++ b/packages/claude-agents/README.md @@ -137,10 +137,10 @@ asyncio.run(main()) ```python import os -from launchdarkly_ai_server import FDv2SkillStore, config, set_skill_store +from launchdarkly_ai_server import LaunchDarklySkillStore, config, set_skill_store from launchdarkly_ai_claude_agents import create_claude_agents_skills_handler -store = FDv2SkillStore(os.environ["LD_SDK_KEY"]).start() +store = LaunchDarklySkillStore(os.environ["LD_SDK_KEY"]).start() store.wait_for_skills(timeout=10) set_skill_store(store) diff --git a/packages/claude-agents/src/launchdarkly_ai_claude_agents/skills.py b/packages/claude-agents/src/launchdarkly_ai_claude_agents/skills.py index 9d2b56f4..6b8375ee 100644 --- a/packages/claude-agents/src/launchdarkly_ai_claude_agents/skills.py +++ b/packages/claude-agents/src/launchdarkly_ai_claude_agents/skills.py @@ -155,10 +155,10 @@ def create_claude_agents_skills_handler( import os - from launchdarkly_ai_server import FDv2SkillStore, config, set_skill_store + from launchdarkly_ai_server import LaunchDarklySkillStore, config, set_skill_store from launchdarkly_ai_claude_agents import create_claude_agents_skills_handler - store = FDv2SkillStore(os.environ["LD_SDK_KEY"]).start() + store = LaunchDarklySkillStore(os.environ["LD_SDK_KEY"]).start() store.wait_for_skills(timeout=10) set_skill_store(store) diff --git a/packages/client/README.md b/packages/client/README.md index 89ba2d07..dd3ef840 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -33,8 +33,8 @@ No code changes are required — `init_client()` detects the packages at runtime | Variable | Required | Description | |---|---|---| | `LD_SDK_KEY` | Yes | LaunchDarkly server-side SDK key | -| `LD_BASE_URI` | No | Override the LaunchDarkly polling base URI (e.g. for staging). `FDv2SkillStore` reads it too when given no URI option | -| `LD_STREAM_URI` | No | Override the streaming URI. `FDv2SkillStore` reads it too when given no URI option | +| `LD_BASE_URI` | No | Override the LaunchDarkly polling base URI (e.g. for staging). `LaunchDarklySkillStore` reads it too when given no URI option | +| `LD_STREAM_URI` | No | Override the streaming URI. `LaunchDarklySkillStore` reads it too when given no URI option | | `LD_EVENTS_URI` | No | Override the events URI | | `LD_SERVICE_NAME` | No | OTel `service.name` resource attribute (default: `python-sdk`) | | `LD_ENVIRONMENT` | No | `deployment.environment` resource attribute attached to telemetry | @@ -670,8 +670,8 @@ worth alerting on too. verification passes, and the usual cause is a bug in a custom `SkillStore` adapter (a stale cache entry, a colliding key, a wrong index lookup) rather than tampering, so neither inflates LaunchDarkly's integrity counter. Both still write this record, so a rule on -`ld.skills.integrity_failure` catches them. If `FDv2SkillStore` is your only store, treat them -like `hash_mismatch`; behind a custom adapter, suspect the adapter first. +`ld.skills.integrity_failure` catches them. If `LaunchDarklySkillStore` is your only store, +treat them like `hash_mismatch`; behind a custom adapter, suspect the adapter first. `get_skill` returns `None` for a `version_mismatch` like any other failure, so this record is the only place it is visible unless you use `get_skill_result`, which reports it as the @@ -725,16 +725,16 @@ be resolved and log a count at WARN. Use `get_skill_result` per key when you nee #### Receiving skills from LaunchDarkly `InMemorySkillStore` is for tests and bring-your-own-content. In production, skill content -arrives through `FDv2SkillStore`, which uses LaunchDarkly's SDK-facing FDv2 delivery channel +arrives through `LaunchDarklySkillStore`, which uses LaunchDarkly's SDK-facing FDv2 delivery channel (the `GET /sdk/poll` and `GET /sdk/stream` endpoints the base SDK's FDv2 data source uses), authenticated with the environment's server-side SDK key. ```python import os -from launchdarkly_ai_server import FDv2SkillStore, set_skill_store, watch_skills +from launchdarkly_ai_server import LaunchDarklySkillStore, set_skill_store, watch_skills -store = FDv2SkillStore(os.environ["LD_SDK_KEY"]).start() +store = LaunchDarklySkillStore(os.environ["LD_SDK_KEY"]).start() if not store.wait_for_skills(timeout=10): # No payload arrived. Reconciling now would find an empty store; see below. print(f"skill delivery has not answered yet: {store.failed or 'still waiting'}") @@ -752,9 +752,9 @@ finally: **A reconcile that runs before delivery answers does not prune.** A store still waiting for its first payload looks the same as an environment with no skills, and `write_skills("*")` would otherwise treat that as every skill revoked and delete the files from a previous run. -`FDv2SkillStore` reports readiness through the optional `is_initialized()`; until it is true, a -reconcile reports the retrieval unavailable (`report.ok` is `False`, and the error says why) -and leaves the disk alone. +`LaunchDarklySkillStore` reports readiness through the optional `is_initialized()`; until it +is true, a reconcile reports the retrieval unavailable (`report.ok` is `False`, and the error +says why) and leaves the disk alone. **A 422 means this connection will never be assigned a skill payload, and delivery stops.** Every request declares the payload it wants (`kinds=agent-skill`), and LaunchDarkly answers @@ -914,7 +914,7 @@ that skips verification. | `write_skills(skills, root, *, prune=True, timeout=10.0, on_unavailable="keep")` | Materialize skills under `root`, returning a `ReconcileReport`. `skills` is a sequence of `Skill` / `SkillReference` / key strings, `"*"`, or a `SkillSource`, which is read once; a source that raises or answers anything malformed is an unavailable retrieval. `prune` removes formerly-managed skills no longer requested. `on_unavailable="raise"` raises instead of reporting when content cannot be retrieved. Raises `ValueError` for an unusable root, a negative or non-finite `timeout`, or an unrecognised `on_unavailable`. `timeout=0` is valid and makes every skill report an error. **Performs synchronous filesystem I/O — see the note below.** | | `SkillStore` | The structural interface content arrives through: `get_object(kind, key, version=None)`, `all_objects(kind)`, optional `is_initialized()`, `add_listener(kind, fn)` / `remove_listener(kind, fn)`. A store without `is_initialized()` is treated as initialized. Both shipped stores deliver only the skill kind, so `add_listener` on any other kind raises. | | `InMemorySkillStore(objects=None)` | A dict-backed store with `put(raw)`, for local development and testing. Holds several versions of a key. | -| `FDv2SkillStore(sdk_key, *, base_uri=…, stream_uri=…, mode="stream", …)` | The delivery transport: a store fed by LaunchDarkly over the SDK-facing FDv2 channel. `start()`, `wait_for_skills(timeout)`, `is_initialized()`, `close()`, `diagnostics`, `failed`; also a context manager. `close()` is **final** — `start()` afterwards raises. `poll_interval`, `read_timeout`, `initial_backoff` and `max_backoff` must be positive and finite, and `initial_backoff` may not exceed `max_backoff`. **Server-side only.** See *Receiving skills from LaunchDarkly* above. | +| `LaunchDarklySkillStore(sdk_key, *, base_uri=…, stream_uri=…, mode="stream", …)` | The delivery transport: a store fed by LaunchDarkly over the SDK-facing FDv2 channel. `start()`, `wait_for_skills(timeout)`, `is_initialized()`, `close()`, `diagnostics`, `failed`; also a context manager. `close()` is **final** — `start()` afterwards raises. `poll_interval`, `read_timeout`, `initial_backoff` and `max_backoff` must be positive and finite, and `initial_backoff` may not exceed `max_backoff`. **Server-side only.** See *Receiving skills from LaunchDarkly* above. | | `watch_skills(skills, root, *, debounce=0.5, on_reconcile=None, …)` | `write_skills` plus a re-reconcile on every delivery change, and, for a `SkillSource`, on every change to one of its `config_keys`, so revocation takes effect once changes have been quiet for `debounce` rather than at the next restart. `watcher.notify()` schedules a reconcile through the same debounce. Returns `(initial report, SkillWatcher)`; close the watcher when done. `debounce` is in **seconds**, non-negative and finite. `on_reconcile` receives each *subsequent* report. One watcher per root. | | `StoreDiagnostics` | What the transport has seen: `payloads_transferred`, `skill_objects_received`, `objects_ignored`, `objects_revoked`, `payloads_ignored`, `hashless_objects`, `connection_failures`, `last_error`. | diff --git a/packages/client/agents.md b/packages/client/agents.md index 499f6435..3eea9a15 100644 --- a/packages/client/agents.md +++ b/packages/client/agents.md @@ -31,7 +31,7 @@ No other `launchdarkly-ai-*` package may define or duplicate these. They import | `src/launchdarkly_ai_server/types_validation.py` | `parse_ai_config` — validates flag variation shape; `is_valid_skill_key` / `is_valid_skill_version` / `skill_key_rejection_reason` (the canonical key-grammar explanation every layer quotes) | | `src/launchdarkly_ai_server/skills.py` | Agent Skills, retrieval half — `skill_refs`, `get_skill`/`get_skills`/`all_skills`, `InMemorySkillStore`, and the store/telemetry injection points `_set_store` / `_set_emitter_for_testing` | | `src/launchdarkly_ai_server/skills_core.py` | Shared skills internals — the `SkillStore` seam, module state, the telemetry seam and its three recorders, integrity verification, and store resolution. Imported by both `skills.py` and the materialization layer; imports neither | -| `src/launchdarkly_ai_server/skills_fdv2.py` | Agent Skills, delivery transport — the FDv2 protocol, the wire-key/`version` translation, the held object set, and `FDv2SkillStore`. Sits **below** the store interface; imports `skills_core` only, and nothing imports it | +| `src/launchdarkly_ai_server/skills_fdv2.py` | Agent Skills, delivery transport — the FDv2 protocol, the wire-key/`version` translation, the held object set, and `LaunchDarklySkillStore`. Sits **below** the store interface; imports `skills_core` only, and nothing imports it | | `src/launchdarkly_ai_server/skills_watch.py` | Agent Skills, eager re-reconcile — `watch_skills` / `SkillWatcher`, wiring the store's change listener, and for a `SkillSource` the base client's `flag_tracker` listener, to `write_skills`. Sits **above** `skills_fs` and modifies none of it | | `src/launchdarkly_ai_server/skills_source.py` | Agent Skills, config-scoped requested set — `config_skills(key, context)`, which evaluates one AI Config per read and classifies every failure as a raised read, never `[]`. With `skills_watch`, the only skills module that imports `lifecycle`, and it takes only `get_client` | | `src/launchdarkly_ai_server/skills_fs.py` | Agent Skills, materialization half — `write_skills`, request resolution, the manifest format and on-disk filenames, per-skill reconcile, and pruning | @@ -82,7 +82,7 @@ from launchdarkly_ai_server.experimental.evaluations import init_evaluations # Agent Skills from launchdarkly_ai_server import ( - set_skill_store, SkillStore, InMemorySkillStore, FDv2SkillStore, StoreDiagnostics, + set_skill_store, SkillStore, InMemorySkillStore, LaunchDarklySkillStore, StoreDiagnostics, skill_refs, get_skill, get_skill_result, get_skills, all_skills, write_skills, watch_skills, SkillWatcher, SkillSource, config_skills, Skill, SkillReference, SkillOutcome, ReconcileAction, ReconcileReport, @@ -270,7 +270,7 @@ user's files. ### The delivery transport, and the one field that will bite you -`FDv2SkillStore` speaks LaunchDarkly's SDK-facing FDv2 channel (`GET /sdk/poll`, +`LaunchDarklySkillStore` speaks LaunchDarkly's SDK-facing FDv2 channel (`GET /sdk/poll`, `GET /sdk/stream`, server-side SDK key in `Authorization`, `kinds` + `basis` params, `If-None-Match`/304). It sits below the store interface and produces raw objects in the shape `skills_core.SkillStore` documents; **nothing above that interface knows it exists**. @@ -571,7 +571,7 @@ Store data is **untrusted input**; the transport is not part of the trust bounda directory skips its unlink, drops the manifest entry, and reports `removed` while the revoked skill stays on disk. - **Every numeric option rejects non-finite values, not just negative ones.** - `write_skills`'s `timeout`, `watch_skills`'s `debounce`, and `FDv2SkillStore`'s + `write_skills`'s `timeout`, `watch_skills`'s `debounce`, and `LaunchDarklySkillStore`'s `poll_interval` and `read_timeout` share one rule. `nan` and `inf` both pass a `< 0` check and then mean opposite things downstream: an unbounded vs. an already-expired deadline, a zero vs. a never-closing coalescing window, a poll loop that spins vs. one that stops while @@ -819,7 +819,7 @@ create false confidence. The operator's verification steps are in the README. `timeout` is a monotonic deadline, checked before each retrieval, each write, and each prune; only the final manifest rewrite runs past it, so files already written are never orphaned. Bounded retries are **not** implemented at this layer; retry policy belongs to the delivery -transport (`FDv2SkillStore`'s capped backoff). Why: +transport (`LaunchDarklySkillStore`'s capped backoff). Why: 1. **There is nothing transient to retry.** `SkillStore.get_object` is a synchronous in-process read of already-delivered data, modelled on the LaunchDarkly data-store API. A diff --git a/packages/client/api-report/launchdarkly_ai_server.txt b/packages/client/api-report/launchdarkly_ai_server.txt index f7b2f7dd..4ed2c00e 100644 --- a/packages/client/api-report/launchdarkly_ai_server.txt +++ b/packages/client/api-report/launchdarkly_ai_server.txt @@ -29,20 +29,7 @@ execute_and_track(*, config_key: str, config: AiConfigRep, meta: VariationMeta, ExecuteStreamDoneEvent = dict[str, Any] ExecuteStreamEvent = dict[str, Any] extract_variation(config_key: str, context: dict[str, Any]) -> dict[str, Any] [async] -FDv2SkillStore [class] - __init__(sdk_key: str, *, base_uri: str | None = None, stream_uri: str | None = None, mode: Mode = 'stream', poll_interval: float = 30.0, read_timeout: float | None = None, initial_backoff: float = 1.0, max_backoff: float = 30.0, _requester: Any = None) - __enter__() -> FDv2SkillStore - __exit__(*_exc: Any) -> None - add_listener(kind: str, fn: Callable[[dict[str, Any]], Any]) -> None - all_objects(kind: str) -> dict[str, dict[str, Any]] - close(timeout: float = 5.0) -> None - diagnostics: StoreDiagnostics [property] - failed: str | None [property] - get_object(kind: str, key: str, version: int | None = None) -> dict[str, Any] | None - is_initialized() -> bool - remove_listener(kind: str, fn: Callable[[dict[str, Any]], Any]) -> None - start() -> FDv2SkillStore - wait_for_skills(timeout: float = 10.0) -> bool +FDv2SkillStore = LaunchDarklySkillStore get_client() -> Any get_skill(key: str, *, version: int | None = None) -> Skill | None [async] get_skill_result(key: str, *, version: int | None = None) -> SkillOutcome [async] @@ -139,6 +126,20 @@ lang_chain_content_text(content: Any) -> str lang_chain_finish_reasons(source: Any) -> list[str] | None lang_chain_span_messages(messages: list[Any]) -> tuple[str | None, list[SpanMessage]] lang_chain_span_usage(usage: dict[str, Any] | None) -> SpanUsage | None +LaunchDarklySkillStore [class] + __init__(sdk_key: str, *, base_uri: str | None = None, stream_uri: str | None = None, mode: Mode = 'stream', poll_interval: float = 30.0, read_timeout: float | None = None, initial_backoff: float = 1.0, max_backoff: float = 30.0, _requester: Any = None) + __enter__() -> LaunchDarklySkillStore + __exit__(*_exc: Any) -> None + add_listener(kind: str, fn: Callable[[dict[str, Any]], Any]) -> None + all_objects(kind: str) -> dict[str, dict[str, Any]] + close(timeout: float = 5.0) -> None + diagnostics: StoreDiagnostics [property] + failed: str | None [property] + get_object(kind: str, key: str, version: int | None = None) -> dict[str, Any] | None + is_initialized() -> bool + remove_listener(kind: str, fn: Callable[[dict[str, Any]], Any]) -> None + start() -> LaunchDarklySkillStore + wait_for_skills(timeout: float = 10.0) -> bool LDClientInterface [class] close() -> None [async] flush() -> None [async] diff --git a/packages/client/src/launchdarkly_ai_server/__init__.py b/packages/client/src/launchdarkly_ai_server/__init__.py index eb0aa169..f69d4e4b 100644 --- a/packages/client/src/launchdarkly_ai_server/__init__.py +++ b/packages/client/src/launchdarkly_ai_server/__init__.py @@ -66,7 +66,7 @@ ) from .skills import _skill_refs as _skill_refs from .skills_core import SkillStore -from .skills_fdv2 import FDv2SkillStore, StoreDiagnostics +from .skills_fdv2 import LaunchDarklySkillStore, StoreDiagnostics from .skills_fs import ( MANIFEST_FILENAME, MANIFEST_VERSION, @@ -149,6 +149,9 @@ ) from .utils import _create_handler as _create_handler +# Deprecated: the 0.5.0 name. Removed in the next major release. +FDv2SkillStore = LaunchDarklySkillStore + __all__ = [ # noqa: RUF022 # types "NATIVE_TOOL_KEY", @@ -271,6 +274,7 @@ "SkillStore", "InMemorySkillStore", # Agent Skills: LaunchDarkly delivery store and on-change re-reconcile + "LaunchDarklySkillStore", "FDv2SkillStore", "StoreDiagnostics", "watch_skills", diff --git a/packages/client/src/launchdarkly_ai_server/skills.py b/packages/client/src/launchdarkly_ai_server/skills.py index 2374c96b..60b67842 100644 --- a/packages/client/src/launchdarkly_ai_server/skills.py +++ b/packages/client/src/launchdarkly_ai_server/skills.py @@ -58,7 +58,7 @@ def set_skill_store(store: SkillStore | None) -> None: replacement. Args: - store: ``FDv2SkillStore`` to receive skills from LaunchDarkly, or + store: ``LaunchDarklySkillStore`` to receive skills from LaunchDarkly, or ``InMemorySkillStore`` for local development and tests. Raises: diff --git a/packages/client/src/launchdarkly_ai_server/skills_core.py b/packages/client/src/launchdarkly_ai_server/skills_core.py index 101fcd8c..ff1ee127 100644 --- a/packages/client/src/launchdarkly_ai_server/skills_core.py +++ b/packages/client/src/launchdarkly_ai_server/skills_core.py @@ -95,7 +95,7 @@ NO_STORE_MESSAGE = ( "No skill store is configured, so skill content cannot be retrieved. Configure " "one with set_skill_store(store) from launchdarkly_ai_server " - "— FDv2SkillStore receives content from LaunchDarkly, and InMemorySkillStore is " + "— LaunchDarklySkillStore receives content from LaunchDarkly, and InMemorySkillStore is " "available for local development and testing." ) """What the accessors report when no store is configured. Callers match on diff --git a/packages/client/src/launchdarkly_ai_server/skills_fdv2.py b/packages/client/src/launchdarkly_ai_server/skills_fdv2.py index ecb49fd7..0d9283a3 100644 --- a/packages/client/src/launchdarkly_ai_server/skills_fdv2.py +++ b/packages/client/src/launchdarkly_ai_server/skills_fdv2.py @@ -1,7 +1,7 @@ """ Agent Skills — the FDv2 delivery transport. -``FDv2SkillStore`` is the ``SkillStore`` that receives skills from LaunchDarkly +``LaunchDarklySkillStore`` is the ``SkillStore`` that receives skills from LaunchDarkly over ``GET /sdk/poll`` or ``GET /sdk/stream``, authenticated with the environment's server-side SDK key. It holds raw wire objects and serves them to the accessors; it uses only the standard library. @@ -173,20 +173,20 @@ def _require_server_side_credential(sdk_key: str) -> None: """ if not isinstance(sdk_key, str) or not sdk_key.strip(): raise ValueError( - "FDv2SkillStore requires a LaunchDarkly server-side SDK key " + "LaunchDarklySkillStore requires a LaunchDarkly server-side SDK key " "(sdk-...); none was given." ) key = sdk_key.strip() if key.startswith(_MOBILE_KEY_PREFIX): raise ValueError( - "FDv2SkillStore was given a mobile key (mob-...). Agent Skills are a " + "LaunchDarklySkillStore was given a mobile key (mob-...). Agent Skills are a " "server-side feature: skill content is customer-confidential and is " "never delivered to a mobile or client-side process. Use the " "environment's server-side SDK key (sdk-...)." ) if _CLIENT_SIDE_ID.match(key): raise ValueError( - "FDv2SkillStore was given what looks like a client-side environment " + "LaunchDarklySkillStore was given what looks like a client-side environment " "ID. Agent Skills are a server-side feature: skill content is " "customer-confidential and is never delivered to a client-side " "process. Use the environment's server-side SDK key (sdk-...)." @@ -194,7 +194,7 @@ def _require_server_side_credential(sdk_key: str) -> None: if not key.startswith(_SERVER_KEY_PREFIX): # Warn only: private instances and test doubles may use unprefixed keys. logger.warning( - "The credential given to FDv2SkillStore does not look like a " + "The credential given to LaunchDarklySkillStore does not look like a " "LaunchDarkly server-side SDK key (sdk-...). Skills are delivered " "only to server-side credentials; if this is a client-side or mobile " "credential the connection will be rejected or will deliver nothing." @@ -214,7 +214,7 @@ def _require_https_uri(base_uri: str, option: str = "base_uri") -> None: """ if not isinstance(base_uri, str) or not base_uri.strip(): raise ValueError( - f"FDv2SkillStore requires an https:// URI for {option}; none was given." + f"LaunchDarklySkillStore requires an https:// URI for {option}; none was given." ) parts = urllib.parse.urlsplit(base_uri.strip()) if parts.scheme == "https" and parts.hostname: @@ -223,7 +223,7 @@ def _require_https_uri(base_uri: str, option: str = "base_uri") -> None: return if parts.scheme == "http": raise ValueError( - f"FDv2SkillStore refuses {option} {base_uri!r}: a plain http:// URI " + f"LaunchDarklySkillStore refuses {option} {base_uri!r}: a plain http:// URI " "would send the server-side SDK key in cleartext. Use https:// " "(the defaults are https://sdk.launchdarkly.com for polling and " "https://stream.launchdarkly.com for streaming). Plain http:// is " @@ -231,7 +231,7 @@ def _require_https_uri(base_uri: str, option: str = "base_uri") -> None: "serving a local test double." ) raise ValueError( - f"FDv2SkillStore refuses {option} {base_uri!r}: expected an https:// URI " + f"LaunchDarklySkillStore refuses {option} {base_uri!r}: expected an https:// URI " "with a host, such as https://sdk.launchdarkly.com." ) @@ -1358,14 +1358,14 @@ def _backoff_delay( # --------------------------------------------------------------------------- -class FDv2SkillStore: +class LaunchDarklySkillStore: """ A ``SkillStore`` fed by LaunchDarkly's SDK-facing FDv2 delivery channel. Constructed with the environment's server-side SDK key, started explicitly, and passed to ``set_skill_store``:: - store = FDv2SkillStore(sdk_key=os.environ["LD_SDK_KEY"]) + store = LaunchDarklySkillStore(sdk_key=os.environ["LD_SDK_KEY"]) store.start() if not store.wait_for_skills(timeout=10): ... # no payload yet: see is_initialized @@ -1529,7 +1529,7 @@ def __init__( # -- lifecycle --------------------------------------------------------- - def start(self) -> FDv2SkillStore: + def start(self) -> LaunchDarklySkillStore: """ Starts the delivery thread. Idempotent; returns ``self`` so it chains. @@ -1542,10 +1542,10 @@ def start(self) -> FDv2SkillStore: with self._lock: if self._closed: raise RuntimeError( - "This FDv2SkillStore has been closed, and close() is final: " + "This LaunchDarklySkillStore has been closed, and close() is final: " "delivery cannot be resumed, so a restarted store would " "report itself started and never deliver. Construct a new " - "FDv2SkillStore to resume delivery. Held content is still " + "LaunchDarklySkillStore to resume delivery. Held content is still " "readable from the closed store." ) # Read before the rearm clears ``_failed_reason``: a thread inside @@ -1608,7 +1608,7 @@ def close(self, timeout: float = 5.0) -> None: ): thread.join(timeout=timeout) - def __enter__(self) -> FDv2SkillStore: + def __enter__(self) -> LaunchDarklySkillStore: return self.start() def __exit__(self, *_exc: Any) -> None: @@ -1694,7 +1694,7 @@ def add_listener(self, kind: str, fn: Callable[[dict[str, Any]], Any]) -> None: """ if kind != SKILL_OBJECT_KIND: raise ValueError( - f"FDv2SkillStore notifies only {SKILL_OBJECT_KIND!r} changes, so " + f"LaunchDarklySkillStore notifies only {SKILL_OBJECT_KIND!r} changes, so " f"a listener on {kind!r} would never fire. Register it on " f"{SKILL_OBJECT_KIND!r}." ) diff --git a/packages/client/src/launchdarkly_ai_server/skills_fs.py b/packages/client/src/launchdarkly_ai_server/skills_fs.py index 2c1ed1fa..bc72a087 100644 --- a/packages/client/src/launchdarkly_ai_server/skills_fs.py +++ b/packages/client/src/launchdarkly_ai_server/skills_fs.py @@ -403,7 +403,7 @@ def _available_store(deadline: float, subject: str) -> SkillStore | _RetrievalBl "the skill store has not received its initial data, so " f"{subject} could not be retrieved and nothing on disk was " "changed. Wait for delivery before reconciling: " - "FDv2SkillStore.wait_for_skills(timeout) returns True once the " + "LaunchDarklySkillStore.wait_for_skills(timeout) returns True once the " "first payload has arrived." ) ) diff --git a/packages/client/src/launchdarkly_ai_server/skills_watch.py b/packages/client/src/launchdarkly_ai_server/skills_watch.py index 01fb59e3..e19a9873 100644 --- a/packages/client/src/launchdarkly_ai_server/skills_watch.py +++ b/packages/client/src/launchdarkly_ai_server/skills_watch.py @@ -304,7 +304,7 @@ async def watch_skills( "watch_skills needs a skill store that implements add_listener(kind, " "fn); the configured store does not, so delivery changes cannot be " "observed. Use write_skills for a one-shot reconcile, or configure a " - "store with a delivery transport (FDv2SkillStore)." + "store with a delivery transport (LaunchDarklySkillStore)." ) # A bare ``< 0`` check misses both non-finite values: ``Event.wait(nan)`` # returns immediately (no debouncing) and ``Event.wait(inf)`` never returns. diff --git a/packages/client/tests/test_public_surface.py b/packages/client/tests/test_public_surface.py index 376060c9..bf106ef8 100644 --- a/packages/client/tests/test_public_surface.py +++ b/packages/client/tests/test_public_surface.py @@ -120,6 +120,7 @@ "set_skill_store", "SkillStore", "InMemorySkillStore", + "LaunchDarklySkillStore", "FDv2SkillStore", "StoreDiagnostics", "watch_skills", @@ -154,3 +155,9 @@ def test_root_surface_is_exactly_the_snapshot() -> None: assert len(package.__all__) == len(ROOT_SURFACE), "duplicate name in __all__" for name in ROOT_SURFACE: assert hasattr(package, name), name + + +def test_deprecated_fdv2_skill_store_name_is_the_same_class() -> None: + from launchdarkly_ai_server import FDv2SkillStore, LaunchDarklySkillStore + + assert FDv2SkillStore is LaunchDarklySkillStore diff --git a/packages/client/tests/test_sdk_usage.py b/packages/client/tests/test_sdk_usage.py index 5e609bd8..73f0e33a 100644 --- a/packages/client/tests/test_sdk_usage.py +++ b/packages/client/tests/test_sdk_usage.py @@ -11,8 +11,8 @@ import launchdarkly_ai_server.sdk_usage as sdk_usage_module from launchdarkly_ai_server import ( SDK_INFO_CONTEXT, - FDv2SkillStore, InMemorySkillStore, + LaunchDarklySkillStore, ProviderHandler, SkillReference, __version__, @@ -660,5 +660,5 @@ async def test_accessors_do_not_report_each_other( async def test_store_setup_is_not_a_helper(client: MagicMock) -> None: set_skill_store(InMemorySkillStore()) - FDv2SkillStore("sdk-test-key") + LaunchDarklySkillStore("sdk-test-key") assert _usage(client) == [] diff --git a/packages/client/tests/test_skills.py b/packages/client/tests/test_skills.py index 89f9b9c2..08912712 100644 --- a/packages/client/tests/test_skills.py +++ b/packages/client/tests/test_skills.py @@ -381,7 +381,7 @@ def test_requires_no_client_or_store(self) -> None: "set_skill_store", "SkillStore", "InMemorySkillStore", - "FDv2SkillStore", + "LaunchDarklySkillStore", "StoreDiagnostics", "watch_skills", "SkillWatcher", @@ -771,9 +771,11 @@ async def test_the_no_store_message_names_the_setter_and_both_stores( await get_skill("a") message = str(reported.value) assert "set_skill_store" in message - assert "FDv2SkillStore" in message + assert "LaunchDarklySkillStore" in message assert "InMemorySkillStore" in message - assert message.index("FDv2SkillStore") < message.index("InMemorySkillStore") + assert message.index("LaunchDarklySkillStore") < message.index( + "InMemorySkillStore" + ) async def test_shutdown_clears_the_store( self, make_raw_skill: Any, mock_ld_client: Any @@ -1520,7 +1522,7 @@ async def test_a_listed_object_is_filed_under_its_own_key( The interface never promised a map key spells a skill key, either: a store holding several versions of one key has reason to spell it - ``key:version``, which is exactly what ``FDv2SkillStore`` does. Pinned + ``key:version``, which is exactly what ``LaunchDarklySkillStore`` does. Pinned because the asymmetry with the pinned path is surprising enough to invite a "fix" that would break every multi-version store. """ diff --git a/packages/client/tests/test_skills_fdv2.py b/packages/client/tests/test_skills_fdv2.py index 4670a954..f2ed212c 100644 --- a/packages/client/tests/test_skills_fdv2.py +++ b/packages/client/tests/test_skills_fdv2.py @@ -40,8 +40,8 @@ import launchdarkly_ai_server.skills_core as skills_core import launchdarkly_ai_server.skills_fdv2 as skills_fdv2 from launchdarkly_ai_server import ( - FDv2SkillStore, InMemorySkillStore, + LaunchDarklySkillStore, all_skills, get_skill, get_skill_result, @@ -398,8 +398,8 @@ def endpoint() -> Any: server.close() -def poll_store(endpoint: Any, **kwargs: Any) -> FDv2SkillStore: - return FDv2SkillStore( +def poll_store(endpoint: Any, **kwargs: Any) -> LaunchDarklySkillStore: + return LaunchDarklySkillStore( SDK_KEY, base_uri=endpoint.base_uri, mode="poll", @@ -1773,7 +1773,7 @@ class TestStreamingAgainstTheEndpoint: def test_a_streamed_payload_lands(self, endpoint: Any) -> None: endpoint.hold_stream_open = True endpoint.queue_stream(full_payload(("put-object", put_skill()))) - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, base_uri=endpoint.base_uri, mode="stream", initial_backoff=0.01 ) try: @@ -1786,7 +1786,9 @@ def test_a_streamed_payload_lands(self, endpoint: Any) -> None: def test_the_stream_request_advertises_event_stream(self, endpoint: Any) -> None: endpoint.hold_stream_open = True endpoint.queue_stream(full_payload(("put-object", put_skill()))) - store = FDv2SkillStore(SDK_KEY, base_uri=endpoint.base_uri, mode="stream") + store = LaunchDarklySkillStore( + SDK_KEY, base_uri=endpoint.base_uri, mode="stream" + ) try: store.start() store.wait_for_skills(timeout=5) @@ -1800,7 +1802,9 @@ def test_the_stream_request_declares_the_payload_kind(self, endpoint: Any) -> No against different origins.""" endpoint.hold_stream_open = True endpoint.queue_stream(full_payload(("put-object", put_skill()))) - store = FDv2SkillStore(SDK_KEY, base_uri=endpoint.base_uri, mode="stream") + store = LaunchDarklySkillStore( + SDK_KEY, base_uri=endpoint.base_uri, mode="stream" + ) try: store.start() store.wait_for_skills(timeout=5) @@ -1823,7 +1827,7 @@ def test_a_revocation_after_an_up_to_date_answer_arrives( ("payload-transferred", transferred("basis-2")), ) ) - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, base_uri=endpoint.base_uri, mode="stream", @@ -1857,7 +1861,9 @@ def test_a_streamed_revocation_arrives_without_a_restart( ("payload-transferred", transferred("basis-2")), ) ) - store = FDv2SkillStore(SDK_KEY, base_uri=endpoint.base_uri, mode="stream") + store = LaunchDarklySkillStore( + SDK_KEY, base_uri=endpoint.base_uri, mode="stream" + ) try: store.start() assert wait_until( @@ -1876,7 +1882,7 @@ def test_a_dropped_stream_reconnects_with_the_basis_it_reached( full_payload(("put-object", put_skill()), state="basis-1") ) endpoint.queue_stream(events(("heart-beat", None))) - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, base_uri=endpoint.base_uri, mode="stream", @@ -1898,7 +1904,9 @@ def test_close_returns_promptly_while_a_stream_is_open(self, endpoint: Any) -> N """ endpoint.hold_stream_open = True endpoint.queue_stream(full_payload(("put-object", put_skill()))) - store = FDv2SkillStore(SDK_KEY, base_uri=endpoint.base_uri, mode="stream") + store = LaunchDarklySkillStore( + SDK_KEY, base_uri=endpoint.base_uri, mode="stream" + ) store.start() assert store.wait_for_skills(timeout=5) is True started = time.monotonic() @@ -1910,7 +1918,9 @@ def test_an_interrupted_stream_is_not_reported_as_a_failure( ) -> None: endpoint.hold_stream_open = True endpoint.queue_stream(full_payload(("put-object", put_skill()))) - store = FDv2SkillStore(SDK_KEY, base_uri=endpoint.base_uri, mode="stream") + store = LaunchDarklySkillStore( + SDK_KEY, base_uri=endpoint.base_uri, mode="stream" + ) store.start() store.wait_for_skills(timeout=5) store.close() @@ -1919,7 +1929,7 @@ def test_an_interrupted_stream_is_not_reported_as_a_failure( def test_content_survives_a_reconnect(self, endpoint: Any) -> None: endpoint.queue_stream(full_payload(("put-object", put_skill()))) endpoint.queue_stream(events(("heart-beat", None))) - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, base_uri=endpoint.base_uri, mode="stream", initial_backoff=0.01 ) try: @@ -2133,8 +2143,8 @@ def recording(attempt: int, **kwargs: Any) -> float: return attempts -def stream_store(**kwargs: Any) -> FDv2SkillStore: - return FDv2SkillStore( +def stream_store(**kwargs: Any) -> LaunchDarklySkillStore: + return LaunchDarklySkillStore( SDK_KEY, mode="stream", initial_backoff=kwargs.pop("initial_backoff", 0.001), @@ -2252,7 +2262,7 @@ def test_a_restart_resets_the_failure_count(self) -> None: _RecoverableTransportError("x"), _FatalTransportError("401"), ) - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, mode="poll", initial_backoff=0.001, @@ -2298,7 +2308,7 @@ def poll(self, basis: str | None, etag: str | None) -> Any: _RecoverableTransportError("x"), _FatalTransportError("401"), ) - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, mode="poll", initial_backoff=0.001, @@ -2634,10 +2644,10 @@ def test_there_is_no_consecutive_failure_option(self) -> None: """ assert ( "max_consecutive_failures" - not in inspect.signature(FDv2SkillStore.__init__).parameters + not in inspect.signature(LaunchDarklySkillStore.__init__).parameters ) with pytest.raises(TypeError): - FDv2SkillStore(SDK_KEY, max_consecutive_failures=3) # type: ignore[call-arg] + LaunchDarklySkillStore(SDK_KEY, max_consecutive_failures=3) # type: ignore[call-arg] @pytest.mark.parametrize("option", ["initial_backoff", "max_backoff"]) @pytest.mark.parametrize("value", [0, 0.0, -5, math.nan, math.inf]) @@ -2648,13 +2658,13 @@ def test_a_backoff_option_must_be_positive_and_finite( retry loop: zero or less reconnects as fast as the network allows.""" kwargs = {"initial_backoff": 1.0, "max_backoff": 30.0, option: value} with pytest.raises(ValueError, match=option): - FDv2SkillStore(SDK_KEY, **kwargs) + LaunchDarklySkillStore(SDK_KEY, **kwargs) def test_the_initial_backoff_may_not_exceed_the_cap(self) -> None: with pytest.raises(ValueError, match="must not exceed"): - FDv2SkillStore(SDK_KEY, initial_backoff=5.0, max_backoff=1.0) + LaunchDarklySkillStore(SDK_KEY, initial_backoff=5.0, max_backoff=1.0) # Equal is a fixed delay, and fine. - FDv2SkillStore(SDK_KEY, initial_backoff=2.0, max_backoff=2.0) + LaunchDarklySkillStore(SDK_KEY, initial_backoff=2.0, max_backoff=2.0) def test_wait_for_skills_runs_to_its_timeout_during_an_outage(self) -> None: """Recoverable failures no longer end delivery, so the store does not @@ -2708,7 +2718,7 @@ def test_the_second_400_still_stops_with_a_recoverable_failure_between( def test_recoverable_failures_are_retried_indefinitely(self) -> None: """Well past ten in a row, the bound this replaces, and still retrying.""" requester = _ScriptedRequester() - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, mode="poll", poll_interval=0.01, @@ -2945,7 +2955,7 @@ class _CorruptedThenClean(_FakeRequester): def __init__(self) -> None: self.bases: list[str | None] = [] self.held_at_reconnect: dict[str, bool] = {} - self.store: FDv2SkillStore | None = None + self.store: LaunchDarklySkillStore | None = None def stream(self, basis: str | None) -> Any: self.bases.append(basis) @@ -3155,7 +3165,7 @@ def test_a_retry_after_header_is_honoured(self) -> None: requester = _ScriptedRequester( _RecoverableTransportError("slow down", retry_after=0.5), ) - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, mode="poll", poll_interval=10.0, @@ -3188,7 +3198,7 @@ def test_a_retry_after_of_zero_still_waits_the_initial_backoff(self) -> None: requester = _ScriptedRequester( _RecoverableTransportError("slow down", retry_after=0.0), ) - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, mode="poll", poll_interval=10.0, @@ -3286,7 +3296,7 @@ def test_jitter_never_exceeds_the_cap(self) -> None: def test_a_malformed_polling_envelope_is_recoverable_not_fatal( self, endpoint: Any ) -> None: - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, mode="poll", poll_interval=0.01, @@ -3441,7 +3451,7 @@ def test_an_over_cap_stream_event_abandons_the_payload_in_flight( ("payload-transferred", transferred("basis-2")), ) ) - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, base_uri=endpoint.base_uri, mode="stream", @@ -3613,7 +3623,7 @@ def test_a_redirect_in_stream_mode_stops_delivery( self, endpoint: Any, second_endpoint: Any ) -> None: endpoint.redirect_stream_to = second_endpoint.base_uri + "/sdk/stream" - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, base_uri=endpoint.base_uri, mode="stream", initial_backoff=0.01 ) with store: @@ -3910,30 +3920,30 @@ async def test_the_payload_version_is_not_resolvable_as_a_skill_version( class TestServerSideOnly: def test_a_mobile_key_is_refused(self) -> None: with pytest.raises(ValueError, match="mobile key"): - FDv2SkillStore("mob-00000000-0000-4000-8000-000000000000") + LaunchDarklySkillStore("mob-00000000-0000-4000-8000-000000000000") def test_a_client_side_environment_id_is_refused(self) -> None: with pytest.raises(ValueError, match="client-side"): - FDv2SkillStore("0123456789abcdef01234567") + LaunchDarklySkillStore("0123456789abcdef01234567") def test_an_empty_credential_is_refused(self) -> None: with pytest.raises(ValueError, match="server-side SDK key"): - FDv2SkillStore(" ") + LaunchDarklySkillStore(" ") def test_a_server_side_key_is_accepted(self) -> None: - assert FDv2SkillStore(SDK_KEY) is not None + assert LaunchDarklySkillStore(SDK_KEY) is not None def test_an_unrecognised_credential_shape_warns_but_is_allowed( self, caplog: Any ) -> None: """Private instances and test doubles issue keys without the public prefix.""" with caplog.at_level("WARNING"): - FDv2SkillStore("my-private-instance-credential") + LaunchDarklySkillStore("my-private-instance-credential") assert any("server-side SDK key" in r.message for r in caplog.records) def test_an_unknown_mode_is_refused(self) -> None: with pytest.raises(ValueError, match="stream"): - FDv2SkillStore(SDK_KEY, mode="mobile") # type: ignore[arg-type] + LaunchDarklySkillStore(SDK_KEY, mode="mobile") # type: ignore[arg-type] class TestBaseUriScheme: @@ -3946,7 +3956,7 @@ class TestBaseUriScheme: def test_a_plain_http_base_uri_is_refused(self) -> None: with pytest.raises(ValueError, match="cleartext") as excinfo: - FDv2SkillStore(SDK_KEY, base_uri="http://sdk.launchdarkly.com") + LaunchDarklySkillStore(SDK_KEY, base_uri="http://sdk.launchdarkly.com") assert "https://" in str(excinfo.value) def test_a_plain_http_base_uri_is_refused_even_with_a_requester_injected( @@ -3954,7 +3964,7 @@ def test_a_plain_http_base_uri_is_refused_even_with_a_requester_injected( ) -> None: """The check is on the store, not on the socket it happens to open.""" with pytest.raises(ValueError, match="cleartext"): - FDv2SkillStore( + LaunchDarklySkillStore( SDK_KEY, base_uri="http://relay.internal:8030", _requester=_FakeRequester(), @@ -3970,12 +3980,12 @@ def test_a_plain_http_base_uri_is_refused_even_with_a_requester_injected( ], ) def test_plain_http_to_a_loopback_host_is_allowed(self, base_uri: str) -> None: - assert FDv2SkillStore(SDK_KEY, base_uri=base_uri) is not None + assert LaunchDarklySkillStore(SDK_KEY, base_uri=base_uri) is not None def test_a_private_address_is_not_loopback(self) -> None: """Only the machine itself is exempt; the LAN is not.""" with pytest.raises(ValueError, match="cleartext"): - FDv2SkillStore(SDK_KEY, base_uri="http://10.0.0.5:8030") + LaunchDarklySkillStore(SDK_KEY, base_uri="http://10.0.0.5:8030") @pytest.mark.parametrize( "base_uri", @@ -3985,16 +3995,19 @@ def test_anything_but_an_https_url_with_a_host_is_refused( self, base_uri: str ) -> None: with pytest.raises(ValueError, match="https://"): - FDv2SkillStore(SDK_KEY, base_uri=base_uri) + LaunchDarklySkillStore(SDK_KEY, base_uri=base_uri) def test_https_is_accepted(self) -> None: - assert FDv2SkillStore(SDK_KEY, base_uri="https://sdk.example.com/") is not None - assert FDv2SkillStore(SDK_KEY) is not None + assert ( + LaunchDarklySkillStore(SDK_KEY, base_uri="https://sdk.example.com/") + is not None + ) + assert LaunchDarklySkillStore(SDK_KEY) is not None def test_a_plain_http_stream_uri_is_refused_by_name(self) -> None: """The streaming host is checked too, and the message names it.""" with pytest.raises(ValueError, match="cleartext") as excinfo: - FDv2SkillStore( + LaunchDarklySkillStore( SDK_KEY, base_uri="https://sdk.example.com", stream_uri="http://stream.example.com", @@ -4013,14 +4026,14 @@ class TestStreamHostDefaults: """ @staticmethod - def _origins(store: FDv2SkillStore) -> tuple[str, str]: + def _origins(store: LaunchDarklySkillStore) -> tuple[str, str]: requester = store._requester return requester._base_uri, requester._stream_uri def test_the_two_defaults_are_different_hosts(self) -> None: assert DEFAULT_BASE_URI == "https://sdk.launchdarkly.com" assert DEFAULT_STREAM_URI == "https://stream.launchdarkly.com" - assert self._origins(FDv2SkillStore(SDK_KEY)) == ( + assert self._origins(LaunchDarklySkillStore(SDK_KEY)) == ( DEFAULT_BASE_URI, DEFAULT_STREAM_URI, ) @@ -4029,12 +4042,12 @@ def test_a_base_uri_alone_serves_both_endpoints(self) -> None: # A relay or a private instance serving both from one host needs one # option, not two. assert self._origins( - FDv2SkillStore(SDK_KEY, base_uri="https://relay.example.com") + LaunchDarklySkillStore(SDK_KEY, base_uri="https://relay.example.com") ) == ("https://relay.example.com", "https://relay.example.com") def test_naming_both_overrides_them_independently(self) -> None: assert self._origins( - FDv2SkillStore( + LaunchDarklySkillStore( SDK_KEY, base_uri="https://sdk.example.com/", stream_uri="https://stream.example.com/", @@ -4043,7 +4056,7 @@ def test_naming_both_overrides_them_independently(self) -> None: def test_a_stream_uri_alone_leaves_polling_on_its_default(self) -> None: assert self._origins( - FDv2SkillStore(SDK_KEY, stream_uri="https://stream.example.com") + LaunchDarklySkillStore(SDK_KEY, stream_uri="https://stream.example.com") ) == (DEFAULT_BASE_URI, "https://stream.example.com") def test_the_stream_request_goes_to_the_stream_host( @@ -4076,7 +4089,7 @@ class TestEndpointsFromTheEnvironment: """ @staticmethod - def _origins(store: FDv2SkillStore) -> tuple[str, str]: + def _origins(store: LaunchDarklySkillStore) -> tuple[str, str]: requester = store._requester return requester._base_uri, requester._stream_uri @@ -4086,7 +4099,7 @@ def test_ld_base_uri_alone_moves_both_endpoints( # The store's own rule for a lone base URI, not ``init_client``'s, # which leaves flag streaming on its default host. monkeypatch.setenv("LD_BASE_URI", "https://sdk.catfood.example.com") - assert self._origins(FDv2SkillStore(SDK_KEY)) == ( + assert self._origins(LaunchDarklySkillStore(SDK_KEY)) == ( "https://sdk.catfood.example.com", "https://sdk.catfood.example.com", ) @@ -4096,7 +4109,7 @@ def test_both_variables_set_each_endpoint( ) -> None: monkeypatch.setenv("LD_BASE_URI", "https://sdk.catfood.example.com") monkeypatch.setenv("LD_STREAM_URI", "https://stream.catfood.example.com/") - assert self._origins(FDv2SkillStore(SDK_KEY)) == ( + assert self._origins(LaunchDarklySkillStore(SDK_KEY)) == ( "https://sdk.catfood.example.com", "https://stream.catfood.example.com", ) @@ -4105,7 +4118,7 @@ def test_ld_stream_uri_alone_leaves_polling_on_its_default( self, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("LD_STREAM_URI", "https://stream.catfood.example.com") - assert self._origins(FDv2SkillStore(SDK_KEY)) == ( + assert self._origins(LaunchDarklySkillStore(SDK_KEY)) == ( DEFAULT_BASE_URI, "https://stream.catfood.example.com", ) @@ -4118,10 +4131,10 @@ def test_either_option_turns_both_variables_off( monkeypatch.setenv("LD_BASE_URI", "https://sdk.catfood.example.com") monkeypatch.setenv("LD_STREAM_URI", "https://stream.catfood.example.com") assert self._origins( - FDv2SkillStore(SDK_KEY, base_uri="https://relay.example.com") + LaunchDarklySkillStore(SDK_KEY, base_uri="https://relay.example.com") ) == ("https://relay.example.com", "https://relay.example.com") assert self._origins( - FDv2SkillStore(SDK_KEY, stream_uri="https://stream.example.com") + LaunchDarklySkillStore(SDK_KEY, stream_uri="https://stream.example.com") ) == (DEFAULT_BASE_URI, "https://stream.example.com") @pytest.mark.parametrize("blank", ["", " "]) @@ -4130,7 +4143,7 @@ def test_a_blank_value_is_unset( ) -> None: monkeypatch.setenv("LD_BASE_URI", blank) monkeypatch.setenv("LD_STREAM_URI", blank) - assert self._origins(FDv2SkillStore(SDK_KEY)) == ( + assert self._origins(LaunchDarklySkillStore(SDK_KEY)) == ( DEFAULT_BASE_URI, DEFAULT_STREAM_URI, ) @@ -4143,7 +4156,7 @@ def test_a_plain_http_value_is_refused_by_the_variable_name( passed.""" monkeypatch.setenv(name, "http://sdk.catfood.example.com") with pytest.raises(ValueError, match="cleartext") as excinfo: - FDv2SkillStore(SDK_KEY) + LaunchDarklySkillStore(SDK_KEY) message = str(excinfo.value) assert name in message assert "base_uri" not in message @@ -4153,7 +4166,7 @@ def test_the_variables_are_read_once_at_construction( self, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("LD_BASE_URI", "https://sdk.catfood.example.com") - store = FDv2SkillStore(SDK_KEY) + store = LaunchDarklySkillStore(SDK_KEY) monkeypatch.setenv("LD_BASE_URI", "https://sdk.elsewhere.example.com") monkeypatch.setenv("LD_STREAM_URI", "https://stream.elsewhere.example.com") assert self._origins(store) == ( @@ -4165,7 +4178,7 @@ def test_the_module_defaults_keep_the_production_hosts( self, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("LD_BASE_URI", "https://sdk.catfood.example.com") - FDv2SkillStore(SDK_KEY) + LaunchDarklySkillStore(SDK_KEY) assert DEFAULT_BASE_URI == "https://sdk.launchdarkly.com" assert DEFAULT_STREAM_URI == "https://stream.launchdarkly.com" @@ -4177,7 +4190,7 @@ def test_the_module_defaults_keep_the_production_hosts( class TestWatchSkillsOverTheTransport: """ - ``watch_skills`` against a live ``FDv2SkillStore``. The watcher's own + ``watch_skills`` against a live ``LaunchDarklySkillStore``. The watcher's own behaviour — debounce, refusal of a store without ``add_listener``, detaching on close — is covered in ``test_skills_watch.py`` against the in-memory store; these are the cases that only mean something with a transport @@ -4364,7 +4377,7 @@ def test_a_closed_store_does_not_restart(self, endpoint: Any) -> None: with pytest.raises(RuntimeError, match="close\\(\\) is final") as excinfo: store.start() # The remedy is in the message, not only in the docs. - assert "Construct a new FDv2SkillStore" in str(excinfo.value) + assert "Construct a new LaunchDarklySkillStore" in str(excinfo.value) def test_a_store_closed_before_it_started_also_refuses_to_start( self, endpoint: Any @@ -4412,7 +4425,7 @@ def test_a_closed_store_still_answers_from_what_it_received( assert store.get_object(SKILL_OBJECT_KIND, "pdf-extraction") is not None def test_wait_for_skills_times_out_rather_than_hanging(self) -> None: - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, mode="poll", poll_interval=60, _requester=_ScriptedRequester() ) try: @@ -4421,7 +4434,7 @@ def test_wait_for_skills_times_out_rather_than_hanging(self) -> None: store.close() def test_the_store_satisfies_the_interface_before_it_starts(self) -> None: - store = FDv2SkillStore(SDK_KEY) + store = LaunchDarklySkillStore(SDK_KEY) assert store.get_object(SKILL_OBJECT_KIND, "anything") is None assert store.all_objects(SKILL_OBJECT_KIND) == {} @@ -4485,10 +4498,10 @@ def test_it_adds_no_dependency(self) -> None: def test_nothing_in_the_feature_imports_it(self) -> None: """Only the package root names it, to re-export - ``FDv2SkillStore`` and ``StoreDiagnostics``. + ``LaunchDarklySkillStore`` and ``StoreDiagnostics``. That re-export is the published surface rather than a dependency: it is - what makes ``FDv2SkillStore`` reachable without importing the + what makes ``LaunchDarklySkillStore`` reachable without importing the implementation module. A *feature* module importing the transport would be the layering inverting, and the accessors would start being able to tell which store answered them. @@ -4718,7 +4731,7 @@ def test_a_stream_against_an_unresponsive_host_fails_within_read_timeout( def test_the_store_reports_the_timeout_and_keeps_going( self, black_hole: Any ) -> None: - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, base_uri=black_hole.base_uri, mode="poll", @@ -4738,20 +4751,20 @@ def test_the_store_reports_the_timeout_and_keeps_going( def test_the_default_bound_depends_on_the_mode(self) -> None: assert DEFAULT_POLL_TIMEOUT == 10.0 assert DEFAULT_STREAM_READ_TIMEOUT == 300.0 - polling = FDv2SkillStore(SDK_KEY, mode="poll") - streaming = FDv2SkillStore(SDK_KEY, mode="stream") + polling = LaunchDarklySkillStore(SDK_KEY, mode="poll") + streaming = LaunchDarklySkillStore(SDK_KEY, mode="stream") assert polling._requester._read_timeout == DEFAULT_POLL_TIMEOUT assert streaming._requester._read_timeout == DEFAULT_STREAM_READ_TIMEOUT @pytest.mark.parametrize("mode", ["poll", "stream"]) def test_an_explicit_read_timeout_overrides_the_default(self, mode: Any) -> None: - store = FDv2SkillStore(SDK_KEY, mode=mode, read_timeout=42.0) + store = LaunchDarklySkillStore(SDK_KEY, mode=mode, read_timeout=42.0) assert store._requester._read_timeout == 42.0 @pytest.mark.parametrize("value", [0.0, -1.0, float("inf"), float("nan")]) def test_a_non_positive_read_timeout_is_rejected(self, value: float) -> None: with pytest.raises(ValueError, match="read_timeout"): - FDv2SkillStore(SDK_KEY, read_timeout=value) + LaunchDarklySkillStore(SDK_KEY, read_timeout=value) @pytest.mark.parametrize( "value", @@ -4769,18 +4782,18 @@ def test_a_non_positive_poll_interval_is_rejected(self, value: float) -> None: never again while reporting itself healthy. """ with pytest.raises(ValueError, match="poll_interval"): - FDv2SkillStore(SDK_KEY, mode="poll", poll_interval=value) + LaunchDarklySkillStore(SDK_KEY, mode="poll", poll_interval=value) def test_a_finite_positive_poll_interval_is_accepted(self) -> None: """Positive control, so the guard above cannot reject every value.""" - store = FDv2SkillStore(SDK_KEY, mode="poll", poll_interval=0.5) + store = LaunchDarklySkillStore(SDK_KEY, mode="poll", poll_interval=0.5) assert store.failed is None def test_there_is_no_separate_connect_timeout(self) -> None: # ``urllib`` cannot bound the connect separately from the reads, so the # constructor does not offer a parameter that would only pretend to. with pytest.raises(TypeError): - FDv2SkillStore(SDK_KEY, connect_timeout=2.0) # type: ignore[call-arg] + LaunchDarklySkillStore(SDK_KEY, connect_timeout=2.0) # type: ignore[call-arg] class TestWaitingForSkills: @@ -4920,7 +4933,7 @@ def poll_until_interrupted() -> None: def test_close_during_a_stalled_poll_returns_promptly( self, stalled_body: Any ) -> None: - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, base_uri=stalled_body.base_uri, mode="poll", @@ -4941,7 +4954,7 @@ def test_a_poll_we_interrupted_is_not_a_delivery_failure( # Our own shutdown is not an outage: counting it would spend a retry # from the bounded budget and leave a misleading ``last_error`` behind # on a store whose content is still perfectly good. - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, base_uri=stalled_body.base_uri, mode="poll", @@ -4963,7 +4976,7 @@ def test_a_close_that_timed_out_is_still_final(self) -> None: # and a ``start`` that adopted the dying thread would leave a store # reporting itself started and never delivering. Raising says so. requester = _SlowPollRequester() - store = FDv2SkillStore( + store = LaunchDarklySkillStore( SDK_KEY, mode="poll", poll_interval=0.01, _requester=requester ) try: