Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 84 additions & 13 deletions .github/workflows/code-quality.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,54 @@ on:
- synchronize

jobs:
legacy-python-3-6:
runs-on: ubuntu-24.04
container: ubuntu:20.04
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.6
uses: actions/setup-python@v7
with:
python-version: "3.6"
- name: Display Python version
run: python -c "import sys; print(sys.version)"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest pytest-mock
python -m pip install -r requirements.txt
python -m pip install --no-deps .
- name: Lint with flake8
run: python -m flake8 .
- name: Test with pytest
run: python -m pytest

build:
# compatible for py 3.6
runs-on: ubuntu-20.04
runs-on: ${{ matrix.runner }}
strategy:
# You can use PyPy versions in python-version.
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
include:
- python-version: "3.7"
runner: ubuntu-22.04
- python-version: "3.8"
runner: ubuntu-24.04
- python-version: "3.9"
runner: ubuntu-24.04
- python-version: "3.10"
runner: ubuntu-24.04
- python-version: "3.11"
runner: ubuntu-24.04
- python-version: "3.12"
runner: ubuntu-24.04
- python-version: "3.13"
runner: ubuntu-24.04
- python-version: "3.14"
runner: ubuntu-24.04

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
# You can test your matrix by printing the current Python version
Expand All @@ -28,21 +64,56 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest pytest-mock build
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
python -m pip install flake8 pytest pytest-mock build
if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi
python -m pip install --no-deps .
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors
flake8 .
python -m flake8 .
- name: Test with pytest
run: |
pytest
python -m pytest
- name: Audit concurrency, memory, and thread cleanup
if: matrix.python-version == '3.12'
if: matrix.python-version == '3.12' || matrix.python-version == '3.14'
run: python scripts/resource_audit.py --evaluations 20000 --workers 8
- name: Build production distributions
if: matrix.python-version == '3.12'
if: matrix.python-version == '3.12' || matrix.python-version == '3.14'
run: python -m build
- name: Verify wheel contains production packages only
if: matrix.python-version == '3.12'
if: matrix.python-version == '3.12' || matrix.python-version == '3.14'
run: python -c "import glob, zipfile; names = zipfile.ZipFile(glob.glob('dist/*.whl')[0]).namelist(); assert not any(name.startswith(('tests/', 'featbit_openfeature/')) for name in names)"
- name: Install and smoke-test the built wheel
if: matrix.python-version == '3.14'
run: |
python -m venv "$RUNNER_TEMP/featbit-wheel-test"
"$RUNNER_TEMP/featbit-wheel-test/bin/python" -m pip install --upgrade pip
"$RUNNER_TEMP/featbit-wheel-test/bin/python" -m pip install dist/*.whl
"$RUNNER_TEMP/featbit-wheel-test/bin/python" -m pip check
"$RUNNER_TEMP/featbit-wheel-test/bin/python" scripts/wheel_smoke.py \
--bootstrap tests/fbclient_test_data.json \
--expected-version-file release/package.json

minimum-dependencies:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.14
uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Install exact minimum dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest pytest-mock
python -m pip install \
certifi==2018.4.16 \
urllib3==1.26.5 \
python-dateutil==2.8.2 \
websocket-client==1.0.0
python -m pip install --no-deps .
python -m pip check
- name: Test with pytest
run: python -m pytest
- name: Audit concurrency, memory, and thread cleanup
run: python scripts/resource_audit.py --evaluations 20000 --workers 8
39 changes: 26 additions & 13 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
deploy:
permissions:
contents: write
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
environment: py-production
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Read current version
id: version
run: |
Expand All @@ -31,26 +31,39 @@ jobs:
with open("release/package.json") as file:
print("value=%s" % json.load(file)["version"])
PY
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "v${{ steps.version.outputs.value }}" \
--title "v${{ steps.version.outputs.value }}" \
--notes-file release/description.md
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v7
with:
python-version: '3.x'
python-version: '3.14'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
python -m pip install build twine
- name: Build package
run: python -m build
- name: Check package metadata
run: python -m twine check dist/*
- name: Install and smoke-test the built wheel
env:
VERSION: ${{ steps.version.outputs.value }}
run: |
python -m venv "$RUNNER_TEMP/featbit-wheel-test"
"$RUNNER_TEMP/featbit-wheel-test/bin/python" -m pip install --upgrade pip
"$RUNNER_TEMP/featbit-wheel-test/bin/python" -m pip install dist/*.whl
"$RUNNER_TEMP/featbit-wheel-test/bin/python" -m pip check
"$RUNNER_TEMP/featbit-wheel-test/bin/python" scripts/wheel_smoke.py \
--bootstrap tests/fbclient_test_data.json \
--expected-version "$VERSION"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.value }}
run: |
gh release create "v$VERSION" \
--title "v$VERSION" \
--notes-file release/description.md
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
user: __token__
password: ${{ secrets.PYPI_PASSWORD }}
verify-metadata: false
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ If you want to use your own data source, see [Offline Mode](#offline-mode).
## Get Started

### Installation
Install the SDK using pip. This version is compatible with Python 3.6 through 3.12.
Install the SDK using pip. This version is compatible with Python 3.6 through 3.14.
CI is configured to test compatibility through Python 3.14, including the
full unit suite and SDK resource-lifecycle audit.
Free-threaded CPython 3.14 builds are not currently tested or supported.

```shell
pip install fb-python-sdk
Expand Down
4 changes: 2 additions & 2 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
certifi>=2018.4.16
urllib3>=1.22.0
urllib3>=1.26.5
websocket-client>=1.0.0
python-dateutil>=2.8.2
flake8
pytest
pytest-mock
autopep8
build
twine
twine
4 changes: 3 additions & 1 deletion release/description.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

version: 1.1.8
version: 1.1.9

## Break changes

Expand All @@ -16,6 +16,8 @@ version: 1.1.8

## Updates

- support and test CPython versions 3.13 and 3.14
- require urllib3 1.26.5 or newer for CPython 3.14 compatibility
- handle Python versions 3.12.x
- make client, WebSocket, event, and notice shutdown deterministic and idempotent
- isolate runtime event and shutdown failures from application code
Expand Down
2 changes: 1 addition & 1 deletion release/package.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version":"1.1.8"
"version":"1.1.9"
}
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
certifi>=2018.4.16
urllib3>=1.22.0
urllib3>=1.26.5
python-dateutil>=2.8.2
websocket-client>=1.0.0
websocket-client>=1.0.0
115 changes: 115 additions & 0 deletions scripts/wheel_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Exercise the installed wheel through the public offline client API."""

import argparse
import base64
import json
import site
import threading
from importlib import metadata
from pathlib import Path

import fbclient
from fbclient.client import FBClient
from fbclient.config import Config


PACKAGE_NAME = "fb-python-sdk"
FAKE_ENV_SECRET = base64.b64encode(b"fake_env_secret").decode()
FAKE_URL = "http://fake"
USER_ENABLED = {"key": "test-user-1", "name": "test-user-1", "country": "us"}
USER_DISABLED = {
"key": "test-user-3",
"name": "test-user-3",
"country": "cn",
"major": "cs",
}


def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--bootstrap", type=Path, required=True)
parser.add_argument("--expected-version")
parser.add_argument("--expected-version-file", type=Path)
return parser.parse_args()


def assert_installed_package():
package_path = Path(fbclient.__file__).resolve()
site_paths = [Path(path).resolve() for path in site.getsitepackages()]
assert any(package_path.is_relative_to(path) for path in site_paths), (
"fbclient was not imported from the wheel environment: %s" % package_path
)


def assert_metadata(expected_version):
distribution = metadata.distribution(PACKAGE_NAME)
if expected_version:
assert distribution.version == expected_version

package_metadata = distribution.metadata
assert package_metadata["Requires-Python"] == ">=3.6, <3.15"
classifiers = package_metadata.get_all("Classifier") or []
assert "Programming Language :: Python :: 3.14" in classifiers
requirements = distribution.requires or []
assert any(requirement.startswith("urllib3>=1.26.5") for requirement in requirements)
return distribution.version


def featbit_thread_ids():
return {
thread.ident
for thread in threading.enumerate()
if thread.ident is not None and thread.name.startswith("featbit-")
}


def exercise_client(bootstrap_path):
baseline_threads = featbit_thread_ids()
states = []
config = Config(
FAKE_ENV_SECRET,
event_url=FAKE_URL,
streaming_url=FAKE_URL,
offline=True,
)
client = FBClient(config)

def on_status_change(state):
states.append(state.state_type.name)

client.update_status_provider.add_listener(on_status_change)
try:
assert client.initialize_from_external_json(bootstrap_path.read_text())
assert client.initialize
assert states == ["OK"]
assert client.variation("ff-test-bool", USER_ENABLED, False) is True
assert client.variation("ff-test-bool", USER_DISABLED, True) is False
assert client.variation("missing-flag", USER_ENABLED, False) is False
for _ in range(100):
assert client.variation("ff-test-bool", USER_ENABLED, False) is True
finally:
client.update_status_provider.remove_listener(on_status_change)
client.stop()
client.stop()

leaked_threads = featbit_thread_ids() - baseline_threads
assert not leaked_threads, "FeatBit worker threads leaked: %s" % leaked_threads
return states


def main():
args = parse_args()
expected_version = args.expected_version
if args.expected_version_file:
expected_version = json.loads(
args.expected_version_file.read_text()
)["version"]
assert_installed_package()
version = assert_metadata(expected_version)
states = exercise_client(args.bootstrap)
print(json.dumps({"package": PACKAGE_NAME, "states": states, "version": version}))


if __name__ == "__main__":
main()
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ def parse_requirements(filename):
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
],
extras_require={
"dev": dev_reqs
},
python_requires='>=3.6, <3.13'
python_requires='>=3.6, <3.15'
)