interloper-api 0.9.0__tar.gz → 0.10.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 (28) hide show
  1. {interloper_api-0.9.0 → interloper_api-0.10.0}/PKG-INFO +1 -1
  2. {interloper_api-0.9.0 → interloper_api-0.10.0}/pyproject.toml +1 -10
  3. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/app.py +1 -1
  4. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/dependencies.py +2 -2
  5. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/admin.py +15 -14
  6. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/assets.py +5 -5
  7. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/auth.py +18 -17
  8. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/backfills.py +1 -1
  9. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/destinations.py +1 -1
  10. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/external/google_ads.py +1 -1
  11. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/jobs.py +4 -4
  12. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/organisations.py +16 -15
  13. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/resources.py +4 -4
  14. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/runs.py +2 -2
  15. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/sources.py +10 -10
  16. {interloper_api-0.9.0 → interloper_api-0.10.0}/README.md +0 -0
  17. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/__init__.py +0 -0
  18. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/email.py +0 -0
  19. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/__init__.py +0 -0
  20. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/agent.py +0 -0
  21. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/catalog.py +0 -0
  22. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/external/__init__.py +0 -0
  23. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/external/amazon_ads.py +0 -0
  24. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/external/facebook_ads.py +0 -0
  25. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/external/pinterest_ads.py +0 -0
  26. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/external/snapchat_ads.py +0 -0
  27. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/oauth.py +0 -0
  28. {interloper_api-0.9.0 → interloper_api-0.10.0}/src/interloper_api/routes/ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: interloper-api
3
- Version: 0.9.0
3
+ Version: 0.10.0
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.9.0"
6
+ version = "0.10.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -42,12 +42,3 @@ extend-select = ["E", "I", "UP", "ANN001", "ANN201", "ANN202"]
42
42
  [tool.ruff.lint.per-file-ignores]
43
43
  "__init__.py" = ["F401", "F403"]
44
44
  "tests/**" = ["ANN", "F811"]
