Skip to content
Merged
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
39 changes: 39 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[flake8]

################### FILE PATTERNS ##########################

# Provide a comma-separated list of glob patterns to exclude from checks.
exclude =
# git folder
.git,
# python cache
__pycache__,
# pytest cache
.pytest_cache,
# mypy cache
.mypy_cache,
build,
dist
# Provide a comma-separate list of glob patterns to include for checks.
filename = *.py

########## Options ##########

# Report all errors, even if it is on the same line as a `# NOQA` comment.
disable-noqa = False

# Set the maximum length that any line (with some exceptions) may be.
max-line-length = 199
# Set the maximum allowed McCabe complexity value for a block of code.
max-complexity = 10

########## Rules ##########
ignore =
E133,
E203,
W503,
C901,
E722

per-file-ignores =
__init__.py:F401,F403
94 changes: 94 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,97 @@ dmypy.json

# Pyre type checker
.pyre/

### IDEs

#### VSCode

.vscode/*
!.vscode/tasks.json
!.vscode/extensions.json
*.code-workspace

# Local History for Visual Studio Code
.history/

#### JetBrains IDEs

# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/.gitignore
.idea/**/inspectionProfiles/profiles_settings.xml
.idea/**/vcs.xml
.idea/**/misc.xml
.idea/**/modules.xml
.idea/**/*.iml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

intergration_tests/*
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include requirements.txt
include README.md
include dev-requirements.txt
132 changes: 131 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,131 @@
# featbit-python-sdk
# FeatBit python sdk

## Introduction

This is the Python Server SDK for the feature management platform FeatBit. It is
intended for use in a multiple-users python server applications.

This SDK has two main purposes:

- Store the available feature flags and evaluate the feature flags by given user in the server side SDK
- Sends feature flags usage, and custom events for the insights and A/B/n testing.

## Data synchonization

We use websocket to make the local data synchronized with the server, and then store them in the memory by default.
Whenever there is any changes to a feature flag or his related data, the changes would be pushed to the SDK, the average
synchronization time is less than **100** ms. Be aware the websocket connection can be interrupted by any error or
internet interruption, but it would be restored automatically right after the problem is gone.

## Offline mode support

In the offline mode, SDK DOES not exchange any data with feature flag center, this mode is only use for internal test for instance.

To open the offline mode:
```python
config = Config(env_secret, event_url, streaming_url, offline=True)
```

## Evaluation of a feature flag

SDK will initialize all the related data(feature flags, segments etc.) in the bootstrapping and receive the data updates
in real time, as mentioned in the above

After initialization, the SDK has all the feature flags in the memory and all evaluation is done locally and synchronously, the average evaluation time is < **10** ms.

## Installation
install the sdk in using pip, this version of the SDK is compatible with Python 3.6 through 3.10.

```
pip install fb-python-sdk
```

## SDK

Applications SHOULD instantiate a single instance for the lifetime of the application. In the case where an application
needs to evaluate feature flags from different environments, you may create multiple clients, but they should still be
retained for the lifetime of the application rather than created per request or per thread.

### Bootstrapping

The bootstrapping is in fact the call of constructor of `FFCClient`, in which the SDK will be initialized and connect to feature flag center

The constructor will return when it successfully connects, or when the timeout(default: 15 seconds) expires, whichever comes first. If it has not succeeded in connecting when the timeout elapses, you will receive the client in an uninitialized state where feature flags will return default values; it will still continue trying to connect in the background unless there has been a network error or you close the client(using `stop()`). You can detect whether initialization has succeeded by calling `initialize()`.

The best way to use the SDK as a singleton, first make sure you have called `fbclient.set_config()` at startup time. Then `fbclient.get()` will return the same shared `fbclient.client.FFCClient` instance each time. The client will be initialized if it runs first time.
```python
from fbclient.config import Config
from fbclient import get, set_config

set_config(Config(env_secret, event_url, streaming_url))
client = get()

if client.initialize:
# your code

```
You can also manage your `fbclient.client.FBClient`, the SDK will be initialized if you call `fbclient.client.FBClient` constructor.
```python
from fbclient.config import Config
from fbclient.client import FBClient

client = FBClient(Config(env_secret, event_url, streaming_url), start_wait=15)

if client.initialize:
# your code

```
If you prefer to have the constructor return immediately, and then wait for initialization to finish at some other point, you can use `fbclient.client.fbclient.update_status_provider` object, which provides an asynchronous way, as follows:

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

client = FFCClient(Config(env_secret), start_wait=0)
if client._update_status_provider.wait_for_OKState():
# your code

```


### Evaluation

SDK calculates the value of a feature flag for a given user, and returns a flag vlaue/an object that describes the way that the value was determined.

`User`: A dictionary of attributes that can affect flag evaluation, usually corresponding to a user of your application.
This object contains built-in properties(`key`, `name`). The `key` and `name` are required. The `key` must uniquely identify each user; this could be a username or email address for authenticated users, or a ID for anonymous users. The `name` is used to search your user quickly. You may also define custom properties with arbitrary names and values.
For instance, the custom key should be a string; the custom value should be a string or a number

```python
if client.initialize:
user = {'key': user_key, 'name': user_name, 'age': age}
flag_value = client.variation(flag_key, user, default_value)
# your if/else code according to flag value

```
If evaluation called before SDK client initialized or you set the wrong flag key or user for the evaluation, SDK will return
the default value you set. The `fbclient.common_types.FlagState` will explain the details of the last evaluation including error raison.

If you would like to get variations of all feature flags in a special environment, you can use `fbclient.client.FBClient.get_all_latest_flag_variations`, SDK will return `fbclient.common_types.AllFlagStates`, that explain the details of all feature flags
```python
if client.initialize:
user = {'key': user_key, 'name': user_name}
all_flag_values = client.get_all_latest_flag_variations(user)
ed = all_flag_values.get(flag_key)
flag_value = ed.variation
# your if/else code according to flag value


```

### Experiments (A/B/n Testing)
We support automatic experiments for pageviews and clicks, you just need to set your experiment on our SaaS platform, then you should be able to see the result in near real time after the experiment is started.

In case you need more control over the experiment data sent to our server, we offer a method to send custom event.
```python
client.track_metric(user, event_name, numeric_value);
```
**numeric_value** is not mandatory, the default value is **1**.

Make sure `track_metric` is called after the related feature flag is evaluated by simply calling `variation` or `variation_detail`
otherwise, the custom event may not be included into the experiment result.
10 changes: 10 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
certifi>=2018.4.16
urllib3>=1.22.0
websocket-client>=1.0.0
python-dateutil>=2.8.2
flake8
pytest
pytest-mock
autopep8
build
twine
71 changes: 71 additions & 0 deletions fbclient/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from fbclient.client import FBClient
from fbclient.config import Config
from fbclient.utils.rwlock import ReadWriteLock
from fbclient.utils import log

"""Settings."""
start_wait = 15

__client = None
__config = None
__lock = ReadWriteLock()


def get() -> FBClient:
"""Returns the singleton Python SDK client instance, using the current configuration.

To use the SDK as a singleton, first make sure you have called :func:`fbclient.set_config()`
at startup time. Then :func:`fbclient.get()` will return the same shared :class:`fbclient.client.FBClient`
instance each time. The client will be initialized if it runs first time.
```
set_config(Config(env_secret, event_url, streaming_url))
client = get()
```
If you need to create multiple client instances with different environments, instead of this
singleton approach you can call directly the :class:`fbclient.client.FBClient` constructor.
"""
global __config
global __client
global __lock

try:
__lock.read_lock()
if __client:
return __client
if not __config:
raise Exception("config is not initialized")
finally:
__lock.release_read_lock()

try:
__lock.write_lock()
if not __client:
log.info("FB Python SDK: FB Python Client is initializing...")
__client = FBClient(__config, start_wait)
return __client
finally:
__lock.release_write_lock()


def set_config(config: Config):
"""Sets the configuration for the shared SDK client instance.

If this is called prior to :func:`fbclient.get()`, it stores the configuration that will be used when the
client is initialized. If it is called after the client has already been initialized, the client will be
re-initialized with the new configuration.

:param config: the client configuration
"""
global __config
global __client
global __lock

try:
__lock.write_lock()
if __client:
__client.stop()
log.info('FB Python SDK: FB Python Client is reinitializing...')
__client = FBClient(config, start_wait)
finally:
__config = config
__lock.release_write_lock()
34 changes: 34 additions & 0 deletions fbclient/category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class Category:
"""
This class is used only by the internals of the feature flag storage mechanism.
This type will be passed to the feature flag storage methods;
its ``name`` property tells the feature store which collection of data is being referenced ("featureflags", "segments", etc.)
The purpose is for the storage module to store data as completely generic JSON database
"""

def __init__(self, name, tag):
self._name = name
self._tag = tag

@property
def name(self):
return self._name

@property
def tag(self):
return self._tag


FEATURE_FLAGS = Category('featureFlags', 'ff')

SEGMENTS = Category('segments', 'seg')

DATATEST = Category('datatest', 'test')

"""
An enumeration of all supported types. Applications should not need to reference this object directly.
Custom data storage implementations can determine what kinds of model objects may need to be stored.
"""
ALL_CATS = [FEATURE_FLAGS, SEGMENTS, DATATEST]

ALL_CAT_NAMES = ['featureFlags', 'segments', 'datatest']
Loading