interloper-api 0.58.0__tar.gz → 0.59.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of interloper-api might be problematic. Click here for more details.
- {interloper_api-0.58.0 → interloper_api-0.59.0}/PKG-INFO +1 -1
- {interloper_api-0.58.0 → interloper_api-0.59.0}/pyproject.toml +1 -1
- {interloper_api-0.58.0 → interloper_api-0.59.0}/pyproject.toml.orig +1 -1
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/admin.py +87 -6
- {interloper_api-0.58.0 → interloper_api-0.59.0}/README.md +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/__init__.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/app.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/components.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/dependencies.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/email.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/__init__.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/agent.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/auth.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/backfills.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/catalog.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/components.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/oauth.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/organisations.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/runs.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/tokens.py +0 -0
- {interloper_api-0.58.0 → interloper_api-0.59.0}/src/interloper_api/routes/ws.py +0 -0
|
@@ -19,7 +19,7 @@ from uuid import UUID
|
|
|
19
19
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
20
20
|
from interloper_db import Organisation, Profile, Store
|
|
21
21
|
from interloper_db.store.quotas import METRIC_SUCCESSFUL_RUNS
|
|
22
|
-
from pydantic import BaseModel
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
23
|
|
|
24
24
|
from interloper_api.dependencies import get_admin_config, get_quota_defaults, get_store, require_super_admin
|
|
25
25
|
from interloper_api.email import send_invite_email
|
|
@@ -41,6 +41,9 @@ class AdminOrganisationResponse(BaseModel):
|
|
|
41
41
|
name: str
|
|
42
42
|
member_count: int
|
|
43
43
|
created_at: datetime | None = None
|
|
44
|
+
#: Soft-deleted orgs stay listed (their retained history and ledger are
|
|
45
|
+
#: still attributable) but are no longer manageable.
|
|
46
|
+
deleted_at: datetime | None = None
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
class AdminUserOrganisation(BaseModel):
|
|
@@ -238,6 +241,8 @@ class AdminOrgQuotaStatus(BaseModel):
|
|
|
238
241
|
|
|
239
242
|
id: UUID
|
|
240
243
|
name: str
|
|
244
|
+
#: Soft-deleted orgs keep their ledger visible but are read-only.
|
|
245
|
+
deleted_at: datetime | None = None
|
|
241
246
|
limits: AdminQuotaLimits
|
|
242
247
|
effective: AdminQuotaLimits
|
|
243
248
|
sources: int
|
|
@@ -257,6 +262,23 @@ class AdminQuotasResponse(BaseModel):
|
|
|
257
262
|
organisations: list[AdminOrgQuotaStatus]
|
|
258
263
|
|
|
259
264
|
|
|
265
|
+
class AdminQuotaUpdateRequest(BaseModel):
|
|
266
|
+
"""Per-org quota overrides. Omitted fields keep their value; null clears one."""
|
|
267
|
+
|
|
268
|
+
max_sources: int | None = Field(default=None, ge=0)
|
|
269
|
+
max_assets_per_source: int | None = Field(default=None, ge=0)
|
|
270
|
+
max_successful_runs_per_month: int | None = Field(default=None, ge=0)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class AdminActivityEntry(BaseModel):
|
|
274
|
+
"""One event in an organisation's derived activity feed."""
|
|
275
|
+
|
|
276
|
+
kind: str
|
|
277
|
+
when: datetime
|
|
278
|
+
title: str
|
|
279
|
+
detail: str | None = None
|
|
280
|
+
|
|
281
|
+
|
|
260
282
|
class AdminConfigResponse(BaseModel):
|
|
261
283
|
"""Read-only, secrets-redacted snapshot of the instance configuration."""
|
|
262
284
|
|
|
@@ -443,7 +465,9 @@ def build_config_snapshot(settings: Any, features: dict[str, bool], catalog: Any
|
|
|
443
465
|
|
|
444
466
|
|
|
445
467
|
def _quota_limits(limits: Any) -> AdminQuotaLimits:
|
|
446
|
-
"""Map
|
|
468
|
+
"""Map limits from a ``{key: limit}`` dict (store) or attributes (settings)."""
|
|
469
|
+
if isinstance(limits, dict):
|
|
470
|
+
return AdminQuotaLimits(**{key: limits.get(key) for key in AdminQuotaLimits.model_fields})
|
|
447
471
|
return AdminQuotaLimits(
|
|
448
472
|
max_sources=getattr(limits, "max_sources", None),
|
|
449
473
|
max_assets_per_source=getattr(limits, "max_assets_per_source", None),
|
|
@@ -540,7 +564,7 @@ def get_quotas(
|
|
|
540
564
|
"""Quota limits and current-period usage for every organisation."""
|
|
541
565
|
defaults = _quota_limits(quota_defaults)
|
|
542
566
|
period_start = store.current_period_start()
|
|
543
|
-
overrides =
|
|
567
|
+
overrides = store.list_quota_overrides()
|
|
544
568
|
usage = {
|
|
545
569
|
row.org_id: row
|
|
546
570
|
for row in store.list_usage(period_start=period_start)
|
|
@@ -552,12 +576,13 @@ def get_quotas(
|
|
|
552
576
|
|
|
553
577
|
organisations = []
|
|
554
578
|
for org, _member_count in store.list_all_organisations():
|
|
555
|
-
limits = _quota_limits(overrides.get(org.id))
|
|
579
|
+
limits = _quota_limits(overrides.get(org.id, {}))
|
|
556
580
|
org_usage = usage.get(org.id)
|
|
557
581
|
organisations.append(
|
|
558
582
|
AdminOrgQuotaStatus(
|
|
559
583
|
id=org.id,
|
|
560
584
|
name=org.name,
|
|
585
|
+
deleted_at=org.deleted_at,
|
|
561
586
|
limits=limits,
|
|
562
587
|
effective=_effective_limits(limits, defaults),
|
|
563
588
|
sources=sources.get(org.id, 0),
|
|
@@ -570,6 +595,57 @@ def get_quotas(
|
|
|
570
595
|
return AdminQuotasResponse(period_start=period_start, defaults=defaults, organisations=organisations)
|
|
571
596
|
|
|
572
597
|
|
|
598
|
+
def _activity_title(entry: dict[str, Any]) -> tuple[str, str | None]:
|
|
599
|
+
"""Presentation strings for a derived activity entry."""
|
|
600
|
+
kind, subject, extra = entry["kind"], entry["subject"], entry["extra"]
|
|
601
|
+
if kind == "org_created":
|
|
602
|
+
return "Organisation created", None
|
|
603
|
+
if kind == "org_deleted":
|
|
604
|
+
return "Organisation deleted", "Retained read-only for billing history."
|
|
605
|
+
if kind == "member_joined":
|
|
606
|
+
return f"{subject} joined the organisation", f"Role: {extra}" if extra else None
|
|
607
|
+
if kind == "invitation_sent":
|
|
608
|
+
return f"Invitation sent to {subject}", f"Invited by {extra}" if extra else None
|
|
609
|
+
if kind == "source_added":
|
|
610
|
+
return f"Source added — {subject}", None
|
|
611
|
+
if kind == "runs_completed":
|
|
612
|
+
count = int(subject)
|
|
613
|
+
return f"{count:,} run{'' if count == 1 else 's'} completed successfully", None
|
|
614
|
+
return kind, None
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@router.get("/organisations/{org_id}/activity")
|
|
618
|
+
def get_organisation_activity(
|
|
619
|
+
org_id: UUID,
|
|
620
|
+
user: Profile = Depends(require_super_admin),
|
|
621
|
+
store: Store = Depends(get_store),
|
|
622
|
+
) -> list[AdminActivityEntry]:
|
|
623
|
+
"""Derived activity feed for one organisation, newest first.
|
|
624
|
+
|
|
625
|
+
Composed from existing records (memberships, pending invitations,
|
|
626
|
+
sources, run aggregates, the org row itself) — there is no audit
|
|
627
|
+
trail, so actor attribution is limited to what those rows carry.
|
|
628
|
+
"""
|
|
629
|
+
entries = []
|
|
630
|
+
for entry in store.list_organisation_activity(org_id):
|
|
631
|
+
title, detail = _activity_title(entry)
|
|
632
|
+
entries.append(AdminActivityEntry(kind=entry["kind"], when=entry["when"], title=title, detail=detail))
|
|
633
|
+
return entries
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
@router.patch("/organisations/{org_id}/quota")
|
|
637
|
+
def update_org_quota(
|
|
638
|
+
org_id: UUID,
|
|
639
|
+
body: AdminQuotaUpdateRequest,
|
|
640
|
+
user: Profile = Depends(require_super_admin),
|
|
641
|
+
store: Store = Depends(get_store),
|
|
642
|
+
) -> AdminQuotaLimits:
|
|
643
|
+
"""Set an organisation's quota overrides; omitted fields keep their value, null clears one."""
|
|
644
|
+
_require_org(store, org_id)
|
|
645
|
+
overrides = store.set_quota(org_id, body.model_dump(exclude_unset=True))
|
|
646
|
+
return _quota_limits(overrides)
|
|
647
|
+
|
|
648
|
+
|
|
573
649
|
# -- Users --------------------------------------------------------------------
|
|
574
650
|
|
|
575
651
|
|
|
@@ -614,13 +690,14 @@ def list_all_organisations(
|
|
|
614
690
|
user: Profile = Depends(require_super_admin),
|
|
615
691
|
store: Store = Depends(get_store),
|
|
616
692
|
) -> list[AdminOrganisationResponse]:
|
|
617
|
-
"""List every organisation with its member count."""
|
|
693
|
+
"""List every organisation with its member count, soft-deleted ones included."""
|
|
618
694
|
return [
|
|
619
695
|
AdminOrganisationResponse(
|
|
620
696
|
id=org.id,
|
|
621
697
|
name=org.name,
|
|
622
698
|
member_count=count,
|
|
623
699
|
created_at=org.created_at,
|
|
700
|
+
deleted_at=org.deleted_at,
|
|
624
701
|
)
|
|
625
702
|
for org, count in store.list_all_organisations()
|
|
626
703
|
]
|
|
@@ -667,7 +744,11 @@ def delete_organisation(
|
|
|
667
744
|
user: Profile = Depends(require_super_admin),
|
|
668
745
|
store: Store = Depends(get_store),
|
|
669
746
|
) -> dict[str, str]:
|
|
670
|
-
"""
|
|
747
|
+
"""Soft-delete an organisation: purges its data, keeps execution history and the usage ledger.
|
|
748
|
+
|
|
749
|
+
The body must repeat the exact name. Deleted organisations stay in the
|
|
750
|
+
list (read-only) and read as missing everywhere else.
|
|
751
|
+
"""
|
|
671
752
|
org = _require_org(store, org_id)
|
|
672
753
|
if body.name != org.name:
|
|
673
754
|
raise HTTPException(status_code=400, detail="Organisation name does not match")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|