Skip to main content

bursar.interface.memory module

In-memory credit store for testing and development.

class bursar.interface.memory.MemoryStore(clock: Callable[[], datetime] | None = None)

Bases: CreditStore

Credit store backed by in-memory dicts. Zero dependencies.

Useful for unit testing and local development without a database. All data is lost when the process exits.

Thread-safety (contract §3, C2): every mutating/reading method takes a single re-entrant lock so each emulated “transaction” — most importantly the read-modify-write inside deduct_with_allowance() — is atomic and cannot double-spend under concurrent callers. The lock is re-entrant so helpers can be called while held.

Args: : clock: Optional injectable clock (a zero-arg callable returning a : UTC-aware datetime), used everywhere the store would otherwise call datetime.now(UTC). Defaults to the real clock; tests pass a fake clock to fast-forward time (e.g. to simulate an allowance billing-period rollover) without any wall-clock sleep (WS9f).

activate_pricing(version: int) → str

Activate a specific pricing version (deactivates all others).

Args: : version: The version number to activate.

Returns: : The activated config id.

add_credits(user_id: str, amount: Decimal, type: str = 'adjustment', metadata: CreditMetadata | None = None, expires_at: datetime | None = None, tier: str | None = None, idempotency_key: str | None = None) → AddCreditsResult

Atomically add credits and log a transaction.

Args: : amount: Fractional credit amount (Decimal). expires_at: Optional datetime after which the credits expire. tier: Optional tier key to grant into. When no tiers are

configured, must be None or "default". When tiers are configured and omitted, resolves to the tier with is_default=True (raises if none is marked default).


idempotency_key: Optional user-scoped replay key. A retried grant : with the same key (e.g. a webhook redelivered by the sender) returns the original transaction’s result rather than granting a second time — no double-mutation, no second ledger row. Follows the same replay idiom already used by deduct_with_allowance()/settle_lease()/ deduct_team().

add_team_member(team_id: str, user_id: str, role: str = 'member', spend_cap: Decimal | None = None) → AddTeamMemberResult

Add a user to a team.

Args: : team_id: The team’s UUID. user_id: The user’s UUID. role: Member role (e.g. “member”, “admin”). spend_cap: Optional per-user spend cap.

Returns: : AddTeamMemberResult confirming membership.

aggregate_stats(start: datetime, end: datetime) → AggregateStatsRow

Aggregate statistics across all users in a time window.

check_allowance(user_id: str, period_start: date | None = None) → AllowanceResult

Get remaining free allowance for current billing period.

period_start overrides the window key for rolling_30d/anniversary plans (resolved by the manager via bursar.allowance.resolve_allowance_window()); None keeps the calendar-month default (WS9).

check_feature_limit(user_id: str, feature: str, max_calls: int, period_start: date, period_end: date) → FeatureLimitResult

Advisory, non-locking read of invocation-count usage (UI only).

Mirrors check_spend_cap()/check_allowance(): the caller (the manager) has already resolved the FeatureLimit from the user’s plan and the calendar window via bursar.allowance.resolve_calendar_window() — this method only counts. Counting is ledger-derived (see deduct_with_allowance()): the number of committed usage transactions with metadata.feature == feature in [period_start, period_end). Never used for admission control.

Args: : user_id: The user to check. feature: The feature name. max_calls: The configured limit (FeatureLimit.max_calls). period_start: Inclusive window start (resolved by the caller). period_end: Exclusive window end (resolved by the caller).

Returns: : FeatureLimitResult with limited=True and the count/remaining.

check_spend_cap(user_id: str, model: str | None = None, amount: Decimal | None = None) → CapCheckResult

Check whether a pending deduction would exceed any configured cap.

Args: : user_id: The user to check caps for. model: Optional model name for per-model caps. amount: The pending deduction amount.

Returns: : CapCheckResult with the check result.

create_lease(user_id: str, amount: Decimal, operation_type: str, , billing_mode: str = 'strict', floor: Decimal = Decimal('0'), max_concurrent: int | None = None, ttl_seconds: int = 600, model: str | None = None, overdraft_floor: Decimal | None = None, metadata: CreditMetadata | None = None, period_start: date | None = None, feature: str | None = None, feature_limit: FeatureLimit | None = None, feature_period_start: date | None = None) → LeaseResult

Atomically acquire a lease (hold) — the only admission control (D4).

Under one lock the store: (1) ensures the balance row exists; (2) enforces max_concurrent by counting active leases for (user_id, operation_type); (3) enforces deny spend caps for amount (admission gate); (3b) enforces a deny feature_limit for feature — deny-only at admission, exactly like spend caps: the same ledger-derived count as deduct_with_allowance() (counts prior committed usage transactions tagged metadata.feature == feature in the window; warn/notify are NOT checked here, mirroring how admission only ever enforces deny spend caps); (4) computes available = balance − Σ active holds and rejects with error="insufficient_credits" if available − amount < floor; (5) inserts an active lease expiring after ttl_seconds. No lease-record change is needed for feature limits: since create_lease never inserts a usage transaction (only settle_lease() does), a lease that is later released instead of settled was never counted in the first place — nothing to undo.

