108 lines
3.9 KiB
Python
108 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from typing import Any, Mapping
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_core.tenancy.scope import Tenant
|
|
|
|
|
|
DEFAULT_SYNCHRONOUS_SEND_MAX_RECIPIENT_JOBS = 25
|
|
ABSOLUTE_SYNCHRONOUS_SEND_MAX_RECIPIENT_JOBS = 500
|
|
SYNCHRONOUS_SEND_MAX_ENV = "GOVOPLAN_CAMPAIGN_SYNCHRONOUS_SEND_MAX_RECIPIENTS"
|
|
CAMPAIGN_DELIVERY_POLICY_SETTINGS_KEY = "campaign_delivery_policy"
|
|
SYNCHRONOUS_SEND_MAX_SETTINGS_KEY = "synchronous_send_max_recipients"
|
|
|
|
|
|
class CampaignDeliveryPolicyError(RuntimeError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SynchronousSendPolicy:
|
|
max_recipient_jobs: int
|
|
source: str
|
|
deployment_max_recipient_jobs: int
|
|
tenant_max_recipient_jobs: int | None = None
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"max_recipient_jobs": self.max_recipient_jobs,
|
|
"source": self.source,
|
|
"deployment_max_recipient_jobs": self.deployment_max_recipient_jobs,
|
|
"tenant_max_recipient_jobs": self.tenant_max_recipient_jobs,
|
|
"deployment_setting": SYNCHRONOUS_SEND_MAX_ENV,
|
|
"tenant_setting": (
|
|
f"tenant.settings.{CAMPAIGN_DELIVERY_POLICY_SETTINGS_KEY}."
|
|
f"{SYNCHRONOUS_SEND_MAX_SETTINGS_KEY}"
|
|
),
|
|
}
|
|
|
|
|
|
def effective_synchronous_send_policy(
|
|
session: Session,
|
|
*,
|
|
tenant_id: str,
|
|
environ: Mapping[str, str] | None = None,
|
|
) -> SynchronousSendPolicy:
|
|
env = os.environ if environ is None else environ
|
|
deployment_value = _configured_limit(
|
|
env.get(SYNCHRONOUS_SEND_MAX_ENV),
|
|
source=SYNCHRONOUS_SEND_MAX_ENV,
|
|
default=DEFAULT_SYNCHRONOUS_SEND_MAX_RECIPIENT_JOBS,
|
|
)
|
|
tenant = session.get(Tenant, tenant_id)
|
|
tenant_raw = _tenant_limit_value(tenant.settings if tenant is not None else None)
|
|
if tenant_raw is None:
|
|
return SynchronousSendPolicy(
|
|
max_recipient_jobs=deployment_value,
|
|
source=("deployment" if env.get(SYNCHRONOUS_SEND_MAX_ENV) not in (None, "") else "deployment_default"),
|
|
deployment_max_recipient_jobs=deployment_value,
|
|
)
|
|
|
|
tenant_value = _configured_limit(
|
|
tenant_raw,
|
|
source=(
|
|
f"tenant.settings.{CAMPAIGN_DELIVERY_POLICY_SETTINGS_KEY}."
|
|
f"{SYNCHRONOUS_SEND_MAX_SETTINGS_KEY}"
|
|
),
|
|
)
|
|
effective_value = min(deployment_value, tenant_value)
|
|
return SynchronousSendPolicy(
|
|
max_recipient_jobs=effective_value,
|
|
source="tenant" if tenant_value <= deployment_value else "deployment_ceiling",
|
|
deployment_max_recipient_jobs=deployment_value,
|
|
tenant_max_recipient_jobs=tenant_value,
|
|
)
|
|
|
|
|
|
def _tenant_limit_value(settings: Mapping[str, Any] | None) -> object | None:
|
|
if not isinstance(settings, Mapping):
|
|
return None
|
|
policy = settings.get(CAMPAIGN_DELIVERY_POLICY_SETTINGS_KEY)
|
|
if not isinstance(policy, Mapping):
|
|
return None
|
|
return policy.get(SYNCHRONOUS_SEND_MAX_SETTINGS_KEY)
|
|
|
|
|
|
def _configured_limit(value: object, *, source: str, default: int | None = None) -> int:
|
|
if value is None or (isinstance(value, str) and not value.strip()):
|
|
if default is not None:
|
|
return default
|
|
raise CampaignDeliveryPolicyError(f"{source} must be configured as an integer")
|
|
if isinstance(value, bool):
|
|
raise CampaignDeliveryPolicyError(f"{source} must be an integer, not a boolean")
|
|
try:
|
|
parsed = int(value)
|
|
except (TypeError, ValueError) as exc:
|
|
raise CampaignDeliveryPolicyError(f"{source} must be an integer") from exc
|
|
if str(parsed) != str(value).strip() and not isinstance(value, int):
|
|
raise CampaignDeliveryPolicyError(f"{source} must be an integer")
|
|
if parsed < 0 or parsed > ABSOLUTE_SYNCHRONOUS_SEND_MAX_RECIPIENT_JOBS:
|
|
raise CampaignDeliveryPolicyError(
|
|
f"{source} must be between 0 and {ABSOLUTE_SYNCHRONOUS_SEND_MAX_RECIPIENT_JOBS}"
|
|
)
|
|
return parsed
|