Skip to content

feat: add OpenFeature Python server provider#1

Open
WuJiayi0307 wants to merge 5 commits into
featbit:mainfrom
WuJiayi0307:feat/openfeature-provider
Open

feat: add OpenFeature Python server provider#1
WuJiayi0307 wants to merge 5 commits into
featbit:mainfrom
WuJiayi0307:feat/openfeature-provider

Conversation

@WuJiayi0307

@WuJiayi0307 WuJiayi0307 commented Jul 24, 2026

Copy link
Copy Markdown

Summary

Add the standalone FeatBit OpenFeature Python server provider as a thin wrapper around the existing FeatBit Python Server SDK.

  • implement the OpenFeature FeatureProvider and all typed resolution methods
  • convert OpenFeature evaluation contexts to FeatBit users
  • convert FeatBit evaluation details, including stable variation IDs, to OpenFeature resolution details
  • bridge FeatBit SDK data-update states to OpenFeature provider status and lifecycle events
  • support tracking and configuration-change events
  • isolate evaluation, tracking, listener, and shutdown failures from application code
  • add packaging metadata, type information, documentation, CI, and offline integration coverage

Status mapping

  • FeatBit OK → OpenFeature READY
  • FeatBit INTERRUPTED → OpenFeature STALE
  • FeatBit OFF → OpenFeature FATAL
  • provider shutdown → OpenFeature NOT_READY

Dependencies

  • fb-python-sdk>=1.1.8,<2
  • openfeature-sdk>=0.10,<1
  • Python 3.10–3.12

The required variation-ID and thread-safe status-listener capabilities are proposed in featbit/featbit-python-sdk#14.

Validation

  • 53 passed
  • Apache-2.0 package metadata matches the repository license
  • Flake8 passed
  • wheel and source distribution built successfully
  • isolated installation smoke test passed
  • offline end-to-end evaluation verified: OpenFeature Client → Provider → real FeatBit evaluator, returning value v2, variant variation-v2, and provider status READY

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds a packaged FeatBit OpenFeature provider with evaluation-context and resolution converters, typed flag evaluation, lifecycle and status handling, tracking, documentation, CI configuration, and unit plus offline integration tests.

Changes

FeatBit OpenFeature provider