floor is the resolved admission floor (>= 0 for strict; the negative overdraft_floor for overdraft). billing_mode/overdraft_floor are persisted on the lease for settle-time/observability. Business failures are returned via LeaseResult.error (including "feature_limit_reached"); the store never raises domain exceptions.

period_start (WS9) keys the allowance-headroom lookup for rolling_30d/anniversary plans; None falls back to the current UTC calendar month. feature/feature_limit/feature_period_start are resolved by the manager exactly as in deduct_with_allowance(); feature_limit=None skips enforcement.

create_team(name: str, initial_balance: Decimal = Decimal('0')) → CreateTeamResult

Create a team with a shared credit balance pool.

Args: : name: Human-readable team name. initial_balance: Starting credit balance.

Returns: : CreateTeamResult with the new team id.

daily_spend(start: datetime, end: datetime) → list[DailySpendRow]

Daily spend aggregation in a time window.

deduct_team(team_id: str, user_id: str, amount: Decimal, metadata: CreditMetadata | None = None, idempotency_key: str | None = None) → TeamDeductionResult

Deduct credits from a team pool, attributed to a user.

Args: : team_id: The team’s UUID. user_id: The user to attribute the deduction to. amount: Credits to deduct (Decimal). metadata: Extra metadata. idempotency_key: Optional replay key. A retried team deduction with

the same key returns the original result rather than charging the shared pool again (contract §2/H12).

Returns: : TeamDeductionResult with transaction details.

deduct_with_allowance(user_id: str, amount: Decimal, , idempotency_key: str | None = None, min_balance: Decimal = Decimal('0'), model: str | None = None, metadata: CreditMetadata | None = None, skip_allowance: bool = False, period_start: date | None = None, feature: str | None = None, feature_limit: FeatureLimit | None = None, feature_period_start: date | None = None) → DeductionResult

Atomic calculate-then-charge under the store lock (contract §2).

Mirrors deduct_with_allowance in 009_deduct_and_leases.sql: the entire pipeline (idempotency replay → allowance consume → cap deny on net → feature-limit deny → balance floor → debit → ledger insert) runs while holding self._lock so it is all-or-nothing and cannot double-spend under threads.

period_start (WS9) keys allowance consumption to a specific rolling_30d/anniversary window; None resolves the window from the user’s plan (falling back to the current UTC calendar month).

feature/feature_limit/feature_period_start enforce a per-feature invocation-count limit (see the check_feature_limit docstring for the ledger-derived counting model); feature_limit=None skips enforcement entirely (the transaction is still tagged with feature when given).

get_active_pricing() → PricingConfigResult | None

Fetch the active pricing configuration from the store.

get_available(user_id: str) → AvailableResult

Advisory, non-locking read of available = balance − Σ active holds.

For UI only — never an admission gate (D4/H3); the value may be stale the instant it is read.

get_balance(user_id: str) → BalanceResult

Return current balance and lifetime purchased amount.

get_credit_tiers(user_id: str) → TierBalancesResult

Return per-tier balance breakdown for a user, ordered by priority ascending.

When no tiers are configured, returns a single synthetic "default" tier entry so the shape is uniform regardless of whether tiers are configured.

get_pricing_config(version: int) → PricingConfigResult | None

Fetch a specific pricing config by version number.

get_pricing_history() → list[PricingConfigHistoryItem]

List all pricing config versions (newest first).

get_team_balance(team_id: str) → TeamBalanceResult

Fetch team balance and member count.

Args: : team_id: The team’s UUID.

Returns: : TeamBalanceResult with balance and member count.

get_team_members(team_id: str) → list[TeamMember]

List a team’s members.

total_spent is the SAME monthly-windowed team_usage spend that deduct_team enforces the per-user cap against (contract §3 / M2): a single source of truth, reset monthly, attributed via metadata team_id.

get_user_plan(user_id: str) → GetUserPlanResult

Fetch user’s current plan (including feature entitlements).

increment_usage_window(user_id: str, plan_id: str, amount: Decimal) → None

Record allowance consumption for current billing period.

list_user_transactions(user_id: str, types: list[str] | None = None, from_date: datetime | None = None, to_date: datetime | None = None, limit: int = 50, offset: int = 0) → list[TransactionRow]

List credit transactions for a user with pagination.

Args: : user_id: The user to query. types: Optional filter by transaction types (e.g. [“usage”]). from_date: Optional start of date range (inclusive). to_date: Optional end of date range (inclusive). limit: Maximum rows to return (default 50). offset: Number of rows to skip (default 0).

Returns: : List of TransactionRow objects. Each row includes total_count representing the total matching rows before pagination.

refund_credits(transaction_id: str, amount: Decimal | None = None, reason: str | None = None, metadata: CreditMetadata | None = None) → RefundResult

Refund a previous credit deduction.

Args: : transaction_id: The transaction to refund. amount: Optional partial refund amount. Full refund if omitted. reason: Optional reason for the refund. metadata: Extra metadata to attach to the refund transaction.

Returns: : RefundResult with the refund transaction details, or error set if the transaction doesn’t exist or is already refunded.

release_lease(user_id: str, lease_id: str) → ReleaseResult

Release a lease without charging (work failed/aborted).

Idempotent and safe on missing/already-finalized leases (resolves H1): transitions an active/expired lease to released and reports released=True; otherwise reports released=False with a reason.

renew_lease(user_id: str, lease_id: str, ttl_seconds: int) → LeaseResult

Extend an active lease’s TTL (long batch/agentic jobs, resolves B4).

Returns error="lease_expired" if the TTL already elapsed and error="lease_not_found" if missing/other-user/finalized.

set_active_pricing(config: PricingConfigData, label: str | None = None) → str

Publish a new pricing configuration.

Deactivates the previous active config and inserts a new one. Returns the new config id.

set_spend_cap(cap: SpendCap) → None

Configure a spend cap (MemoryStore-only helper for testing).

set_user_plan(user_id: str, plan_id: str) → SetUserPlanResult

Assign a plan to a user.

settle_lease(user_id: str, lease_id: str, amount: Decimal, , idempotency_key: str | None = None, min_balance: Decimal = Decimal('0'), model: str | None = None, metadata: CreditMetadata | None = None, skip_allowance: bool = False, period_start: date | None = None, feature: str | None = None, feature_limit: FeatureLimit | None = None, feature_period_start: date | None = None) → DeductionResult

Charge the actual cost against a lease, then mark it settled (D5).

De-clamped: charges amount even if it exceeds the lease hold (overdraft), never clamps to the lease amount. Pipeline: idempotency replay → allowance consume (skipped when skip_allowance=True, Fix 7) → spend-cap (advisory at settle: a breach sets cap_warning but never blocks) → feature-limit (advisory at settle, same reasoning: the work already happened, so a breach only sets feature_limit_warning and never blocks) → debit (no floor block; balance may go negative in overdraft) → ledger row (tagged metadata.feature when feature is given — this is what makes the call countable for future checks) → mark lease settled. amount == 0 releases the lease without charging (and does not tag/count anything).

skip_allowance=True prevents the free inference allowance from being consumed at settle time — mirrors deduct_with_allowance() Fix 7 so the lease path and the direct-deduct path behave consistently.

period_start (WS9) keys allowance consumption for rolling_30d/anniversary plans; None falls back to the current UTC calendar month. feature/feature_limit/feature_period_start mirror deduct_with_allowance() — the caller (manager) re-supplies feature at settle time exactly as it already re-supplies model for per-model spend-cap accuracy at settle (no feature name is persisted on the lease itself).

Lease-state failures are returned via DeductionResult.error: lease_not_found (missing / other user / released), lease_expired (TTL elapsed — call renew_lease() for long jobs). A replayed settle (same idempotency key, or a re-settle of an already-settled lease) returns the original result with idempotent=True.

setup(database_url: str | None = None) → SetupResult

Run bundled SQL migrations (tables, indexes, RPCs).

Idempotent — safe to call on every deploy.

Args: : database_url: Postgres connection string. Required for stores : that manage schema setup directly (HttpxSupabaseStore, PostgresStore). Ignored by in-memory stores.

spend_by_model(start: datetime, end: datetime) → list[SpendByModelRow]

Aggregate spend by model in a time window.

spend_by_user(start: datetime, end: datetime) → list[SpendByUserRow]

Aggregate spend by user in a time window.

sweep_expired_credits(dry_run: bool = False, user_id: str | None = None) → SweepResult

Sweep expired credits from all users’ balances, or a single user’s.

Swept grants are marked with swept_at (H4) so a second sweep reports zero and never double-debits — parity with the SQL expire_credits.

Grouping is per (user_id, tier_key) (not just per-user): each grant’s tier is read from metadata["tier"] (defaulting to "default" for pre-existing transactions written before tiers existed) — a tier’s expires flag is only consulted at add_credits time, so a transaction’s fate is fixed regardless of later config changes. The existing clamp (never expire more than what’s left) is unchanged, just re-scoped to the tier’s own balance instead of the user’s aggregate.

user_id (lazy per-user expiry): when given, the scan is restricted to that user’s own transactions only — other users’ expired grants are left completely untouched (they still show up in a later global or per-user sweep). user_id=None (the default) preserves the exact prior global-sweep behavior/output shape.

top_users(limit: int, start: datetime, end: datetime) → list[TopUserRow]

Top users by spend in a time window.