interloper-api 0.13.0__tar.gz → 0.14.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 (29) hide show
  1. {interloper_api-0.13.0 → interloper_api-0.14.0}/PKG-INFO +1 -1
  2. {interloper_api-0.13.0 → interloper_api-0.14.0}/pyproject.toml +1 -1
  3. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/google_cloud.py +41 -6
  4. {interloper_api-0.13.0 → interloper_api-0.14.0}/README.md +0 -0
  5. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/__init__.py +0 -0
  6. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/app.py +0 -0
  7. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/dependencies.py +0 -0
  8. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/email.py +0 -0
  9. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/__init__.py +0 -0
  10. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/admin.py +0 -0
  11. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/agent.py +0 -0
  12. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/assets.py +0 -0
  13. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/auth.py +0 -0
  14. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/backfills.py +0 -0
  15. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/catalog.py +0 -0
  16. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/destinations.py +0 -0
  17. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/__init__.py +0 -0
  18. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/amazon_ads.py +0 -0
  19. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/facebook_ads.py +0 -0
  20. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/google_ads.py +0 -0
  21. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/pinterest_ads.py +0 -0
  22. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/snapchat_ads.py +0 -0
  23. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/jobs.py +0 -0
  24. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/oauth.py +0 -0
  25. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/organisations.py +0 -0
  26. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/resources.py +0 -0
  27. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/runs.py +0 -0
  28. {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/sources.py +0 -0
  29. {interloper_api-0.13.0 → interloper_api-0.14.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.13.0
3
+ Version: 0.14.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.13.0"
6
+ version = "0.14.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -18,8 +18,13 @@ from interloper_api.routes.external import handle_error
18
18
  sub_router = APIRouter()
19
19
 
20
20
  _TOKEN_URL = "https://oauth2.googleapis.com/token"
21
- _PROJECTS_URL = "https://cloudresourcemanager.googleapis.com/v1/projects"
22
- _SCOPE = "https://www.googleapis.com/auth/cloud-platform.read-only"
21
+ # BigQuery's own projects.list: returns the projects the credential holds a
22
+ # BigQuery role on -- exactly the candidates for a BigQuery destination --
23
+ # and only requires the BigQuery API, which is necessarily enabled wherever
24
+ # the destination can work (unlike the Cloud Resource Manager API, which is
25
+ # frequently disabled).
26
+ _PROJECTS_URL = "https://bigquery.googleapis.com/bigquery/v2/projects"
27
+ _SCOPE = "https://www.googleapis.com/auth/bigquery.readonly"
23
28
 
24
29
 
25
30
  class GoogleCloudConnectionRequest(BaseModel):
@@ -88,7 +93,7 @@ async def _get_access_token(client: httpx.AsyncClient, key_info: dict[str, Any])
88
93
 
89
94
 
90
95
  async def _list_projects(client: httpx.AsyncClient, access_token: str) -> list[dict[str, str]]:
91
- """List the active projects visible to the credential, following pagination.
96
+ """List the projects the credential has BigQuery access to, following pagination.
92
97
 
93
98
  Returns:
94
99
  Project options with ``project_id`` and a display ``name``.
@@ -96,7 +101,7 @@ async def _list_projects(client: httpx.AsyncClient, access_token: str) -> list[d
96
101
  results: list[dict[str, str]] = []
97
102
  page_token: str | None = None
98
103
  while True:
99
- params: dict[str, str] = {"filter": "lifecycleState:ACTIVE"}
104
+ params: dict[str, str] = {"maxResults": "500"}
100
105
  if page_token:
101
106
  params["pageToken"] = page_token
102
107
  resp = await client.get(
@@ -107,8 +112,8 @@ async def _list_projects(client: httpx.AsyncClient, access_token: str) -> list[d
107
112
  resp.raise_for_status()
108
113
  data = resp.json()
109
114
  for project in data.get("projects", []):
110
- project_id = project["projectId"]
111
- name = project.get("name") or project_id
115
+ project_id = project["id"]
116
+ name = project.get("friendlyName") or project_id
112
117
  results.append({"project_id": project_id, "name": f"{name} ({project_id})"})
113
118
  page_token = data.get("nextPageToken")
114
119
  if not page_token:
@@ -116,6 +121,26 @@ async def _list_projects(client: httpx.AsyncClient, access_token: str) -> list[d
116
121
  return sorted(results, key=lambda p: p["name"].lower())
117
122
 
118
123
 
124
+ def _upstream_detail(exc: httpx.HTTPStatusError) -> str:
125
+ """Extract the human-readable error message from a Google error response.
126
+
127
+ The token endpoint answers ``{"error": ..., "error_description": ...}``;
128
+ the Cloud Resource Manager answers ``{"error": {"message": ...}}``.
129
+
130
+ Returns:
131
+ Google's error message, or the raw body as a fallback.
132
+ """
133
+ try:
134
+ payload = exc.response.json()
135
+ except ValueError:
136
+ return exc.response.text[:200]
137
+ error = payload.get("error")
138
+ if isinstance(error, dict):
139
+ return str(error.get("message") or error)
140
+ description = payload.get("error_description")
141
+ return str(description or error or exc.response.text[:200])
142
+
143
+
119
144
  @sub_router.post("/google-cloud/projects")
120
145
  async def google_cloud_projects(
121
146
  body: GoogleCloudConnectionRequest,
@@ -127,6 +152,16 @@ async def google_cloud_projects(
127
152
  async with httpx.AsyncClient(timeout=30) as client:
128
153
  access_token = await _get_access_token(client, key_info)
129
154
  return await _list_projects(client, access_token)
155
+ except httpx.HTTPStatusError as exc:
156
+ # Surface Google's own message (e.g. "Cloud Resource Manager API has
157
+ # not been used in project ...", "Invalid JWT Signature.") so the
158
+ # form error is actionable, instead of the generic handle_error text.
159
+ status = exc.response.status_code
160
+ detail = _upstream_detail(exc)
161
+ raise HTTPException(
162
+ status_code=status if status in (401, 403, 404) else 502,
163
+ detail=f"Google Cloud error while fetching projects: {detail}",
164
+ )
130
165
  except Exception as exc:
131
166
  handle_error(exc, "fetching Google Cloud projects")
132
167
  return [] # unreachable, but satisfies type checker