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.
@@ -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()