interloper-api 0.77.0__tar.gz → 0.78.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.
Files changed (25) hide show
  1. {interloper_api-0.77.0 → interloper_api-0.78.0}/PKG-INFO +1 -1
  2. {interloper_api-0.77.0 → interloper_api-0.78.0}/pyproject.toml +4 -2
  3. {interloper_api-0.77.0 → interloper_api-0.78.0}/pyproject.toml.orig +3 -3
  4. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/dependencies/__init__.py +22 -0
  5. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/dependencies/auth.py +7 -0
  6. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/dependencies/rbac.py +9 -1
  7. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/dependencies/state.py +11 -1
  8. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/admin.py +42 -38
  9. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/agent.py +19 -14
  10. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/auth.py +24 -19
  11. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/backfills.py +14 -15
  12. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/catalog.py +7 -8
  13. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/components.py +97 -87
  14. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/oauth.py +3 -4
  15. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/organisations.py +32 -27
  16. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/runs.py +38 -19
  17. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/tokens.py +15 -11
  18. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/websocket.py +2 -1
  19. {interloper_api-0.77.0 → interloper_api-0.78.0}/README.md +0 -0
  20. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/__init__.py +0 -0
  21. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/app.py +0 -0
  22. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/notifications/__init__.py +0 -0
  23. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/notifications/invitations.py +0 -0
  24. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/__init__.py +0 -0
  25. {interloper_api-0.77.0 → interloper_api-0.78.0}/src/interloper_api/routes/health.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: interloper-api
3
- Version: 0.77.0
3
+ Version: 0.78.0
4
4
  Summary: Interloper FastAPI routes
5
5
  Author: Guillaume Onfroy
6
6
  Author-email: Guillaume Onfroy <guillaume@digitlcloud.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "interloper-api"
3
- version = "0.77.0"
3
+ version = "0.78.0"
4
4
  description = "Interloper FastAPI routes"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -27,7 +27,7 @@ otel = [
27
27
  ]
28
28
 
29
29
  [build-system]
30
- requires = ["uv_build>=0.11.5,<0.12"]
30
+ requires = ["uv_build>=0.12.9,<0.13"]
31
31
  build-backend = "uv_build"
32
32
 
33
33
  [tool.uv.sources.interloper-core]
@@ -84,4 +84,6 @@ extend-immutable-calls = [
84
84
  "D103",
85
85
  "D104",
86
86
  "D107",
87
+ "RUF069",
88
+ "PLW0108",
87
89
  ]
@@ -3,7 +3,7 @@
3
3
  # ###############
4
4
  [project]
5
5
  name = "interloper-api"
6
- version = "0.77.0"
6
+ version = "0.78.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -27,7 +27,7 @@ otel = [
27
27
  ]
28
28
 
29
29
  [build-system]
30
- requires = ["uv_build>=0.11.5,<0.12"]
30
+ requires = ["uv_build>=0.12.9,<0.13"]
31
31
  build-backend = "uv_build"
32
32
 
33
33
  [tool.uv.sources]
