"""Manually grant software seats to specific people. Idempotent per (user, plan) — if the person already has an ``active`` seat on that plan we skip. Otherwise we create both the ``EmployeeRequest`` (request_type=software_seat, status=approved) and the active ``SoftwareSeatAllocation``, mirroring what the backfill script does. Edit ``GRANTS`` below and run with ``--commit``. Defaults to --dry-run. """ from __future__ import annotations import argparse import asyncio from datetime import UTC, datetime from uuid import uuid4 from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession # Register every model so SQLAlchemy mapper configuration succeeds end-to-end. import app.approval.models # noqa: F401 import app.audit.models # noqa: F401 import app.auth.models # noqa: F401 import app.domains.apikey.models # noqa: F401 import app.domains.asset.models # noqa: F401 import app.domains.cmdb.models # noqa: F401 import app.domains.email_group.models # noqa: F401 import app.domains.hr.models # noqa: F401 import app.domains.offboarding.models # noqa: F401 import app.domains.permission.models # noqa: F401 import app.domains.provisioning.models # noqa: F401 import app.identity.person_models # noqa: F401 from app.core.database import async_session_factory from app.domains.asset.models import ( AssetSoftwareCatalog, EmployeeRequest, SoftwareLicensePlan, SoftwareSeatAllocation, ) from app.identity.person_models import IdentityPerson # (work_email, software_name, plan_name) GRANTS: list[tuple[str, str, str]] = [ ("shuai.feng@pingcap.com", "Claude", "Premium"), ("shuai.feng@pingcap.com", "OPEN AI", "Team"), ("jie.ni@pingcap.com", "OPEN AI", "Team"), ] async def _find_plan( session: AsyncSession, software_name: str, plan_name: str, ) -> SoftwareLicensePlan: sw = (await session.execute( select(AssetSoftwareCatalog).where(AssetSoftwareCatalog.name == software_name) )).scalar_one() plans = (await session.execute( select(SoftwareLicensePlan).where(SoftwareLicensePlan.software_id == sw.id) )).scalars().all() for p in plans: if (p.name or "").strip() == plan_name: return p raise RuntimeError( f"Plan {plan_name!r} not found under {software_name!r} " f"(candidates: {[p.name for p in plans]})" ) async def run(*, dry_run: bool) -> None: now = datetime.now(UTC) async with async_session_factory() as session: for email, software_name, plan_name in GRANTS: person = (await session.execute( select(IdentityPerson).where(IdentityPerson.work_email == email) )).scalar_one_or_none() if person is None: print(f"SKIP {email:35} | person not found") continue plan = await _find_plan(session, software_name, plan_name) existing = (await session.execute( select(SoftwareSeatAllocation).where( and_( SoftwareSeatAllocation.plan_id == plan.id, SoftwareSeatAllocation.user_id == person.id, SoftwareSeatAllocation.status == "active", ) ) )).scalar_one_or_none() if existing is not None: print( f"SKIP {email:35} | {software_name}/{plan_name} — " f"already has active allocation {existing.id}" ) continue request_number = f"MANUAL-SEAT-{uuid4().hex[:10].upper()}" er = EmployeeRequest( request_number=request_number, request_type="software_seat", status="approved", requester_id=person.id, reason=f"Manual backfill ({software_name} {plan_name})", detail={ "source": "manual_backfill", "software_seat": { "software_id": str(plan.software_id), "software_name": software_name, "plan_id": str(plan.id), "plan_name": plan.name, }, }, processed_at=now, ) session.add(er) await session.flush() alloc = SoftwareSeatAllocation( plan_id=plan.id, user_id=person.id, status="active", reason=f"Manual backfill ({software_name} {plan_name})", allocated_at=now, ) session.add(alloc) await session.flush() print( f"GRANT {email:35} | {software_name}/{plan_name} " f"request_no={request_number} alloc_id={alloc.id}" ) if dry_run: await session.rollback() print("\n[DRY-RUN] Rolled back; no changes persisted.") else: await session.commit() print("\n[COMMIT] Changes persisted.") def _main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--commit", action="store_true", help="persist changes") args = ap.parse_args() asyncio.run(run(dry_run=not args.commit)) if __name__ == "__main__": _main()