interloper-api 0.40.0__tar.gz → 0.43.3__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 (20) hide show
  1. {interloper_api-0.40.0 → interloper_api-0.43.3}/PKG-INFO +1 -1
  2. {interloper_api-0.40.0 → interloper_api-0.43.3}/pyproject.toml +1 -1
  3. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/app.py +12 -10
  4. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/agent.py +1 -1
  5. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/backfills.py +1 -1
  6. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/catalog.py +1 -1
  7. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/components.py +1 -1
  8. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/runs.py +1 -1
  9. interloper_api-0.43.3/src/interloper_api/routes/tokens.py +120 -0
  10. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/ws.py +1 -1
  11. {interloper_api-0.40.0 → interloper_api-0.43.3}/README.md +0 -0
  12. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/__init__.py +0 -0
  13. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/components.py +0 -0
  14. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/dependencies.py +0 -0
  15. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/email.py +0 -0
  16. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/__init__.py +0 -0
  17. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/admin.py +0 -0
  18. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/auth.py +0 -0
  19. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/oauth.py +0 -0
  20. {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/organisations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: interloper-api
3
- Version: 0.40.0
3
+ Version: 0.43.3
4
4
  Summary: Interloper FastAPI routes
5
5
  Author: Guillaume Onfroy
6
6
  Author-email: Guillaume Onfroy <guillaume@digitlcloud.com>
@@ -3,7 +3,7 @@
3
3
  # ###############
4
4
  [project]
5
5
  name = "interloper-api"
6
- version = "0.40.0"
6
+ version = "0.43.3"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -21,6 +21,7 @@ from interloper_api.routes import (
21
21
  oauth,
22
22
  organisations,
23
23
  runs,
24
+ tokens,
24
25
  ws,
25
26
  )
26
27
  from interloper_api.routes import catalog as catalog_routes
@@ -88,22 +89,23 @@ def create_app(
88
89
  set_smtp_config(smtp_config)
89
90
 
90
91
  api = APIRouter(prefix="/api")
91
- api.include_router(auth.router, tags=["auth"])
92
- api.include_router(organisations.router, tags=["organisations"])
93
- api.include_router(admin.router, tags=["admin"])
94
- api.include_router(catalog_routes.router, prefix="/catalog", tags=["catalog"])
95
- api.include_router(components.router, prefix="/components", tags=["components"])
96
- api.include_router(runs.router, prefix="/runs", tags=["runs"])
97
- api.include_router(backfills.router, prefix="/backfills", tags=["backfills"])
98
- api.include_router(oauth.router, tags=["oauth"])
99
- api.include_router(ws.router, tags=["ws"])
92
+ api.include_router(auth.router)
93
+ api.include_router(organisations.router)
94
+ api.include_router(admin.router)
95
+ api.include_router(catalog_routes.router)
96
+ api.include_router(components.router)
97
+ api.include_router(runs.router)
98
+ api.include_router(backfills.router)
99
+ api.include_router(oauth.router)
100
+ api.include_router(tokens.router)
101
+ api.include_router(ws.router)
100
102
 
101
103
  agent_available = False
102
104
  if agent_config is None or agent_config.enabled:
103
105
  try:
104
106
  from interloper_api.routes import agent as agent_routes
105
107
 
106
- api.include_router(agent_routes.router, prefix="/agent", tags=["agent"])
108
+ api.include_router(agent_routes.router)
107
109
  agent_available = True
108
110
  except ImportError:
109
111
  logger.warning(
@@ -29,7 +29,7 @@ from interloper_api.dependencies import get_catalog, get_org_id, get_store, requ
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
32
- router = APIRouter()
32
+ router = APIRouter(prefix="/agent", tags=["agent"])
33
33
 
34
34
  APP_NAME = "interloper_agent"
35
35
 
@@ -20,7 +20,7 @@ from interloper_api.dependencies import (
20
20
  require_viewer,
21
21
  )
22
22
 
23
- router = APIRouter()
23
+ router = APIRouter(prefix="/backfills", tags=["backfills"])
24
24
 
25
25
 
26
26
  class BackfillCreateRequest(BaseModel):
@@ -9,7 +9,7 @@ from interloper.catalog.base import Catalog
9
9
 
10
10
  from interloper_api.dependencies import get_catalog
11
11
 
12
- router = APIRouter()
12
+ router = APIRouter(prefix="/catalog", tags=["catalog"])
13
13
 
14
14
 
15
15
  @router.get("/")
@@ -51,7 +51,7 @@ from interloper_api.dependencies import (
51
51
 
52
52
  logger = logging.getLogger(__name__)
53
53
 
54
- router = APIRouter()
54
+ router = APIRouter(prefix="/components", tags=["components"])
55
55
 
56
56
 
57
57
  # -- Request/Response models --------------------------------------------------
@@ -22,7 +22,7 @@ from interloper_api.dependencies import (
22
22
  require_viewer,
23
23
  )
24
24
 
25
- router = APIRouter()
25
+ router = APIRouter(prefix="/runs", tags=["runs"])
26
26
 
27
27
  #: Hard cap on the number of events returned in a single page, regardless of
28
28
  #: the requested ``limit``. Keeps a pathological ``?limit=1000000`` from loading
@@ -0,0 +1,120 @@
1
+ """Personal access token routes — mint, list, and revoke API tokens.
2
+
3
+ Tokens authenticate programmatic clients (the MCP server, CLIs) as their
4
+ holder in one organisation, with the holder's live role. Management is
5
+ session-cookie-only by design: a leaked token must not be able to mint
6
+ further tokens.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime, timedelta, timezone
12
+ from uuid import UUID
13
+
14
+ from fastapi import APIRouter, Depends, HTTPException
15
+ from interloper_db import Profile, Store
16
+ from pydantic import BaseModel, Field
17
+
18
+ from interloper_api.dependencies import get_current_user, get_org_id, get_store, require_viewer
19
+
20
+ router = APIRouter(prefix="/tokens", tags=["tokens"])
21
+
22
+
23
+ # -- Response / Request models ------------------------------------------------
24
+
25
+
26
+ class CreateTokenRequest(BaseModel):
27
+ """Request body for creating a token."""
28
+
29
+ name: str = Field(min_length=1, max_length=100)
30
+ expires_in_days: int | None = Field(default=90, ge=1, le=3650)
31
+
32
+
33
+ class TokenResponse(BaseModel):
34
+ """Token metadata — never carries secret material."""
35
+
36
+ id: UUID
37
+ name: str
38
+ token_prefix: str
39
+ organisation_id: UUID
40
+ created_at: datetime | None = None
41
+ expires_at: datetime | None = None
42
+ last_used_at: datetime | None = None
43
+ revoked_at: datetime | None = None
44
+
45
+
46
+ class CreatedTokenResponse(TokenResponse):
47
+ """Creation response: the only place the raw token ever appears."""
48
+
49
+ token: str
50
+
51
+
52
+ # -- Routes --------------------------------------------------------------------
53
+
54
+
55
+ @router.post("", status_code=201)
56
+ def create_token(
57
+ body: CreateTokenRequest,
58
+ user: Profile = Depends(require_viewer),
59
+ org_id: UUID = Depends(get_org_id),
60
+ store: Store = Depends(get_store),
61
+ ) -> CreatedTokenResponse:
62
+ """Create a personal access token scoped to the active organisation.
63
+
64
+ Any org member may mint one: the token conveys only the holder's own
65
+ live role, so no escalation is possible. The raw token is returned
66
+ exactly once and cannot be recovered afterwards.
67
+ """
68
+ expires_at = None
69
+ if body.expires_in_days is not None:
70
+ expires_at = datetime.now(timezone.utc) + timedelta(days=body.expires_in_days)
71
+
72
+ row, raw = store.create_token(user.id, org_id, name=body.name, expires_at=expires_at)
73
+ return CreatedTokenResponse(
74
+ id=row.id,
75
+ name=row.name,
76
+ token_prefix=row.token_prefix,
77
+ organisation_id=row.organisation_id,
78
+ created_at=row.created_at,
79
+ expires_at=row.expires_at,
80
+ last_used_at=row.last_used_at,
81
+ revoked_at=row.revoked_at,
82
+ token=raw,
83
+ )
84
+
85
+
86
+ @router.get("")
87
+ def list_tokens(
88
+ user: Profile = Depends(require_viewer),
89
+ org_id: UUID = Depends(get_org_id),
90
+ store: Store = Depends(get_store),
91
+ ) -> list[TokenResponse]:
92
+ """List the caller's tokens in the active organisation."""
93
+ rows = store.list_tokens(user.id, org_id)
94
+ return [TokenResponse.model_validate(row, from_attributes=True) for row in rows]
95
+
96
+
97
+ @router.delete("/{token_id}")
98
+ def revoke_token(
99
+ token_id: UUID,
100
+ user: Profile = Depends(get_current_user),
101
+ store: Store = Depends(get_store),
102
+ ) -> dict[str, str]:
103
+ """Revoke a token.
104
+
105
+ The owner may revoke their own tokens; an org admin may revoke any token
106
+ scoped to their organisation. Missing and unauthorized get the same 404,
107
+ so token IDs don't act as an existence oracle.
108
+ """
109
+ detail = f"Token {token_id} not found"
110
+ row = store.get_token(token_id)
111
+ if row is None:
112
+ raise HTTPException(status_code=404, detail=detail)
113
+
114
+ if row.user_id != user.id:
115
+ role = store.get_user_role(user.id, row.organisation_id)
116
+ if role != "admin":
117
+ raise HTTPException(status_code=404, detail=detail)
118
+
119
+ store.revoke_token(token_id)
120
+ return {"status": "revoked"}
@@ -24,7 +24,7 @@ from fastapi import APIRouter, Cookie, FastAPI, WebSocket, WebSocketDisconnect
24
24
  from interloper_api.dependencies import get_store
25
25
 
26
26
  logger = logging.getLogger(__name__)
27
- router = APIRouter()
27
+ router = APIRouter(tags=["ws"])
28
28
 
29
29
 
30
30
  class ConnectionManager: