From c4684098c74a821b81e590f950386ddfdb34505e Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Thu, 5 Nov 2026 17:20:35 +0100 Subject: [PATCH 1/2] refactor(feature_flags): follow the module layout (moves only) constants, service, invalidation and tenant_cleanup move to feature_flags/core/; deps and endpoints to feature_flags/web/; the page components and shared types to ui/. Tests mirror the areas. feature_flags.constants stays as a deprecated alias until 0.0.46. Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq --- CHANGELOG.md | 1 + docs/module-layout.md | 2 +- docs/modules/feature_flags.md | 2 +- .../core/tests/test_sm003_page_render.py | 2 +- host/tests/test_module_layout.py | 5 -- modules/feature_flags/README.md | 2 +- .../feature_flags/feature_flags/constants.py | 53 +++---------------- .../feature_flags/core/__init__.py | 1 + .../feature_flags/core/constants.py | 50 +++++++++++++++++ .../feature_flags/{ => core}/invalidation.py | 2 +- .../feature_flags/{ => core}/service.py | 4 +- .../{ => core}/tenant_cleanup.py | 2 +- modules/feature_flags/feature_flags/models.py | 2 +- modules/feature_flags/feature_flags/module.py | 12 ++--- .../feature_flags/pages/Browse.tsx | 8 +-- .../{pages => ui}/components/FlagTable.tsx | 0 .../{pages => ui}/components/ScopeCard.tsx | 0 .../{pages => ui}/components/TenantPicker.tsx | 0 .../components/ToggleConfirmDialog.tsx | 0 .../feature_flags/{pages => ui}/types.ts | 0 .../feature_flags/web/__init__.py | 1 + .../feature_flags/{ => web}/deps.py | 2 +- .../{ => web}/endpoints/__init__.py | 0 .../feature_flags/{ => web}/endpoints/api.py | 6 +-- .../{ => web}/endpoints/views.py | 4 +- .../tests-js/feature_flags_Browse.test.tsx | 2 +- .../test_feature_flags_commit_invalidation.py | 4 +- .../module}/test_feature_flags_audit_links.py | 6 +-- .../test_feature_flags_menu_section.py | 4 +- .../test_feature_flags_description_i18n.py} | 2 +- .../service}/test_feature_flags_service.py | 4 +- ...st_feature_flags_store_outage_fallback.py} | 4 +- .../tenancy}/test_feature_flags_tenancy.py | 4 +- .../test_feature_flags_tenant_cleanup.py | 2 +- .../endpoints}/test_feature_flags_api.py | 0 .../tests/test_direct_grants_everywhere.py | 2 +- 36 files changed, 102 insertions(+), 93 deletions(-) create mode 100644 modules/feature_flags/feature_flags/core/__init__.py create mode 100644 modules/feature_flags/feature_flags/core/constants.py rename modules/feature_flags/feature_flags/{ => core}/invalidation.py (93%) rename modules/feature_flags/feature_flags/{ => core}/service.py (98%) rename modules/feature_flags/feature_flags/{ => core}/tenant_cleanup.py (92%) rename modules/feature_flags/feature_flags/{pages => ui}/components/FlagTable.tsx (100%) rename modules/feature_flags/feature_flags/{pages => ui}/components/ScopeCard.tsx (100%) rename modules/feature_flags/feature_flags/{pages => ui}/components/TenantPicker.tsx (100%) rename modules/feature_flags/feature_flags/{pages => ui}/components/ToggleConfirmDialog.tsx (100%) rename modules/feature_flags/feature_flags/{pages => ui}/types.ts (100%) create mode 100644 modules/feature_flags/feature_flags/web/__init__.py rename modules/feature_flags/feature_flags/{ => web}/deps.py (96%) rename modules/feature_flags/feature_flags/{ => web}/endpoints/__init__.py (100%) rename modules/feature_flags/feature_flags/{ => web}/endpoints/api.py (98%) rename modules/feature_flags/feature_flags/{ => web}/endpoints/views.py (98%) rename modules/feature_flags/tests/{ => core/invalidation}/test_feature_flags_commit_invalidation.py (96%) rename modules/feature_flags/tests/{ => core/module}/test_feature_flags_audit_links.py (96%) rename modules/feature_flags/tests/{ => core/module}/test_feature_flags_menu_section.py (89%) rename modules/feature_flags/tests/{test_flag_description_i18n.py => core/service/test_feature_flags_description_i18n.py} (96%) rename modules/feature_flags/tests/{ => core/service}/test_feature_flags_service.py (98%) rename modules/feature_flags/tests/{test_store_outage_fallback.py => core/service/test_feature_flags_store_outage_fallback.py} (94%) rename modules/feature_flags/tests/{ => core/tenancy}/test_feature_flags_tenancy.py (98%) rename modules/feature_flags/tests/{ => core/tenant_cleanup}/test_feature_flags_tenant_cleanup.py (97%) rename modules/feature_flags/tests/{ => web/endpoints}/test_feature_flags_api.py (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2969816f..342e29dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to this project are documented in this file. The format is b - `mail` follows the module layout: `mail.core.{constants,settings,state,data_compatibility}` and `mail.delivery.{sender,runtime}`. `mail.settings` and `mail.constants` still import, with a `DeprecationWarning`, until 0.0.46. - `site_lock` follows the module layout: `site_lock.core.{constants,settings,state}` and `site_lock.web.{middleware,page,rate_limit}` (the unlock template moves with the page). `site_lock.settings` still imports, with a `DeprecationWarning`, until 0.0.46. - `dashboard` follows the module layout: `dashboard.core.constants`, `dashboard.web.{endpoints,stats,doctor}`, and the page components move from `pages/components/` to `ui/components/`. Nothing outside the module imported the old paths, so no aliases remain. +- `feature_flags` follows the module layout: `feature_flags.core.{constants,service,invalidation,tenant_cleanup}`, `feature_flags.web.{deps,endpoints}`, and the page components and types move to `ui/`. `feature_flags.constants` still imports, with a `DeprecationWarning`, until 0.0.46. ### Fixed diff --git a/docs/module-layout.md b/docs/module-layout.md index 1140f906b..2eab70afe 100644 --- a/docs/module-layout.md +++ b/docs/module-layout.md @@ -202,7 +202,7 @@ when a later module PR moves files. | background_tasks | 49 | 83 | flat | | branding | 13 | 20 | flat | | dashboard | 2 | 0 | follows this layout | -| feature_flags | 8 | 9 | flat | +| feature_flags | 3 (+1 shim) | 0 | follows this layout | | file_storage | 39 | 65 | flat | | keycloak | 13 | 19 | flat | | mail | 2 (+2 shims) | 0 | follows this layout | diff --git a/docs/modules/feature_flags.md b/docs/modules/feature_flags.md index a2afd6cf2..827968b3e 100644 --- a/docs/modules/feature_flags.md +++ b/docs/modules/feature_flags.md @@ -35,7 +35,7 @@ Once registered, the flag shows up at `/admin/feature-flags` in the admin UI. Un ## Reading a flag at request time ```python -from feature_flags.deps import FeatureFlagRegistryDep +from feature_flags.web.deps import FeatureFlagRegistryDep @router.post("/checkout") diff --git a/framework/core/tests/test_sm003_page_render.py b/framework/core/tests/test_sm003_page_render.py index 98f29fa89..1087e0180 100644 --- a/framework/core/tests/test_sm003_page_render.py +++ b/framework/core/tests/test_sm003_page_render.py @@ -39,7 +39,7 @@ async def test_resolves_constant_imported_from_sibling_file(self, tmp_path: Path endpoints = src_dir / "endpoints" endpoints.mkdir() (endpoints / "views.py").write_text( - "from feature_flags.constants import PAGE_BROWSE\n" + "from feature_flags.core.constants import PAGE_BROWSE\n" "async def view(inertia):\n" " return await inertia.render(PAGE_BROWSE, {})\n" ) diff --git a/host/tests/test_module_layout.py b/host/tests/test_module_layout.py index 0da4777cc..1baf9bd0f 100644 --- a/host/tests/test_module_layout.py +++ b/host/tests/test_module_layout.py @@ -74,11 +74,6 @@ services.py settings.py shared_props.py tenant_branding.py tenant_definitions.py """.split() ), - "feature_flags": frozenset( - """ - constants.py deps.py endpoints invalidation.py service.py tenant_cleanup.py - """.split() - ), "file_storage": frozenset( """ aggregates.py audit_labels.py backend_guard.py backends body_limits.py checksum.py diff --git a/modules/feature_flags/README.md b/modules/feature_flags/README.md index 18bcb88a9..2dcb39e3b 100644 --- a/modules/feature_flags/README.md +++ b/modules/feature_flags/README.md @@ -33,7 +33,7 @@ Gate a route using the registry dependency: ```python from fastapi import APIRouter, HTTPException -from feature_flags.deps import FeatureFlagRegistryDep +from feature_flags.web.deps import FeatureFlagRegistryDep router = APIRouter() diff --git a/modules/feature_flags/feature_flags/constants.py b/modules/feature_flags/feature_flags/constants.py index 5826487dc..f7cf7dcd8 100644 --- a/modules/feature_flags/feature_flags/constants.py +++ b/modules/feature_flags/feature_flags/constants.py @@ -1,50 +1,11 @@ -"""Stable identifiers for the feature_flags module.""" +"""Moved to :mod:`feature_flags.core.constants`; this alias goes in 0.0.46 (module layout).""" -PERM_FEATURE_FLAGS_VIEW = "feature_flags.view" -PERM_FEATURE_FLAGS_MANAGE = "feature_flags.manage" -# Shown as a heading in the role editor, so it follows the same sentence case -# as the menu label and the page title rather than reading "Feature Flags" -# beside them. -PERM_GROUP = "Feature flags" +from __future__ import annotations -# Prefix is load-bearing on SQLite, which has no per-module schemas. -TABLE_OVERRIDE = "feature_flags_override" -UQ_OVERRIDE_SCOPE_NAME = "uq_feature_flags_override_scope_scope_id_name" +import importlib +import sys -SCOPE_SYSTEM = "system" -SCOPE_TENANT = "tenant" -ALL_SCOPES = (SCOPE_SYSTEM, SCOPE_TENANT) -# Empty string, not NULL: PG treats NULLs as distinct in unique indexes, -# which would let two "system" rows for the same flag coexist. -SYSTEM_SCOPE_ID = "" +from simple_module_core.deprecation import deprecated_module -SCOPE_MAX_LENGTH = 10 -SCOPE_ID_MAX_LENGTH = 64 - -LOCALE_NAMESPACE = "feature_flags" - -MENU_LABEL = "Feature flags" -# Trailing slash: the browse route is registered at "/" under the view -# prefix, so linking to the bare prefix costs a 307 on every navigation. -MENU_URL = "/admin/feature-flags/" -MENU_ICON = "flag" -MENU_ORDER = 110 - -PAGE_BROWSE = "FeatureFlags/Browse" - -API_PREFIX = "/api/feature_flags" -VIEW_PREFIX = "/admin/feature-flags" -QP_TENANT_ID = "tenant_id" -# Audit rows carry an override's primary key and nothing else. There is no -# per-override page to land on, so the link opens the flags screen and names -# the row it came from; an AuditLink without an "{id}" slot is rejected at -# boot because every row would otherwise resolve to the same URL. -QP_OVERRIDE = "override" -AUDIT_LINK_LABEL = "Feature flag override" -AUDIT_LINK_LABEL_KEY = "feature_flags.audit.override" - -# Module name and browse URL of the audit log, used to decide whether the -# "View change history" link has anywhere to go and where. Not a dependency: -# flags work perfectly well without an audit log installed. -AUDIT_LOG_MODULE = "AuditLog" -AUDIT_LOG_VIEW_URL = "/admin/audit-log/" +deprecated_module("feature_flags.constants", remove_in="0.0.46", use="feature_flags.core.constants") +sys.modules[__name__] = importlib.import_module("feature_flags.core.constants") diff --git a/modules/feature_flags/feature_flags/core/__init__.py b/modules/feature_flags/feature_flags/core/__init__.py new file mode 100644 index 000000000..099b36836 --- /dev/null +++ b/modules/feature_flags/feature_flags/core/__init__.py @@ -0,0 +1 @@ +"""What every feature-flag feature builds on: constants, the flag service, cache invalidation and tenant cleanup.""" diff --git a/modules/feature_flags/feature_flags/core/constants.py b/modules/feature_flags/feature_flags/core/constants.py new file mode 100644 index 000000000..5826487dc --- /dev/null +++ b/modules/feature_flags/feature_flags/core/constants.py @@ -0,0 +1,50 @@ +"""Stable identifiers for the feature_flags module.""" + +PERM_FEATURE_FLAGS_VIEW = "feature_flags.view" +PERM_FEATURE_FLAGS_MANAGE = "feature_flags.manage" +# Shown as a heading in the role editor, so it follows the same sentence case +# as the menu label and the page title rather than reading "Feature Flags" +# beside them. +PERM_GROUP = "Feature flags" + +# Prefix is load-bearing on SQLite, which has no per-module schemas. +TABLE_OVERRIDE = "feature_flags_override" +UQ_OVERRIDE_SCOPE_NAME = "uq_feature_flags_override_scope_scope_id_name" + +SCOPE_SYSTEM = "system" +SCOPE_TENANT = "tenant" +ALL_SCOPES = (SCOPE_SYSTEM, SCOPE_TENANT) +# Empty string, not NULL: PG treats NULLs as distinct in unique indexes, +# which would let two "system" rows for the same flag coexist. +SYSTEM_SCOPE_ID = "" + +SCOPE_MAX_LENGTH = 10 +SCOPE_ID_MAX_LENGTH = 64 + +LOCALE_NAMESPACE = "feature_flags" + +MENU_LABEL = "Feature flags" +# Trailing slash: the browse route is registered at "/" under the view +# prefix, so linking to the bare prefix costs a 307 on every navigation. +MENU_URL = "/admin/feature-flags/" +MENU_ICON = "flag" +MENU_ORDER = 110 + +PAGE_BROWSE = "FeatureFlags/Browse" + +API_PREFIX = "/api/feature_flags" +VIEW_PREFIX = "/admin/feature-flags" +QP_TENANT_ID = "tenant_id" +# Audit rows carry an override's primary key and nothing else. There is no +# per-override page to land on, so the link opens the flags screen and names +# the row it came from; an AuditLink without an "{id}" slot is rejected at +# boot because every row would otherwise resolve to the same URL. +QP_OVERRIDE = "override" +AUDIT_LINK_LABEL = "Feature flag override" +AUDIT_LINK_LABEL_KEY = "feature_flags.audit.override" + +# Module name and browse URL of the audit log, used to decide whether the +# "View change history" link has anywhere to go and where. Not a dependency: +# flags work perfectly well without an audit log installed. +AUDIT_LOG_MODULE = "AuditLog" +AUDIT_LOG_VIEW_URL = "/admin/audit-log/" diff --git a/modules/feature_flags/feature_flags/invalidation.py b/modules/feature_flags/feature_flags/core/invalidation.py similarity index 93% rename from modules/feature_flags/feature_flags/invalidation.py rename to modules/feature_flags/feature_flags/core/invalidation.py index 938fe595c..40c47bed9 100644 --- a/modules/feature_flags/feature_flags/invalidation.py +++ b/modules/feature_flags/feature_flags/core/invalidation.py @@ -5,7 +5,7 @@ from simple_module_core.invalidation_refresh import InvalidationRefresh from simple_module_core.tenant_events import TENANT_DELETED_CHANNEL -from feature_flags.service import FeatureFlagService +from feature_flags.core.service import FeatureFlagService CHANNEL = "feature_flags.overrides" diff --git a/modules/feature_flags/feature_flags/service.py b/modules/feature_flags/feature_flags/core/service.py similarity index 98% rename from modules/feature_flags/feature_flags/service.py rename to modules/feature_flags/feature_flags/core/service.py index e6e7b7b6a..a43b0ca9e 100644 --- a/modules/feature_flags/feature_flags/service.py +++ b/modules/feature_flags/feature_flags/core/service.py @@ -15,8 +15,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlmodel import col -from feature_flags.constants import SCOPE_SYSTEM, SCOPE_TENANT, SYSTEM_SCOPE_ID from feature_flags.contracts.schemas import FeatureFlagOverrideOut, FeatureFlagView +from feature_flags.core.constants import SCOPE_SYSTEM, SCOPE_TENANT, SYSTEM_SCOPE_ID from feature_flags.models import FeatureFlagOverride @@ -219,7 +219,7 @@ def _on_commit( ) -> None: async def committed() -> None: if self.app is not None: - from feature_flags.invalidation import CHANNEL + from feature_flags.core.invalidation import CHANNEL await self.app.state.sm.invalidation.publish(CHANNEL, key=name) elif enabled is None: diff --git a/modules/feature_flags/feature_flags/tenant_cleanup.py b/modules/feature_flags/feature_flags/core/tenant_cleanup.py similarity index 92% rename from modules/feature_flags/feature_flags/tenant_cleanup.py rename to modules/feature_flags/feature_flags/core/tenant_cleanup.py index ea1320cf2..7780e4355 100644 --- a/modules/feature_flags/feature_flags/tenant_cleanup.py +++ b/modules/feature_flags/feature_flags/core/tenant_cleanup.py @@ -4,7 +4,7 @@ from simple_module_core.tenant_events import TenantDeleted from sqlmodel import col, delete -from feature_flags.constants import SCOPE_TENANT +from feature_flags.core.constants import SCOPE_TENANT from feature_flags.models import FeatureFlagOverride diff --git a/modules/feature_flags/feature_flags/models.py b/modules/feature_flags/feature_flags/models.py index 4923e79ef..4e9724949 100644 --- a/modules/feature_flags/feature_flags/models.py +++ b/modules/feature_flags/feature_flags/models.py @@ -17,7 +17,7 @@ from sqlalchemy import UniqueConstraint from sqlmodel import Field -from feature_flags.constants import ( +from feature_flags.core.constants import ( SCOPE_ID_MAX_LENGTH, SCOPE_MAX_LENGTH, SCOPE_SYSTEM, diff --git a/modules/feature_flags/feature_flags/module.py b/modules/feature_flags/feature_flags/module.py index e5217028c..823323bb2 100644 --- a/modules/feature_flags/feature_flags/module.py +++ b/modules/feature_flags/feature_flags/module.py @@ -13,7 +13,7 @@ from simple_module_core.module import ModuleBase, ModuleMeta from simple_module_core.permissions import PermissionRegistry -from feature_flags.constants import ( +from feature_flags.core.constants import ( AUDIT_LINK_LABEL, AUDIT_LINK_LABEL_KEY, LOCALE_NAMESPACE, @@ -72,7 +72,7 @@ class FeatureFlagsModule(ModuleBase): def register_event_handlers(self, bus: EventBus, app: FastAPI | None = None) -> None: from simple_module_core.tenant_events import TenantDeleted - from feature_flags.tenant_cleanup import purge_tenant_data + from feature_flags.core.tenant_cleanup import purge_tenant_data bus.subscribe_durable(TenantDeleted, purge_tenant_data) if app is not None: @@ -83,13 +83,13 @@ async def forget_tenant(event: TenantDeleted) -> None: bus.subscribe(TenantDeleted, forget_tenant) def register_invalidations(self, bus: InvalidationBus, app: FastAPI) -> None: - from feature_flags.invalidation import subscribe + from feature_flags.core.invalidation import subscribe self._refresh = subscribe(bus, app) def register_routes(self, api_router: APIRouter, view_router: APIRouter) -> None: - from feature_flags.endpoints.api import router as api - from feature_flags.endpoints.views import router as views + from feature_flags.web.endpoints.api import router as api + from feature_flags.web.endpoints.views import router as views api_router.include_router(api) view_router.include_router(views) @@ -160,7 +160,7 @@ async def on_startup(self, app: FastAPI) -> None: conservative choice — admins can re-toggle overrides once the store is healthy again. """ - from feature_flags.service import FeatureFlagService + from feature_flags.core.service import FeatureFlagService sm = app.state.sm try: diff --git a/modules/feature_flags/feature_flags/pages/Browse.tsx b/modules/feature_flags/feature_flags/pages/Browse.tsx index a868d2f7e..d0d0a2bba 100644 --- a/modules/feature_flags/feature_flags/pages/Browse.tsx +++ b/modules/feature_flags/feature_flags/pages/Browse.tsx @@ -5,10 +5,10 @@ import { usePermissions } from '@simple-module-py/ui/hooks/use-permissions'; import { AdminLayout } from '@simple-module-py/ui/layouts/AdminLayout'; import { useState } from 'react'; import { toast } from 'sonner'; -import { FlagTable } from './components/FlagTable'; -import { ScopeCard } from './components/ScopeCard'; -import { type PendingToggle, ToggleConfirmDialog } from './components/ToggleConfirmDialog'; -import type { FeatureFlag } from './types'; +import { FlagTable } from '../ui/components/FlagTable'; +import { ScopeCard } from '../ui/components/ScopeCard'; +import { type PendingToggle, ToggleConfirmDialog } from '../ui/components/ToggleConfirmDialog'; +import type { FeatureFlag } from '../ui/types'; interface Props { flags: FeatureFlag[]; diff --git a/modules/feature_flags/feature_flags/pages/components/FlagTable.tsx b/modules/feature_flags/feature_flags/ui/components/FlagTable.tsx similarity index 100% rename from modules/feature_flags/feature_flags/pages/components/FlagTable.tsx rename to modules/feature_flags/feature_flags/ui/components/FlagTable.tsx diff --git a/modules/feature_flags/feature_flags/pages/components/ScopeCard.tsx b/modules/feature_flags/feature_flags/ui/components/ScopeCard.tsx similarity index 100% rename from modules/feature_flags/feature_flags/pages/components/ScopeCard.tsx rename to modules/feature_flags/feature_flags/ui/components/ScopeCard.tsx diff --git a/modules/feature_flags/feature_flags/pages/components/TenantPicker.tsx b/modules/feature_flags/feature_flags/ui/components/TenantPicker.tsx similarity index 100% rename from modules/feature_flags/feature_flags/pages/components/TenantPicker.tsx rename to modules/feature_flags/feature_flags/ui/components/TenantPicker.tsx diff --git a/modules/feature_flags/feature_flags/pages/components/ToggleConfirmDialog.tsx b/modules/feature_flags/feature_flags/ui/components/ToggleConfirmDialog.tsx similarity index 100% rename from modules/feature_flags/feature_flags/pages/components/ToggleConfirmDialog.tsx rename to modules/feature_flags/feature_flags/ui/components/ToggleConfirmDialog.tsx diff --git a/modules/feature_flags/feature_flags/pages/types.ts b/modules/feature_flags/feature_flags/ui/types.ts similarity index 100% rename from modules/feature_flags/feature_flags/pages/types.ts rename to modules/feature_flags/feature_flags/ui/types.ts diff --git a/modules/feature_flags/feature_flags/web/__init__.py b/modules/feature_flags/feature_flags/web/__init__.py new file mode 100644 index 000000000..7d668357f --- /dev/null +++ b/modules/feature_flags/feature_flags/web/__init__.py @@ -0,0 +1 @@ +"""The HTTP side: request deps and the REST and Inertia endpoints.""" diff --git a/modules/feature_flags/feature_flags/deps.py b/modules/feature_flags/feature_flags/web/deps.py similarity index 96% rename from modules/feature_flags/feature_flags/deps.py rename to modules/feature_flags/feature_flags/web/deps.py index 264f225fe..7563395a1 100644 --- a/modules/feature_flags/feature_flags/deps.py +++ b/modules/feature_flags/feature_flags/web/deps.py @@ -10,7 +10,7 @@ from simple_module_db.deps import get_db from sqlalchemy.ext.asyncio import AsyncSession -from feature_flags.service import FeatureFlagService +from feature_flags.core.service import FeatureFlagService async def get_feature_flag_service( diff --git a/modules/feature_flags/feature_flags/endpoints/__init__.py b/modules/feature_flags/feature_flags/web/endpoints/__init__.py similarity index 100% rename from modules/feature_flags/feature_flags/endpoints/__init__.py rename to modules/feature_flags/feature_flags/web/endpoints/__init__.py diff --git a/modules/feature_flags/feature_flags/endpoints/api.py b/modules/feature_flags/feature_flags/web/endpoints/api.py similarity index 98% rename from modules/feature_flags/feature_flags/endpoints/api.py rename to modules/feature_flags/feature_flags/web/endpoints/api.py index d6a575c61..7a846e03a 100644 --- a/modules/feature_flags/feature_flags/endpoints/api.py +++ b/modules/feature_flags/feature_flags/web/endpoints/api.py @@ -6,13 +6,13 @@ from simple_module_core.feature_flags import FeatureFlagRegistry from simple_module_hosting.permissions import RequiresPermission -from feature_flags.constants import ( +from feature_flags.contracts.schemas import FeatureFlagView, ToggleRequest +from feature_flags.core.constants import ( PERM_FEATURE_FLAGS_MANAGE, PERM_FEATURE_FLAGS_VIEW, SCOPE_TENANT, ) -from feature_flags.contracts.schemas import FeatureFlagView, ToggleRequest -from feature_flags.deps import ( +from feature_flags.web.deps import ( FeatureFlagRegistryDep, FeatureFlagServiceDep, require_known_tenant, diff --git a/modules/feature_flags/feature_flags/endpoints/views.py b/modules/feature_flags/feature_flags/web/endpoints/views.py similarity index 98% rename from modules/feature_flags/feature_flags/endpoints/views.py rename to modules/feature_flags/feature_flags/web/endpoints/views.py index 5d3c61883..61e62458d 100644 --- a/modules/feature_flags/feature_flags/endpoints/views.py +++ b/modules/feature_flags/feature_flags/web/endpoints/views.py @@ -15,7 +15,7 @@ from simple_module_inertia import InertiaResponse from starlette.responses import RedirectResponse -from feature_flags.constants import ( +from feature_flags.core.constants import ( AUDIT_LOG_MODULE, AUDIT_LOG_VIEW_URL, MENU_URL, @@ -27,7 +27,7 @@ SCOPE_TENANT, SYSTEM_SCOPE_ID, ) -from feature_flags.deps import ( +from feature_flags.web.deps import ( FeatureFlagRegistryDep, FeatureFlagServiceDep, require_known_tenant, diff --git a/modules/feature_flags/tests-js/feature_flags_Browse.test.tsx b/modules/feature_flags/tests-js/feature_flags_Browse.test.tsx index 0d99b0fa3..f365a7bd8 100644 --- a/modules/feature_flags/tests-js/feature_flags_Browse.test.tsx +++ b/modules/feature_flags/tests-js/feature_flags_Browse.test.tsx @@ -21,7 +21,7 @@ vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); import { toast } from 'sonner'; import Browse from '../feature_flags/pages/Browse'; -import type { FeatureFlag } from '../feature_flags/pages/types'; +import type { FeatureFlag } from '../feature_flags/ui/types'; const FLAGS: FeatureFlag[] = [ { diff --git a/modules/feature_flags/tests/test_feature_flags_commit_invalidation.py b/modules/feature_flags/tests/core/invalidation/test_feature_flags_commit_invalidation.py similarity index 96% rename from modules/feature_flags/tests/test_feature_flags_commit_invalidation.py rename to modules/feature_flags/tests/core/invalidation/test_feature_flags_commit_invalidation.py index 634b85064..042757640 100644 --- a/modules/feature_flags/tests/test_feature_flags_commit_invalidation.py +++ b/modules/feature_flags/tests/core/invalidation/test_feature_flags_commit_invalidation.py @@ -1,8 +1,8 @@ """Flag changes must survive commit before reaching any worker's registry.""" import pytest +from feature_flags.core.service import FeatureFlagService from feature_flags.models import FeatureFlagOverride -from feature_flags.service import FeatureFlagService from simple_module_core.feature_flags import FeatureFlagDefinition, FeatureFlagRegistry from simple_module_core.invalidation import Invalidation from simple_module_db.transaction import finalize_session, rollback_session @@ -58,7 +58,7 @@ async def test_remote_flag_changes_and_deletions_reload(app, scope, scope_id): @pytest.mark.parametrize("commit", [False, True]) @pytest.mark.parametrize("scope,scope_id", [("system", ""), ("tenant", "acme")]) async def test_flag_request_publishes_only_after_commit(app, commit, scope, scope_id): - from feature_flags.deps import get_feature_flag_service + from feature_flags.web.deps import get_feature_flag_service from starlette.requests import Request registry = app.state.sm.feature_flags diff --git a/modules/feature_flags/tests/test_feature_flags_audit_links.py b/modules/feature_flags/tests/core/module/test_feature_flags_audit_links.py similarity index 96% rename from modules/feature_flags/tests/test_feature_flags_audit_links.py rename to modules/feature_flags/tests/core/module/test_feature_flags_audit_links.py index 388e4e1b5..da51f317f 100644 --- a/modules/feature_flags/tests/test_feature_flags_audit_links.py +++ b/modules/feature_flags/tests/core/module/test_feature_flags_audit_links.py @@ -13,7 +13,7 @@ from types import SimpleNamespace import httpx -from feature_flags.constants import ( +from feature_flags.core.constants import ( AUDIT_LOG_MODULE, AUDIT_LOG_VIEW_URL, MENU_URL, @@ -21,9 +21,9 @@ TABLE_OVERRIDE, VIEW_PREFIX, ) -from feature_flags.endpoints.views import _audit_log_url from feature_flags.models import FeatureFlagOverride from feature_flags.module import FeatureFlagsModule +from feature_flags.web.endpoints.views import _audit_log_url from simple_module_core.audit_links import AuditLinkRegistry @@ -82,7 +82,7 @@ def test_label_key_resolves_in_the_shipped_catalog() -> None: assert link is not None catalog = json.loads( - (Path(__file__).resolve().parents[1] / "feature_flags/locales/en.json").read_text() + (Path(__file__).resolve().parents[3] / "feature_flags/locales/en.json").read_text() ) namespace, *path = link.label_key.split(".") assert namespace == "feature_flags" diff --git a/modules/feature_flags/tests/test_feature_flags_menu_section.py b/modules/feature_flags/tests/core/module/test_feature_flags_menu_section.py similarity index 89% rename from modules/feature_flags/tests/test_feature_flags_menu_section.py rename to modules/feature_flags/tests/core/module/test_feature_flags_menu_section.py index 41a2dac1c..3bb4b29de 100644 --- a/modules/feature_flags/tests/test_feature_flags_menu_section.py +++ b/modules/feature_flags/tests/core/module/test_feature_flags_menu_section.py @@ -2,7 +2,7 @@ from __future__ import annotations -from feature_flags.constants import MENU_URL +from feature_flags.core.constants import MENU_URL from feature_flags.module import FeatureFlagsModule from simple_module_core.menu import MenuRegistry, MenuSection @@ -18,7 +18,7 @@ def test_feature_flags_menu_item_is_in_admin_sidebar() -> None: def test_permission_group_matches_the_menu_label() -> None: """The role editor lists this group beside the sidebar entry it governs; "Feature Flags" next to "Feature flags" reads as two different things.""" - from feature_flags.constants import MENU_LABEL, PERM_GROUP + from feature_flags.core.constants import MENU_LABEL, PERM_GROUP from simple_module_core.permissions import PermissionRegistry registry = PermissionRegistry() diff --git a/modules/feature_flags/tests/test_flag_description_i18n.py b/modules/feature_flags/tests/core/service/test_feature_flags_description_i18n.py similarity index 96% rename from modules/feature_flags/tests/test_flag_description_i18n.py rename to modules/feature_flags/tests/core/service/test_feature_flags_description_i18n.py index 39c48a08b..e90d72430 100644 --- a/modules/feature_flags/tests/test_flag_description_i18n.py +++ b/modules/feature_flags/tests/core/service/test_feature_flags_description_i18n.py @@ -2,7 +2,7 @@ from __future__ import annotations -from feature_flags.service import FeatureFlagService +from feature_flags.core.service import FeatureFlagService from simple_module_core.feature_flags import FeatureFlagDefinition, FeatureFlagRegistry from simple_module_core.i18n import Translator diff --git a/modules/feature_flags/tests/test_feature_flags_service.py b/modules/feature_flags/tests/core/service/test_feature_flags_service.py similarity index 98% rename from modules/feature_flags/tests/test_feature_flags_service.py rename to modules/feature_flags/tests/core/service/test_feature_flags_service.py index 009589b40..4b9d27e1a 100644 --- a/modules/feature_flags/tests/test_feature_flags_service.py +++ b/modules/feature_flags/tests/core/service/test_feature_flags_service.py @@ -2,8 +2,8 @@ from __future__ import annotations -from feature_flags.constants import SCOPE_TENANT -from feature_flags.service import FeatureFlagService +from feature_flags.core.constants import SCOPE_TENANT +from feature_flags.core.service import FeatureFlagService from simple_module_core.feature_flags import FeatureFlagDefinition, FeatureFlagRegistry from simple_module_db.transaction import finalize_session from sqlalchemy.ext.asyncio import AsyncSession diff --git a/modules/feature_flags/tests/test_store_outage_fallback.py b/modules/feature_flags/tests/core/service/test_feature_flags_store_outage_fallback.py similarity index 94% rename from modules/feature_flags/tests/test_store_outage_fallback.py rename to modules/feature_flags/tests/core/service/test_feature_flags_store_outage_fallback.py index d5e44cfb2..fe1b0ed2b 100644 --- a/modules/feature_flags/tests/test_store_outage_fallback.py +++ b/modules/feature_flags/tests/core/service/test_feature_flags_store_outage_fallback.py @@ -51,12 +51,12 @@ async def __aexit__(self, *args): db=SimpleNamespace(session_factory=lambda: _SessionCM()), ) - # ``on_startup`` does ``from feature_flags.service import FeatureFlagService`` + # ``on_startup`` does ``from feature_flags.core.service import FeatureFlagService`` # *inside* the method body — patching ``feature_flags.module.FeatureFlagService`` # would be a no-op since the name is never bound there. Patch the source # module so the deferred import resolves to ``_BoomService``. + import feature_flags.core.service as ff_service import feature_flags.module as ff_module - import feature_flags.service as ff_service monkeypatch.setattr(ff_service, "FeatureFlagService", lambda *a, **kw: _BoomService()) diff --git a/modules/feature_flags/tests/test_feature_flags_tenancy.py b/modules/feature_flags/tests/core/tenancy/test_feature_flags_tenancy.py similarity index 98% rename from modules/feature_flags/tests/test_feature_flags_tenancy.py rename to modules/feature_flags/tests/core/tenancy/test_feature_flags_tenancy.py index 4b3bce923..4f7b33be3 100644 --- a/modules/feature_flags/tests/test_feature_flags_tenancy.py +++ b/modules/feature_flags/tests/core/tenancy/test_feature_flags_tenancy.py @@ -9,8 +9,8 @@ import pytest from fastapi import Depends -from feature_flags.constants import PERM_FEATURE_FLAGS_MANAGE, PERM_FEATURE_FLAGS_VIEW -from feature_flags.service import FeatureFlagService +from feature_flags.core.constants import PERM_FEATURE_FLAGS_MANAGE, PERM_FEATURE_FLAGS_VIEW +from feature_flags.core.service import FeatureFlagService from simple_module_core.feature_flags import FeatureFlagRegistry, require_flag from simple_module_core.tenancy import TenantRole, tenant_role diff --git a/modules/feature_flags/tests/test_feature_flags_tenant_cleanup.py b/modules/feature_flags/tests/core/tenant_cleanup/test_feature_flags_tenant_cleanup.py similarity index 97% rename from modules/feature_flags/tests/test_feature_flags_tenant_cleanup.py rename to modules/feature_flags/tests/core/tenant_cleanup/test_feature_flags_tenant_cleanup.py index 99e13646e..2eb884e1c 100644 --- a/modules/feature_flags/tests/test_feature_flags_tenant_cleanup.py +++ b/modules/feature_flags/tests/core/tenant_cleanup/test_feature_flags_tenant_cleanup.py @@ -1,6 +1,6 @@ """Tenant deletion purges tenant overrides without touching other scopes.""" -from feature_flags.constants import SCOPE_SYSTEM, SCOPE_TENANT +from feature_flags.core.constants import SCOPE_SYSTEM, SCOPE_TENANT from feature_flags.models import FeatureFlagOverride from feature_flags.module import FeatureFlagsModule from simple_module_core.durable_events import DurableContext, event_key diff --git a/modules/feature_flags/tests/test_feature_flags_api.py b/modules/feature_flags/tests/web/endpoints/test_feature_flags_api.py similarity index 100% rename from modules/feature_flags/tests/test_feature_flags_api.py rename to modules/feature_flags/tests/web/endpoints/test_feature_flags_api.py diff --git a/modules/permissions/tests/test_direct_grants_everywhere.py b/modules/permissions/tests/test_direct_grants_everywhere.py index ab7518853..34b3692a3 100644 --- a/modules/permissions/tests/test_direct_grants_everywhere.py +++ b/modules/permissions/tests/test_direct_grants_everywhere.py @@ -13,7 +13,7 @@ import httpx import pytest from fastapi import FastAPI -from feature_flags.constants import PERM_FEATURE_FLAGS_VIEW, VIEW_PREFIX +from feature_flags.core.constants import PERM_FEATURE_FLAGS_VIEW, VIEW_PREFIX from permissions.deps import RequiresPermission as ModuleRequiresPermission from permissions.grants import clear_grants_cache from simple_module_hosting.permissions import RequiresPermission From 31ec0321c2181335b4cc5d0e65d74cc686d5fd30 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Thu, 5 Nov 2026 17:22:34 +0100 Subject: [PATCH 2/2] fix(feature_flags): shorten core docstring to satisfy E501 Claude-Session: https://claude.ai/code/session_01FXHA3ZicrtZ4C5QPEgxfgq --- modules/feature_flags/feature_flags/core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feature_flags/feature_flags/core/__init__.py b/modules/feature_flags/feature_flags/core/__init__.py index 099b36836..f571fddcf 100644 --- a/modules/feature_flags/feature_flags/core/__init__.py +++ b/modules/feature_flags/feature_flags/core/__init__.py @@ -1 +1 @@ -"""What every feature-flag feature builds on: constants, the flag service, cache invalidation and tenant cleanup.""" +"""What every feature-flag feature builds on: constants, service, invalidation, tenant cleanup."""