Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature flag changes #140

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions src/identity/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ classifiers = [
]

dependencies = [
"injector",
"pysaml2",
"requests"
]
Expand Down
2 changes: 2 additions & 0 deletions src/platform/Ligare/platform/feature_flag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .caching_feature_flag_router import FeatureFlag as CacheFeatureFlag
from .db_feature_flag_router import DBFeatureFlagRouter
from .db_feature_flag_router import FeatureFlag as DBFeatureFlag
from .decorators import feature_flag
from .feature_flag_router import FeatureFlag, FeatureFlagChange, FeatureFlagRouter

__all__ = (
Expand All @@ -12,4 +13,5 @@
"CacheFeatureFlag",
"DBFeatureFlag",
"FeatureFlagChange",
"feature_flag",
)
40 changes: 40 additions & 0 deletions src/platform/Ligare/platform/feature_flag/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import Any, Callable

from injector import Injector, inject
from typing_extensions import overload

from .feature_flag_router import FeatureFlag, FeatureFlagRouter


@overload
def feature_flag(
feature_flag_name: str, *, enabled_callback: Callable[..., Any]
) -> Callable[..., Callable[..., Any]]: ...
@overload
def feature_flag(
feature_flag_name: str, *, disabled_callback: Callable[..., Any]
) -> Callable[..., Callable[..., Any]]: ...


def feature_flag(
feature_flag_name: str,
*,
enabled_callback: Callable[..., None] = lambda: None,
disabled_callback: Callable[..., None] = lambda: None,
) -> Callable[..., Callable[..., Any]]:
def decorator(fn: Callable[..., Any]):
@inject
def wrapper(
feature_flag_router: FeatureFlagRouter[FeatureFlag],
injector: Injector,
):
if feature_flag_router.feature_is_enabled(feature_flag_name):
enabled_callback()
else:
disabled_callback()

return injector.call_with_injection(fn)

return wrapper

return decorator
4 changes: 3 additions & 1 deletion src/platform/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ classifiers = [
]

dependencies = [
"Ligare.database"
"Ligare.database",

"injector"
]

dynamic = ["version", "readme"]
Expand Down
25 changes: 20 additions & 5 deletions src/web/Ligare/web/middleware/feature_flags/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ class FeatureFlagPatch:


class FeatureFlagRouterModule(ConfigurableModule, Generic[TFeatureFlag]):
def __init__(self, t_feature_flag: type[FeatureFlagRouter[TFeatureFlag]]) -> None:
self._t_feature_flag = t_feature_flag
def __init__(
self, t_feature_flag: type[FeatureFlagRouter[TFeatureFlag]] | None = None
) -> None:
self._t_feature_flag = type(self) if t_feature_flag is None else t_feature_flag
super().__init__()

@override
Expand All @@ -72,7 +74,7 @@ def get_config_type() -> type[AbstractConfig]:
def _provide_feature_flag_router(
self, injector: Injector
) -> FeatureFlagRouter[FeatureFlag]:
return injector.get(self._t_feature_flag)
return cast(FeatureFlagRouter[FeatureFlag], injector.get(self._t_feature_flag))


class DBFeatureFlagRouterModule(FeatureFlagRouterModule[DBFeatureFlag]):
Expand All @@ -84,7 +86,9 @@ def __init__(self) -> None:
def _provide_db_feature_flag_router(
self, injector: Injector
) -> FeatureFlagRouter[DBFeatureFlag]:
return injector.get(self._t_feature_flag)
return cast(
FeatureFlagRouter[DBFeatureFlag], injector.get(self._t_feature_flag)
)

@singleton
@provider
Expand All @@ -103,7 +107,9 @@ def __init__(self) -> None:
def _provide_caching_feature_flag_router(
self, injector: Injector
) -> FeatureFlagRouter[CachingFeatureFlag]:
return injector.get(self._t_feature_flag)
return cast(
FeatureFlagRouter[CachingFeatureFlag], injector.get(self._t_feature_flag)
)


P = ParamSpec("P")
Expand Down Expand Up @@ -213,6 +219,7 @@ def feature_flag(feature_flag_router: FeatureFlagRouter[FeatureFlag]): # pyrigh