45
-
46
- # ###############
47
- # PYRIGHT
48
- # ###############
49
- [tool.pyright]
50
- include = ["src"]
51
- typeCheckingMode = "basic"
52
- reportMissingParameterType = true
53
- ignore = ["tests/**"]
@@ -54,7 +54,7 @@ def create_app(
54
54
 
55
55
  if cors_origins:
56
56
  app.add_middleware(
57
- CORSMiddleware, # type: ignore[arg-type]
57
+ CORSMiddleware,
58
58
  allow_origins=cors_origins,
59
59
  allow_credentials=True,
60
60
  allow_methods=["*"],
@@ -211,7 +211,7 @@ def get_org_id(
211
211
  Returns:
212
212
  The organisation UUID.
213
213
  """
214
- return org.id # type: ignore[return-value]
214
+ return org.id
215
215
 
216
216
 
217
217
  # -- RBAC dependencies -------------------------------------------------------
@@ -237,7 +237,7 @@ def _check_role(
237
237
  Raises:
238
238
  HTTPException: 403 if insufficient permissions.
239
239
  """
240
- role = store.get_user_role(user.id, org_id) # type: ignore[arg-type]
240
+ role = store.get_user_role(user.id, org_id)
241
241
  if role is None:
242
242
  raise HTTPException(status_code=403, detail="Not a member of this organisation")
243
243
  if _ROLE_RANK.get(role, -1) < _ROLE_RANK[minimum]:
@@ -10,10 +10,11 @@ from __future__ import annotations
10
10
 
11
11
  import logging
12
12
  from datetime import datetime
13
+ from typing import Any
13
14
  from uuid import UUID
14
15
 
15
16
  from fastapi import APIRouter, Depends, HTTPException, Request
16
- from interloper_db import Profile, Store
17
+ from interloper_db import Organisation, Profile, Store
17
18
  from pydantic import BaseModel
18
19
 
19
20
  from interloper_api.dependencies import get_store, require_super_admin
@@ -86,7 +87,7 @@ class InvitationResponse(BaseModel):
86
87
  # -- Helpers ------------------------------------------------------------------
87
88
 
88
89
 
89
- def _require_org(store: Store, org_id: UUID) -> object:
90
+ def _require_org(store: Store, org_id: UUID) -> Organisation:
90
91
  """Fetch an organisation or raise 404."""
91
92
  org = store.get_organisation(org_id)
92
93
  if not org:
@@ -103,7 +104,7 @@ def _validate_role(role: str) -> str:
103
104
 
104
105
  def _send_invitation_email(
105
106
  request: Request,
106
- invitation: object,
107
+ invitation: Any,
107
108
  org_name: str,
108
109
  inviter_name: str,
109
110
  ) -> None:
@@ -111,11 +112,11 @@ def _send_invitation_email(
111
112
  from interloper_api.dependencies import get_smtp_config
112
113
 
113
114
  smtp_config = get_smtp_config()
114
- if not smtp_config or not smtp_config.enabled: # type: ignore[attr-defined]
115
+ if not smtp_config or not smtp_config.enabled:
115
116
  return
116
117
 
117
- token = invitation.token # type: ignore[attr-defined]
118
- email = invitation.email # type: ignore[attr-defined]
118
+ token = invitation.token
119
+ email = invitation.email
119
120
  base_url = str(request.base_url).rstrip("/")
120
121
  invite_url = f"{base_url}/invite/{token}"
121
122
 
@@ -142,7 +143,7 @@ def list_all_organisations(
142
143
  """List every organisation with its member count."""
143
144
  return [
144
145
  AdminOrganisationResponse(
145
- id=org.id, # type: ignore[arg-type]
146
+ id=org.id,
146
147
  name=org.name,
147
148
  member_count=count,
148
149
  created_at=org.created_at,
@@ -160,7 +161,7 @@ def create_organisation(
160
161
  """Create an organisation. The super-admin is not added as a member."""
161
162
  org = store.create_organisation(name=body.name)
162
163
  return AdminOrganisationResponse(
163
- id=org.id, # type: ignore[arg-type]
164
+ id=org.id,
164
165
  name=org.name,
165
166
  member_count=0,
166
167
  created_at=org.created_at,
@@ -180,7 +181,7 @@ def update_organisation(
180
181
  raise HTTPException(status_code=404, detail="Organisation not found")
181
182
  members = store.list_org_members(org_id)
182
183
  return AdminOrganisationResponse(
183
- id=org.id, # type: ignore[arg-type]
184
+ id=org.id,
184
185
  name=org.name,
185
186
  member_count=len(members),
186
187
  created_at=org.created_at,
@@ -201,7 +202,7 @@ def list_members(
201
202
  members = store.list_org_members(org_id)
202
203
  return [
203
204
  MemberResponse(
204
- id=profile.id, # type: ignore[arg-type]
205
+ id=profile.id,
205
206
  email=profile.email,
206
207
  name=profile.name,
207
208
  avatar_url=profile.avatar_url,
@@ -252,7 +253,7 @@ def list_invitations(
252
253
  _require_org(store, org_id)
253
254
  return [
254
255
  InvitationResponse(
255
- id=inv.id, # type: ignore[arg-type]
256
+ id=inv.id,
256
257
  email=inv.email,
257
258
  role=inv.role,
258
259
  created_at=inv.created_at,
@@ -277,14 +278,14 @@ def invite_member(
277
278
  org_id=org_id,
278
279
  email=body.email.strip(),
279
280
  role=body.role,
280
- invited_by=user.id, # type: ignore[arg-type]
281
+ invited_by=user.id,
281
282
  )
282
283
 
283
284
  inviter_name = user.name or user.email
284
- _send_invitation_email(request, invitation, org.name, inviter_name) # type: ignore[attr-defined]
285
+ _send_invitation_email(request, invitation, org.name, inviter_name)
285
286
 
286
287
  return InvitationResponse(
287
- id=invitation.id, # type: ignore[arg-type]
288
+ id=invitation.id,
288
289
  email=invitation.email,
289
290
  role=invitation.role,
290
291
  created_at=invitation.created_at,
@@ -102,16 +102,16 @@ def _resource_map(junction_cls: type, fk_column: str, fk_value: UUID) -> dict[st
102
102
  col = getattr(junction_cls, fk_column)
103
103
  with Session(get_engine()) as s:
104
104
  rows = s.exec(select(junction_cls).where(col == fk_value)).all()
105
- return {r.key: str(r.resource_id) for r in rows}
105
+ return {r.key: str(r.resource_id) for r in rows} # ty: ignore[unresolved-attribute]
106
106
 
107
107
 
108
108
  def _dest_to_response(dest: Destination) -> DestinationResponse:
109
109
  return DestinationResponse(
110
- id=dest.id, # type: ignore[arg-type]
110
+ id=dest.id,
111
111
  key=dest.key,
112
112
  name=dest.name,
113
113
  config=dest.config,
114
- resources=_resource_map(DestinationResource, "destination_id", dest.id), # type: ignore[arg-type]
114
+ resources=_resource_map(DestinationResource, "destination_id", dest.id),
115
115
  created_at=str(dest.created_at) if dest.created_at else None,
116
116
  )
117
117
 
@@ -119,13 +119,13 @@ def _dest_to_response(dest: Destination) -> DestinationResponse:
119
119
  def _asset_to_response(asset: Asset) -> AssetResponse:
120
120
  """Convert a DB Asset to an AssetResponse."""
121
121
  return AssetResponse(
122
- id=asset.id, # type: ignore[arg-type]
122
+ id=asset.id,
123
123
  source_id=asset.source_id,
124
124
  org_id=asset.org_id,
125
125
  key=asset.key,
126
126
  materializable=asset.materializable,
127
127
  config=asset.config,
128
- resources=_resource_map(AssetResource, "asset_id", asset.id), # type: ignore[arg-type]
128
+ resources=_resource_map(AssetResource, "asset_id", asset.id),
129
129
  destinations=[_dest_to_response(d) for d in asset.destinations],
130
130
  created_at=str(asset.created_at) if asset.created_at else None,
131
131
  )
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from datetime import datetime
6
+ from typing import Any
6
7
  from urllib.parse import urlencode
7
8
  from uuid import UUID
8
9
 
@@ -45,11 +46,11 @@ class AuthUserResponse(BaseModel):
45
46
  @router.get("/google")
46
47
  def google_login(
47
48
  redirect: str | None = None,
48
- auth_config: object = Depends(get_auth_config),
49
+ auth_config: Any = Depends(get_auth_config),
49
50
  ) -> RedirectResponse:
50
51
  """Redirect user to Google's OAuth consent screen."""
51
- client_id = auth_config.google_client_id # type: ignore[attr-defined]
52
- redirect_uri = auth_config.google_redirect_uri # type: ignore[attr-defined]
52
+ client_id = auth_config.google_client_id
53
+ redirect_uri = auth_config.google_redirect_uri
53
54
 
54
55
  if not client_id:
55
56
  raise HTTPException(status_code=500, detail="Google OAuth not configured")
@@ -73,14 +74,14 @@ def google_callback(
73
74
  code: str,
74
75
  state: str | None = None,
75
76
  store: Store = Depends(get_store),
76
- auth_config: object = Depends(get_auth_config),
77
+ auth_config: Any = Depends(get_auth_config),
77
78
  ) -> RedirectResponse:
78
79
  """Exchange Google authorization code for tokens, upsert profile, create session."""
79
- client_id = auth_config.google_client_id # type: ignore[attr-defined]
80
- client_secret = auth_config.google_client_secret # type: ignore[attr-defined]
81
- redirect_uri = auth_config.google_redirect_uri # type: ignore[attr-defined]
82
- cookie_secure: bool = auth_config.cookie_secure # type: ignore[attr-defined]
83
- session_expiry_days: int = auth_config.session_expiry_days # type: ignore[attr-defined]
80
+ client_id = auth_config.google_client_id
81
+ client_secret = auth_config.google_client_secret
82
+ redirect_uri = auth_config.google_redirect_uri
83
+ cookie_secure: bool = auth_config.cookie_secure
84
+ session_expiry_days: int = auth_config.session_expiry_days
84
85
 
85
86
  if not client_id or not client_secret:
86
87
  raise HTTPException(status_code=500, detail="Google OAuth not configured")
@@ -130,7 +131,7 @@ def google_callback(
130
131
  )
131
132
 
132
133
  # Create session (no org context — frontend resolves org after login)
133
- token = store.create_session(user_id=profile.id) # type: ignore[arg-type]
134
+ token = store.create_session(user_id=profile.id)
134
135
 
135
136
  redirect_url = state if state and state.startswith("/") else "/"
136
137
  response = RedirectResponse(url=redirect_url, status_code=302)
@@ -153,7 +154,7 @@ def logout(
153
154
  store: Store = Depends(get_store),
154
155
  ) -> dict[str, str]:
155
156
  """Delete all sessions for the current user and clear the cookie."""
156
- store.delete_user_sessions(user.id) # type: ignore[arg-type]
157
+ store.delete_user_sessions(user.id)
157
158
  response.delete_cookie("session_token", path="/")
158
159
  return {"status": "ok"}
159
160
 
@@ -179,12 +180,12 @@ def get_me(
179
180
  org = store.get_organisation(session_row.organisation_id)
180
181
 
181
182
  if org and profile.id:
182
- user_role = store.get_user_role(profile.id, org.id) # type: ignore[arg-type]
183
+ user_role = store.get_user_role(profile.id, org.id)
183
184
  if user_role:
184
185
  role = user_role
185
186
 
186
187
  return AuthUserResponse(
187
- id=profile.id, # type: ignore[arg-type]
188
+ id=profile.id,
188
189
  email=profile.email,
189
190
  name=profile.name,
190
191
  avatar_url=profile.avatar_url,
@@ -209,12 +210,12 @@ def switch_org(
209
210
  session_token: str | None = Cookie(default=None),
210
211
  ) -> dict[str, str]:
211
212
  """Switch the session's active organisation. User must be a member."""
212
- role = store.get_user_role(user.id, body.organisation_id) # type: ignore[arg-type]
213
+ role = store.get_user_role(user.id, body.organisation_id)
213
214
  if not role:
214
215
  raise HTTPException(status_code=403, detail="Not a member of this organisation")
215
216
 
216
217
  if session_token:
217
- store.set_session_org(session_token, body.organisation_id, user_id=user.id) # type: ignore[arg-type]
218
+ store.set_session_org(session_token, body.organisation_id, user_id=user.id)
218
219
  return {"status": "ok"}
219
220
 
220
221
 
@@ -232,12 +233,12 @@ def accept_invite(
232
233
  session_token: str | None = Cookie(default=None),
233
234
  ) -> dict[str, str]:
234
235
  """Accept an organisation invitation using its token."""
235
- org = store.accept_invitation(body.token, user.id) # type: ignore[arg-type]
236
+ org = store.accept_invitation(body.token, user.id)
236
237
  if not org:
237
238
  raise HTTPException(status_code=400, detail="Invalid or expired invitation")
238
239
 
239
240
  # Switch to the new org
240
241
  if session_token:
241
- store.set_session_org(session_token, org.id, user_id=user.id) # type: ignore[arg-type]
242
+ store.set_session_org(session_token, org.id, user_id=user.id)
242
243
 
243
244
  return {"status": "ok"}
@@ -43,7 +43,7 @@ def _backfill_to_response(backfill: Backfill) -> BackfillResponse:
43
43
  The response model.
44
44
  """
45
45
  return BackfillResponse(
46
- id=backfill.id, # type: ignore[arg-type]
46
+ id=backfill.id,
47
47
  org_id=backfill.org_id,
48
48
  job_id=backfill.job_id,
49
49
  status=backfill.status,
@@ -49,7 +49,7 @@ def _resource_map(dest: Destination) -> dict[str, str]:
49
49
 
50
50
  def _to_response(dest: Destination) -> DestinationResponse:
51
51
  return DestinationResponse(
52
- id=dest.id, # type: ignore[arg-type]
52
+ id=dest.id,
53
53
  key=dest.key,
54
54
  name=dest.name,
55
55
  config=dest.config,
@@ -37,7 +37,7 @@ async def _get_access_token(client: httpx.AsyncClient, body: GoogleAdsConnection
37
37
  },
38
38
  )
39
39
  resp.raise_for_status()
40
- return resp.json()["access_token"] # type: ignore[no-any-return]
40
+ return resp.json()["access_token"]
41
41
 
42
42
 
43
43
  def _auth_headers(access_token: str, developer_token: str) -> dict[str, str]:
@@ -72,7 +72,7 @@ def _job_to_response(job: Job) -> JobResponse:
72
72
  The response model.
73
73
  """
74
74
  return JobResponse(
75
- id=job.id, # type: ignore[arg-type]
75
+ id=job.id,
76
76
  org_id=job.org_id,
77
77
  name=job.name,
78
78
  cron=job.cron,
@@ -80,7 +80,7 @@ def _job_to_response(job: Job) -> JobResponse:
80
80
  enabled=job.enabled,
81
81
  partitioned=job.partitioned,
82
82
  backfill_days=job.backfill_days,
83
- source_ids=[s.id for s in job.sources], # type: ignore[misc]
83
+ source_ids=[s.id for s in job.sources],
84
84
  asset_ids=[a.id for a in job.assets if a.id],
85
85
  last_run_at=str(job.last_run_at) if job.last_run_at else None,
86
86
  next_run_at=str(job.next_run_at) if job.next_run_at else None,
@@ -132,7 +132,7 @@ def create_job(
132
132
  partitioned=body.partitioned,
133
133
  backfill_days=body.backfill_days,
134
134
  )
135
- return _job_to_response(store.get_job(job.id)) # type: ignore[arg-type]
135
+ return _job_to_response(store.get_job(job.id))
136
136
 
137
137
 
138
138
  @router.put("/{job_id}")
@@ -157,7 +157,7 @@ def update_job(
157
157
  )
158
158
  except NotFoundError:
159
159
  raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
160
- return _job_to_response(store.get_job(job.id)) # type: ignore[arg-type]
160
+ return _job_to_response(store.get_job(job.id))
161
161
 
162
162
 
163
163
  @router.delete("/{job_id}")
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  from datetime import datetime
7
+ from typing import Any
7
8
  from uuid import UUID
8
9
 
9
10
  from fastapi import APIRouter, Cookie, Depends, HTTPException, Request
@@ -65,7 +66,7 @@ class InvitationResponse(BaseModel):
65
66
  # -- Helpers ------------------------------------------------------------------
66
67
 
67
68
 
68
- def _get_smtp_config() -> object | None:
69
+ def _get_smtp_config() -> Any | None:
69
70
  """Return the SMTP config if available, without raising."""
70
71
  from interloper_api.dependencies import get_smtp_config
71
72
 
@@ -74,8 +75,8 @@ def _get_smtp_config() -> object | None:
74
75
 
75
76
  def _send_invitation_email(
76
77
  request: Request,
77
- smtp_config: object,
78
- invitation: object,
78
+ smtp_config: Any,
79
+ invitation: Any,
79
80
  org_name: str,
80
81
  inviter_name: str,
81
82
  ) -> None:
@@ -88,8 +89,8 @@ def _send_invitation_email(
88
89
  org_name: Organisation name.
89
90
  inviter_name: Inviter display name.
90
91
  """
91
- token = invitation.token # type: ignore[attr-defined]
92
- email = invitation.email # type: ignore[attr-defined]
92
+ token = invitation.token
93
+ email = invitation.email
93
94
  base_url = str(request.base_url).rstrip("/")
94
95
  invite_url = f"{base_url}/invite/{token}"
95
96
 
@@ -116,11 +117,11 @@ def create_organisation(
116
117
  session_token: str | None = Cookie(default=None),
117
118
  ) -> OrganisationResponse:
118
119
  """Create a new organisation. The creating user becomes its admin."""
119
- org = store.create_organisation(name=body.name, creator_id=user.id) # type: ignore[arg-type]
120
+ org = store.create_organisation(name=body.name, creator_id=user.id)
120
121
 
121
122
  # Set as active org in session
122
123
  if session_token:
123
- store.set_session_org(session_token, org.id, user_id=user.id) # type: ignore[arg-type]
124
+ store.set_session_org(session_token, org.id, user_id=user.id)
124
125
 
125
126
  return OrganisationResponse.model_validate(org, from_attributes=True)
126
127
 
@@ -131,7 +132,7 @@ def list_organisations(
131
132
  store: Store = Depends(get_store),
132
133
  ) -> list[OrganisationResponse]:
133
134
  """List all organisations the user belongs to."""
134
- orgs = store.list_user_organisations(user.id) # type: ignore[arg-type]
135
+ orgs = store.list_user_organisations(user.id)
135
136
  return [OrganisationResponse.model_validate(o, from_attributes=True) for o in orgs]
136
137
 
137
138
 
@@ -148,7 +149,7 @@ def list_members(
148
149
  members = store.list_org_members(org_id)
149
150
  return [
150
151
  MemberResponse(
151
- id=profile.id, # type: ignore[arg-type]
152
+ id=profile.id,
152
153
  email=profile.email,
153
154
  name=profile.name,
154
155
  avatar_url=profile.avatar_url,
@@ -188,7 +189,7 @@ def list_invitations(
188
189
  invitations = store.list_invitations(org_id)
189
190
  return [
190
191
  InvitationResponse(
191
- id=inv.id, # type: ignore[arg-type]
192
+ id=inv.id,
192
193
  email=inv.email,
193
194
  role=inv.role,
194
195
  created_at=inv.created_at,
@@ -211,19 +212,19 @@ def invite_member(
211
212
  org_id=org_id,
212
213
  email=body.email.strip(),
213
214
  role=body.role,
214
- invited_by=user.id, # type: ignore[arg-type]
215
+ invited_by=user.id,
215
216
  )
216
217
 
217
218
  # Send invitation email if SMTP is configured
218
219
  smtp_config = _get_smtp_config()
219
- if smtp_config and smtp_config.enabled: # type: ignore[attr-defined]
220
+ if smtp_config and smtp_config.enabled:
220
221
  org = store.get_organisation(org_id)
221
222
  org_name = org.name if org else "Unknown"
222
223
  inviter_name = user.name or user.email
223
224
  _send_invitation_email(request, smtp_config, invitation, org_name, inviter_name)
224
225
 
225
226
  return InvitationResponse(
226
- id=invitation.id, # type: ignore[arg-type]
227
+ id=invitation.id,
227
228
  email=invitation.email,
228
229
  role=invitation.role,
229
230
  created_at=invitation.created_at,
@@ -264,12 +265,12 @@ def resend_invitation(
264
265
  org_id=org_id,
265
266
  email=target.email,
266
267
  role=target.role,
267
- invited_by=user.id, # type: ignore[arg-type]
268
+ invited_by=user.id,
268
269
  )
269
270
 
270
271
  # Send invitation email if SMTP is configured
271
272
  smtp_config = _get_smtp_config()
272
- if smtp_config and smtp_config.enabled: # type: ignore[attr-defined]
273
+ if smtp_config and smtp_config.enabled:
273
274
  org = store.get_organisation(org_id)
274
275
  org_name = org.name if org else "Unknown"
275
276
  inviter_name = user.name or user.email
@@ -74,7 +74,7 @@ def list_resources(
74
74
  resources = store.list_resources(org_id, kind=kind)
75
75
  return [
76
76
  ResourceResponse(
77
- id=r.id, # type: ignore[arg-type]
77
+ id=r.id,
78
78
  org_id=r.org_id,
79
79
  kind=r.kind,
80
80
  key=r.key,
@@ -100,7 +100,7 @@ def get_resource(
100
100
  raise HTTPException(status_code=404, detail=f"Resource {resource_id} not found")
101
101
 
102
102
  return ResourceDetailResponse(
103
- id=r.id, # type: ignore[arg-type]
103
+ id=r.id,
104
104
  org_id=r.org_id,
105
105
  kind=r.kind,
106
106
  key=r.key,
@@ -129,7 +129,7 @@ def create_resource(
129
129
  encrypted=body.encrypted,
130
130
  )
131
131
  return ResourceResponse(
132
- id=resource.id, # type: ignore[arg-type]
132
+ id=resource.id,
133
133
  org_id=resource.org_id,
134
134
  kind=resource.kind,
135
135
  key=resource.key,
@@ -160,7 +160,7 @@ def update_resource(
160
160
  except NotFoundError:
161
161
  raise HTTPException(status_code=404, detail=f"Resource {resource_id} not found")
162
162
  return ResourceResponse(
163
- id=resource.id, # type: ignore[arg-type]
163
+ id=resource.id,
164
164
  org_id=resource.org_id,
165
165
  kind=resource.kind,
166
166
  key=resource.key,
@@ -84,7 +84,7 @@ def _run_to_response(run: Run) -> RunResponse:
84
84
  The response model.
85
85
  """
86
86
  return RunResponse(
87
- id=run.id, # type: ignore[arg-type]
87
+ id=run.id,
88
88
  org_id=run.org_id,
89
89
  job_id=run.job_id,
90
90
  backfill_id=run.backfill_id,
@@ -109,7 +109,7 @@ def _event_to_response(event: Event) -> EventResponse:
109
109
  The response model.
110
110
  """
111
111
  return EventResponse(
112
- id=event.id, # type: ignore[arg-type]
112
+ id=event.id,
113
113
  org_id=event.org_id,
114
114
  run_id=event.run_id,
115
115
  event_type=event.event_type,
@@ -67,31 +67,31 @@ def _resource_map(session: Session, junction_cls: type, fk_column: str, fk_value
67
67
  """Build a {slot_key: resource_id} map from junction rows."""
68
68
  col = getattr(junction_cls, fk_column)
69
69
  rows = session.exec(select(junction_cls).where(col == fk_value)).all()
70
- return {r.key: str(r.resource_id) for r in rows}
70
+ return {r.key: str(r.resource_id) for r in rows} # ty: ignore[unresolved-attribute]
71
71
 
72
72
 
73
73
  def _build_source_response(session: Session, source: Source) -> SourceResponse:
74
74
  """Convert a DB Source to a SourceResponse within a session."""
75
75
  return SourceResponse(
76
- id=source.id, # type: ignore[arg-type]
76
+ id=source.id,
77
77
  org_id=source.org_id,
78
78
  key=source.key,
79
79
  name=source.name,
80
80
  config=source.config,
81
- resources=_resource_map(session, SourceResource, "source_id", source.id), # type: ignore[arg-type]
81
+ resources=_resource_map(session, SourceResource, "source_id", source.id),
82
82
  destinations=[
83
83
  DestinationResponse(
84
- id=d.id, # type: ignore[arg-type]
84
+ id=d.id,
85
85
  key=d.key,
86
86
  name=d.name,
87
87
  config=d.config,
88
- resources=_resource_map(session, DestinationResource, "destination_id", d.id), # type: ignore[arg-type]
88
+ resources=_resource_map(session, DestinationResource, "destination_id", d.id),
89
89
  created_at=str(d.created_at) if d.created_at else None,
90
90
  )
91
91
  for d in source.destinations
92
92
  ],
93
93
  assets=[
94
- AssetResponse(id=a.id, key=a.key, materializable=a.materializable) # type: ignore[arg-type]
94
+ AssetResponse(id=a.id, key=a.key, materializable=a.materializable)
95
95
  for a in source.assets
96
96
  ],
97
97
  created_at=str(source.created_at) if source.created_at else None,
@@ -107,9 +107,9 @@ def _load_source_for_response(source_id: UUID) -> SourceResponse:
107
107
  Source,
108
108
  source_id,
109
109
  options=[
110
- selectinload(Source.assets), # type: ignore[arg-type]
111
- selectinload(Source.resources), # type: ignore[arg-type]
112
- selectinload(Source.destinations).selectinload(Destination.resources), # type: ignore[arg-type]
110
+ selectinload(Source.assets), # ty: ignore[invalid-argument-type]
111
+ selectinload(Source.resources), # ty: ignore[invalid-argument-type]
112
+ selectinload(Source.destinations).selectinload(Destination.resources), # ty: ignore[invalid-argument-type]
113
113
  ],
114
114
  )
115
115
  if not source:
@@ -149,7 +149,7 @@ def create_source(
149
149
  destination_ids=body.destination_ids,
150
150
  cross_deps=body.cross_deps,
151
151
  )
152
- return _load_source_for_response(source.id) # type: ignore[arg-type]
152
+ return _load_source_for_response(source.id)
153
153
 
154
154
 
155
155
  @router.put("/{source_id}")