interloper-api 0.15.1__tar.gz → 0.17.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.15.1 → interloper_api-0.17.0}/PKG-INFO +1 -1
- {interloper_api-0.15.1 → interloper_api-0.17.0}/pyproject.toml +1 -1
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/app.py +7 -1
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/__init__.py +6 -0
- interloper_api-0.17.0/src/interloper_api/routes/external/criteo.py +68 -0
- interloper_api-0.17.0/src/interloper_api/routes/external/impact.py +54 -0
- interloper_api-0.17.0/src/interloper_api/routes/external/tiktok_ads.py +70 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/README.md +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/__init__.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/dependencies.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/email.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/__init__.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/admin.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/agent.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/assets.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/auth.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/backfills.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/catalog.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/destinations.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/amazon_ads.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/facebook_ads.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/google_ads.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/google_cloud.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/pinterest_ads.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/snapchat_ads.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/jobs.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/oauth.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/organisations.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/resources.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/runs.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/sources.py +0 -0
- {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/ws.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from fastapi import APIRouter, FastAPI
|
|
@@ -27,6 +28,8 @@ from interloper_api.routes import (
|
|
|
27
28
|
)
|
|
28
29
|
from interloper_api.routes import catalog as catalog_routes
|
|
29
30
|
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
30
33
|
|
|
31
34
|
def create_app(
|
|
32
35
|
store: Store | None = None,
|
|
@@ -91,7 +94,10 @@ def create_app(
|
|
|
91
94
|
|
|
92
95
|
api.include_router(agent_routes.router, prefix="/agent", tags=["agent"])
|
|
93
96
|
except ImportError:
|
|
94
|
-
|
|
97
|
+
logger.warning(
|
|
98
|
+
"Agent routes not mounted: the 'agent' extra is not installed "
|
|
99
|
+
"(install interloper-api[agent]); /agent endpoints will return 404."
|
|
100
|
+
)
|
|
95
101
|
|
|
96
102
|
@api.get("/health")
|
|
97
103
|
def health() -> dict[str, str]:
|
{interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/__init__.py
RENAMED
|
@@ -36,15 +36,21 @@ def handle_error(error: Exception, context: str) -> None:
|
|
|
36
36
|
|
|
37
37
|
# Import and register sub-routers after helpers are defined.
|
|
38
38
|
from interloper_api.routes.external.amazon_ads import sub_router as amazon_ads_router # noqa: E402
|
|
39
|
+
from interloper_api.routes.external.criteo import sub_router as criteo_router # noqa: E402
|
|
39
40
|
from interloper_api.routes.external.facebook_ads import sub_router as facebook_ads_router # noqa: E402
|
|
40
41
|
from interloper_api.routes.external.google_ads import sub_router as google_ads_router # noqa: E402
|
|
41
42
|
from interloper_api.routes.external.google_cloud import sub_router as google_cloud_router # noqa: E402
|
|
43
|
+
from interloper_api.routes.external.impact import sub_router as impact_router # noqa: E402
|
|
42
44
|
from interloper_api.routes.external.pinterest_ads import sub_router as pinterest_ads_router # noqa: E402
|
|
43
45
|
from interloper_api.routes.external.snapchat_ads import sub_router as snapchat_ads_router # noqa: E402
|
|
46
|
+
from interloper_api.routes.external.tiktok_ads import sub_router as tiktok_ads_router # noqa: E402
|
|
44
47
|
|
|
45
48
|
router.include_router(amazon_ads_router)
|
|
49
|
+
router.include_router(criteo_router)
|
|
46
50
|
router.include_router(facebook_ads_router)
|
|
47
51
|
router.include_router(google_ads_router)
|
|
48
52
|
router.include_router(google_cloud_router)
|
|
53
|
+
router.include_router(impact_router)
|
|
49
54
|
router.include_router(pinterest_ads_router)
|
|
50
55
|
router.include_router(snapchat_ads_router)
|
|
56
|
+
router.include_router(tiktok_ads_router)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Criteo external API routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from fastapi import APIRouter, Depends
|
|
7
|
+
from interloper_db import Profile
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from interloper_api.dependencies import require_viewer
|
|
11
|
+
from interloper_api.routes.external import handle_error
|
|
12
|
+
|
|
13
|
+
sub_router = APIRouter()
|
|
14
|
+
|
|
15
|
+
_BASE_URL = "https://api.criteo.com"
|
|
16
|
+
_TOKEN_URL = f"{_BASE_URL}/oauth2/token"
|
|
17
|
+
_ADVERTISERS_URL = f"{_BASE_URL}/2025-04/advertisers/me"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CriteoConnectionRequest(BaseModel):
|
|
21
|
+
"""Criteo connection credentials (matches CriteoConnection fields)."""
|
|
22
|
+
|
|
23
|
+
client_id: str
|
|
24
|
+
client_secret: str
|
|
25
|
+
refresh_token: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _get_access_token(client: httpx.AsyncClient, body: CriteoConnectionRequest) -> str:
|
|
29
|
+
"""Exchange the refresh token for an access token."""
|
|
30
|
+
resp = await client.post(
|
|
31
|
+
_TOKEN_URL,
|
|
32
|
+
data={
|
|
33
|
+
"grant_type": "refresh_token",
|
|
34
|
+
"client_id": body.client_id,
|
|
35
|
+
"client_secret": body.client_secret,
|
|
36
|
+
"refresh_token": body.refresh_token,
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
resp.raise_for_status()
|
|
40
|
+
return resp.json()["access_token"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _list_advertisers(client: httpx.AsyncClient, body: CriteoConnectionRequest) -> list[dict[str, str]]:
|
|
44
|
+
"""List the advertisers the connection can access, sorted by name."""
|
|
45
|
+
token = await _get_access_token(client, body)
|
|
46
|
+
resp = await client.get(_ADVERTISERS_URL, headers={"Authorization": f"Bearer {token}"})
|
|
47
|
+
resp.raise_for_status()
|
|
48
|
+
|
|
49
|
+
advertisers = resp.json().get("data", [])
|
|
50
|
+
results = [
|
|
51
|
+
{"id": a["id"], "name": a.get("attributes", {}).get("advertiserName") or a["id"]}
|
|
52
|
+
for a in advertisers
|
|
53
|
+
]
|
|
54
|
+
return sorted(results, key=lambda a: a["name"].lower())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@sub_router.post("/criteo/advertisers")
|
|
58
|
+
async def criteo_advertisers(
|
|
59
|
+
body: CriteoConnectionRequest,
|
|
60
|
+
_user: Profile = Depends(require_viewer),
|
|
61
|
+
) -> list[dict[str, str]]:
|
|
62
|
+
"""Fetch the Criteo advertisers accessible by the connection."""
|
|
63
|
+
try:
|
|
64
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
65
|
+
return await _list_advertisers(client, body)
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
handle_error(exc, "fetching Criteo advertisers")
|
|
68
|
+
return []
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Impact external API routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from fastapi import APIRouter, Depends
|
|
7
|
+
from interloper_db import Profile
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from interloper_api.dependencies import require_viewer
|
|
11
|
+
from interloper_api.routes.external import handle_error
|
|
12
|
+
|
|
13
|
+
sub_router = APIRouter()
|
|
14
|
+
|
|
15
|
+
_BASE_URL = "https://api.impact.com"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ImpactConnectionRequest(BaseModel):
|
|
19
|
+
"""Impact connection credentials (matches ImpactConnection fields)."""
|
|
20
|
+
|
|
21
|
+
account_sid: str
|
|
22
|
+
auth_token: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@sub_router.post("/impact/programs")
|
|
26
|
+
async def impact_programs(
|
|
27
|
+
body: ImpactConnectionRequest,
|
|
28
|
+
_user: Profile = Depends(require_viewer),
|
|
29
|
+
) -> list[dict[str, str]]:
|
|
30
|
+
"""Fetch Impact programs (campaigns) accessible by the connection."""
|
|
31
|
+
try:
|
|
32
|
+
programs: list[dict[str, str]] = []
|
|
33
|
+
async with httpx.AsyncClient(
|
|
34
|
+
timeout=30,
|
|
35
|
+
auth=httpx.BasicAuth(body.account_sid, body.auth_token),
|
|
36
|
+
headers={"Accept": "application/json"},
|
|
37
|
+
) as client:
|
|
38
|
+
page, num_pages = 1, 1
|
|
39
|
+
while page <= num_pages:
|
|
40
|
+
resp = await client.get(
|
|
41
|
+
f"{_BASE_URL}/Advertisers/{body.account_sid}/Campaigns",
|
|
42
|
+
params={"Page": page},
|
|
43
|
+
)
|
|
44
|
+
resp.raise_for_status()
|
|
45
|
+
data = resp.json()
|
|
46
|
+
num_pages = int(data["@numpages"])
|
|
47
|
+
for campaign in data.get("Campaigns", []):
|
|
48
|
+
programs.append({"Id": campaign["Id"], "Name": campaign.get("Name", campaign["Id"])})
|
|
49
|
+
page += 1
|
|
50
|
+
|
|
51
|
+
return programs
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
handle_error(exc, "fetching Impact programs")
|
|
54
|
+
return []
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""TikTok Ads external API routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
9
|
+
from interloper_db import Profile
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from interloper_api.dependencies import require_viewer
|
|
13
|
+
from interloper_api.routes.external import handle_error
|
|
14
|
+
|
|
15
|
+
sub_router = APIRouter()
|
|
16
|
+
|
|
17
|
+
_BASE_URL = "https://business-api.tiktok.com/open_api/v1.3"
|
|
18
|
+
_ADVERTISERS_URL = f"{_BASE_URL}/oauth2/advertiser/get/"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TiktokAdsConnectionRequest(BaseModel):
|
|
22
|
+
"""TikTok Ads connection credentials (matches TiktokAdsConnection fields)."""
|
|
23
|
+
|
|
24
|
+
access_token: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _list_advertisers(
|
|
28
|
+
client: httpx.AsyncClient,
|
|
29
|
+
access_token: str,
|
|
30
|
+
app_id: str,
|
|
31
|
+
secret: str,
|
|
32
|
+
) -> list[dict[str, str]]:
|
|
33
|
+
"""List the advertisers the access token can access, sorted by name."""
|
|
34
|
+
resp = await client.get(
|
|
35
|
+
_ADVERTISERS_URL,
|
|
36
|
+
params={"app_id": app_id, "secret": secret},
|
|
37
|
+
headers={"Access-Token": access_token},
|
|
38
|
+
)
|
|
39
|
+
resp.raise_for_status()
|
|
40
|
+
body = resp.json()
|
|
41
|
+
if body.get("code") != 0:
|
|
42
|
+
raise HTTPException(status_code=502, detail=f"TikTok API error: {body.get('message')}")
|
|
43
|
+
|
|
44
|
+
advertisers = body.get("data", {}).get("list", [])
|
|
45
|
+
results = [
|
|
46
|
+
{"advertiser_id": a["advertiser_id"], "name": a.get("advertiser_name") or a["advertiser_id"]}
|
|
47
|
+
for a in advertisers
|
|
48
|
+
]
|
|
49
|
+
return sorted(results, key=lambda a: a["name"].lower())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@sub_router.post("/tiktok-ads/advertisers")
|
|
53
|
+
async def tiktok_ads_advertisers(
|
|
54
|
+
body: TiktokAdsConnectionRequest,
|
|
55
|
+
_user: Profile = Depends(require_viewer),
|
|
56
|
+
) -> list[dict[str, str]]:
|
|
57
|
+
"""Fetch the TikTok advertisers accessible by the connection.
|
|
58
|
+
|
|
59
|
+
Listing advertisers needs the connector app's ``app_id`` / ``secret`` — read
|
|
60
|
+
from the provider-scoped environment (``TIKTOK_CLIENT_ID`` /
|
|
61
|
+
``TIKTOK_CLIENT_SECRET``) — alongside the connection's access token.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
app_id = os.environ.get("TIKTOK_CLIENT_ID", "")
|
|
65
|
+
secret = os.environ.get("TIKTOK_CLIENT_SECRET", "")
|
|
66
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
67
|
+
return await _list_advertisers(client, body.access_token, app_id, secret)
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
handle_error(exc, "fetching TikTok Ads advertisers")
|
|
70
|
+
return []
|
|
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.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/amazon_ads.py
RENAMED
|
File without changes
|
{interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/facebook_ads.py
RENAMED
|
File without changes
|
{interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/google_ads.py
RENAMED
|
File without changes
|
{interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/google_cloud.py
RENAMED
|
File without changes
|
{interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/pinterest_ads.py
RENAMED
|
File without changes
|
{interloper_api-0.15.1 → interloper_api-0.17.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
|