@feature_flag_blueprint.route("/feature_flag", methods=("PATCH",))
@_login_required(True)
# @_login_required(False)
@inject
async def feature_flag_patch(feature_flag_router: FeatureFlagRouter[FeatureFlag]): # pyright: ignore[reportUnusedFunction]
feature_flags_request: list[FeatureFlagPatchRequest] = await request.json()
Expand Down Expand Up @@ -258,8 +265,16 @@ class FeatureFlagMiddlewareModule(Module):
Enable the use of Feature Flags and a Feature Flag management API.
"""

def __init__(
self,
feature_flag_router_module_type: type[FeatureFlagRouterModule[TFeatureFlag]],
) -> None:
super().__init__()
self._feature_flag_router_module_type = feature_flag_router_module_type

@override
def configure(self, binder: Binder) -> None:
binder.install(self._feature_flag_router_module_type())
super().configure(binder)

def register_middleware(self, app: FlaskApp):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ sqlalchemy_echo = false
app_name = '{{application.module_name}}'
env = 'development'
host = "localhost"
# FIXME this should be an int
port = "5000"

[flask.session]
Expand All @@ -27,7 +26,7 @@ log_level = 'INFO'
format = 'JSON'

[web.security.cors]
origins = ['http://localhost:5000']
origins = ['http://localhost:5000', 'http://127.0.0.1:5000']
allow_credentials = true
allow_methods = ['GET', 'POST', 'PATCH', 'OPTIONS']

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from Ligare.web.config import Config

from {{application.module_name}} import create_app

if __name__ == '__main__':
# TODO port must be taken from config
create_app().app_injector.app.run(port=5000)
if __name__ == "__main__":
result = create_app()

app_config = result.app_injector.flask_injector.injector.get(Config)
host = app_config.flask and app_config.flask.host
port = 5000 if app_config.flask is None else int(app_config.flask.port)

result.app_injector.app.run(host=host, port=port)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ sqlalchemy_echo = false
app_name = '{{application.module_name}}'
env = 'development'
host = "localhost"
# FIXME this should be an int
port = "5000"

[flask.openapi]
Expand All @@ -34,7 +33,7 @@ log_level = 'INFO'
format = 'plaintext'

[web.security.cors]
origins = ['http://localhost:5000']
origins = ['http://localhost:5000', 'http://127.0.0.1:5000']
allow_credentials = true
allow_methods = ['GET', 'POST', 'PATCH', 'OPTIONS']

Expand Down
1 change: 1 addition & 0 deletions src/web/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies = [
"connexion[uvicorn]",
"uvicorn-worker",
"swagger_ui_bundle",
"injector",
"python-dotenv",
"json-logging",
"lib_programname",
Expand Down
20 changes: 12 additions & 8 deletions src/web/test/unit/middleware/test_feature_flags_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ def _user_session_app_init_hook(
bases=[],
)
)
application_modules.append(CachingFeatureFlagRouterModule)
application_modules.append(FeatureFlagMiddlewareModule())
application_modules.append(
FeatureFlagMiddlewareModule(CachingFeatureFlagRouterModule)
)

def test__FeatureFlagMiddleware__feature_flag_api_GET_requires_user_session_when_flask_login_is_configured(
self,
Expand All @@ -108,8 +109,9 @@ def app_init_hook(
application_configs: list[type[AbstractConfig]],
application_modules: list[Module | type[Module]],
):
application_modules.append(CachingFeatureFlagRouterModule)
application_modules.append(FeatureFlagMiddlewareModule())
application_modules.append(
FeatureFlagMiddlewareModule(CachingFeatureFlagRouterModule)
)

openapi_mock_controller.begin()
app = next(
Expand Down Expand Up @@ -142,8 +144,9 @@ def app_init_hook(
):
if not flask_login_is_configured:
application_modules.clear()
application_modules.append(CachingFeatureFlagRouterModule)
application_modules.append(FeatureFlagMiddlewareModule())
application_modules.append(
FeatureFlagMiddlewareModule(CachingFeatureFlagRouterModule)
)

def client_init_hook(app: CreateAppResult[FlaskApp]):
caching_feature_flag_router = app.app_injector.flask_injector.injector.get(
Expand Down Expand Up @@ -363,8 +366,9 @@ def app_init_hook(
):
if not flask_login_is_configured:
application_modules.clear()
application_modules.append(CachingFeatureFlagRouterModule)
application_modules.append(FeatureFlagMiddlewareModule())
application_modules.append(
FeatureFlagMiddlewareModule(CachingFeatureFlagRouterModule)
)

openapi_mock_controller.begin()
app = next(
Expand Down
Loading