first
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""Generic read-only Graph passthrough.
|
||||
|
||||
The typed endpoints cover the entities and metrics we expect to use daily, but
|
||||
the Graph API is far larger than that (pages, Instagram accounts, custom
|
||||
audiences, ad rules, …). Rather than force a deploy every time something new is
|
||||
needed, this exposes the whole **read** surface behind one endpoint.
|
||||
|
||||
It is GET-only by construction, so it cannot be used to create, modify or delete
|
||||
anything - the read-only stance of this phase holds even here. Writes will be
|
||||
explicit, typed endpoints with their own guard rails (see
|
||||
documentation/overview.md).
|
||||
|
||||
Credentials: see ``app.credentials.get_meta_credentials``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
|
||||
from ..clients.graph import MetaGraphClient
|
||||
from ..credentials import MetaCredentials, get_meta_credentials
|
||||
from ..errors import UpstreamError
|
||||
|
||||
router = APIRouter(prefix="/graph", tags=["meta-ads: generic read"])
|
||||
|
||||
# Params the proxy owns. A caller must not be able to override the credentials
|
||||
# we derived from the headers, and all_pages is ours, not Graph's.
|
||||
_RESERVED_PARAMS = {"access_token", "appsecret_proof", "all_pages"}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{graph_path:path}",
|
||||
summary="Any Graph API GET (read-only escape hatch)",
|
||||
)
|
||||
async def graph_get(
|
||||
request: Request,
|
||||
graph_path: str = Path(
|
||||
...,
|
||||
description="Graph path WITHOUT the version prefix, e.g. "
|
||||
"'act_123456/customaudiences' or '17841400000000000/media'.",
|
||||
),
|
||||
fields: str | None = Query(None, description="Comma-separated Graph fields."),
|
||||
limit: int | None = Query(None, ge=1, le=500),
|
||||
after: str | None = Query(None, description="Paging cursor."),
|
||||
all_pages: bool = Query(
|
||||
True,
|
||||
description="Follow paging.next and return every page (default, capped "
|
||||
"by META_MAX_PAGES). Set false for a single raw page.",
|
||||
),
|
||||
creds: MetaCredentials = Depends(get_meta_credentials),
|
||||
) -> Any:
|
||||
path = graph_path.strip().lstrip("/")
|
||||
if not path:
|
||||
raise UpstreamError("A Graph path is required.", status=400)
|
||||
|
||||
# The version comes from config/X-Meta-Api-Version; a version in the path
|
||||
# would silently override that, so reject it instead of double-prefixing.
|
||||
first = path.split("/", 1)[0]
|
||||
if first.startswith("v") and first[1:].replace(".", "").isdigit():
|
||||
raise UpstreamError(
|
||||
f"Do not include the API version ('{first}') in the path - it is "
|
||||
"taken from X-Meta-Api-Version or the service default.",
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Forward any extra query params verbatim so the full Graph surface stays
|
||||
# reachable, minus the ones the proxy controls.
|
||||
params: dict[str, Any] = {
|
||||
key: value
|
||||
for key, value in request.query_params.items()
|
||||
if key not in _RESERVED_PARAMS
|
||||
}
|
||||
params.update({"fields": fields, "limit": limit, "after": after})
|
||||
|
||||
client = MetaGraphClient(creds, service_name="Meta Graph API")
|
||||
if all_pages:
|
||||
return await client.get_all_pages(path, params)
|
||||
return await client.get(path, params)
|
||||
Reference in New Issue
Block a user