Skip to content

Commit 65a5255

Browse files
authored
python sdk 1.0.0 (#1)
1 parent c8b5db4 commit 65a5255

38 files changed

+4902
-1
lines changed

.flake8

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[flake8]
2+
3+
################### FILE PATTERNS ##########################
4+
5+
# Provide a comma-separated list of glob patterns to exclude from checks.
6+
exclude =
7+
# git folder
8+
.git,
9+
# python cache
10+
__pycache__,
11+
# pytest cache
12+
.pytest_cache,
13+
# mypy cache
14+
.mypy_cache,
15+
build,
16+
dist
17+
# Provide a comma-separate list of glob patterns to include for checks.
18+
filename = *.py
19+
20+
########## Options ##########
21+
22+
# Report all errors, even if it is on the same line as a `# NOQA` comment.
23+
disable-noqa = False
24+
25+
# Set the maximum length that any line (with some exceptions) may be.
26+
max-line-length = 199
27+
# Set the maximum allowed McCabe complexity value for a block of code.
28+
max-complexity = 10
29+
30+
########## Rules ##########
31+
ignore =
32+
E133,
33+
E203,
34+
W503,
35+
C901,
36+
E722
37+
38+
per-file-ignores =
39+
__init__.py:F401,F403

.gitignore

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,97 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
### IDEs
132+
133+
#### VSCode
134+
135+
.vscode/*
136+
!.vscode/tasks.json
137+
!.vscode/extensions.json
138+
*.code-workspace
139+
140+
# Local History for Visual Studio Code
141+
.history/
142+
143+
#### JetBrains IDEs
144+
145+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
146+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
147+
148+
# User-specific stuff
149+
.idea/**/workspace.xml
150+
.idea/**/tasks.xml
151+
.idea/**/usage.statistics.xml
152+
.idea/**/dictionaries
153+
.idea/**/shelf
154+
.idea/**/.gitignore
155+
.idea/**/inspectionProfiles/profiles_settings.xml
156+
.idea/**/vcs.xml
157+
.idea/**/misc.xml
158+
.idea/**/modules.xml
159+
.idea/**/*.iml
160+
161+
# Generated files
162+
.idea/**/contentModel.xml
163+
164+
# Sensitive or high-churn files
165+
.idea/**/dataSources/
166+
.idea/**/dataSources.ids
167+
.idea/**/dataSources.local.xml
168+
.idea/**/sqlDataSources.xml
169+
.idea/**/dynamic.xml
170+
.idea/**/uiDesigner.xml
171+
.idea/**/dbnavigator.xml
172+
173+
# Gradle
174+
.idea/**/gradle.xml
175+
.idea/**/libraries
176+
177+
# Gradle and Maven with auto-import
178+
# When using Gradle or Maven with auto-import, you should exclude module files,
179+
# since they will be recreated, and may cause churn. Uncomment if using
180+
# auto-import.
181+
# .idea/artifacts
182+
# .idea/compiler.xml
183+
# .idea/jarRepositories.xml
184+
# .idea/modules.xml
185+
# .idea/*.iml
186+
# .idea/modules
187+
# *.iml
188+
# *.ipr
189+
190+
# CMake
191+
cmake-build-*/
192+
193+
# Mongo Explorer plugin
194+
.idea/**/mongoSettings.xml
195+
196+
# File-based project format
197+
*.iws
198+
199+
# IntelliJ
200+
out/
201+
202+
# mpeltonen/sbt-idea plugin
203+
.idea_modules/
204+
205+
# JIRA plugin
206+
atlassian-ide-plugin.xml
207+
208+
# Cursive Clojure plugin
209+
.idea/replstate.xml
210+
211+
# Crashlytics plugin (for Android Studio and IntelliJ)
212+
com_crashlytics_export_strings.xml
213+
crashlytics.properties
214+
crashlytics-build.properties
215+
fabric.properties
216+
217+
# Editor-based Rest Client
218+
.idea/httpRequests
219+
220+
# Android studio 3.1+ serialized cache file
221+
.idea/caches/build_file_checksums.ser
222+
223+
intergration_tests/*

MANIFEST.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include requirements.txt
2+
include README.md
3+
include dev-requirements.txt

README.md

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,131 @@
1-
# featbit-python-sdk
1+
# FeatBit python sdk
2+
3+
## Introduction
4+
5+
This is the Python Server SDK for the feature management platform FeatBit. It is
6+
intended for use in a multiple-users python server applications.
7+
8+
This SDK has two main purposes:
9+
10+
- Store the available feature flags and evaluate the feature flags by given user in the server side SDK
11+
- Sends feature flags usage, and custom events for the insights and A/B/n testing.
12+
13+
## Data synchonization
14+
15+
We use websocket to make the local data synchronized with the server, and then store them in the memory by default.
16+
Whenever there is any changes to a feature flag or his related data, the changes would be pushed to the SDK, the average
17+
synchronization time is less than **100** ms. Be aware the websocket connection can be interrupted by any error or
18+
internet interruption, but it would be restored automatically right after the problem is gone.
19+
20+
## Offline mode support
21+
22+
In the offline mode, SDK DOES not exchange any data with feature flag center, this mode is only use for internal test for instance.
23+
24+
To open the offline mode:
25+
```python
26+
config = Config(env_secret, event_url, streaming_url, offline=True)
27+
```
28+
29+
## Evaluation of a feature flag
30+
31+
SDK will initialize all the related data(feature flags, segments etc.) in the bootstrapping and receive the data updates
32+
in real time, as mentioned in the above
33+
34+
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.
35+
36+
## Installation
37+
install the sdk in using pip, this version of the SDK is compatible with Python 3.6 through 3.10.
38+
39+
```
40+
pip install fb-python-sdk
41+
```
42+
43+
## SDK
44+
45+
Applications SHOULD instantiate a single instance for the lifetime of the application. In the case where an application
46+
needs to evaluate feature flags from different environments, you may create multiple clients, but they should still be
47+
retained for the lifetime of the application rather than created per request or per thread.
48+
49+
### Bootstrapping
50+
51+
The bootstrapping is in fact the call of constructor of `FFCClient`, in which the SDK will be initialized and connect to feature flag center
52+
53+
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()`.
54+
55+
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.
56+
```python
57+
from fbclient.config import Config
58+
from fbclient import get, set_config
59+
60+
set_config(Config(env_secret, event_url, streaming_url))
61+
client = get()
62+
63+
if client.initialize:
64+
# your code
65+
66+
```
67+
You can also manage your `fbclient.client.FBClient`, the SDK will be initialized if you call `fbclient.client.FBClient` constructor.
68+
```python
69+
from fbclient.config import Config
70+
from fbclient.client import FBClient
71+
72+
client = FBClient(Config(env_secret, event_url, streaming_url), start_wait=15)
73+
74+
if client.initialize:
75+
# your code
76+
77+
```
78+
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:
79+
80+
``` python
81+
from fbclient.config import Config
82+
from fbclient.client import FBClient
83+
84+
client = FFCClient(Config(env_secret), start_wait=0)
85+
if client._update_status_provider.wait_for_OKState():
86+
# your code
87+
88+
```
89+
90+
91+
### Evaluation
92+
93+
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.
94+
95+
`User`: A dictionary of attributes that can affect flag evaluation, usually corresponding to a user of your application.
96+
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.
97+
For instance, the custom key should be a string; the custom value should be a string or a number
98+
99+
```python
100+
if client.initialize:
101+
user = {'key': user_key, 'name': user_name, 'age': age}
102+
flag_value = client.variation(flag_key, user, default_value)
103+
# your if/else code according to flag value
104+
105+
```
106+
If evaluation called before SDK client initialized or you set the wrong flag key or user for the evaluation, SDK will return
107+
the default value you set. The `fbclient.common_types.FlagState` will explain the details of the last evaluation including error raison.
108+
109+
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
110+
```python
111+
if client.initialize:
112+
user = {'key': user_key, 'name': user_name}
113+
all_flag_values = client.get_all_latest_flag_variations(user)
114+
ed = all_flag_values.get(flag_key)
115+
flag_value = ed.variation
116+
# your if/else code according to flag value
117+
118+
119+
```
120+
121+
### Experiments (A/B/n Testing)
122+
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.
123+
124+
In case you need more control over the experiment data sent to our server, we offer a method to send custom event.
125+
```python
126+
client.track_metric(user, event_name, numeric_value);
127+
```
128+
**numeric_value** is not mandatory, the default value is **1**.
129+
130+
Make sure `track_metric` is called after the related feature flag is evaluated by simply calling `variation` or `variation_detail`
131+
otherwise, the custom event may not be included into the experiment result.

dev-requirements.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
certifi>=2018.4.16
2+
urllib3>=1.22.0
3+
websocket-client>=1.0.0
4+
python-dateutil>=2.8.2
5+
flake8
6+
pytest
7+
pytest-mock
8+
autopep8
9+
build
10+
twine

fbclient/__init__.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from fbclient.client import FBClient
2+
from fbclient.config import Config
3+
from fbclient.utils.rwlock import ReadWriteLock
4+
from fbclient.utils import log
5+
6+
"""Settings."""
7+
start_wait = 15
8+
9+
__client = None
10+
__config = None
11+
__lock = ReadWriteLock()
12+
13+
14+
def get() -> FBClient:
15+
"""Returns the singleton Python SDK client instance, using the current configuration.
16+
17+
To use the SDK as a singleton, first make sure you have called :func:`fbclient.set_config()`
18+
at startup time. Then :func:`fbclient.get()` will return the same shared :class:`fbclient.client.FBClient`
19+
instance each time. The client will be initialized if it runs first time.
20+
```
21+
set_config(Config(env_secret, event_url, streaming_url))
22+
client = get()
23+
```
24+
If you need to create multiple client instances with different environments, instead of this
25+
singleton approach you can call directly the :class:`fbclient.client.FBClient` constructor.
26+
"""
27+
global __config
28+
global __client
29+
global __lock
30+
31+
try:
32+
__lock.read_lock()
33+
if __client:
34+
return __client
35+
if not __config:
36+
raise Exception("config is not initialized")
37+
finally:
38+
__lock.release_read_lock()
39+
40+
try:
41+
__lock.write_lock()
42+
if not __client:
43+
log.info("FB Python SDK: FB Python Client is initializing...")
44+
__client = FBClient(__config, start_wait)
45+
return __client
46+
finally:
47+
__lock.release_write_lock()
48+
49+
50+
def set_config(config: Config):
51+
"""Sets the configuration for the shared SDK client instance.
52+
53+
If this is called prior to :func:`fbclient.get()`, it stores the configuration that will be used when the
54+
client is initialized. If it is called after the client has already been initialized, the client will be
55+
re-initialized with the new configuration.
56+
57+
:param config: the client configuration
58+
"""
59+
global __config
60+
global __client
61+
global __lock
62+
63+
try:
64+
__lock.write_lock()
65+
if __client:
66+
__client.stop()
67+
log.info('FB Python SDK: FB Python Client is reinitializing...')
68+
__client = FBClient(config, start_wait)
69+
finally:
70+
__config = config
71+
__lock.release_write_lock()

fbclient/category.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
class Category:
2+
"""
3+
This class is used only by the internals of the feature flag storage mechanism.
4+
This type will be passed to the feature flag storage methods;
5+
its ``name`` property tells the feature store which collection of data is being referenced ("featureflags", "segments", etc.)
6+
The purpose is for the storage module to store data as completely generic JSON database
7+
"""
8+
9+
def __init__(self, name, tag):
10+
self._name = name
11+
self._tag = tag
12+
13+
@property
14+
def name(self):
15+
return self._name
16+
17+
@property
18+
def tag(self):
19+
return self._tag
20+
21+
22+
FEATURE_FLAGS = Category('featureFlags', 'ff')
23+
24+
SEGMENTS = Category('segments', 'seg')
25+
26+
DATATEST = Category('datatest', 'test')
27+
28+
"""
29+
An enumeration of all supported types. Applications should not need to reference this object directly.
30+
Custom data storage implementations can determine what kinds of model objects may need to be stored.
31+
"""
32+
ALL_CATS = [FEATURE_FLAGS, SEGMENTS, DATATEST]
33+
34+
ALL_CAT_NAMES = ['featureFlags', 'segments', 'datatest']

0 commit comments

Comments
 (0)