app.services.strategy_definition_service module¶
Service for the persistent strategy model.
Implements the versioning rules for the persistent strategy model:
Creating uses
version=1andstatus=DRAFT.Editing a draft mutates the row in place.
Editing a published row forks
version + 1as a new draft instead of mutating the published copy.Publishing a draft transitions it to
PUBLISHEDand archives any sibling that was previously published, so a(realmId, name)family only ever has at most one live row.Archiving moves a row out of the active set without deleting history.
Tenancy is enforced at this layer: every read/write takes realmId
and we never accept it from the caller body - the endpoint resolves it
from the auth context and passes it in.
- class app.services.strategy_definition_service.RollbackResult(strategy, games_reassigned, tasks_reassigned)[source]¶
Bases:
objectOutcome of a rollback operation, returned by the service so the endpoint can include cascade counts in the audit log without re-hitting the DB.
- Parameters:
strategy (StrategyDefinitionRead)
games_reassigned (int)
tasks_reassigned (int)
- class app.services.strategy_definition_service.StrategyDefinitionService(strategy_definition_repository, game_repository=None, task_repository=None)[source]¶
Bases:
BaseServiceCRUD + lifecycle operations for custom strategies.
- Parameters:
strategy_definition_repository (StrategyDefinitionRepository)
- __init__(strategy_definition_repository, game_repository=None, task_repository=None)[source]¶
game_repositoryandtask_repositoryare optional so legacy call sites that only need CRUD/lifecycle (most tests, the simulation service) keep working without a wider DI graph. The The rollback flow requires both - when missing,rollback()raises a precise error rather than silently leaving cascade UPDATEs undone.- Parameters:
strategy_definition_repository (StrategyDefinitionRepository)
- Return type:
None
- async name_exists(*, realmId, name)[source]¶
Whether any version of
(realmId, name)already exists.Used by the import endpoint to decide whether the incoming bundle needs an auto-rename to avoid colliding with the
UNIQUE(realmId, name, version)constraint.- Parameters:
realmId (str | None)
name (str)
- Return type:
bool
- async list_strategies(*, realmId, status=None, type=None, limit=100)[source]¶
List strategy definitions for a realm, optionally filtered.
- Parameters:
realmId (Optional[str]) – Realm/tenant to scope to.
status (Optional[str]) – Optional lifecycle-status filter.
type (Optional[str]) – Optional strategy-type filter.
limit (int) – Maximum rows to return.
- Returns:
List[StrategyDefinitionRead] – The matching definitions.
- Return type:
List[StrategyDefinitionRead]
- async get_strategy(*, id, realmId)[source]¶
Fetch one strategy definition scoped to a realm.
- Parameters:
id (str) – Strategy definition identifier.
realmId (Optional[str]) – Realm/tenant the strategy must belong to.
- Returns:
StrategyDefinitionRead – The matching definition.
- Raises:
NotFoundError – If no matching strategy exists in the realm.
- Return type:
StrategyDefinitionRead
- async create(*, payload, realmId, createdBy, apiKey_used, oauth_user_id)[source]¶
Create a new strategy-definition draft.
Validates the type/parent combination and any provided AST, rejects a name that already exists in the realm, and persists the draft as version 1.
- Parameters:
payload (StrategyDefinitionCreate) – The strategy to create.
realmId (Optional[str]) – Realm/tenant that will own it.
createdBy (Optional[str]) – Identity recorded as the author.
apiKey_used (Optional[str]) – API key used for the request, if any.
oauth_user_id (Optional[str]) – OAuth subject, if any.
- Returns:
StrategyDefinitionRead – The newly created draft.
- Raises:
BadRequestError – If the type/parent combination is invalid.
DuplicatedError – If a strategy with the same name already exists.
- Return type:
StrategyDefinitionRead
- async update(*, id, payload, realmId, createdBy, apiKey_used, oauth_user_id)[source]¶
Apply an update.
On a DRAFT row: patch in place and return the same id.
On a PUBLISHED row: fork a new DRAFT at
version + 1with the patched fields applied, leaving the published row untouched so it keeps running until an explicit publish.On an ARCHIVED row: refuse - archived strategies are immutable.
- Parameters:
id (str)
payload (StrategyDefinitionUpdate)
realmId (str | None)
createdBy (str | None)
apiKey_used (str | None)
oauth_user_id (str | None)
- Return type:
StrategyDefinitionRead
- async publish(*, id, realmId)[source]¶
Publish a draft strategy, archiving any previously-published sibling.
Idempotent: re-publishing an already-published row is a no-op. If another version of the same name is published, it is archived first so only one published version exists per name.
- Parameters:
id (str) – Strategy definition identifier.
realmId (Optional[str]) – Realm/tenant the strategy must belong to.
- Returns:
StrategyDefinitionRead – The published strategy.
- Raises:
NotFoundError – If no matching strategy exists in the realm.
ConflictError – If the strategy is archived.
- Return type:
StrategyDefinitionRead
- async archive(*, id, realmId)[source]¶
Archive a strategy so it can no longer be published or assigned.
Idempotent: archiving an already-archived row returns it unchanged.
- Parameters:
id (str) – Strategy definition identifier.
realmId (Optional[str]) – Realm/tenant the strategy must belong to.
- Returns:
StrategyDefinitionRead – The archived strategy.
- Raises:
NotFoundError – If no matching strategy exists in the realm.
- Return type:
StrategyDefinitionRead
- async list_versions(*, id, realmId)[source]¶
Return every version in the family that contains
id, newest first. The caller passes a single id (typically the current published version) and we resolve the family name from it so the endpoint contract stays “one id in, full history out”.Tenant-scoped:
get_for_realm404s when the row belongs to another realm, so cross-tenant probing returns nothing.- Parameters:
id (str)
realmId (str | None)
- Return type:
List[StrategyDefinitionRead]
- async get_usage(*, id, realmId)[source]¶
Reverse lookup: which games/tasks are assigned to this exact strategy version.
Consumers store the assignable id
custom:<uuid>, so usage is an exact match on that string - the same value the rollback cascade rewrites. We report per-version (not per-family) because each published version is a distinct uuid that games point at individually; that matches what an admin needs to see before reassigning, archiving or rolling back this version.Tenant-scoped via
get_for_realm: a cross-realm id 404s. No cross-tenant leak through the usage lists either - a game can only be assigned a strategy validated to live in its own realm, so every consumer ofcustom:<uuid>shares the strategy’s realm.- Parameters:
id (str)
realmId (str | None)
- Return type:
StrategyUsageRead
- async rollback(*, id, target_version, realmId)[source]¶
Promote
target_versionback to PUBLISHED in the family that containsid, archive whichever version is currently published, and rewrite everyGames.strategyId/Tasks.strategyIdpointing at the displaced row so no consumer is left referencing an ARCHIVED row.iddoesn’t have to be PUBLISHED: an admin may initiate rollback from any version of the family (e.g. browsing the version history UI). We always treat the family’s current PUBLISHED row as the one to archive + reassign, regardless of which id was clicked.- Errors:
404 if
iddoesn’t resolve (or is in another realm).404 if
target_versionisn’t a version of that family.409 if the requested target is the row that’s already PUBLISHED - rolling back to the current state would be a no-op that falsely advertises a status change in the audit log.
- Parameters:
id (str)
target_version (int)
realmId (str | None)
- Return type: