interloper-api 0.15.0__tar.gz → 0.16.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 (31) hide show
  1. {interloper_api-0.15.0 → interloper_api-0.16.0}/PKG-INFO +1 -1
  2. {interloper_api-0.15.0 → interloper_api-0.16.0}/pyproject.toml +1 -1
  3. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/external/__init__.py +4 -0
  4. interloper_api-0.16.0/src/interloper_api/routes/external/impact.py +54 -0
  5. interloper_api-0.16.0/src/interloper_api/routes/external/tiktok_ads.py +70 -0
  6. {interloper_api-0.15.0 → interloper_api-0.16.0}/README.md +0 -0
  7. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/__init__.py +0 -0
  8. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/app.py +0 -0
  9. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/dependencies.py +0 -0
  10. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/email.py +0 -0
  11. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/__init__.py +0 -0
  12. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/admin.py +0 -0
  13. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/agent.py +0 -0
  14. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/assets.py +0 -0
  15. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/auth.py +0 -0
  16. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/backfills.py +0 -0
  17. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/catalog.py +0 -0
  18. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/destinations.py +0 -0
  19. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/external/amazon_ads.py +0 -0
  20. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/external/facebook_ads.py +0 -0
  21. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/external/google_ads.py +0 -0
  22. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/external/google_cloud.py +0 -0
  23. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/external/pinterest_ads.py +0 -0
  24. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/external/snapchat_ads.py +0 -0
  25. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/jobs.py +0 -0
  26. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/oauth.py +0 -0
  27. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/organisations.py +0 -0
  28. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/resources.py +0 -0
  29. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/runs.py +0 -0
  30. {interloper_api-0.15.0 → interloper_api-0.16.0}/src/interloper_api/routes/sources.py +0 -0
  31. {interloper_api-0.15.0 → interloper_api-0.16.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.0
3
+ Version: 0.16.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.0"
6
+ version = "0.16.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -39,12 +39,16 @@ from interloper_api.routes.external.amazon_ads import sub_router as amazon_ads_r
39
39
  from interloper_api.routes.external.facebook_ads import sub_router as facebook_ads_router # noqa: E402
40
40
  from interloper_api.routes.external.google_ads import sub_router as google_ads_router # noqa: E402
41
41
  from interloper_api.routes.external.google_cloud import sub_router as google_cloud_router # noqa: E402
42
+ from interloper_api.routes.external.impact import sub_router as impact_router # noqa: E402
42
43
  from interloper_api.routes.external.pinterest_ads import sub_router as pinterest_ads_router # noqa: E402
43
44
  from interloper_api.routes.external.snapchat_ads import sub_router as snapchat_ads_router # noqa: E402
45
+ from interloper_api.routes.external.tiktok_ads import sub_router as tiktok_ads_router # noqa: E402
44
46
 
45
47
  router.include_router(amazon_ads_router)
46
48
  router.include_router(facebook_ads_router)
47
49
  router.include_router(google_ads_router)
48
50
  router.include_router(google_cloud_router)
51
+ router.include_router(impact_router)
49
52
  router.include_router(pinterest_ads_router)
50
53
  router.include_router(snapchat_ads_router)
54
+ router.include_router(tiktok_ads_router)
@@ -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 []