simple-module-feature-flags 0.0.38__tar.gz → 0.0.39__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.
Files changed (38) hide show
  1. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/PKG-INFO +4 -4
  2. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/module.py +8 -0
  3. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/service.py +4 -3
  4. simple_module_feature_flags-0.0.39/feature_flags/tenant_cleanup.py +18 -0
  5. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/package.json +1 -1
  6. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/pyproject.toml +4 -4
  7. simple_module_feature_flags-0.0.39/tests/test_feature_flags_tenant_cleanup.py +34 -0
  8. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/.gitignore +0 -0
  9. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/LICENSE +0 -0
  10. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/README.md +0 -0
  11. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/__init__.py +0 -0
  12. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/constants.py +0 -0
  13. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/contracts/__init__.py +0 -0
  14. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/contracts/schemas.py +0 -0
  15. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/deps.py +0 -0
  16. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/endpoints/__init__.py +0 -0
  17. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/endpoints/api.py +0 -0
  18. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/endpoints/views.py +0 -0
  19. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/locales/de.json +0 -0
  20. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/locales/en.json +0 -0
  21. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/models.py +0 -0
  22. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/pages/Browse.tsx +0 -0
  23. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/pages/components/FlagTable.tsx +0 -0
  24. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/pages/components/ScopeCard.tsx +0 -0
  25. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/pages/components/TenantPicker.tsx +0 -0
  26. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/pages/components/ToggleConfirmDialog.tsx +0 -0
  27. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/pages/types.ts +0 -0
  28. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/feature_flags/py.typed +0 -0
  29. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tests/conftest.py +0 -0
  30. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tests/test_feature_flags_api.py +0 -0
  31. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tests/test_feature_flags_audit_links.py +0 -0
  32. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tests/test_feature_flags_menu_section.py +0 -0
  33. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tests/test_feature_flags_service.py +0 -0
  34. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tests/test_feature_flags_tenancy.py +0 -0
  35. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tests/test_flag_description_i18n.py +0 -0
  36. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tests/test_store_outage_fallback.py +0 -0
  37. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tests-js/feature_flags_Browse.test.tsx +0 -0
  38. {simple_module_feature_flags-0.0.38 → simple_module_feature_flags-0.0.39}/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_feature_flags
3
- Version: 0.0.38
3
+ Version: 0.0.39
4
4
  Summary: Simple feature-flag module with per-tenant overrides and a consumer API for simple_module
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -21,9 +21,9 @@ Classifier: Topic :: Internet :: WWW/HTTP
21
21
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.12
24
- Requires-Dist: simple-module-core==0.0.38
25
- Requires-Dist: simple-module-db==0.0.38
26
- Requires-Dist: simple-module-hosting==0.0.38
24
+ Requires-Dist: simple-module-core==0.0.39
25
+ Requires-Dist: simple-module-db==0.0.39
26
+ Requires-Dist: simple-module-hosting==0.0.39
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  # simple_module_feature_flags
@@ -29,6 +29,7 @@ from feature_flags.constants import (
29
29
  )
30
30
 
31
31
  if TYPE_CHECKING:
32
+ from simple_module_core.events import EventBus
32
33
  from sqlalchemy.ext.asyncio import AsyncSession
33
34
 
34
35
  _logger = logging.getLogger(__name__)
@@ -67,6 +68,13 @@ class FeatureFlagsModule(ModuleBase):
67
68
  i18n_audience="admin",
68
69
  )
69
70
 
71
+ def register_event_handlers(self, bus: EventBus, app: FastAPI | None = None) -> None:
72
+ from simple_module_core.tenant_events import TenantDeleted
73
+
74
+ from feature_flags.tenant_cleanup import purge_tenant_data
75
+
76
+ bus.subscribe_durable(TenantDeleted, purge_tenant_data)
77
+
70
78
  def register_routes(self, api_router: APIRouter, view_router: APIRouter) -> None:
71
79
  from feature_flags.endpoints.api import router as api
72
80
  from feature_flags.endpoints.views import router as views
@@ -12,6 +12,7 @@ from collections.abc import Callable
12
12
  from simple_module_core.feature_flags import FeatureFlagDefinition, FeatureFlagRegistry
13
13
  from sqlalchemy import select
14
14
  from sqlalchemy.ext.asyncio import AsyncSession
15
+ from sqlmodel import col
15
16
 
16
17
  from feature_flags.constants import SCOPE_SYSTEM, SCOPE_TENANT, SYSTEM_SCOPE_ID
17
18
  from feature_flags.contracts.schemas import FeatureFlagOverrideOut, FeatureFlagView
@@ -151,9 +152,9 @@ class FeatureFlagService:
151
152
  async def _find(self, scope: str, scope_id: str, name: str) -> FeatureFlagOverride | None:
152
153
  result = await self.db.execute(
153
154
  select(FeatureFlagOverride).where(
154
- FeatureFlagOverride.scope == scope, # ty: ignore[invalid-argument-type]
155
- FeatureFlagOverride.scope_id == scope_id, # ty: ignore[invalid-argument-type]
156
- FeatureFlagOverride.name == name, # ty: ignore[invalid-argument-type]
155
+ col(FeatureFlagOverride.scope) == scope,
156
+ col(FeatureFlagOverride.scope_id) == scope_id,
157
+ col(FeatureFlagOverride.name) == name,
157
158
  )
158
159
  )
159
160
  return result.scalar_one_or_none()
@@ -0,0 +1,18 @@
1
+ """Durable cleanup of tenant-scoped feature flag overrides."""
2
+
3
+ from simple_module_core.durable_events import DurableContext
4
+ from simple_module_core.tenant_events import TenantDeleted
5
+ from sqlmodel import col, delete
6
+
7
+ from feature_flags.constants import SCOPE_TENANT
8
+ from feature_flags.models import FeatureFlagOverride
9
+
10
+
11
+ async def purge_tenant_data(event: TenantDeleted, ctx: DurableContext) -> None:
12
+ """Keep system/other tenants' overrides; acknowledge with the worker transaction."""
13
+ await ctx.db.execute(
14
+ delete(FeatureFlagOverride).where(
15
+ col(FeatureFlagOverride.scope) == SCOPE_TENANT,
16
+ col(FeatureFlagOverride.scope_id) == event.tenant_id,
17
+ )
18
+ )
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simple-module-py/feature_flags",
3
- "version": "0.0.38",
3
+ "version": "0.0.39",
4
4
  "private": true,
5
5
  "description": "Frontend assets for the FeatureFlags module",
6
6
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_feature_flags"
3
- version = "0.0.38"
3
+ version = "0.0.39"
4
4
  description = "Simple feature-flag module with per-tenant overrides and a consumer API for simple_module"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,9 +21,9 @@ classifiers = [
21
21
  "Typing :: Typed",
22
22
  ]
23
23
  dependencies = [
24
- "simple_module_core==0.0.38",
25
- "simple_module_db==0.0.38",
26
- "simple_module_hosting==0.0.38",
24
+ "simple_module_core==0.0.39",
25
+ "simple_module_db==0.0.39",
26
+ "simple_module_hosting==0.0.39",
27
27
  ]
28
28
 
29
29
  [project.entry-points.simple_module]
@@ -0,0 +1,34 @@
1
+ """Tenant deletion purges tenant overrides without touching other scopes."""
2
+
3
+ from feature_flags.constants import SCOPE_SYSTEM, SCOPE_TENANT
4
+ from feature_flags.models import FeatureFlagOverride
5
+ from feature_flags.module import FeatureFlagsModule
6
+ from simple_module_core.durable_events import DurableContext, event_key
7
+ from simple_module_core.events import EventBus
8
+ from simple_module_core.tenant_events import TenantDeleted
9
+ from sqlmodel import select
10
+
11
+
12
+ async def test_durable_tenant_cleanup_is_scoped_idempotent_and_transactional(db_session):
13
+ bus = EventBus()
14
+ FeatureFlagsModule().register_event_handlers(bus)
15
+ names = bus.durable.handler_names(TenantDeleted)
16
+ assert len(names) == 1
17
+ subscription = bus.durable.lookup(event_key(TenantDeleted), names[0])
18
+ assert subscription is not None
19
+ scopes = [(SCOPE_TENANT, "deleted"), (SCOPE_TENANT, "other"), (SCOPE_SYSTEM, "deleted")]
20
+ for scope, scope_id in scopes:
21
+ db_session.add(FeatureFlagOverride(scope=scope, scope_id=scope_id, name="test.flag"))
22
+ await db_session.commit()
23
+ event = TenantDeleted(tenant_id="deleted", slug="deleted", name="Deleted")
24
+ ctx = DurableContext(db=db_session, attempt=1)
25
+ await subscription.handler(event, ctx)
26
+ await db_session.rollback()
27
+ assert len((await db_session.execute(select(FeatureFlagOverride))).scalars().all()) == 3
28
+ await subscription.handler(event, ctx)
29
+ await subscription.handler(event, DurableContext(db=db_session, attempt=2))
30
+ rows = (await db_session.execute(select(FeatureFlagOverride))).scalars().all()
31
+ assert {(row.scope, row.scope_id) for row in rows} == {
32
+ (SCOPE_TENANT, "other"),
33
+ (SCOPE_SYSTEM, "deleted"),
34
+ }