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.
Files changed (32) hide show
  1. {interloper_api-0.15.1 → interloper_api-0.17.0}/PKG-INFO +1 -1
  2. {interloper_api-0.15.1 → interloper_api-0.17.0}/pyproject.toml +1 -1
  3. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/app.py +7 -1
  4. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/__init__.py +6 -0
  5. interloper_api-0.17.0/src/interloper_api/routes/external/criteo.py +68 -0
  6. interloper_api-0.17.0/src/interloper_api/routes/external/impact.py +54 -0
  7. interloper_api-0.17.0/src/interloper_api/routes/external/tiktok_ads.py +70 -0
  8. {interloper_api-0.15.1 → interloper_api-0.17.0}/README.md +0 -0
  9. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/__init__.py +0 -0
  10. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/dependencies.py +0 -0
  11. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/email.py +0 -0
  12. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/__init__.py +0 -0
  13. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/admin.py +0 -0
  14. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/agent.py +0 -0
  15. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/assets.py +0 -0
  16. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/auth.py +0 -0
  17. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/backfills.py +0 -0
  18. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/catalog.py +0 -0
  19. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/destinations.py +0 -0
  20. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/amazon_ads.py +0 -0
  21. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/facebook_ads.py +0 -0
  22. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/google_ads.py +0 -0
  23. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/google_cloud.py +0 -0
  24. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/pinterest_ads.py +0 -0
  25. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/external/snapchat_ads.py +0 -0
  26. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/jobs.py +0 -0
  27. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/oauth.py +0 -0
  28. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/organisations.py +0 -0
  29. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/resources.py +0 -0
  30. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/runs.py +0 -0
  31. {interloper_api-0.15.1 → interloper_api-0.17.0}/src/interloper_api/routes/sources.py +0 -0
  32. {interloper_api-0.15.1 → interloper_api-0.17.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.15.1
3
+ Version: 0.17.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.15.1"
6
+ version = "0.17.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -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
- pass
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]:
@@ -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 []