Add IDM assignment lifecycle worker contract
This commit is contained in:
@@ -19,6 +19,10 @@ from govoplan_core.core.events import (
|
||||
PlatformEventOutbox,
|
||||
publish_platform_event,
|
||||
)
|
||||
from govoplan_core.core.idm import (
|
||||
CAPABILITY_IDM_ASSIGNMENT_LIFECYCLE,
|
||||
IdmAssignmentLifecycle,
|
||||
)
|
||||
from govoplan_core.core.module_management import load_startup_enabled_modules, startup_candidate_module_ids
|
||||
from govoplan_core.core.mail import CAPABILITY_MAIL_DELIVERY_OUTBOX, MailDeliveryOutboxProvider
|
||||
from govoplan_core.core.modules import ModuleContext
|
||||
@@ -62,6 +66,7 @@ celery.conf.update(
|
||||
"govoplan.postbox.dispatch_routes": {"queue": "postbox"},
|
||||
"govoplan.events.dispatch_outbox": {"queue": "events"},
|
||||
"govoplan.events.purge_outbox": {"queue": "events"},
|
||||
"govoplan.idm.expire_assignments": {"queue": "idm"},
|
||||
},
|
||||
worker_prefetch_multiplier=1,
|
||||
task_acks_late=True,
|
||||
@@ -117,6 +122,11 @@ celery.conf.update(
|
||||
"schedule": 24 * 60 * 60.0,
|
||||
"args": (500,),
|
||||
},
|
||||
"idm-assignment-expiry-every-minute": {
|
||||
"task": "govoplan.idm.expire_assignments",
|
||||
"schedule": 60.0,
|
||||
"args": (None, 100),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -238,6 +248,20 @@ def _platform_event_outbox(
|
||||
return capability
|
||||
|
||||
|
||||
def _idm_assignment_lifecycle(
|
||||
registry: PlatformRegistry | None = None,
|
||||
) -> IdmAssignmentLifecycle | None:
|
||||
registry = registry or _platform_registry()
|
||||
if not registry.has_capability(CAPABILITY_IDM_ASSIGNMENT_LIFECYCLE):
|
||||
return None
|
||||
capability = registry.require_capability(
|
||||
CAPABILITY_IDM_ASSIGNMENT_LIFECYCLE
|
||||
)
|
||||
if not isinstance(capability, IdmAssignmentLifecycle):
|
||||
raise RuntimeError("IDM assignment lifecycle capability is invalid")
|
||||
return capability
|
||||
|
||||
|
||||
@celery.task(name="govoplan.campaigns.send_email", bind=True, max_retries=0)
|
||||
def send_email(self, job_id: str):
|
||||
"""Send one explicitly queued campaign job.
|
||||
@@ -485,6 +509,35 @@ def dispatch_postbox_routes(
|
||||
return result
|
||||
|
||||
|
||||
@celery.task(
|
||||
name="govoplan.idm.expire_assignments",
|
||||
bind=True,
|
||||
max_retries=0,
|
||||
)
|
||||
def expire_idm_assignments(
|
||||
self,
|
||||
tenant_id: str | None = None,
|
||||
limit: int = 100,
|
||||
):
|
||||
"""Emit idempotent lifecycle events for elapsed IDM assignments."""
|
||||
|
||||
from govoplan_core.db.session import get_database
|
||||
|
||||
with get_database().SessionLocal() as session:
|
||||
provider = _idm_assignment_lifecycle()
|
||||
if provider is None:
|
||||
return {"selected": 0, "expired": 0, "assignment_ids": []}
|
||||
result = dict(
|
||||
provider.process_expired(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
limit=limit,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
return result
|
||||
|
||||
|
||||
@celery.task(
|
||||
name="govoplan.events.dispatch_outbox",
|
||||
bind=True,
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Literal, Protocol, runtime_checkable
|
||||
IDM_MODULE_ID = "idm"
|
||||
CAPABILITY_IDM_DIRECTORY = "idm.directory"
|
||||
CAPABILITY_IDM_FUNCTION_ASSIGNMENTS = "idm.function_assignments"
|
||||
CAPABILITY_IDM_ASSIGNMENT_LIFECYCLE = "idm.assignmentLifecycle"
|
||||
|
||||
IdmStatus = Literal["active", "inactive", "suspended"]
|
||||
OrganizationFunctionAssignmentSource = Literal["direct", "delegated", "acting_for", "directory", "governance", "system"]
|
||||
@@ -106,3 +107,36 @@ class IdmFunctionAssignmentDirectory(Protocol):
|
||||
effective_at: datetime | None = None,
|
||||
) -> Mapping[str, OrganizationFunctionIncumbencyRef]:
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class IdmAssignmentLifecycle(Protocol):
|
||||
"""Worker boundary for time-driven function-assignment transitions."""
|
||||
|
||||
def process_expired(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
effective_at: datetime | None = None,
|
||||
limit: int = 100,
|
||||
) -> Mapping[str, object]:
|
||||
...
|
||||
|
||||
|
||||
def idm_assignment_lifecycle(
|
||||
registry: object | None,
|
||||
) -> IdmAssignmentLifecycle | None:
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not hasattr(registry, "capability")
|
||||
or not registry.has_capability(CAPABILITY_IDM_ASSIGNMENT_LIFECYCLE)
|
||||
):
|
||||
return None
|
||||
capability = registry.capability(CAPABILITY_IDM_ASSIGNMENT_LIFECYCLE)
|
||||
return (
|
||||
capability
|
||||
if isinstance(capability, IdmAssignmentLifecycle)
|
||||
else None
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user