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.
- {interloper_api-0.13.0 → interloper_api-0.14.0}/PKG-INFO +1 -1
- {interloper_api-0.13.0 → interloper_api-0.14.0}/pyproject.toml +1 -1
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/google_cloud.py +41 -6
- {interloper_api-0.13.0 → interloper_api-0.14.0}/README.md +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/__init__.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/app.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/dependencies.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/email.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/__init__.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/admin.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/agent.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/assets.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/auth.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/backfills.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/catalog.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/destinations.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/__init__.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/amazon_ads.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/facebook_ads.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/google_ads.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/pinterest_ads.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/snapchat_ads.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/jobs.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/oauth.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/organisations.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/resources.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/runs.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/sources.py +0 -0
- {interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/ws.py +0 -0
{interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/google_cloud.py
RENAMED
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|
|
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] = {"
|
|
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["
|
|
111
|
-
name = project.get("
|
|
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
|
|
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
|
{interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/__init__.py
RENAMED
|
File without changes
|
{interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/amazon_ads.py
RENAMED
|
File without changes
|
{interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/facebook_ads.py
RENAMED
|
File without changes
|
{interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/google_ads.py
RENAMED
|
File without changes
|
{interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/pinterest_ads.py
RENAMED
|
File without changes
|
{interloper_api-0.13.0 → interloper_api-0.14.0}/src/interloper_api/routes/external/snapchat_ads.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|