featureflip 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- featureflip-0.1.0/.gitignore +77 -0
- featureflip-0.1.0/PKG-INFO +182 -0
- featureflip-0.1.0/README.md +152 -0
- featureflip-0.1.0/pyproject.toml +107 -0
- featureflip-0.1.0/src/featureflip/__init__.py +51 -0
- featureflip-0.1.0/src/featureflip/_events.py +123 -0
- featureflip-0.1.0/src/featureflip/_http.py +238 -0
- featureflip-0.1.0/src/featureflip/_polling.py +81 -0
- featureflip-0.1.0/src/featureflip/_streaming.py +144 -0
- featureflip-0.1.0/src/featureflip/client.py +583 -0
- featureflip-0.1.0/src/featureflip/config.py +55 -0
- featureflip-0.1.0/src/featureflip/context.py +66 -0
- featureflip-0.1.0/src/featureflip/detail.py +49 -0
- featureflip-0.1.0/src/featureflip/evaluation.py +311 -0
- featureflip-0.1.0/src/featureflip/exceptions.py +19 -0
- featureflip-0.1.0/src/featureflip/models.py +306 -0
- featureflip-0.1.0/tests/__init__.py +1 -0
- featureflip-0.1.0/tests/integration/__init__.py +1 -0
- featureflip-0.1.0/tests/integration/test_api.py +699 -0
- featureflip-0.1.0/tests/test_client.py +718 -0
- featureflip-0.1.0/tests/test_config.py +50 -0
- featureflip-0.1.0/tests/test_context.py +44 -0
- featureflip-0.1.0/tests/test_detail.py +45 -0
- featureflip-0.1.0/tests/test_evaluation.py +1023 -0
- featureflip-0.1.0/tests/test_events.py +279 -0
- featureflip-0.1.0/tests/test_exceptions.py +49 -0
- featureflip-0.1.0/tests/test_http.py +277 -0
- featureflip-0.1.0/tests/test_models.py +175 -0
- featureflip-0.1.0/tests/test_polling.py +178 -0
- featureflip-0.1.0/tests/test_streaming.py +281 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
|
|
49
|
+
# Translations
|
|
50
|
+
*.mo
|
|
51
|
+
*.pot
|
|
52
|
+
|
|
53
|
+
# Environments
|
|
54
|
+
.env
|
|
55
|
+
.venv
|
|
56
|
+
env/
|
|
57
|
+
venv/
|
|
58
|
+
ENV/
|
|
59
|
+
env.bak/
|
|
60
|
+
venv.bak/
|
|
61
|
+
|
|
62
|
+
# mypy
|
|
63
|
+
.mypy_cache/
|
|
64
|
+
.dmypy.json
|
|
65
|
+
dmypy.json
|
|
66
|
+
|
|
67
|
+
# ruff
|
|
68
|
+
.ruff_cache/
|
|
69
|
+
|
|
70
|
+
# IDE
|
|
71
|
+
.idea/
|
|
72
|
+
.vscode/
|
|
73
|
+
*.swp
|
|
74
|
+
*.swo
|
|
75
|
+
|
|
76
|
+
# uv lock file (optional, use pip with pyproject.toml)
|
|
77
|
+
uv.lock
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: featureflip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Featureflip - a feature flag SaaS platform
|
|
5
|
+
Project-URL: Homepage, https://featureflip.io
|
|
6
|
+
Project-URL: Documentation, https://featureflip.io/docs/sdks/python/
|
|
7
|
+
Author: Featureflip Team
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: feature-flags,feature-toggles,sdk
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: httpx-sse>=0.4.0
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: structlog>=24.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Featureflip Python SDK
|
|
32
|
+
|
|
33
|
+
Python SDK for [Featureflip](https://github.com/featureflip/featureflip) - evaluate feature flags locally with near-zero latency.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install featureflip
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from featureflip import FeatureflipClient
|
|
45
|
+
|
|
46
|
+
# Initialize the client (blocks until flags are loaded)
|
|
47
|
+
client = FeatureflipClient(sdk_key="your-sdk-key")
|
|
48
|
+
|
|
49
|
+
# Evaluate a feature flag
|
|
50
|
+
enabled = client.variation("my-feature", {"user_id": "user-123"}, default=False)
|
|
51
|
+
|
|
52
|
+
if enabled:
|
|
53
|
+
print("Feature is enabled!")
|
|
54
|
+
else:
|
|
55
|
+
print("Feature is disabled")
|
|
56
|
+
|
|
57
|
+
# Clean shutdown
|
|
58
|
+
client.close()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from featureflip import FeatureflipClient, Config
|
|
65
|
+
|
|
66
|
+
client = FeatureflipClient(
|
|
67
|
+
sdk_key="your-sdk-key",
|
|
68
|
+
config=Config(
|
|
69
|
+
base_url="https://eval.featureflip.io", # Evaluation API URL
|
|
70
|
+
streaming=True, # Use SSE for real-time updates (default)
|
|
71
|
+
poll_interval=30.0, # Polling interval if streaming=False
|
|
72
|
+
send_events=True, # Enable analytics event tracking
|
|
73
|
+
flush_interval=30.0, # Event flush interval in seconds
|
|
74
|
+
init_timeout=10.0, # Max seconds to wait for initialization
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The SDK key can also be set via the `FEATUREFLIP_SDK_KEY` environment variable.
|
|
80
|
+
|
|
81
|
+
## Context Manager
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
with FeatureflipClient(sdk_key="your-sdk-key") as client:
|
|
85
|
+
enabled = client.variation("my-feature", {"user_id": "123"}, default=False)
|
|
86
|
+
# Automatically closes and flushes events on exit
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Evaluation
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Boolean flag
|
|
93
|
+
enabled = client.variation("feature-key", {"user_id": "123"}, default=False)
|
|
94
|
+
|
|
95
|
+
# String flag
|
|
96
|
+
tier = client.variation("pricing-tier", {"user_id": "123"}, default="free")
|
|
97
|
+
|
|
98
|
+
# Number flag
|
|
99
|
+
limit = client.variation("rate-limit", {"user_id": "123"}, default=100)
|
|
100
|
+
|
|
101
|
+
# JSON flag
|
|
102
|
+
config = client.variation("ui-config", {"user_id": "123"}, default={"theme": "light"})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Detailed Evaluation
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
detail = client.variation_detail("feature-key", {"user_id": "123"}, default=False)
|
|
109
|
+
|
|
110
|
+
print(detail.value) # The evaluated value
|
|
111
|
+
print(detail.reason) # "RULE_MATCH", "FALLTHROUGH", "FLAG_DISABLED", etc.
|
|
112
|
+
print(detail.rule_id) # Rule ID if reason is RULE_MATCH
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Event Tracking
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# Track custom events
|
|
119
|
+
client.track("checkout-completed", {"user_id": "123"}, metadata={"total": 99.99})
|
|
120
|
+
|
|
121
|
+
# Identify users for segment building
|
|
122
|
+
client.identify({"user_id": "123", "email": "user@example.com", "plan": "pro"})
|
|
123
|
+
|
|
124
|
+
# Force flush pending events
|
|
125
|
+
client.flush()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Testing
|
|
129
|
+
|
|
130
|
+
Use the test client for deterministic unit tests:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from featureflip import FeatureflipClient
|
|
134
|
+
|
|
135
|
+
# Create a test client with fixed values (no network calls)
|
|
136
|
+
client = FeatureflipClient.for_testing({
|
|
137
|
+
"my-feature": True,
|
|
138
|
+
"pricing-tier": "pro",
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
# Evaluations return the configured values
|
|
142
|
+
assert client.variation("my-feature", {}, default=False) is True
|
|
143
|
+
assert client.variation("pricing-tier", {}, default="free") == "pro"
|
|
144
|
+
|
|
145
|
+
# Unknown flags return the default
|
|
146
|
+
assert client.variation("unknown", {}, default="fallback") == "fallback"
|
|
147
|
+
|
|
148
|
+
# Update values at runtime
|
|
149
|
+
client.set_test_value("my-feature", False)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Features
|
|
153
|
+
|
|
154
|
+
- **Client-side evaluation** - Near-zero latency after initialization
|
|
155
|
+
- **Real-time updates** - SSE streaming with polling fallback
|
|
156
|
+
- **Event tracking** - Automatic batching and flushing of analytics events
|
|
157
|
+
- **Test support** - `for_testing()` factory for deterministic unit tests
|
|
158
|
+
- **Type-safe** - Full type hints with mypy strict mode compliance
|
|
159
|
+
|
|
160
|
+
## Requirements
|
|
161
|
+
|
|
162
|
+
- Python 3.10+
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Install development dependencies
|
|
168
|
+
pip install -e ".[dev]"
|
|
169
|
+
|
|
170
|
+
# Run tests
|
|
171
|
+
pytest
|
|
172
|
+
|
|
173
|
+
# Run linting
|
|
174
|
+
ruff check src/featureflip tests
|
|
175
|
+
|
|
176
|
+
# Run type checking
|
|
177
|
+
mypy src/featureflip --strict
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Featureflip Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for [Featureflip](https://github.com/featureflip/featureflip) - evaluate feature flags locally with near-zero latency.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install featureflip
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from featureflip import FeatureflipClient
|
|
15
|
+
|
|
16
|
+
# Initialize the client (blocks until flags are loaded)
|
|
17
|
+
client = FeatureflipClient(sdk_key="your-sdk-key")
|
|
18
|
+
|
|
19
|
+
# Evaluate a feature flag
|
|
20
|
+
enabled = client.variation("my-feature", {"user_id": "user-123"}, default=False)
|
|
21
|
+
|
|
22
|
+
if enabled:
|
|
23
|
+
print("Feature is enabled!")
|
|
24
|
+
else:
|
|
25
|
+
print("Feature is disabled")
|
|
26
|
+
|
|
27
|
+
# Clean shutdown
|
|
28
|
+
client.close()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from featureflip import FeatureflipClient, Config
|
|
35
|
+
|
|
36
|
+
client = FeatureflipClient(
|
|
37
|
+
sdk_key="your-sdk-key",
|
|
38
|
+
config=Config(
|
|
39
|
+
base_url="https://eval.featureflip.io", # Evaluation API URL
|
|
40
|
+
streaming=True, # Use SSE for real-time updates (default)
|
|
41
|
+
poll_interval=30.0, # Polling interval if streaming=False
|
|
42
|
+
send_events=True, # Enable analytics event tracking
|
|
43
|
+
flush_interval=30.0, # Event flush interval in seconds
|
|
44
|
+
init_timeout=10.0, # Max seconds to wait for initialization
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The SDK key can also be set via the `FEATUREFLIP_SDK_KEY` environment variable.
|
|
50
|
+
|
|
51
|
+
## Context Manager
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
with FeatureflipClient(sdk_key="your-sdk-key") as client:
|
|
55
|
+
enabled = client.variation("my-feature", {"user_id": "123"}, default=False)
|
|
56
|
+
# Automatically closes and flushes events on exit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Evaluation
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# Boolean flag
|
|
63
|
+
enabled = client.variation("feature-key", {"user_id": "123"}, default=False)
|
|
64
|
+
|
|
65
|
+
# String flag
|
|
66
|
+
tier = client.variation("pricing-tier", {"user_id": "123"}, default="free")
|
|
67
|
+
|
|
68
|
+
# Number flag
|
|
69
|
+
limit = client.variation("rate-limit", {"user_id": "123"}, default=100)
|
|
70
|
+
|
|
71
|
+
# JSON flag
|
|
72
|
+
config = client.variation("ui-config", {"user_id": "123"}, default={"theme": "light"})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Detailed Evaluation
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
detail = client.variation_detail("feature-key", {"user_id": "123"}, default=False)
|
|
79
|
+
|
|
80
|
+
print(detail.value) # The evaluated value
|
|
81
|
+
print(detail.reason) # "RULE_MATCH", "FALLTHROUGH", "FLAG_DISABLED", etc.
|
|
82
|
+
print(detail.rule_id) # Rule ID if reason is RULE_MATCH
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Event Tracking
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# Track custom events
|
|
89
|
+
client.track("checkout-completed", {"user_id": "123"}, metadata={"total": 99.99})
|
|
90
|
+
|
|
91
|
+
# Identify users for segment building
|
|
92
|
+
client.identify({"user_id": "123", "email": "user@example.com", "plan": "pro"})
|
|
93
|
+
|
|
94
|
+
# Force flush pending events
|
|
95
|
+
client.flush()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Testing
|
|
99
|
+
|
|
100
|
+
Use the test client for deterministic unit tests:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from featureflip import FeatureflipClient
|
|
104
|
+
|
|
105
|
+
# Create a test client with fixed values (no network calls)
|
|
106
|
+
client = FeatureflipClient.for_testing({
|
|
107
|
+
"my-feature": True,
|
|
108
|
+
"pricing-tier": "pro",
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
# Evaluations return the configured values
|
|
112
|
+
assert client.variation("my-feature", {}, default=False) is True
|
|
113
|
+
assert client.variation("pricing-tier", {}, default="free") == "pro"
|
|
114
|
+
|
|
115
|
+
# Unknown flags return the default
|
|
116
|
+
assert client.variation("unknown", {}, default="fallback") == "fallback"
|
|
117
|
+
|
|
118
|
+
# Update values at runtime
|
|
119
|
+
client.set_test_value("my-feature", False)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Features
|
|
123
|
+
|
|
124
|
+
- **Client-side evaluation** - Near-zero latency after initialization
|
|
125
|
+
- **Real-time updates** - SSE streaming with polling fallback
|
|
126
|
+
- **Event tracking** - Automatic batching and flushing of analytics events
|
|
127
|
+
- **Test support** - `for_testing()` factory for deterministic unit tests
|
|
128
|
+
- **Type-safe** - Full type hints with mypy strict mode compliance
|
|
129
|
+
|
|
130
|
+
## Requirements
|
|
131
|
+
|
|
132
|
+
- Python 3.10+
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Install development dependencies
|
|
138
|
+
pip install -e ".[dev]"
|
|
139
|
+
|
|
140
|
+
# Run tests
|
|
141
|
+
pytest
|
|
142
|
+
|
|
143
|
+
# Run linting
|
|
144
|
+
ruff check src/featureflip tests
|
|
145
|
+
|
|
146
|
+
# Run type checking
|
|
147
|
+
mypy src/featureflip --strict
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "featureflip"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for Featureflip - a feature flag SaaS platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Featureflip Team" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["feature-flags", "feature-toggles", "sdk"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.27.0",
|
|
28
|
+
"httpx-sse>=0.4.0",
|
|
29
|
+
"structlog>=24.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0.0",
|
|
35
|
+
"pytest-cov>=4.0.0",
|
|
36
|
+
"pytest-asyncio>=0.23.0",
|
|
37
|
+
"respx>=0.21.0",
|
|
38
|
+
"ruff>=0.3.0",
|
|
39
|
+
"mypy>=1.8.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://featureflip.io"
|
|
44
|
+
Documentation = "https://featureflip.io/docs/sdks/python/"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/featureflip"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
asyncio_mode = "auto"
|
|
52
|
+
addopts = [
|
|
53
|
+
"-ra",
|
|
54
|
+
"-q",
|
|
55
|
+
"--strict-markers",
|
|
56
|
+
]
|
|
57
|
+
markers = [
|
|
58
|
+
"unit: Unit tests",
|
|
59
|
+
"integration: Integration tests",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
target-version = "py310"
|
|
64
|
+
line-length = 88
|
|
65
|
+
src = ["src", "tests"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = [
|
|
69
|
+
"E", # pycodestyle errors
|
|
70
|
+
"W", # pycodestyle warnings
|
|
71
|
+
"F", # Pyflakes
|
|
72
|
+
"I", # isort
|
|
73
|
+
"B", # flake8-bugbear
|
|
74
|
+
"C4", # flake8-comprehensions
|
|
75
|
+
"UP", # pyupgrade
|
|
76
|
+
"ARG", # flake8-unused-arguments
|
|
77
|
+
"SIM", # flake8-simplify
|
|
78
|
+
"TCH", # flake8-type-checking
|
|
79
|
+
"PTH", # flake8-use-pathlib
|
|
80
|
+
"ERA", # eradicate
|
|
81
|
+
"RUF", # Ruff-specific rules
|
|
82
|
+
]
|
|
83
|
+
ignore = [
|
|
84
|
+
"E501", # line too long (handled by formatter)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint.isort]
|
|
88
|
+
known-first-party = ["featureflip"]
|
|
89
|
+
|
|
90
|
+
[tool.mypy]
|
|
91
|
+
python_version = "3.10"
|
|
92
|
+
strict = true
|
|
93
|
+
warn_return_any = true
|
|
94
|
+
warn_unused_ignores = true
|
|
95
|
+
disallow_untyped_defs = true
|
|
96
|
+
disallow_incomplete_defs = true
|
|
97
|
+
check_untyped_defs = true
|
|
98
|
+
disallow_untyped_decorators = true
|
|
99
|
+
no_implicit_optional = true
|
|
100
|
+
warn_redundant_casts = true
|
|
101
|
+
warn_unused_configs = true
|
|
102
|
+
show_error_codes = true
|
|
103
|
+
files = ["src/featureflip"]
|
|
104
|
+
|
|
105
|
+
[[tool.mypy.overrides]]
|
|
106
|
+
module = "tests.*"
|
|
107
|
+
disallow_untyped_defs = false
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Featureflip Python SDK."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from featureflip.client import FeatureflipClient
|
|
6
|
+
from featureflip.config import Config
|
|
7
|
+
from featureflip.context import EvaluationContext
|
|
8
|
+
from featureflip.detail import EvaluationDetail, EvaluationReason
|
|
9
|
+
from featureflip.exceptions import (
|
|
10
|
+
ConfigurationError,
|
|
11
|
+
FeatureflipError,
|
|
12
|
+
InitializationError,
|
|
13
|
+
)
|
|
14
|
+
from featureflip.models import (
|
|
15
|
+
Condition,
|
|
16
|
+
ConditionGroup,
|
|
17
|
+
ConditionLogic,
|
|
18
|
+
ConditionOperator,
|
|
19
|
+
FlagConfiguration,
|
|
20
|
+
FlagType,
|
|
21
|
+
Segment,
|
|
22
|
+
ServeConfig,
|
|
23
|
+
ServeType,
|
|
24
|
+
TargetingRule,
|
|
25
|
+
Variation,
|
|
26
|
+
WeightedVariation,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__: list[str] = [
|
|
30
|
+
"Condition",
|
|
31
|
+
"ConditionGroup",
|
|
32
|
+
"ConditionLogic",
|
|
33
|
+
"ConditionOperator",
|
|
34
|
+
"Config",
|
|
35
|
+
"ConfigurationError",
|
|
36
|
+
"EvaluationContext",
|
|
37
|
+
"EvaluationDetail",
|
|
38
|
+
"EvaluationReason",
|
|
39
|
+
"FeatureflipClient",
|
|
40
|
+
"FeatureflipError",
|
|
41
|
+
"FlagConfiguration",
|
|
42
|
+
"FlagType",
|
|
43
|
+
"InitializationError",
|
|
44
|
+
"Segment",
|
|
45
|
+
"ServeConfig",
|
|
46
|
+
"ServeType",
|
|
47
|
+
"TargetingRule",
|
|
48
|
+
"Variation",
|
|
49
|
+
"WeightedVariation",
|
|
50
|
+
"__version__",
|
|
51
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Event processor for batching and flushing analytics events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
logger = structlog.get_logger()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HttpClientProtocol(Protocol):
|
|
14
|
+
"""Protocol for HTTP client to allow loose coupling."""
|
|
15
|
+
|
|
16
|
+
def post_events(self, events: list[dict[str, Any]]) -> None:
|
|
17
|
+
"""Send events to the API."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EventProcessor:
|
|
22
|
+
"""Batches and flushes analytics events.
|
|
23
|
+
|
|
24
|
+
This processor collects events in an internal queue and sends them to the API
|
|
25
|
+
either when the batch reaches a threshold size or after a time interval.
|
|
26
|
+
Events are also flushed when the processor is stopped.
|
|
27
|
+
|
|
28
|
+
Thread-safe: multiple threads can safely queue events concurrently.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
http_client: HttpClientProtocol,
|
|
34
|
+
flush_interval: float = 30.0,
|
|
35
|
+
flush_batch_size: int = 100,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize the event processor.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
http_client: HTTP client for sending events to the API.
|
|
41
|
+
flush_interval: Seconds between automatic flushes (default 30).
|
|
42
|
+
flush_batch_size: Number of events that triggers an immediate flush (default 100).
|
|
43
|
+
"""
|
|
44
|
+
self._http = http_client
|
|
45
|
+
self._flush_interval = flush_interval
|
|
46
|
+
self._flush_batch_size = flush_batch_size
|
|
47
|
+
self._queue: list[dict[str, Any]] = []
|
|
48
|
+
self._lock = threading.Lock()
|
|
49
|
+
self._stop_event = threading.Event()
|
|
50
|
+
self._thread: threading.Thread | None = None
|
|
51
|
+
|
|
52
|
+
def queue_event(self, event: dict[str, Any]) -> None:
|
|
53
|
+
"""Add an event to the queue.
|
|
54
|
+
|
|
55
|
+
Thread-safe. If the queue reaches the flush_batch_size threshold,
|
|
56
|
+
an immediate flush is triggered.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
event: Event dictionary to queue. Should contain at minimum a 'type' field.
|
|
60
|
+
"""
|
|
61
|
+
should_flush = False
|
|
62
|
+
with self._lock:
|
|
63
|
+
self._queue.append(event)
|
|
64
|
+
if len(self._queue) >= self._flush_batch_size:
|
|
65
|
+
should_flush = True
|
|
66
|
+
|
|
67
|
+
if should_flush:
|
|
68
|
+
self.flush()
|
|
69
|
+
|
|
70
|
+
def flush(self) -> None:
|
|
71
|
+
"""Flush all queued events immediately.
|
|
72
|
+
|
|
73
|
+
Blocks until the flush is complete. If the queue is empty, no API call is made.
|
|
74
|
+
HTTP errors are logged but not raised.
|
|
75
|
+
"""
|
|
76
|
+
events_to_send: list[dict[str, Any]] = []
|
|
77
|
+
with self._lock:
|
|
78
|
+
if not self._queue:
|
|
79
|
+
return
|
|
80
|
+
events_to_send = self._queue.copy()
|
|
81
|
+
self._queue.clear()
|
|
82
|
+
|
|
83
|
+
if not events_to_send:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
logger.debug("flushing_events", count=len(events_to_send))
|
|
88
|
+
self._http.post_events(events_to_send)
|
|
89
|
+
logger.debug("events_flushed_successfully", count=len(events_to_send))
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.warning("event_flush_error", error=str(e), count=len(events_to_send))
|
|
92
|
+
# Events are lost on error - this is intentional to prevent memory growth
|
|
93
|
+
# In a production system, you might want to implement retry logic
|
|
94
|
+
|
|
95
|
+
def start(self) -> None:
|
|
96
|
+
"""Start the background flush thread.
|
|
97
|
+
|
|
98
|
+
The background thread will periodically flush events at the configured interval.
|
|
99
|
+
"""
|
|
100
|
+
self._stop_event.clear()
|
|
101
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
102
|
+
self._thread.start()
|
|
103
|
+
logger.info("event_processor_started", flush_interval=self._flush_interval)
|
|
104
|
+
|
|
105
|
+
def stop(self) -> None:
|
|
106
|
+
"""Stop the background thread and flush remaining events.
|
|
107
|
+
|
|
108
|
+
Blocks until the thread has stopped and all remaining events are flushed.
|
|
109
|
+
"""
|
|
110
|
+
self._stop_event.set()
|
|
111
|
+
if self._thread and self._thread.is_alive():
|
|
112
|
+
self._thread.join(timeout=5.0)
|
|
113
|
+
# Flush any remaining events
|
|
114
|
+
self.flush()
|
|
115
|
+
logger.info("event_processor_stopped")
|
|
116
|
+
|
|
117
|
+
def _run(self) -> None:
|
|
118
|
+
"""Main loop for the background flush thread."""
|
|
119
|
+
while not self._stop_event.is_set():
|
|
120
|
+
# Wait for either the interval or stop signal
|
|
121
|
+
self._stop_event.wait(self._flush_interval)
|
|
122
|
+
if not self._stop_event.is_set():
|
|
123
|
+
self.flush()
|