Layer / File(s) Summary
Package and CI surface
.flake8, .github/workflows/ci.yml, .gitignore, MANIFEST.in, pyproject.toml, featbit_openfeature/__init__.py, featbit_openfeature/impl/__init__.py, featbit_openfeature/py.typed, README.md
Defines packaging metadata, public exports, type-checker support, distribution contents, lint and CI execution, and provider usage and mapping documentation.
Evaluation context and resolution conversion
featbit_openfeature/impl/context_converter.py, featbit_openfeature/impl/details_converter.py
Converts evaluation contexts into FeatBit users, serializes supported attributes, validates targeting keys, and maps FeatBit evaluation details to OpenFeature resolution details.
Provider evaluation and lifecycle
featbit_openfeature/provider.py
Implements typed resolution, type validation, tracking, initialization, shutdown, listener registration, configuration-change events, and FeatBit status mapping.
Provider validation and offline integration
tests/impl/*, tests/test_provider.py, tests/test_offline_integration.py, tests/fixtures/bootstrap.json
Tests conversion, resolution errors, lifecycle behavior, status transitions, tracking, concurrency, and offline evaluation against bootstrap data.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OpenFeatureClient
  participant FeatBitProvider
  participant EvaluationContextConverter
  participant FBClient
  participant OpenFeatureEvents
  OpenFeatureClient->>FeatBitProvider: resolve typed flag
  FeatBitProvider->>EvaluationContextConverter: convert evaluation context
  EvaluationContextConverter-->>FeatBitProvider: FeatBit user
  FeatBitProvider->>FBClient: variation_detail
  FBClient-->>FeatBitProvider: EvalDetail and variation
  FeatBitProvider-->>OpenFeatureClient: FlagResolutionDetails
  FBClient->>FeatBitProvider: status or flag-change notification
  FeatBitProvider->>OpenFeatureEvents: emit provider event
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a standalone OpenFeature Python server provider.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Line 20: Update the actions/checkout@v4 step to set persist-credentials to
false, ensuring the workflow does not retain the GitHub token in local Git
configuration; leave the remaining checkout behavior unchanged.
- Around line 28-29: The CI dependency installation should not use the moving
`refs/pull/14/head` override. Update the `fb-python-sdk` install command in the
workflow to honor the `pyproject.toml` constraint `>=1.1.8,<2`, or replace it
with an immutable full commit pin and document the temporary override.

In `@featbit_openfeature/provider.py`:
- Around line 74-86: Register the status listeners in initialize
unconditionally, including when wait_for_OKState times out or state is OFF.
Ensure _register_listeners() executes before propagating ProviderFatalError or
ProviderNotReadyError, while preserving the existing error classification and
successful initialization behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a9e9f0cf-2f49-4ce4-b487-a53d642e563c

📥 Commits

Reviewing files that changed from the base of the PR and between 6574599 and b9396b5.

📒 Files selected for processing (17)
  • .flake8
  • .github/workflows/ci.yml
  • .gitignore
  • MANIFEST.in
  • README.md
  • featbit_openfeature/__init__.py
  • featbit_openfeature/impl/__init__.py
  • featbit_openfeature/impl/context_converter.py
  • featbit_openfeature/impl/details_converter.py
  • featbit_openfeature/provider.py
  • featbit_openfeature/py.typed
  • pyproject.toml
  • tests/fixtures/bootstrap.json
  • tests/impl/test_context_converter.py
  • tests/impl/test_details_converter.py
  • tests/test_offline_integration.py
  • tests/test_provider.py

Comment thread .github/workflows/ci.yml
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Disable checkout credential persistence.

PR-controlled install and test code can read the persisted GitHub token from the local Git configuration. Set persist-credentials: false; no later step requires Git authentication.

Proposed fix
       - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 20-20: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 20, Update the actions/checkout@v4 step to
set persist-credentials to false, ensuring the workflow does not retain the
GitHub token in local Git configuration; leave the remaining checkout behavior
unchanged.

Source: Linters/SAST tools

Comment thread .github/workflows/ci.yml
Comment on lines +28 to +29
python -m pip install "fb-python-sdk @ git+https://github.com/featbit/featbit-python-sdk.git@refs/pull/14/head"
python -m pip install -e . --no-deps

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow =="
if [ -f .github/workflows/ci.yml ]; then
  nl -ba .github/workflows/ci.yml | sed -n '1,140p'
else
  echo ".github/workflows/ci.yml not found"
fi

echo
echo "== pyproject / package metadata =="
if [ -f pyproject.toml ]; then
  nl -ba pyproject.toml | sed -n '1,220p'
else
  fd -a 'pyproject.toml|setup.py|setup.cfg|requirements.*' . | sed -n '1,50p'
fi

echo
echo "== references to fb-python-sdk ==
rg -n "fb-python-sdk|featbit-python-sdk|refs/pull/14/head|--no-deps" .

Repository: featbit/openfeature-provider-python-server

Length of output: 235


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow =="
if [ -f .github/workflows/ci.yml ]; then
  cat -n .github/workflows/ci.yml | sed -n '1,140p'
else
  echo ".github/workflows/ci.yml not found"
fi

echo
echo "== pyproject / package metadata =="
if [ -f pyproject.toml ]; then
  cat -n pyproject.toml | sed -n '1,220p'
else
  find . -maxdepth 3 \( -name 'pyproject.toml' -o -name 'setup.py' -o -name 'setup.cfg' -o -name 'requirements*.txt' \) -print | sort
fi

echo
echo "== references to fb-python-sdk / mutable refs / no-deps =="
grep -RIn "fb-python-sdk|featbit-python-sdk|refs/pull/14/head|uses:|pip install|\\-\\-no-deps" .github pyproject.toml 2>/dev/null || true

echo
echo "== current diff/stat summary =="
git diff --stat || true
git diff -- .github/workflows/ci.yml pyproject.toml 2>/dev/null | sed -n '1,220p'

Repository: featbit/openfeature-provider-python-server

Length of output: 2667


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pip availability/version =="
python3 -c "import shutil, sys; print('pip', shutil.which('pip'), sys.executable)" || true
python -m pip --version 2>/dev/null || true
pip --version 2>/dev/null || true

echo
echo "== behavior probe: pip resolver with mutable git ref and declared range constraint =="
python3 - <<'PY'
import subprocess, tempfile, pathlib, shlex, json, sys

cmds = [
    "--dry-run",
    "-r",
    "/dev/stdin",
]

cases = [
    ("declared range", "fb-python-sdk>=1.1.8,<2"),
    ("mutable git ref", "git+https://github.com/featbit/featbit-python-sdk.git@refs/pull/14/head"),
]

for name, req in cases:
    cmdline = [sys.executable, "-m", "pip", "install"] + cmds + [req]
    print(f"\nCASE {name}: {' '.join(shlex.quote(p) for p in cmdline)}")
    try:
        p = subprocess.run(cmdline, text=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=80)
        decoded_stderr = p.stderr.decode("utf-8", "replace")
        decoded_stdout = p.stdout.decode("utf-8", "replace")
        print("returncode=", p.returncode)
        for line in decoded_stderr.splitlines()[-40:]:
            if line.strip().startswith(("Collecting", "Downloading", "Installed", "Ignoring", "Requirement already satisfied", "Resolved:")) or "error" in line.lower().encode("ascii", "ignore"):
                print(line)
        for line in decoded_stdout.splitlines()[-10:]:
            print(line)
    except Exception as e:
        print("error", type(e).__name__, e)
PY

Repository: featbit/openfeature-provider-python-server

Length of output: 863


🌐 Web query:

featbit Python SDK refs/pull/14/head PyPI fb-python-sdk 1.1.8

💡 Result:

As of July 24, 2026, the official PyPI package for the FeatBit Python SDK is fb-python-sdk [1][2]. The most recent version available on PyPI is 1.1.7, which was released on June 5, 2025 [1][3]. There is no official release of version 1.1.8 for the fb-python-sdk on PyPI, nor is there an open or merged pull request numbered 14 in the official featbit/featbit-python-sdk repository that references such a version [1][4][5]. Users should rely on the official releases documented in the GitHub repository and PyPI [1][4].

Citations:


Install an immutable fb-python-sdk requirement instead of the PR ref.

.github/workflows/ci.yml:28 points at refs/pull/14/head, while pyproject.toml declares fb-python-sdk>=1.1.8,<2. Since CI installs the editable package with --no-deps, this bypasses the declared dependency range and depends on a moving PR ref. Use the declared constraint, or pin a full commit and document the temporary override.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 28 - 29, The CI dependency
installation should not use the moving `refs/pull/14/head` override. Update the
`fb-python-sdk` install command in the workflow to honor the `pyproject.toml`
constraint `>=1.1.8,<2`, or replace it with an immutable full commit pin and
document the temporary override.

Comment on lines +74 to +86
def initialize(self, evaluation_context: EvaluationContext) -> None:
if not self._client.initialize:
ready = self._client.update_status_provider.wait_for_OKState(
self._initialization_timeout
)
if not ready:
state = self._client.update_status_provider.current_state
message = self._state_message(state)
if state.state_type == StateType.OFF:
raise ProviderFatalError(message)
raise ProviderNotReadyError(message)

self._register_listeners()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Provider can get permanently stuck NOT_READY/FATAL after an initialization timeout.

_register_listeners() (line 86) only runs when initialize() doesn't raise. If wait_for_OKState times out (or the state is OFF), the method raises before reaching line 86, so no status listener is ever registered. Per the FeatBit SDK docs, the client keeps retrying in the background after a start_wait/wait_for_OKState timeout, but per the OpenFeature spec, once initialize() fails the client status stays NOT_READY/ERROR/FATAL until the provider itself emits a recovery event. Since no listener was attached, this provider can never emit that recovery event — evaluations will keep returning the default value forever, even after FeatBit successfully connects moments later.

Register the listeners unconditionally so late recovery is still observed:

🔧 Proposed fix
     def initialize(self, evaluation_context: EvaluationContext) -> None:
-        if not self._client.initialize:
-            ready = self._client.update_status_provider.wait_for_OKState(
-                self._initialization_timeout
-            )
-            if not ready:
-                state = self._client.update_status_provider.current_state
-                message = self._state_message(state)
-                if state.state_type == StateType.OFF:
-                    raise ProviderFatalError(message)
-                raise ProviderNotReadyError(message)
-
-        self._register_listeners()
+        self._register_listeners()
+        if not self._client.initialize:
+            ready = self._client.update_status_provider.wait_for_OKState(
+                self._initialization_timeout
+            )
+            if not ready:
+                state = self._client.update_status_provider.current_state
+                message = self._state_message(state)
+                if state.state_type == StateType.OFF:
+                    raise ProviderFatalError(message)
+                raise ProviderNotReadyError(message)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def initialize(self, evaluation_context: EvaluationContext) -> None:
if not self._client.initialize:
ready = self._client.update_status_provider.wait_for_OKState(
self._initialization_timeout
)
if not ready:
state = self._client.update_status_provider.current_state
message = self._state_message(state)
if state.state_type == StateType.OFF:
raise ProviderFatalError(message)
raise ProviderNotReadyError(message)
self._register_listeners()
def initialize(self, evaluation_context: EvaluationContext) -> None:
self._register_listeners()
if not self._client.initialize:
ready = self._client.update_status_provider.wait_for_OKState(
self._initialization_timeout
)
if not ready:
state = self._client.update_status_provider.current_state
message = self._state_message(state)
if state.state_type == StateType.OFF:
raise ProviderFatalError(message)
raise ProviderNotReadyError(message)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@featbit_openfeature/provider.py` around lines 74 - 86, Register the status
listeners in initialize unconditionally, including when wait_for_OKState times
out or state is OFF. Ensure _register_listeners() executes before propagating
ProviderFatalError or ProviderNotReadyError, while preserving the existing error
classification and successful initialization behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant