diff --git a/CHANGELOG.md b/CHANGELOG.md index 54de5c729..50cddd248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,7 @@ All notable changes to this project are documented in this file. The format is b - `api_tokens` follows the module layout: `api_tokens.core.{constants,settings,services,tenant_cleanup,user_cleanup}`, `api_tokens.credentials.{format,scopes,owner,service,resolver,audit}` (`tokens.py` is now `credentials/format.py`), `api_tokens.web.{deps,endpoints}`, and the page components and types move to `ui/`. `api_tokens.{constants,service,resolver,tokens}` still import, with a `DeprecationWarning`, until 0.0.46. - `branding` follows the module layout: `branding.core.{constants,settings,services,service,presets,tenant_definitions,tenant_branding}`, `branding.assets.{images,reaper}`, `branding.web.{deps,shared_props,endpoints}`, and the form components move from `components/` to `ui/components/`. `branding.{constants,settings,services,tenant_branding,images}` and `branding.endpoints.assets` still import, with a `DeprecationWarning`, until 0.0.46. - `permissions` follows the module layout: `permissions.core.{constants,service,group_labels,write_safety}`, `permissions.grants.{source,role_invalidation,user_cleanup}` (`grants.py` is now `grants/source.py`), `permissions.web.{deps,endpoints}`, and the page components move from `pages/components/` to `ui/components/`. `permissions.deps` and `permissions.group_labels` still import, with a `DeprecationWarning`, until 0.0.46. The durable handler name `permissions.user_cleanup.purge_user_grants` is unchanged. +- `audit_log` follows the module layout: `audit_log.core.{constants,settings,services,capture}`, `audit_log.entries.{service,filters,resolve,activity,export}`, `audit_log.web.{deps,endpoints}`, and the page components move from `pages/components/` to `ui/components/`. The `simple_module.audit_sinks` entry point now targets `audit_log.core.capture:audit_callback`. Nothing outside the module imported the old paths, so no aliases remain. - Coalesce concurrent tenant membership and permission grant cache misses. In-flight invalidation retries against fresh authorization data; failures and cancellation release waiting readers. Cache unknown tenant subdomains for five seconds and drop the new slug's host lookups (on a new `tenants.hosts` invalidation channel, so no user id is read as a slug) and the owner's memberships after committed tenant creation; other users' caches are not cleared. Services built outside request dependencies pass `invalidate_hosts=make_host_invalidator(app)` beside `invalidate=make_invalidator(app)`. Retries after concurrent invalidation are bounded to three reads (2026-11-05 local performance review). diff --git a/docs/module-layout.md b/docs/module-layout.md index 4787b92b8..87aeed600 100644 --- a/docs/module-layout.md +++ b/docs/module-layout.md @@ -213,7 +213,7 @@ when a later module PR moves files. | Module | Root `.py` files | Flat `tests/test_*.py` | State | | --- | ---: | ---: | --- | | api_tokens | 3 (+4 shims) | 0 | follows this layout | -| audit_log | 13 | 18 | flat | +| audit_log | 3 | 0 | follows this layout | | auth | 8 | 20 | flat | | background_tasks | 49 | 83 | flat | | branding | 2 (+6 shims) | 0 | follows this layout | diff --git a/docs/modules/audit_log.md b/docs/modules/audit_log.md index cb2ddf669..ce69ea59e 100644 --- a/docs/modules/audit_log.md +++ b/docs/modules/audit_log.md @@ -28,7 +28,7 @@ A worker never runs `create_app`, so it has no `app.state.sm.db` for `on_startup ```toml [project.entry-points."simple_module.audit_sinks"] -audit_log = "audit_log.capture:audit_callback" +audit_log = "audit_log.core.capture:audit_callback" ``` `background_tasks` binds whatever `simple_module_db.audit_sinks.discover_audit_callback()` finds to both of its worker engines — the sync engine task bodies and signal handlers use (`sync_db`) and the async engine durable outbox handlers run on (`outbox_worker`). A task's ORM writes, including `TaskExecution` lifecycle rows, are therefore audited exactly like a request's whenever `audit_log` is installed; neither package imports the other. The worker's audit rows carry no `user_id` (no request principal). diff --git a/docs/perf/2026-08-02-baseline.md b/docs/perf/2026-08-02-baseline.md index 362b88394..873f2d94b 100644 --- a/docs/perf/2026-08-02-baseline.md +++ b/docs/perf/2026-08-02-baseline.md @@ -457,7 +457,7 @@ above are with prepared statements warm, as in steady-state serving.) Cumulative on the audit-log navigation: **86.2 ms → 37.0 ms, −57 %**. -Tests: `modules/audit_log/tests/test_distinct_entity_types.py` covers +Tests: `modules/audit_log/tests/entries/service/test_audit_log_distinct_entity_types.py` covers behaviour on the SQLite path and asserts the dialect branch selection, so the Postgres query cannot silently become the SQLite one. diff --git a/framework/core/tests/test_sm003_page_render.py b/framework/core/tests/test_sm003_page_render.py index 1087e0180..e8e074c15 100644 --- a/framework/core/tests/test_sm003_page_render.py +++ b/framework/core/tests/test_sm003_page_render.py @@ -74,7 +74,7 @@ async def test_resolves_annotated_fstring_constant(self, tmp_path: Path): endpoints = src_dir / "endpoints" endpoints.mkdir() (endpoints / "views.py").write_text( - "from audit_log.constants import PAGE_BROWSE\n" + "from audit_log.core.constants import PAGE_BROWSE\n" "async def view(inertia):\n" " return await inertia.render(PAGE_BROWSE, {})\n" ) diff --git a/framework/db/simple_module_db/audit_sinks.py b/framework/db/simple_module_db/audit_sinks.py index 6bcd973df..5c16e86cf 100644 --- a/framework/db/simple_module_db/audit_sinks.py +++ b/framework/db/simple_module_db/audit_sinks.py @@ -7,7 +7,7 @@ declares:: [project.entry-points."simple_module.audit_sinks"] - audit_log = "audit_log.capture:audit_callback" + audit_log = "audit_log.core.capture:audit_callback" and the worker binds the result to its engine with ``EngineTenancy(audit_callback=...)``. Neither side imports the other (#498). diff --git a/host/tests/test_module_layout.py b/host/tests/test_module_layout.py index 719db71b5..ecd886aa5 100644 --- a/host/tests/test_module_layout.py +++ b/host/tests/test_module_layout.py @@ -34,6 +34,7 @@ # Add named domain areas here only when they contain several related features. DOMAIN_AREAS: dict[str, frozenset[str]] = { "api_tokens": frozenset({"credentials"}), + "audit_log": frozenset({"entries"}), "branding": frozenset({"assets"}), "keycloak": frozenset({"identity"}), "mail": frozenset({"delivery"}), @@ -43,12 +44,6 @@ # Frozen inventory, not a computed baseline: moves must remove their old entries. LEGACY_FLAT: dict[str, frozenset[str]] = { - "audit_log": frozenset( - """ - activity.py capture.py constants.py deps.py endpoints export.py filters.py - resolve.py service.py services.py settings.py - """.split() - ), "auth": frozenset( """ _route_match.py constants.py deps.py middleware.py oidc_common state.py diff --git a/modules/audit_log/audit_log/core/__init__.py b/modules/audit_log/audit_log/core/__init__.py new file mode 100644 index 000000000..411bfe373 --- /dev/null +++ b/modules/audit_log/audit_log/core/__init__.py @@ -0,0 +1,4 @@ +"""What every audit_log feature builds on. + +Constants, settings, the app.state container and the audit capture callback. +""" diff --git a/modules/audit_log/audit_log/capture.py b/modules/audit_log/audit_log/core/capture.py similarity index 100% rename from modules/audit_log/audit_log/capture.py rename to modules/audit_log/audit_log/core/capture.py diff --git a/modules/audit_log/audit_log/constants.py b/modules/audit_log/audit_log/core/constants.py similarity index 100% rename from modules/audit_log/audit_log/constants.py rename to modules/audit_log/audit_log/core/constants.py diff --git a/modules/audit_log/audit_log/services.py b/modules/audit_log/audit_log/core/services.py similarity index 84% rename from modules/audit_log/audit_log/services.py rename to modules/audit_log/audit_log/core/services.py index 88df23aca..563881c80 100644 --- a/modules/audit_log/audit_log/services.py +++ b/modules/audit_log/audit_log/core/services.py @@ -4,7 +4,7 @@ from dataclasses import dataclass -from audit_log.settings import AuditLogSettings +from audit_log.core.settings import AuditLogSettings @dataclass diff --git a/modules/audit_log/audit_log/settings.py b/modules/audit_log/audit_log/core/settings.py similarity index 100% rename from modules/audit_log/audit_log/settings.py rename to modules/audit_log/audit_log/core/settings.py diff --git a/modules/audit_log/audit_log/entries/__init__.py b/modules/audit_log/audit_log/entries/__init__.py new file mode 100644 index 000000000..55f9f0aec --- /dev/null +++ b/modules/audit_log/audit_log/entries/__init__.py @@ -0,0 +1,5 @@ +"""Reading audit entries. + +The query service, shared filters, id resolution, the activity feed and CSV +export. +""" diff --git a/modules/audit_log/audit_log/activity.py b/modules/audit_log/audit_log/entries/activity.py similarity index 94% rename from modules/audit_log/audit_log/activity.py rename to modules/audit_log/audit_log/entries/activity.py index 8ec062d7e..9ca4c7eb6 100644 --- a/modules/audit_log/audit_log/activity.py +++ b/modules/audit_log/audit_log/entries/activity.py @@ -13,8 +13,8 @@ from simple_module_core.activity_feed import ActivityItem -from audit_log.resolve import resolve_entity_labels -from audit_log.service import AuditLogService +from audit_log.entries.resolve import resolve_entity_labels +from audit_log.entries.service import AuditLogService class AuditActivityFeed: diff --git a/modules/audit_log/audit_log/export.py b/modules/audit_log/audit_log/entries/export.py similarity index 96% rename from modules/audit_log/audit_log/export.py rename to modules/audit_log/audit_log/entries/export.py index 9835ee323..1050804c5 100644 --- a/modules/audit_log/audit_log/export.py +++ b/modules/audit_log/audit_log/entries/export.py @@ -23,9 +23,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from audit_log.contracts.schemas import AuditEntryRead -from audit_log.filters import EntryFilters -from audit_log.resolve import resolve_actors, resolve_entity_labels -from audit_log.service import AuditLogService +from audit_log.entries.filters import EntryFilters +from audit_log.entries.resolve import resolve_actors, resolve_entity_labels +from audit_log.entries.service import AuditLogService CSV_COLUMNS = ( "time", diff --git a/modules/audit_log/audit_log/filters.py b/modules/audit_log/audit_log/entries/filters.py similarity index 98% rename from modules/audit_log/audit_log/filters.py rename to modules/audit_log/audit_log/entries/filters.py index 85331b1c2..0fcdb0dc7 100644 --- a/modules/audit_log/audit_log/filters.py +++ b/modules/audit_log/audit_log/entries/filters.py @@ -13,7 +13,7 @@ from datetime import datetime, time from typing import Any -from audit_log.constants import PLATFORM_TENANT_FILTER +from audit_log.core.constants import PLATFORM_TENANT_FILTER from audit_log.models import AuditEntry diff --git a/modules/audit_log/audit_log/resolve.py b/modules/audit_log/audit_log/entries/resolve.py similarity index 100% rename from modules/audit_log/audit_log/resolve.py rename to modules/audit_log/audit_log/entries/resolve.py diff --git a/modules/audit_log/audit_log/service.py b/modules/audit_log/audit_log/entries/service.py similarity index 98% rename from modules/audit_log/audit_log/service.py rename to modules/audit_log/audit_log/entries/service.py index 8cf0cda53..7e180053c 100644 --- a/modules/audit_log/audit_log/service.py +++ b/modules/audit_log/audit_log/entries/service.py @@ -10,9 +10,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlmodel import col -from audit_log.constants import DEFAULT_PAGE_SIZE, MAX_PAGE, MAX_PAGE_SIZE from audit_log.contracts.schemas import AuditEntryList, AuditEntryRead -from audit_log.filters import EntryFilters +from audit_log.core.constants import DEFAULT_PAGE_SIZE, MAX_PAGE, MAX_PAGE_SIZE +from audit_log.entries.filters import EntryFilters from audit_log.models import AuditEntry _ENTITY_TYPE_CTE = "distinct_entity_type" diff --git a/modules/audit_log/audit_log/models.py b/modules/audit_log/audit_log/models.py index d7f797cf8..b805a122b 100644 --- a/modules/audit_log/audit_log/models.py +++ b/modules/audit_log/audit_log/models.py @@ -10,7 +10,7 @@ from sqlalchemy import JSON, Index, func from sqlmodel import Column, Field -from audit_log.constants import ( +from audit_log.core.constants import ( ACTION_MAX_LENGTH, CORRELATION_ID_MAX_LENGTH, ENTITY_ID_MAX_LENGTH, diff --git a/modules/audit_log/audit_log/module.py b/modules/audit_log/audit_log/module.py index 90ec1d437..d48bfd365 100644 --- a/modules/audit_log/audit_log/module.py +++ b/modules/audit_log/audit_log/module.py @@ -11,7 +11,7 @@ from simple_module_core.module_settings import register_module_settings from simple_module_core.permissions import PermissionRegistry -from audit_log.constants import ( +from audit_log.core.constants import ( ALL_PERMISSIONS, API_PREFIX, LOCALE_NAMESPACE, @@ -36,16 +36,16 @@ class AuditLogModule(ModuleBase): ) def register_settings(self, app: FastAPI) -> None: - from audit_log.services import AuditLogServices - from audit_log.settings import AuditLogSettings + from audit_log.core.services import AuditLogServices + from audit_log.core.settings import AuditLogSettings register_module_settings( app, MODULE_PACKAGE, AuditLogSettings, lambda s: AuditLogServices(settings=s) ) def register_routes(self, api_router: APIRouter, view_router: APIRouter) -> None: - from audit_log.endpoints.api import router as api - from audit_log.endpoints.views import router as views + from audit_log.web.endpoints.api import router as api + from audit_log.web.endpoints.views import router as views api_router.include_router(api) view_router.include_router(views) @@ -77,8 +77,8 @@ def register_permissions(self, registry: PermissionRegistry) -> None: async def on_startup(self, app: FastAPI) -> None: from simple_module_core.activity_feed import set_activity_feed - from audit_log.activity import AuditActivityFeed - from audit_log.capture import audit_callback + from audit_log.core.capture import audit_callback + from audit_log.entries.activity import AuditActivityFeed db = app.state.sm.db db.audit_callback = audit_callback diff --git a/modules/audit_log/audit_log/pages/Browse.tsx b/modules/audit_log/audit_log/pages/Browse.tsx index 3eaef84f9..2fe6a697f 100644 --- a/modules/audit_log/audit_log/pages/Browse.tsx +++ b/modules/audit_log/audit_log/pages/Browse.tsx @@ -7,16 +7,16 @@ import { Card } from '@simple-module-py/ui/components/ui/card'; import { AdminLayout } from '@simple-module-py/ui/layouts/AdminLayout'; import { Download } from 'lucide-react'; import { useState } from 'react'; -import { BrowseEmpty } from './components/BrowseEmpty'; -import { CorrelationBanner } from './components/Correlation'; -import { type AuditEntryRead, EntriesTable } from './components/EntriesTable'; +import { BrowseEmpty } from '../ui/components/BrowseEmpty'; +import { CorrelationBanner } from '../ui/components/Correlation'; +import { type AuditEntryRead, EntriesTable } from '../ui/components/EntriesTable'; import { ALL, type AppliedFilters, type EntityTypeOption, FilterBar, type FilterState, -} from './components/FilterBar'; +} from '../ui/components/FilterBar'; interface Props { items: AuditEntryRead[]; diff --git a/modules/audit_log/audit_log/pages/components/BrowseEmpty.tsx b/modules/audit_log/audit_log/ui/components/BrowseEmpty.tsx similarity index 100% rename from modules/audit_log/audit_log/pages/components/BrowseEmpty.tsx rename to modules/audit_log/audit_log/ui/components/BrowseEmpty.tsx diff --git a/modules/audit_log/audit_log/pages/components/ChangesList.tsx b/modules/audit_log/audit_log/ui/components/ChangesList.tsx similarity index 100% rename from modules/audit_log/audit_log/pages/components/ChangesList.tsx rename to modules/audit_log/audit_log/ui/components/ChangesList.tsx diff --git a/modules/audit_log/audit_log/pages/components/Correlation.tsx b/modules/audit_log/audit_log/ui/components/Correlation.tsx similarity index 100% rename from modules/audit_log/audit_log/pages/components/Correlation.tsx rename to modules/audit_log/audit_log/ui/components/Correlation.tsx diff --git a/modules/audit_log/audit_log/pages/components/DateRangeField.tsx b/modules/audit_log/audit_log/ui/components/DateRangeField.tsx similarity index 100% rename from modules/audit_log/audit_log/pages/components/DateRangeField.tsx rename to modules/audit_log/audit_log/ui/components/DateRangeField.tsx diff --git a/modules/audit_log/audit_log/pages/components/EntriesTable.tsx b/modules/audit_log/audit_log/ui/components/EntriesTable.tsx similarity index 100% rename from modules/audit_log/audit_log/pages/components/EntriesTable.tsx rename to modules/audit_log/audit_log/ui/components/EntriesTable.tsx diff --git a/modules/audit_log/audit_log/pages/components/EntryCells.tsx b/modules/audit_log/audit_log/ui/components/EntryCells.tsx similarity index 100% rename from modules/audit_log/audit_log/pages/components/EntryCells.tsx rename to modules/audit_log/audit_log/ui/components/EntryCells.tsx diff --git a/modules/audit_log/audit_log/pages/components/FilterBar.tsx b/modules/audit_log/audit_log/ui/components/FilterBar.tsx similarity index 100% rename from modules/audit_log/audit_log/pages/components/FilterBar.tsx rename to modules/audit_log/audit_log/ui/components/FilterBar.tsx diff --git a/modules/audit_log/audit_log/pages/components/format.ts b/modules/audit_log/audit_log/ui/components/format.ts similarity index 100% rename from modules/audit_log/audit_log/pages/components/format.ts rename to modules/audit_log/audit_log/ui/components/format.ts diff --git a/modules/audit_log/audit_log/web/__init__.py b/modules/audit_log/audit_log/web/__init__.py new file mode 100644 index 000000000..7d668357f --- /dev/null +++ b/modules/audit_log/audit_log/web/__init__.py @@ -0,0 +1 @@ +"""The HTTP side: request deps and the REST and Inertia endpoints.""" diff --git a/modules/audit_log/audit_log/deps.py b/modules/audit_log/audit_log/web/deps.py similarity index 89% rename from modules/audit_log/audit_log/deps.py rename to modules/audit_log/audit_log/web/deps.py index 99747f9a4..846346d79 100644 --- a/modules/audit_log/audit_log/deps.py +++ b/modules/audit_log/audit_log/web/deps.py @@ -8,7 +8,7 @@ from simple_module_db.deps import get_db from sqlalchemy.ext.asyncio import AsyncSession -from audit_log.service import AuditLogService +from audit_log.entries.service import AuditLogService async def get_audit_log_service( diff --git a/modules/audit_log/audit_log/endpoints/__init__.py b/modules/audit_log/audit_log/web/endpoints/__init__.py similarity index 100% rename from modules/audit_log/audit_log/endpoints/__init__.py rename to modules/audit_log/audit_log/web/endpoints/__init__.py diff --git a/modules/audit_log/audit_log/endpoints/api.py b/modules/audit_log/audit_log/web/endpoints/api.py similarity index 92% rename from modules/audit_log/audit_log/endpoints/api.py rename to modules/audit_log/audit_log/web/endpoints/api.py index cbca180f6..b8d652e9c 100644 --- a/modules/audit_log/audit_log/endpoints/api.py +++ b/modules/audit_log/audit_log/web/endpoints/api.py @@ -12,12 +12,12 @@ from simple_module_hosting.permissions import RequiresPermission, resolved_permissions_for from sqlalchemy.ext.asyncio import AsyncSession -from audit_log.constants import DEFAULT_PAGE_SIZE, PERM_VIEW from audit_log.contracts.schemas import AuditEntryList -from audit_log.deps import AuditLogServiceDep -from audit_log.export import CSV_FILENAME, CSV_MEDIA_TYPE, stream_csv -from audit_log.filters import EntryFilters -from audit_log.resolve import actor_filter +from audit_log.core.constants import DEFAULT_PAGE_SIZE, PERM_VIEW +from audit_log.entries.export import CSV_FILENAME, CSV_MEDIA_TYPE, stream_csv +from audit_log.entries.filters import EntryFilters +from audit_log.entries.resolve import actor_filter +from audit_log.web.deps import AuditLogServiceDep router = APIRouter() diff --git a/modules/audit_log/audit_log/endpoints/views.py b/modules/audit_log/audit_log/web/endpoints/views.py similarity index 97% rename from modules/audit_log/audit_log/endpoints/views.py rename to modules/audit_log/audit_log/web/endpoints/views.py index 7a50b1980..456cce020 100644 --- a/modules/audit_log/audit_log/endpoints/views.py +++ b/modules/audit_log/audit_log/web/endpoints/views.py @@ -15,22 +15,22 @@ from simple_module_inertia import InertiaResponse from sqlalchemy.ext.asyncio import AsyncSession -from audit_log.constants import ( +from audit_log.core.constants import ( API_PREFIX, DEFAULT_PAGE_SIZE, PAGE_BROWSE, PERM_VIEW, PLATFORM_TENANT_FILTER, ) -from audit_log.deps import AuditLogServiceDep -from audit_log.filters import EntryFilters -from audit_log.resolve import ( +from audit_log.entries.filters import EntryFilters +from audit_log.entries.resolve import ( actor_filter, actor_link, entity_link, resolve_actors, resolve_entity_labels, ) +from audit_log.web.deps import AuditLogServiceDep router = APIRouter() diff --git a/modules/audit_log/pyproject.toml b/modules/audit_log/pyproject.toml index 8244e4967..958194800 100644 --- a/modules/audit_log/pyproject.toml +++ b/modules/audit_log/pyproject.toml @@ -32,7 +32,7 @@ audit_log = "audit_log.module:AuditLogModule" # Read by processes that never build the app (the Celery worker), so their # writes are audited too (#498). See ``simple_module_db.audit_sinks``. [project.entry-points."simple_module.audit_sinks"] -audit_log = "audit_log.capture:audit_callback" +audit_log = "audit_log.core.capture:audit_callback" [project.urls] Homepage = "https://github.com/antosubash/simple_module_python" diff --git a/modules/audit_log/tests-js/BrowseEmpty.test.tsx b/modules/audit_log/tests-js/BrowseEmpty.test.tsx index 2093ec070..731447e7d 100644 --- a/modules/audit_log/tests-js/BrowseEmpty.test.tsx +++ b/modules/audit_log/tests-js/BrowseEmpty.test.tsx @@ -13,7 +13,7 @@ configureI18n({ }, }); -import { BrowseEmpty } from '../audit_log/pages/components/BrowseEmpty'; +import { BrowseEmpty } from '../audit_log/ui/components/BrowseEmpty'; const NONE = { entity_type: null, diff --git a/modules/audit_log/tests-js/ChangesList.test.tsx b/modules/audit_log/tests-js/ChangesList.test.tsx index d3f3fadb8..80ee1ba08 100644 --- a/modules/audit_log/tests-js/ChangesList.test.tsx +++ b/modules/audit_log/tests-js/ChangesList.test.tsx @@ -14,7 +14,7 @@ configureI18n({ }, }); -import { type Change, ChangesList } from '../audit_log/pages/components/ChangesList'; +import { type Change, ChangesList } from '../audit_log/ui/components/ChangesList'; const FOUR: Change[] = [ { field: 'value', old: '', new: 'mail.example.com' }, diff --git a/modules/audit_log/tests-js/format.test.ts b/modules/audit_log/tests-js/format.test.ts index 85ecc034b..39c75f7ea 100644 --- a/modules/audit_log/tests-js/format.test.ts +++ b/modules/audit_log/tests-js/format.test.ts @@ -6,7 +6,7 @@ import { formatEntryTime, parseIsoDate, toIsoDate, -} from '../audit_log/pages/components/format'; +} from '../audit_log/ui/components/format'; describe('formatEntryTime', () => { test('renders the deck format: d MMM HH:mm:ss', () => { diff --git a/modules/audit_log/tests/_entity_label_support.py b/modules/audit_log/tests/_audit_log_support.py similarity index 96% rename from modules/audit_log/tests/_entity_label_support.py rename to modules/audit_log/tests/_audit_log_support.py index 388f7ed83..0da7d0cbf 100644 --- a/modules/audit_log/tests/_entity_label_support.py +++ b/modules/audit_log/tests/_audit_log_support.py @@ -8,7 +8,7 @@ from __future__ import annotations import httpx -from audit_log.constants import ACTION_UPDATED +from audit_log.core.constants import ACTION_UPDATED from audit_log.models import AuditEntry VIEW_URL = "/admin/audit-log/" diff --git a/modules/audit_log/tests/conftest.py b/modules/audit_log/tests/conftest.py new file mode 100644 index 000000000..d5b2e3c29 --- /dev/null +++ b/modules/audit_log/tests/conftest.py @@ -0,0 +1,4 @@ +"""Fixtures shared by every audit_log test folder. + +Its presence puts this directory on ``sys.path``, so ``_audit_log_support`` imports. +""" diff --git a/modules/audit_log/tests/test_audit_log_bulk_capture.py b/modules/audit_log/tests/core/capture/test_audit_log_bulk_capture.py similarity index 100% rename from modules/audit_log/tests/test_audit_log_bulk_capture.py rename to modules/audit_log/tests/core/capture/test_audit_log_bulk_capture.py diff --git a/modules/audit_log/tests/test_tenancy.py b/modules/audit_log/tests/core/capture/test_audit_log_tenancy.py similarity index 97% rename from modules/audit_log/tests/test_tenancy.py rename to modules/audit_log/tests/core/capture/test_audit_log_tenancy.py index eb506bf03..a64034954 100644 --- a/modules/audit_log/tests/test_tenancy.py +++ b/modules/audit_log/tests/core/capture/test_audit_log_tenancy.py @@ -10,8 +10,8 @@ import io import pytest -from audit_log.capture import audit_callback -from audit_log.constants import PLATFORM_TENANT_FILTER +from audit_log.core.capture import audit_callback +from audit_log.core.constants import PLATFORM_TENANT_FILTER from audit_log.models import AuditEntry from simple_module_core.tenancy import TenantRole from simple_module_db import all_tenants, tenant_context diff --git a/modules/audit_log/tests/test_audit_log_activity_feed.py b/modules/audit_log/tests/entries/activity/test_audit_log_activity_feed.py similarity index 97% rename from modules/audit_log/tests/test_audit_log_activity_feed.py rename to modules/audit_log/tests/entries/activity/test_audit_log_activity_feed.py index 8b4f50648..b73285bcd 100644 --- a/modules/audit_log/tests/test_audit_log_activity_feed.py +++ b/modules/audit_log/tests/entries/activity/test_audit_log_activity_feed.py @@ -5,7 +5,7 @@ import uuid from types import SimpleNamespace -from audit_log.activity import AuditActivityFeed +from audit_log.entries.activity import AuditActivityFeed from audit_log.models import AuditEntry from simple_module_core.activity_feed import get_activity_feed from simple_module_core.audit_links import AuditLink, AuditLinkRegistry diff --git a/modules/audit_log/tests/test_export_csv.py b/modules/audit_log/tests/entries/export/test_audit_log_export_csv.py similarity index 99% rename from modules/audit_log/tests/test_export_csv.py rename to modules/audit_log/tests/entries/export/test_audit_log_export_csv.py index 63691310f..1388a797e 100644 --- a/modules/audit_log/tests/test_export_csv.py +++ b/modules/audit_log/tests/entries/export/test_audit_log_export_csv.py @@ -15,7 +15,7 @@ from datetime import UTC, datetime import httpx -from audit_log.constants import ACTION_CREATED, ACTION_DELETED, ACTION_UPDATED +from audit_log.core.constants import ACTION_CREATED, ACTION_DELETED, ACTION_UPDATED from audit_log.models import AuditEntry from simple_module_test import forge_session_cookie from users.models import User diff --git a/modules/audit_log/tests/test_export_csv_injection.py b/modules/audit_log/tests/entries/export/test_audit_log_export_csv_injection.py similarity index 94% rename from modules/audit_log/tests/test_export_csv_injection.py rename to modules/audit_log/tests/entries/export/test_audit_log_export_csv_injection.py index dfb0575b1..957db54d5 100644 --- a/modules/audit_log/tests/test_export_csv_injection.py +++ b/modules/audit_log/tests/entries/export/test_audit_log_export_csv_injection.py @@ -11,7 +11,7 @@ import io import httpx -from audit_log.constants import ACTION_UPDATED +from audit_log.core.constants import ACTION_UPDATED from audit_log.models import AuditEntry from users.models import User @@ -68,13 +68,13 @@ async def test_a_formula_display_name_is_neutralised( assert row["actor"] == f"'{payload}" def test_every_dangerous_lead_character_is_prefixed(self) -> None: - from audit_log.export import escape_formula + from audit_log.entries.export import escape_formula for lead in ("=", "+", "-", "@", "\t", "\r"): assert escape_formula(f"{lead}cmd") == f"'{lead}cmd" def test_ordinary_text_is_untouched(self) -> None: - from audit_log.export import escape_formula + from audit_log.entries.export import escape_formula assert escape_formula("Dana Rivera") == "Dana Rivera" assert escape_formula("") == "" diff --git a/modules/audit_log/tests/test_actor_filter_by_name.py b/modules/audit_log/tests/entries/filters/test_audit_log_actor_filter_by_name.py similarity index 98% rename from modules/audit_log/tests/test_actor_filter_by_name.py rename to modules/audit_log/tests/entries/filters/test_audit_log_actor_filter_by_name.py index 2d831aa9a..e676d1e85 100644 --- a/modules/audit_log/tests/test_actor_filter_by_name.py +++ b/modules/audit_log/tests/entries/filters/test_audit_log_actor_filter_by_name.py @@ -13,7 +13,7 @@ import uuid import httpx -from audit_log.constants import ACTION_UPDATED +from audit_log.core.constants import ACTION_UPDATED from audit_log.models import AuditEntry from users.models import User @@ -153,7 +153,7 @@ async def test_the_filter_is_a_subquery_not_a_list_of_ids(self) -> None: """Pins the mechanism, not just today's row count: an implementation that materialises ids passes the count test right up until the install is big enough to break it.""" - from audit_log.resolve import actor_filter + from audit_log.entries.resolve import actor_filter from users.directory import UsersDirectory rendered = str(actor_filter(UsersDirectory(), "okafor")) @@ -164,7 +164,7 @@ async def test_the_filter_is_a_subquery_not_a_list_of_ids(self) -> None: async def test_without_a_directory_the_term_is_an_exact_actor_id(self) -> None: """No directory can say who is called that, so the box falls back to an id match (a worker name) instead of silently matching nobody.""" - from audit_log.resolve import actor_filter + from audit_log.entries.resolve import actor_filter from simple_module_core.user_directory import NullUserDirectory rendered = str(actor_filter(NullUserDirectory(), "celery-worker-1")) diff --git a/modules/audit_log/tests/test_audit_correlation_filter.py b/modules/audit_log/tests/entries/filters/test_audit_log_correlation_filter.py similarity index 96% rename from modules/audit_log/tests/test_audit_correlation_filter.py rename to modules/audit_log/tests/entries/filters/test_audit_log_correlation_filter.py index 303b24a1c..1ab7f1d64 100644 --- a/modules/audit_log/tests/test_audit_correlation_filter.py +++ b/modules/audit_log/tests/entries/filters/test_audit_log_correlation_filter.py @@ -8,9 +8,9 @@ from __future__ import annotations -from audit_log.constants import ACTION_CREATED, ACTION_UPDATED +from audit_log.core.constants import ACTION_CREATED, ACTION_UPDATED +from audit_log.entries.service import AuditLogService from audit_log.models import AuditEntry -from audit_log.service import AuditLogService from sqlalchemy.ext.asyncio import AsyncSession _REQUEST = "corr-aaa" diff --git a/modules/audit_log/tests/test_date_range_filter.py b/modules/audit_log/tests/entries/filters/test_audit_log_date_range_filter.py similarity index 96% rename from modules/audit_log/tests/test_date_range_filter.py rename to modules/audit_log/tests/entries/filters/test_audit_log_date_range_filter.py index e975b53e2..a8240d8a4 100644 --- a/modules/audit_log/tests/test_date_range_filter.py +++ b/modules/audit_log/tests/entries/filters/test_audit_log_date_range_filter.py @@ -12,8 +12,8 @@ from datetime import UTC, datetime import httpx -from audit_log.constants import ACTION_UPDATED -from audit_log.filters import EntryFilters +from audit_log.core.constants import ACTION_UPDATED +from audit_log.entries.filters import EntryFilters from audit_log.models import AuditEntry VIEW_URL = "/admin/audit-log/" diff --git a/modules/audit_log/tests/test_entity_label_batching.py b/modules/audit_log/tests/entries/resolve/test_audit_log_entity_label_batching.py similarity index 98% rename from modules/audit_log/tests/test_entity_label_batching.py rename to modules/audit_log/tests/entries/resolve/test_audit_log_entity_label_batching.py index 54b871243..6b0e7c398 100644 --- a/modules/audit_log/tests/test_entity_label_batching.py +++ b/modules/audit_log/tests/entries/resolve/test_audit_log_entity_label_batching.py @@ -7,8 +7,8 @@ from __future__ import annotations +from audit_log.entries.resolve import resolve_entity_labels from audit_log.models import AuditEntry -from audit_log.resolve import resolve_entity_labels from simple_module_core.audit_links import AuditLink, AuditLinkRegistry from sqlalchemy import select from sqlalchemy.exc import DBAPIError diff --git a/modules/audit_log/tests/test_entity_label_permission.py b/modules/audit_log/tests/entries/resolve/test_audit_log_entity_label_permission.py similarity index 97% rename from modules/audit_log/tests/test_entity_label_permission.py rename to modules/audit_log/tests/entries/resolve/test_audit_log_entity_label_permission.py index eaf52bc67..5a6ac7815 100644 --- a/modules/audit_log/tests/test_entity_label_permission.py +++ b/modules/audit_log/tests/entries/resolve/test_audit_log_entity_label_permission.py @@ -23,11 +23,11 @@ import httpx import pytest -from _entity_label_support import browse as _browse -from _entity_label_support import entity_of as _entity -from _entity_label_support import seed_entry as _seed_entry -from audit_log.constants import PERM_VIEW -from audit_log.resolve import resolve_entity_labels +from _audit_log_support import browse as _browse +from _audit_log_support import entity_of as _entity +from _audit_log_support import seed_entry as _seed_entry +from audit_log.core.constants import PERM_VIEW +from audit_log.entries.resolve import resolve_entity_labels from simple_module_core.audit_links import AuditLink, AuditLinkRegistry from simple_module_db.stored_settings import Setting from simple_module_test import forge_session_cookie, inertia_headers diff --git a/modules/audit_log/tests/test_entity_labels.py b/modules/audit_log/tests/entries/resolve/test_audit_log_entity_labels.py similarity index 97% rename from modules/audit_log/tests/test_entity_labels.py rename to modules/audit_log/tests/entries/resolve/test_audit_log_entity_labels.py index e39142c58..9ffac00d1 100644 --- a/modules/audit_log/tests/test_entity_labels.py +++ b/modules/audit_log/tests/entries/resolve/test_audit_log_entity_labels.py @@ -13,9 +13,9 @@ import uuid import httpx -from _entity_label_support import browse as _browse -from _entity_label_support import entity_of as _entity -from _entity_label_support import seed_entry as _seed_entry +from _audit_log_support import browse as _browse +from _audit_log_support import entity_of as _entity +from _audit_log_support import seed_entry as _seed_entry from background_tasks.models import TaskExecution from feature_flags.models import FeatureFlagOverride from simple_module_db.stored_settings import Setting diff --git a/modules/audit_log/tests/test_audit_log_resolve.py b/modules/audit_log/tests/entries/resolve/test_audit_log_resolve.py similarity index 98% rename from modules/audit_log/tests/test_audit_log_resolve.py rename to modules/audit_log/tests/entries/resolve/test_audit_log_resolve.py index f0125d084..f1763a323 100644 --- a/modules/audit_log/tests/test_audit_log_resolve.py +++ b/modules/audit_log/tests/entries/resolve/test_audit_log_resolve.py @@ -8,7 +8,7 @@ import uuid -from audit_log.resolve import actor_link, entity_link, resolve_actors +from audit_log.entries.resolve import actor_link, entity_link, resolve_actors from simple_module_core.audit_links import AuditLink, AuditLinkRegistry from simple_module_core.user_directory import NullUserDirectory from sqlalchemy.ext.asyncio import AsyncSession diff --git a/modules/audit_log/tests/test_distinct_entity_types.py b/modules/audit_log/tests/entries/service/test_audit_log_distinct_entity_types.py similarity index 89% rename from modules/audit_log/tests/test_distinct_entity_types.py rename to modules/audit_log/tests/entries/service/test_audit_log_distinct_entity_types.py index 7532e32b2..752aceaa8 100644 --- a/modules/audit_log/tests/test_distinct_entity_types.py +++ b/modules/audit_log/tests/entries/service/test_audit_log_distinct_entity_types.py @@ -14,9 +14,9 @@ from __future__ import annotations import pytest -from audit_log.constants import ACTION_CREATED +from audit_log.core.constants import ACTION_CREATED +from audit_log.entries.service import AuditLogService from audit_log.models import AuditEntry -from audit_log.service import AuditLogService from simple_module_db.provider import DatabaseProvider from sqlalchemy.ext.asyncio import AsyncSession @@ -60,13 +60,13 @@ class TestDialectSelection: """The Postgres branch is chosen by dialect, not by accident.""" def test_postgres_uses_the_recursive_skip_scan(self) -> None: - from audit_log.service import _distinct_stmt_for_dialect + from audit_log.entries.service import _distinct_stmt_for_dialect sql = str(_distinct_stmt_for_dialect(DatabaseProvider.POSTGRESQL)) assert "RECURSIVE" in sql.upper() def test_sqlite_uses_plain_distinct(self) -> None: - from audit_log.service import _distinct_stmt_for_dialect + from audit_log.entries.service import _distinct_stmt_for_dialect sql = str(_distinct_stmt_for_dialect(DatabaseProvider.SQLITE)) assert "RECURSIVE" not in sql.upper() @@ -74,6 +74,6 @@ def test_sqlite_uses_plain_distinct(self) -> None: @pytest.mark.parametrize("dialect", ["sqlite", "postgresql"]) def test_both_dialects_produce_usable_sql(self, dialect: str) -> None: - from audit_log.service import _distinct_stmt_for_dialect + from audit_log.entries.service import _distinct_stmt_for_dialect assert str(_distinct_stmt_for_dialect(DatabaseProvider(dialect))).strip() diff --git a/modules/audit_log/tests/test_entry_batches.py b/modules/audit_log/tests/entries/service/test_audit_log_entry_batches.py similarity index 94% rename from modules/audit_log/tests/test_entry_batches.py rename to modules/audit_log/tests/entries/service/test_audit_log_entry_batches.py index 18abfe404..44eedb0cd 100644 --- a/modules/audit_log/tests/test_entry_batches.py +++ b/modules/audit_log/tests/entries/service/test_audit_log_entry_batches.py @@ -11,10 +11,10 @@ from datetime import UTC, datetime -from audit_log.constants import ACTION_UPDATED -from audit_log.filters import EntryFilters +from audit_log.core.constants import ACTION_UPDATED +from audit_log.entries.filters import EntryFilters +from audit_log.entries.service import AuditLogService from audit_log.models import AuditEntry -from audit_log.service import AuditLogService from sqlalchemy.ext.asyncio import AsyncSession _ENTITY_TYPE = "Widget" diff --git a/modules/audit_log/tests/test_audit_log_api_filter_parity.py b/modules/audit_log/tests/web/endpoints/test_audit_log_api_filter_parity.py similarity index 97% rename from modules/audit_log/tests/test_audit_log_api_filter_parity.py rename to modules/audit_log/tests/web/endpoints/test_audit_log_api_filter_parity.py index 53b43ad6a..6c4c93332 100644 --- a/modules/audit_log/tests/test_audit_log_api_filter_parity.py +++ b/modules/audit_log/tests/web/endpoints/test_audit_log_api_filter_parity.py @@ -6,7 +6,7 @@ from datetime import UTC, datetime import pytest -from audit_log.constants import ACTION_UPDATED +from audit_log.core.constants import ACTION_UPDATED from audit_log.models import AuditEntry from users.models import User diff --git a/modules/audit_log/tests/test_audit_api_pagination_bounds.py b/modules/audit_log/tests/web/endpoints/test_audit_log_api_pagination_bounds.py similarity index 100% rename from modules/audit_log/tests/test_audit_api_pagination_bounds.py rename to modules/audit_log/tests/web/endpoints/test_audit_log_api_pagination_bounds.py diff --git a/modules/audit_log/tests/test_huge_page.py b/modules/audit_log/tests/web/endpoints/test_audit_log_huge_page.py similarity index 100% rename from modules/audit_log/tests/test_huge_page.py rename to modules/audit_log/tests/web/endpoints/test_audit_log_huge_page.py diff --git a/modules/audit_log/tests/test_audit_link_keys_match_audit_trail.py b/modules/audit_log/tests/web/endpoints/test_audit_log_link_keys_match_audit_trail.py similarity index 100% rename from modules/audit_log/tests/test_audit_link_keys_match_audit_trail.py rename to modules/audit_log/tests/web/endpoints/test_audit_log_link_keys_match_audit_trail.py diff --git a/modules/background_tasks/tests/test_bg_worker_audit.py b/modules/background_tasks/tests/test_bg_worker_audit.py index 6d14248e3..755376d2f 100644 --- a/modules/background_tasks/tests/test_bg_worker_audit.py +++ b/modules/background_tasks/tests/test_bg_worker_audit.py @@ -58,7 +58,7 @@ def _audit_rows() -> list[tuple[str, str, str]]: def test_the_audit_log_publishes_its_sink() -> None: - from audit_log.capture import audit_callback + from audit_log.core.capture import audit_callback assert discover_audit_callback() is audit_callback diff --git a/modules/keycloak/tests/identity/directory/test_keycloak_directory.py b/modules/keycloak/tests/identity/directory/test_keycloak_directory.py index db659350d..a80137f47 100644 --- a/modules/keycloak/tests/identity/directory/test_keycloak_directory.py +++ b/modules/keycloak/tests/identity/directory/test_keycloak_directory.py @@ -5,8 +5,8 @@ import uuid from datetime import UTC, datetime, timedelta +from audit_log.entries.resolve import actor_filter, actor_link, resolve_actors from audit_log.models import AuditEntry -from audit_log.resolve import actor_filter, actor_link, resolve_actors from keycloak.identity.directory import KeycloakDirectory from keycloak.models import KeycloakUserCache from simple_module_core.audit_links import AuditLink, AuditLinkRegistry