@@ -65,4 +65,4 @@ extend-immutable-calls = [
65
65
 
66
66
  [tool.ruff.lint.per-file-ignores]
67
67
  "__init__.py" = ["F401", "F403"]
68
- "tests/**" = ["ANN", "F811", "D101", "D102", "D103", "D104", "D107"]
68
+ "tests/**" = ["ANN", "F811", "D101", "D102", "D103", "D104", "D107", "RUF069", "PLW0108"]
@@ -6,12 +6,18 @@ organisation per request; ``rbac`` gates handlers on the caller's role.
6
6
  """
7
7
 
8
8
  from interloper_api.dependencies.auth import (
9
+ CurrentUserDep,
10
+ OrgIdDep,
9
11
  get_current_org,
10
12
  get_current_user,
11
13
  get_org_id,
12
14
  get_session_context,
13
15
  )
14
16
  from interloper_api.dependencies.rbac import (
17
+ AdminDep,
18
+ EditorDep,
19
+ SuperAdminDep,
20
+ ViewerDep,
15
21
  authorize_org_member,
16
22
  load_authorized,
17
23
  require_admin,
@@ -20,6 +26,11 @@ from interloper_api.dependencies.rbac import (
20
26
  require_viewer,
21
27
  )
22
28
  from interloper_api.dependencies.state import (
29
+ AdminConfigDep,
30
+ AuthConfigDep,
31
+ CatalogDep,
32
+ QuotaDefaultsDep,
33
+ StoreDep,
23
34
  get_admin_config,
24
35
  get_auth_config,
25
36
  get_catalog,
@@ -37,6 +48,17 @@ from interloper_api.dependencies.state import (
37
48
  )
38
49
 
39
50
  __all__ = [
51
+ "AdminConfigDep",
52
+ "AdminDep",
53
+ "AuthConfigDep",
54
+ "CatalogDep",
55
+ "CurrentUserDep",
56
+ "EditorDep",
57
+ "OrgIdDep",
58
+ "QuotaDefaultsDep",
59
+ "StoreDep",
60
+ "SuperAdminDep",
61
+ "ViewerDep",
40
62
  "authorize_org_member",
41
63
  "get_admin_config",
42
64
  "get_auth_config",
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Annotated
5
6
  from uuid import UUID
6
7
 
7
8
  from fastapi import Cookie, Depends, HTTPException
@@ -106,3 +107,9 @@ def get_org_id(
106
107
  The organisation UUID.
107
108
  """
108
109
  return org.id
110
+
111
+
112
+ # -- Dependency aliases --------------------------------------------------------
113
+
114
+ CurrentUserDep = Annotated[Profile, Depends(get_current_user)]
115
+ OrgIdDep = Annotated[UUID, Depends(get_org_id)]
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable
6
- from typing import Any
6
+ from typing import Annotated, Any
7
7
  from uuid import UUID
8
8
 
9
9
  from fastapi import Depends, HTTPException
@@ -191,3 +191,11 @@ def require_super_admin(
191
191
  if not user.is_super_admin:
192
192
  raise HTTPException(status_code=403, detail="Requires super-admin privileges")
193
193
  return user
194
+
195
+
196
+ # -- Dependency aliases --------------------------------------------------------
197
+
198
+ ViewerDep = Annotated[Profile, Depends(require_viewer)]
199
+ EditorDep = Annotated[Profile, Depends(require_editor)]
200
+ AdminDep = Annotated[Profile, Depends(require_admin)]
201
+ SuperAdminDep = Annotated[Profile, Depends(require_super_admin)]
@@ -7,8 +7,9 @@ route reaches for an ambient global of its own.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import Any
10
+ from typing import Annotated, Any
11
11
 
12
+ from fastapi import Depends
12
13
  from interloper.catalog.base import Catalog
13
14
  from interloper_db import Store
14
15
 
@@ -169,3 +170,12 @@ def get_quota_defaults() -> Any:
169
170
  The QuotaSettings instance, or None if not configured.
170
171
  """
171
172
  return _quota_defaults
173
+
174
+
175
+ # -- Dependency aliases --------------------------------------------------------
176
+
177
+ StoreDep = Annotated[Store, Depends(get_store)]
178
+ CatalogDep = Annotated[Catalog, Depends(get_catalog)]
179
+ AuthConfigDep = Annotated[Any, Depends(get_auth_config)]
180
+ AdminConfigDep = Annotated[Any, Depends(get_admin_config)]
181
+ QuotaDefaultsDep = Annotated[Any, Depends(get_quota_defaults)]
@@ -16,12 +16,16 @@ from importlib import metadata
16
16
  from typing import Any
17
17
  from uuid import UUID
18
18
 
19
- from fastapi import APIRouter, Depends, HTTPException, Request
20
- from interloper_db import Profile, Store
19
+ from fastapi import APIRouter, HTTPException, Request
21
20
  from interloper_db.store.quotas import METRIC_SUCCESSFUL_RUNS, QUOTAS
22
21
  from pydantic import BaseModel, RootModel, field_validator
23
22
 
24
- from interloper_api.dependencies import get_admin_config, get_quota_defaults, get_store, require_super_admin
23
+ from interloper_api.dependencies import (
24
+ AdminConfigDep,
25
+ QuotaDefaultsDep,
26
+ StoreDep,
27
+ SuperAdminDep,
28
+ )
25
29
  from interloper_api.notifications import InvitationEmail
26
30
 
27
31
  logger = logging.getLogger(__name__)
@@ -647,8 +651,8 @@ def _send_invitation_email(
647
651
 
648
652
  @router.get("/config")
649
653
  def get_instance_config(
650
- user: Profile = Depends(require_super_admin),
651
- config: Any = Depends(get_admin_config),
654
+ user: SuperAdminDep,
655
+ config: AdminConfigDep,
652
656
  ) -> AdminConfigResponse:
653
657
  """Read-only snapshot of the instance configuration (secrets redacted).
654
658
 
@@ -673,9 +677,9 @@ def get_instance_config(
673
677
 
674
678
  @router.get("/quotas")
675
679
  def get_quotas(
676
- user: Profile = Depends(require_super_admin),
677
- store: Store = Depends(get_store),
678
- quota_defaults: Any = Depends(get_quota_defaults),
680
+ user: SuperAdminDep,
681
+ store: StoreDep,
682
+ quota_defaults: QuotaDefaultsDep,
679
683
  ) -> AdminQuotasResponse:
680
684
  """Quota limits and current-period usage for every organisation.
681
685
 
@@ -730,8 +734,8 @@ def get_quotas(
730
734
  def update_org_quota(
731
735
  org_id: UUID,
732
736
  body: AdminQuotaUpdateRequest,
733
- user: Profile = Depends(require_super_admin),
734
- store: Store = Depends(get_store),
737
+ user: SuperAdminDep,
738
+ store: StoreDep,
735
739
  ) -> AdminQuotaLimits:
736
740
  """Set an organisation's quota overrides; omitted keys keep their value, null clears one.
737
741
 
@@ -784,8 +788,8 @@ def _activity_title(entry: dict[str, Any]) -> tuple[str, str | None]:
784
788
  @router.get("/organisations/{org_id}/activity")
785
789
  def get_organisation_activity(
786
790
  org_id: UUID,
787
- user: Profile = Depends(require_super_admin),
788
- store: Store = Depends(get_store),
791
+ user: SuperAdminDep,
792
+ store: StoreDep,
789
793
  ) -> list[AdminActivityEntry]:
790
794
  """Derived activity feed for one organisation, newest first.
791
795
 
@@ -813,8 +817,8 @@ def get_organisation_activity(
813
817
 
814
818
  @router.get("/users")
815
819
  def list_all_users(
816
- user: Profile = Depends(require_super_admin),
817
- store: Store = Depends(get_store),
820
+ user: SuperAdminDep,
821
+ store: StoreDep,
818
822
  ) -> list[AdminUserResponse]:
819
823
  """List every user profile with the organisations it belongs to.
820
824
 
@@ -842,8 +846,8 @@ def list_all_users(
842
846
  @router.delete("/users/{user_id}")
843
847
  def delete_user(
844
848
  user_id: UUID,
845
- user: Profile = Depends(require_super_admin),
846
- store: Store = Depends(get_store),
849
+ user: SuperAdminDep,
850
+ store: StoreDep,
847
851
  ) -> dict[str, str]:
848
852
  """Delete a user entirely: profile, sessions, tokens, memberships, sent invitations.
849
853
 
@@ -869,8 +873,8 @@ def delete_user(
869
873
 
870
874
  @router.get("/organisations")
871
875
  def list_all_organisations(
872
- user: Profile = Depends(require_super_admin),
873
- store: Store = Depends(get_store),
876
+ user: SuperAdminDep,
877
+ store: StoreDep,
874
878
  ) -> list[AdminOrganisationResponse]:
875
879
  """List every organisation with its member count, soft-deleted ones included.
876
880
 
@@ -896,8 +900,8 @@ def list_all_organisations(
896
900
  @router.post("/organisations", status_code=201)
897
901
  def create_organisation(
898
902
  body: CreateOrganisationRequest,
899
- user: Profile = Depends(require_super_admin),
900
- store: Store = Depends(get_store),
903
+ user: SuperAdminDep,
904
+ store: StoreDep,
901
905
  ) -> AdminOrganisationResponse:
902
906
  """Create an organisation. The super-admin is not added as a member.
903
907
 
@@ -922,8 +926,8 @@ def create_organisation(
922
926
  def update_organisation(
923
927
  org_id: UUID,
924
928
  body: UpdateOrganisationRequest,
925
- user: Profile = Depends(require_super_admin),
926
- store: Store = Depends(get_store),
929
+ user: SuperAdminDep,
930
+ store: StoreDep,
927
931
  ) -> AdminOrganisationResponse:
928
932
  """Rename an organisation.
929
933
 
@@ -950,8 +954,8 @@ def update_organisation(
950
954
  def delete_organisation(
951
955
  org_id: UUID,
952
956
  body: DeleteOrganisationRequest,
953
- user: Profile = Depends(require_super_admin),
954
- store: Store = Depends(get_store),
957
+ user: SuperAdminDep,
958
+ store: StoreDep,
955
959
  ) -> dict[str, str]:
956
960
  """Soft-delete an organisation: purges its data, keeps execution history and the usage ledger.
957
961
 
@@ -983,8 +987,8 @@ def delete_organisation(
983
987
  @router.get("/organisations/{org_id}/members")
984
988
  def list_members(
985
989
  org_id: UUID,
986
- user: Profile = Depends(require_super_admin),
987
- store: Store = Depends(get_store),
990
+ user: SuperAdminDep,
991
+ store: StoreDep,
988
992
  ) -> list[MemberResponse]:
989
993
  """List all members of any organisation.
990
994
 
@@ -1014,8 +1018,8 @@ def list_members(
1014
1018
  def join_organisation(
1015
1019
  org_id: UUID,
1016
1020
  body: JoinOrganisationRequest,
1017
- user: Profile = Depends(require_super_admin),
1018
- store: Store = Depends(get_store),
1021
+ user: SuperAdminDep,
1022
+ store: StoreDep,
1019
1023
  ) -> MemberResponse:
1020
1024
  """Add the calling super-admin to any organisation — no invitation needed.
1021
1025
 
@@ -1049,8 +1053,8 @@ def update_member_role(
1049
1053
  org_id: UUID,
1050
1054
  user_id: UUID,
1051
1055
  body: UpdateRoleRequest,
1052
- user: Profile = Depends(require_super_admin),
1053
- store: Store = Depends(get_store),
1056
+ user: SuperAdminDep,
1057
+ store: StoreDep,
1054
1058
  ) -> dict[str, str]:
1055
1059
  """Change a member's role in any organisation.
1056
1060
 
@@ -1073,8 +1077,8 @@ def update_member_role(
1073
1077
  def remove_member(
1074
1078
  org_id: UUID,
1075
1079
  user_id: UUID,
1076
- user: Profile = Depends(require_super_admin),
1077
- store: Store = Depends(get_store),
1080
+ user: SuperAdminDep,
1081
+ store: StoreDep,
1078
1082
  ) -> dict[str, str]:
1079
1083
  """Remove a member from any organisation.
1080
1084
 
@@ -1097,8 +1101,8 @@ def remove_member(
1097
1101
  @router.get("/organisations/{org_id}/invitations")
1098
1102
  def list_invitations(
1099
1103
  org_id: UUID,
1100
- user: Profile = Depends(require_super_admin),
1101
- store: Store = Depends(get_store),
1104
+ user: SuperAdminDep,
1105
+ store: StoreDep,
1102
1106
  ) -> list[InvitationResponse]:
1103
1107
  """List pending invitations for any organisation.
1104
1108
 
@@ -1128,8 +1132,8 @@ def invite_member(
1128
1132
  org_id: UUID,
1129
1133
  body: InviteRequest,
1130
1134
  request: Request,
1131
- user: Profile = Depends(require_super_admin),
1132
- store: Store = Depends(get_store),
1135
+ user: SuperAdminDep,
1136
+ store: StoreDep,
1133
1137
  ) -> InvitationResponse:
1134
1138
  """Invite a user to any organisation by email.
1135
1139
 
@@ -1171,8 +1175,8 @@ def invite_member(
1171
1175
  def cancel_invitation(
1172
1176
  org_id: UUID,
1173
1177
  invitation_id: UUID,
1174
- user: Profile = Depends(require_super_admin),
1175
- store: Store = Depends(get_store),
1178
+ user: SuperAdminDep,
1179
+ store: StoreDep,
1176
1180
  ) -> dict[str, str]:
1177
1181
  """Cancel a pending invitation in any organisation.
1178
1182
 
@@ -12,9 +12,8 @@ from __future__ import annotations
12
12
  import logging
13
13
  from collections.abc import AsyncIterator
14
14
  from typing import Any
15
- from uuid import UUID
16
15
 
17
- from fastapi import APIRouter, Depends, HTTPException, Response
16
+ from fastapi import APIRouter, HTTPException, Response
18
17
  from fastapi.responses import StreamingResponse
19
18
  from google.adk.apps import App
20
19
  from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
@@ -22,10 +21,16 @@ from google.adk.runners import Runner
22
21
  from google.adk.sessions.in_memory_session_service import InMemorySessionService
23
22
  from google.genai import types
24
23
  from interloper.catalog.base import Catalog
25
- from interloper_db import Profile, Store
24
+ from interloper_db import Store
26
25
  from pydantic import BaseModel
27
26
 
28
- from interloper_api.dependencies import get_catalog, get_org_id, get_store, require_editor, require_viewer
27
+ from interloper_api.dependencies import (
28
+ CatalogDep,
29
+ EditorDep,
30
+ OrgIdDep,
31
+ StoreDep,
32
+ ViewerDep,
33
+ )
29
34
 
30
35
  logger = logging.getLogger(__name__)
31
36
 
@@ -128,10 +133,10 @@ class SessionResponse(BaseModel):
128
133
 
129
134
  @router.post("/sessions")
130
135
  async def create_session(
131
- user: Profile = Depends(require_editor),
132
- org_id: UUID = Depends(get_org_id),
133
- store: Store = Depends(get_store),
134
- catalog: Catalog = Depends(get_catalog),
136
+ user: EditorDep,
137
+ org_id: OrgIdDep,
138
+ store: StoreDep,
139
+ catalog: CatalogDep,
135
140
  ) -> SessionResponse:
136
141
  """Create a new agent chat session.
137
142
 
@@ -159,7 +164,7 @@ async def create_session(
159
164
 
160
165
  @router.get("/sessions")
161
166
  async def list_sessions(
162
- user: Profile = Depends(require_viewer),
167
+ user: ViewerDep,
163
168
  ) -> list[SessionResponse]:
164
169
  """List all agent sessions for the current user.
165
170
 
@@ -181,7 +186,7 @@ async def list_sessions(
181
186
  @router.get("/sessions/{session_id}")
182
187
  async def get_session(
183
188
  session_id: str,
184
- user: Profile = Depends(require_viewer),
189
+ user: ViewerDep,
185
190
  ) -> Response:
186
191
  """Get a session with its full event history.
187
192
 
@@ -214,7 +219,7 @@ async def get_session(
214
219
  @router.delete("/sessions/{session_id}")
215
220
  async def delete_session(
216
221
  session_id: str,
217
- user: Profile = Depends(require_editor),
222
+ user: EditorDep,
218
223
  ) -> dict[str, str]:
219
224
  """Delete an agent session.
220
225
 
@@ -248,9 +253,9 @@ async def delete_session(
248
253
  async def chat(
249
254
  session_id: str,
250
255
  body: ChatRequest,
251
- user: Profile = Depends(require_editor),
252
- store: Store = Depends(get_store),
253
- catalog: Catalog = Depends(get_catalog),
256
+ user: EditorDep,
257
+ store: StoreDep,
258
+ catalog: CatalogDep,
254
259
  ) -> StreamingResponse:
255
260
  """Send a message and stream the agent's response as SSE.
256
261
 
@@ -4,19 +4,24 @@ from __future__ import annotations
4
4
 
5
5
  from contextlib import suppress
6
6
  from datetime import datetime
7
- from typing import Any
7
+ from typing import Annotated, Any
8
8
  from urllib.parse import urlencode
9
9
  from uuid import UUID
10
10
  from zoneinfo import ZoneInfo
11
11
 
12
12
  import httpx
13
- from fastapi import APIRouter, Cookie, Depends, HTTPException, Response
13
+ from fastapi import APIRouter, Cookie, HTTPException, Response
14
14
  from fastapi.responses import RedirectResponse
15
15
  from interloper.errors import NotFoundError
16
- from interloper_db import Organisation, Profile, Store
16
+ from interloper_db import Organisation, Store
17
17
  from pydantic import BaseModel, Field
18
18
 
19
- from interloper_api.dependencies import get_auth_config, get_current_user, get_features, get_store
19
+ from interloper_api.dependencies import (
20
+ AuthConfigDep,
21
+ CurrentUserDep,
22
+ StoreDep,
23
+ get_features,
24
+ )
20
25
 
21
26
  GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
22
27
  GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
@@ -83,8 +88,8 @@ class AuthUserResponse(BaseModel):
83
88
 
84
89
  @router.get("/google")
85
90
  def google_login(
91
+ auth_config: AuthConfigDep,
86
92
  redirect: str | None = None,
87
- auth_config: Any = Depends(get_auth_config),
88
93
  ) -> RedirectResponse:
89
94
  """Redirect user to Google's OAuth consent screen.
90
95
 
@@ -123,9 +128,9 @@ def google_login(
123
128
  @router.get("/google/callback")
124
129
  def google_callback(
125
130
  code: str,
131
+ store: StoreDep,
132
+ auth_config: AuthConfigDep,
126
133
  state: str | None = None,
127
- store: Store = Depends(get_store),
128
- auth_config: Any = Depends(get_auth_config),
129
134
  ) -> RedirectResponse:
130
135
  """Exchange Google authorization code for tokens, upsert profile, create session.
131
136
 
@@ -224,8 +229,8 @@ def google_callback(
224
229
  @router.post("/logout")
225
230
  def logout(
226
231
  response: Response,
227
- user: Profile = Depends(get_current_user),
228
- store: Store = Depends(get_store),
232
+ user: CurrentUserDep,
233
+ store: StoreDep,
229
234
  ) -> dict[str, str]:
230
235
  """Delete all sessions for the current user and clear the cookie.
231
236
 
@@ -247,8 +252,8 @@ def logout(
247
252
 
248
253
  @router.get("/me")
249
254
  def get_me(
250
- store: Store = Depends(get_store),
251
- session_token: str | None = Cookie(default=None),
255
+ store: StoreDep,
256
+ session_token: Annotated[str | None, Cookie()] = None,
252
257
  ) -> AuthUserResponse:
253
258
  """Return the current user and their active organisation (if any).
254
259
 
@@ -328,8 +333,8 @@ class ProfileResponse(BaseModel):
328
333
  @router.patch("/me")
329
334
  def update_me(
330
335
  body: UpdateMeRequest,
331
- user: Profile = Depends(get_current_user),
332
- store: Store = Depends(get_store),
336
+ user: CurrentUserDep,
337
+ store: StoreDep,
333
338
  ) -> ProfileResponse:
334
339
  """Update the current user's profile (display name, timezone).
335
340
 
@@ -366,9 +371,9 @@ class SwitchOrgRequest(BaseModel):
366
371
  @router.post("/switch-org")
367
372
  def switch_org(
368
373
  body: SwitchOrgRequest,
369
- user: Profile = Depends(get_current_user),
370
- store: Store = Depends(get_store),
371
- session_token: str | None = Cookie(default=None),
374
+ user: CurrentUserDep,
375
+ store: StoreDep,
376
+ session_token: Annotated[str | None, Cookie()] = None,
372
377
  ) -> dict[str, str]:
373
378
  """Switch the session's active organisation. User must be a member.
374
379
 
@@ -403,9 +408,9 @@ class AcceptInviteRequest(BaseModel):
403
408
  @router.post("/accept-invite")
404
409
  def accept_invite(
405
410
  body: AcceptInviteRequest,
406
- user: Profile = Depends(get_current_user),
407
- store: Store = Depends(get_store),
408
- session_token: str | None = Cookie(default=None),
411
+ user: CurrentUserDep,
412
+ store: StoreDep,
413
+ session_token: Annotated[str | None, Cookie()] = None,
409
414
  ) -> dict[str, str]:
410
415
  """Accept an organisation invitation using its token.
411
416
 
@@ -4,19 +4,18 @@ from __future__ import annotations
4
4
 
5
5
  from uuid import UUID
6
6
 
7
- from fastapi import APIRouter, Depends, HTTPException
7
+ from fastapi import APIRouter, HTTPException
8
8
  from interloper.errors import NotFoundError
9
- from interloper_db import Profile, Store
10
9
  from interloper_db.models import Backfill
11
10
  from pydantic import BaseModel
12
11
 
13
12
  from interloper_api.dependencies import (
13
+ CurrentUserDep,
14
+ OrgIdDep,
15
+ StoreDep,
16
+ ViewerDep,
14
17
  authorize_org_member,
15
- get_current_user,
16
- get_org_id,
17
- get_store,
18
18
  load_authorized,
19
- require_viewer,
20
19
  )
21
20
 
22
21
  router = APIRouter(prefix="/backfills", tags=["backfills"])
@@ -96,10 +95,10 @@ class BackfillResponse(BaseModel):
96
95
 
97
96
  @router.get("/")
98
97
  def list_backfills(
98
+ user: ViewerDep,
99
+ org_id: OrgIdDep,
100
+ store: StoreDep,
99
101
  active_only: bool = False,
100
- user: Profile = Depends(require_viewer),
101
- org_id: UUID = Depends(get_org_id),
102
- store: Store = Depends(get_store),
103
102
  ) -> list[BackfillResponse]:
104
103
  """List backfills for the current organisation.
105
104
 
@@ -123,8 +122,8 @@ def list_backfills(
123
122
  @router.post("/", status_code=201)
124
123
  def create_backfill(
125
124
  body: BackfillCreateRequest,
126
- user: Profile = Depends(get_current_user),
127
- store: Store = Depends(get_store),
125
+ user: CurrentUserDep,
126
+ store: StoreDep,
128
127
  ) -> BackfillResponse:
129
128
  """Queue a backfill for a job over a partition-key range.
130
129
 
@@ -160,8 +159,8 @@ def create_backfill(
160
159
  @router.get("/{backfill_id}")
161
160
  def get_backfill(
162
161
  backfill_id: UUID,
163
- user: Profile = Depends(get_current_user),
164
- store: Store = Depends(get_store),
162
+ user: CurrentUserDep,
163
+ store: StoreDep,
165
164
  ) -> BackfillResponse:
166
165
  """Get a single backfill by ID. Authorized by membership in the backfill's org.
167
166
 
@@ -187,8 +186,8 @@ def get_backfill(
187
186
  @router.post("/{backfill_id}/cancel")
188
187
  def cancel_backfill(
189
188
  backfill_id: UUID,
190
- user: Profile = Depends(get_current_user),
191
- store: Store = Depends(get_store),
189
+ user: CurrentUserDep,
190
+ store: StoreDep,
192
191
  ) -> BackfillResponse:
193
192
  """Cancel a backfill: pending and queued runs are canceled, in-flight runs drain.
194
193