toggly-flask 0.0.1__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,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: toggly-flask
3
+ Version: 0.0.1
4
+ Summary: Flask integration for Toggly feature flag management
5
+ Author-email: Toggly <support@toggly.io>
6
+ Maintainer-email: Toggly <support@toggly.io>
7
+ License: MIT
8
+ Project-URL: Homepage, https://toggly.io
9
+ Project-URL: Documentation, https://docs.toggly.io/sdks/python/flask
10
+ Project-URL: Repository, https://github.com/ops-ai/toggly-sdks
11
+ Keywords: feature-flags,feature-toggles,flask,toggly,experimentation
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: Flask
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ Requires-Dist: toggly>=0.1.0
29
+ Requires-Dist: flask>=2.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
@@ -0,0 +1,79 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "toggly-flask"
7
+ version = "0.0.1"
8
+ description = "Flask integration for Toggly feature flag management"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ {name = "Toggly", email = "support@toggly.io"}
13
+ ]
14
+ maintainers = [
15
+ {name = "Toggly", email = "support@toggly.io"}
16
+ ]
17
+ keywords = [
18
+ "feature-flags",
19
+ "feature-toggles",
20
+ "flask",
21
+ "toggly",
22
+ "experimentation"
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Framework :: Flask",
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.8",
32
+ "Programming Language :: Python :: 3.9",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Topic :: Software Development :: Libraries :: Python Modules",
38
+ "Typing :: Typed"
39
+ ]
40
+ requires-python = ">=3.8"
41
+ dependencies = [
42
+ "toggly>=0.1.0",
43
+ "flask>=2.0"
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://toggly.io"
48
+ Documentation = "https://docs.toggly.io/sdks/python/flask"
49
+ Repository = "https://github.com/ops-ai/toggly-sdks"
50
+
51
+ [project.optional-dependencies]
52
+ dev = [
53
+ "pytest>=7.0.0",
54
+ "pytest-cov>=4.0.0",
55
+ "mypy>=1.0.0",
56
+ "ruff>=0.1.0"
57
+ ]
58
+
59
+ [tool.setuptools.packages.find]
60
+ where = ["src"]
61
+
62
+ [tool.setuptools.package-data]
63
+ toggly_flask = ["py.typed"]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ addopts = "-v --cov=src/toggly_flask --cov-report=xml --cov-report=term-missing"
68
+
69
+ [tool.mypy]
70
+ python_version = "3.8"
71
+ strict = true
72
+
73
+ [tool.ruff]
74
+ target-version = "py38"
75
+ line-length = 100
76
+
77
+ [tool.coverage.run]
78
+ source = ["src/toggly_flask"]
79
+ branch = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,42 @@
1
+ """
2
+ Toggly Flask - Flask integration for Toggly feature flag management.
3
+
4
+ Provides a Flask extension, view decorators, and template context
5
+ for seamless Flask integration.
6
+ """
7
+
8
+ from toggly_flask.decorators import (
9
+ FeatureFlagBlueprint,
10
+ feature_flag_required,
11
+ feature_flag_switch,
12
+ feature_gate_required,
13
+ feature_variant,
14
+ )
15
+ from toggly_flask.extension import (
16
+ FeatureFlagChecker,
17
+ TemplateToggly,
18
+ Toggly,
19
+ TogglyRequestHelper,
20
+ get_client,
21
+ get_context_from_request,
22
+ get_toggly,
23
+ )
24
+
25
+ __version__ = "0.1.0"
26
+ __all__ = [
27
+ # Extension
28
+ "Toggly",
29
+ "TogglyRequestHelper",
30
+ "TemplateToggly",
31
+ "FeatureFlagChecker",
32
+ # Decorators
33
+ "feature_flag_required",
34
+ "feature_gate_required",
35
+ "feature_flag_switch",
36
+ "feature_variant",
37
+ "FeatureFlagBlueprint",
38
+ # Utilities
39
+ "get_toggly",
40
+ "get_client",
41
+ "get_context_from_request",
42
+ ]
@@ -0,0 +1,294 @@
1
+ """Flask view decorators for Toggly feature flags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ from typing import Any, Callable, TypeVar
7
+
8
+ from flask import Response, abort, redirect
9
+ from toggly import FeatureRequirement
10
+
11
+ from toggly_flask.extension import get_client, get_context_from_request
12
+
13
+ F = TypeVar("F", bound=Callable[..., Any])
14
+
15
+
16
+ def feature_flag_required(
17
+ feature_key: str,
18
+ *,
19
+ redirect_url: str | None = None,
20
+ status_code: int = 403,
21
+ fallback_view: Callable[..., Response] | None = None,
22
+ ) -> Callable[[F], F]:
23
+ """Decorator to require a feature flag for a view.
24
+
25
+ Args:
26
+ feature_key: The feature key that must be enabled.
27
+ redirect_url: URL to redirect to if feature is disabled.
28
+ status_code: HTTP status code to return if feature is disabled (default 403).
29
+ fallback_view: Alternative view to call if feature is disabled.
30
+
31
+ Returns:
32
+ Decorated view function.
33
+
34
+ Example:
35
+ @app.route('/new-dashboard')
36
+ @feature_flag_required('new-dashboard')
37
+ def new_dashboard():
38
+ return render_template('new_dashboard.html')
39
+
40
+ @app.route('/beta')
41
+ @feature_flag_required('beta-feature', redirect_url='/coming-soon/')
42
+ def beta_view():
43
+ return render_template('beta.html')
44
+ """
45
+
46
+ def decorator(view_func: F) -> F:
47
+ @functools.wraps(view_func)
48
+ def wrapper(*args: Any, **kwargs: Any) -> Response:
49
+ client = get_client()
50
+
51
+ if client is not None:
52
+ context = get_context_from_request()
53
+ if client.is_enabled(feature_key, context):
54
+ return view_func(*args, **kwargs)
55
+
56
+ # Feature is disabled or client unavailable
57
+ if redirect_url:
58
+ return redirect(redirect_url)
59
+
60
+ if fallback_view:
61
+ return fallback_view(*args, **kwargs)
62
+
63
+ abort(status_code, description=f"Feature '{feature_key}' is not available")
64
+
65
+ return wrapper # type: ignore
66
+
67
+ return decorator
68
+
69
+
70
+ def feature_gate_required(
71
+ feature_keys: list[str],
72
+ *,
73
+ requirement: FeatureRequirement = FeatureRequirement.ALL,
74
+ negate: bool = False,
75
+ redirect_url: str | None = None,
76
+ status_code: int = 403,
77
+ fallback_view: Callable[..., Response] | None = None,
78
+ ) -> Callable[[F], F]:
79
+ """Decorator to require multiple feature flags for a view.
80
+
81
+ Args:
82
+ feature_keys: List of feature keys to check.
83
+ requirement: Whether ALL or ANY features must be enabled.
84
+ negate: Whether to negate the result.
85
+ redirect_url: URL to redirect to if gate fails.
86
+ status_code: HTTP status code to return if gate fails (default 403).
87
+ fallback_view: Alternative view to call if gate fails.
88
+
89
+ Returns:
90
+ Decorated view function.
91
+
92
+ Example:
93
+ @app.route('/admin-reports')
94
+ @feature_gate_required(['admin-tools', 'advanced-reporting'])
95
+ def admin_reports():
96
+ return render_template('admin_reports.html')
97
+
98
+ @app.route('/exclusive')
99
+ @feature_gate_required(
100
+ ['beta', 'premium'],
101
+ requirement=FeatureRequirement.ANY
102
+ )
103
+ def exclusive_content():
104
+ return render_template('exclusive.html')
105
+ """
106
+
107
+ def decorator(view_func: F) -> F:
108
+ @functools.wraps(view_func)
109
+ def wrapper(*args: Any, **kwargs: Any) -> Response:
110
+ client = get_client()
111
+
112
+ if client is not None:
113
+ context = get_context_from_request()
114
+ if client.evaluate_gate(feature_keys, requirement, context, negate):
115
+ return view_func(*args, **kwargs)
116
+
117
+ # Gate failed or client unavailable
118
+ if redirect_url:
119
+ return redirect(redirect_url)
120
+
121
+ if fallback_view:
122
+ return fallback_view(*args, **kwargs)
123
+
124
+ abort(status_code, description="Required features are not available")
125
+
126
+ return wrapper # type: ignore
127
+
128
+ return decorator
129
+
130
+
131
+ def feature_flag_switch(
132
+ feature_key: str,
133
+ enabled_view: Callable[..., Response],
134
+ disabled_view: Callable[..., Response],
135
+ ) -> Callable[..., Response]:
136
+ """Create a view that switches between two implementations.
137
+
138
+ Args:
139
+ feature_key: The feature key to check.
140
+ enabled_view: View to use when feature is enabled.
141
+ disabled_view: View to use when feature is disabled.
142
+
143
+ Returns:
144
+ A view function that delegates based on feature state.
145
+
146
+ Example:
147
+ app.add_url_rule(
148
+ '/checkout',
149
+ 'checkout',
150
+ feature_flag_switch(
151
+ 'new-checkout',
152
+ new_checkout_view,
153
+ old_checkout_view
154
+ )
155
+ )
156
+ """
157
+
158
+ def view(*args: Any, **kwargs: Any) -> Response:
159
+ client = get_client()
160
+
161
+ if client is not None:
162
+ context = get_context_from_request()
163
+ if client.is_enabled(feature_key, context):
164
+ return enabled_view(*args, **kwargs)
165
+
166
+ return disabled_view(*args, **kwargs)
167
+
168
+ return view
169
+
170
+
171
+ def feature_variant(
172
+ feature_key: str,
173
+ default_view: Callable[..., Response],
174
+ variants: dict[str, Callable[..., Response]] | None = None,
175
+ ) -> Callable[[F], F]:
176
+ """Decorator to select view based on feature variant value.
177
+
178
+ This decorator allows routing to different views based on the
179
+ feature's variant/experiment value.
180
+
181
+ Args:
182
+ feature_key: The feature key to check.
183
+ default_view: View to use when feature is disabled or no variant matches.
184
+ variants: Dictionary mapping variant values to view functions.
185
+
186
+ Returns:
187
+ Decorated view function.
188
+
189
+ Example:
190
+ @app.route('/checkout')
191
+ @feature_variant(
192
+ 'checkout-experiment',
193
+ default_view=original_checkout,
194
+ variants={
195
+ 'variant-a': checkout_variant_a,
196
+ 'variant-b': checkout_variant_b,
197
+ }
198
+ )
199
+ def checkout():
200
+ # This function is never called; it's replaced by the variant
201
+ pass
202
+ """
203
+ variants = variants or {}
204
+
205
+ def decorator(view_func: F) -> F:
206
+ @functools.wraps(view_func)
207
+ def wrapper(*args: Any, **kwargs: Any) -> Response:
208
+ client = get_client()
209
+
210
+ if client is not None:
211
+ context = get_context_from_request()
212
+ if client.is_enabled(feature_key, context):
213
+ # Get the feature state for variant info
214
+ state = client.get_feature_state(feature_key, context)
215
+ variant = state.metadata.get("variant")
216
+
217
+ if variant and variant in variants:
218
+ return variants[variant](*args, **kwargs)
219
+
220
+ # Feature is enabled but no matching variant
221
+ return view_func(*args, **kwargs)
222
+
223
+ return default_view(*args, **kwargs)
224
+
225
+ return wrapper # type: ignore
226
+
227
+ return decorator
228
+
229
+
230
+ class FeatureFlagBlueprint:
231
+ """Utility class for creating feature-gated blueprints.
232
+
233
+ Example:
234
+ from flask import Blueprint
235
+ from toggly_flask import FeatureFlagBlueprint
236
+
237
+ bp = Blueprint('beta', __name__)
238
+ feature_bp = FeatureFlagBlueprint(bp, 'beta-features')
239
+
240
+ @feature_bp.route('/new-page')
241
+ def new_page():
242
+ return 'New page!'
243
+ """
244
+
245
+ def __init__(
246
+ self,
247
+ blueprint: Any,
248
+ feature_key: str,
249
+ *,
250
+ redirect_url: str | None = None,
251
+ status_code: int = 403,
252
+ ) -> None:
253
+ """Initialize the feature-gated blueprint.
254
+
255
+ Args:
256
+ blueprint: The Flask Blueprint to wrap.
257
+ feature_key: The feature key required for all routes.
258
+ redirect_url: URL to redirect to if feature is disabled.
259
+ status_code: HTTP status code to return if feature is disabled.
260
+ """
261
+ self._blueprint = blueprint
262
+ self._feature_key = feature_key
263
+ self._redirect_url = redirect_url
264
+ self._status_code = status_code
265
+
266
+ def route(
267
+ self,
268
+ rule: str,
269
+ **options: Any,
270
+ ) -> Callable[[F], F]:
271
+ """Register a route with automatic feature flag checking.
272
+
273
+ Args:
274
+ rule: The URL rule.
275
+ **options: Additional options for Blueprint.route().
276
+
277
+ Returns:
278
+ Decorator function.
279
+ """
280
+
281
+ def decorator(f: F) -> F:
282
+ @self._blueprint.route(rule, **options)
283
+ @feature_flag_required(
284
+ self._feature_key,
285
+ redirect_url=self._redirect_url,
286
+ status_code=self._status_code,
287
+ )
288
+ @functools.wraps(f)
289
+ def wrapped(*args: Any, **kwargs: Any) -> Response:
290
+ return f(*args, **kwargs)
291
+
292
+ return wrapped # type: ignore
293
+
294
+ return decorator