works but need to do more research on how the token should be exactly handled
This commit is contained in:
140
src/meta_api_grabber/discover_accounts.py
Normal file
140
src/meta_api_grabber/discover_accounts.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
Discover all ad accounts accessible via different methods:
|
||||||
|
1. User ad accounts (personal access)
|
||||||
|
2. Business Manager ad accounts (app-level access)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from facebook_business.adobjects.user import User
|
||||||
|
from facebook_business.adobjects.business import Business
|
||||||
|
from facebook_business.api import FacebookAdsApi
|
||||||
|
|
||||||
|
|
||||||
|
def discover_accounts():
|
||||||
|
"""Discover ad accounts through multiple methods."""
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||||
|
app_secret = os.getenv("META_APP_SECRET")
|
||||||
|
app_id = os.getenv("META_APP_ID")
|
||||||
|
|
||||||
|
if not all([access_token, app_secret, app_id]):
|
||||||
|
print("❌ Missing required environment variables")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Initialize Facebook Ads API
|
||||||
|
FacebookAdsApi.init(
|
||||||
|
app_id=app_id,
|
||||||
|
app_secret=app_secret,
|
||||||
|
access_token=access_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("="*70)
|
||||||
|
print("AD ACCOUNT DISCOVERY")
|
||||||
|
print("="*70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Method 1: User ad accounts (what we currently use)
|
||||||
|
print("METHOD 1: User Ad Accounts (Personal Access)")
|
||||||
|
print("-"*70)
|
||||||
|
try:
|
||||||
|
me = User(fbid='me')
|
||||||
|
account_fields = ['name', 'account_id']
|
||||||
|
user_accounts = me.get_ad_accounts(fields=account_fields)
|
||||||
|
user_account_list = list(user_accounts)
|
||||||
|
|
||||||
|
print(f"Found {len(user_account_list)} user ad account(s):")
|
||||||
|
for acc in user_account_list:
|
||||||
|
print(f" - {acc.get('name')} ({acc.get('id')})")
|
||||||
|
print()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error getting user accounts: {e}\n")
|
||||||
|
user_account_list = []
|
||||||
|
|
||||||
|
# Method 2: Business Manager accounts
|
||||||
|
print("METHOD 2: Business Manager Ad Accounts (App-Level Access)")
|
||||||
|
print("-"*70)
|
||||||
|
try:
|
||||||
|
me = User(fbid='me')
|
||||||
|
# Get businesses this user/app has access to
|
||||||
|
businesses = me.get_businesses(fields=['id', 'name'])
|
||||||
|
business_list = list(businesses)
|
||||||
|
|
||||||
|
if not business_list:
|
||||||
|
print("No Business Managers found.")
|
||||||
|
print()
|
||||||
|
print("ℹ️ To access ad accounts at the app level, you need to:")
|
||||||
|
print(" 1. Have a Meta Business Manager")
|
||||||
|
print(" 2. Add your app to the Business Manager")
|
||||||
|
print(" 3. Grant the app access to ad accounts")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print(f"Found {len(business_list)} Business Manager(s):")
|
||||||
|
all_business_accounts = []
|
||||||
|
|
||||||
|
for biz in business_list:
|
||||||
|
biz_id = biz.get('id')
|
||||||
|
biz_name = biz.get('name')
|
||||||
|
print(f"\n Business: {biz_name} ({biz_id})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
business = Business(fbid=biz_id)
|
||||||
|
# Get all ad accounts owned/managed by this business
|
||||||
|
biz_accounts = business.get_owned_ad_accounts(
|
||||||
|
fields=['id', 'name', 'account_status']
|
||||||
|
)
|
||||||
|
biz_account_list = list(biz_accounts)
|
||||||
|
|
||||||
|
print(f" Ad Accounts: {len(biz_account_list)}")
|
||||||
|
for acc in biz_account_list:
|
||||||
|
print(f" - {acc.get('name')} ({acc.get('id')})")
|
||||||
|
all_business_accounts.append(acc)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error accessing business accounts: {e}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Total Business Manager ad accounts: {len(all_business_accounts)}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error getting businesses: {e}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Method 3: App-level access token (for reference)
|
||||||
|
print()
|
||||||
|
print("METHOD 3: App Access Token (NOT recommended for ad accounts)")
|
||||||
|
print("-"*70)
|
||||||
|
print("App access tokens can be generated with:")
|
||||||
|
print(f" curl 'https://graph.facebook.com/oauth/access_token")
|
||||||
|
print(f" ?client_id={app_id}")
|
||||||
|
print(f" &client_secret=YOUR_SECRET")
|
||||||
|
print(f" &grant_type=client_credentials'")
|
||||||
|
print()
|
||||||
|
print("⚠️ However, app access tokens CANNOT access ad accounts directly.")
|
||||||
|
print(" The Marketing API requires user-level permissions for privacy/security.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("="*70)
|
||||||
|
print("SUMMARY")
|
||||||
|
print("="*70)
|
||||||
|
print(f"User Ad Accounts: {len(user_account_list)}")
|
||||||
|
print()
|
||||||
|
print("💡 RECOMMENDATION:")
|
||||||
|
print(" The current implementation using User.get_ad_accounts() is correct.")
|
||||||
|
print(" To access MORE ad accounts, you have two options:")
|
||||||
|
print()
|
||||||
|
print(" Option 1: Use a System User token from Business Manager")
|
||||||
|
print(" (grants access to all Business Manager ad accounts)")
|
||||||
|
print()
|
||||||
|
print(" Option 2: Have other users authorize your app")
|
||||||
|
print(" (each user's token will see their ad accounts)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
exit(discover_accounts())
|
||||||
@@ -5,12 +5,13 @@ Runs periodically to build time-series data for dashboards.
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from facebook_business.adobjects.adaccount import AdAccount
|
from facebook_business.adobjects.adaccount import AdAccount
|
||||||
from facebook_business.adobjects.adsinsights import AdsInsights
|
from facebook_business.adobjects.adsinsights import AdsInsights
|
||||||
|
from facebook_business.adobjects.user import User
|
||||||
from facebook_business.api import FacebookAdsApi
|
from facebook_business.api import FacebookAdsApi
|
||||||
|
|
||||||
from .database import TimescaleDBClient
|
from .database import TimescaleDBClient
|
||||||
@@ -31,13 +32,14 @@ class ScheduledInsightsGrabber:
|
|||||||
- Auto-throttles when approaching limits
|
- Auto-throttles when approaching limits
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, access_token: Optional[str] = None, auto_refresh_token: bool = True):
|
def __init__(self, access_token: Optional[str] = None, auto_refresh_token: bool = True, max_accounts: Optional[int] = None):
|
||||||
"""
|
"""
|
||||||
Initialize the scheduled grabber.
|
Initialize the scheduled grabber.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
access_token: Optional access token. If not provided, loads from env.
|
access_token: Optional access token. If not provided, loads from env.
|
||||||
auto_refresh_token: If True, automatically refreshes tokens before expiry
|
auto_refresh_token: If True, automatically refreshes tokens before expiry
|
||||||
|
max_accounts: Maximum number of ad accounts to process. None = all accounts, or set to a limit (e.g., 3)
|
||||||
"""
|
"""
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ class ScheduledInsightsGrabber:
|
|||||||
|
|
||||||
self.app_secret = os.getenv("META_APP_SECRET")
|
self.app_secret = os.getenv("META_APP_SECRET")
|
||||||
self.app_id = os.getenv("META_APP_ID")
|
self.app_id = os.getenv("META_APP_ID")
|
||||||
self.ad_account_id = os.getenv("META_AD_ACCOUNT_ID")
|
self.max_accounts = max_accounts
|
||||||
|
|
||||||
if not self.access_token:
|
if not self.access_token:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -62,15 +64,16 @@ class ScheduledInsightsGrabber:
|
|||||||
"run 'uv run python src/meta_api_grabber/auth.py'"
|
"run 'uv run python src/meta_api_grabber/auth.py'"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not all([self.app_secret, self.app_id, self.ad_account_id]):
|
if not all([self.app_secret, self.app_id]):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Missing required environment variables (META_APP_SECRET, META_APP_ID, META_AD_ACCOUNT_ID)"
|
"Missing required environment variables (META_APP_SECRET, META_APP_ID)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize Facebook Ads API
|
# Initialize Facebook Ads API
|
||||||
self._init_api()
|
self._init_api()
|
||||||
|
|
||||||
self.ad_account = AdAccount(self.ad_account_id)
|
# Will store list of ad account IDs
|
||||||
|
self.ad_account_ids = []
|
||||||
|
|
||||||
# Database client
|
# Database client
|
||||||
self.db: Optional[TimescaleDBClient] = None
|
self.db: Optional[TimescaleDBClient] = None
|
||||||
@@ -102,10 +105,37 @@ class ScheduledInsightsGrabber:
|
|||||||
print("🔄 Token was refreshed, reinitializing API...")
|
print("🔄 Token was refreshed, reinitializing API...")
|
||||||
self.access_token = new_token
|
self.access_token = new_token
|
||||||
self._init_api()
|
self._init_api()
|
||||||
self.ad_account = AdAccount(self.ad_account_id)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Token refresh check failed: {e}")
|
print(f"⚠️ Token refresh check failed: {e}")
|
||||||
|
|
||||||
|
async def load_ad_accounts(self):
|
||||||
|
"""
|
||||||
|
Load all accessible ad accounts from Meta API.
|
||||||
|
Respects max_accounts limit if set.
|
||||||
|
"""
|
||||||
|
print("Loading accessible ad accounts...")
|
||||||
|
me = User(fbid='me')
|
||||||
|
account_fields = ['name']
|
||||||
|
|
||||||
|
ad_accounts = await self._rate_limited_request(
|
||||||
|
me.get_ad_accounts,
|
||||||
|
fields=account_fields
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert to list and apply limit
|
||||||
|
all_accounts = list(ad_accounts)
|
||||||
|
if self.max_accounts:
|
||||||
|
all_accounts = all_accounts[:self.max_accounts]
|
||||||
|
print(f" Limiting to first {self.max_accounts} account(s)")
|
||||||
|
|
||||||
|
self.ad_account_ids = [acc['id'] for acc in all_accounts]
|
||||||
|
|
||||||
|
print(f" Loaded {len(self.ad_account_ids)} ad account(s):")
|
||||||
|
for acc_id in self.ad_account_ids:
|
||||||
|
print(f" - {acc_id}")
|
||||||
|
|
||||||
|
return self.ad_account_ids
|
||||||
|
|
||||||
async def _rate_limited_request(self, func, *args, **kwargs):
|
async def _rate_limited_request(self, func, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Execute a request with intelligent rate limiting and backoff.
|
Execute a request with intelligent rate limiting and backoff.
|
||||||
@@ -118,45 +148,47 @@ class ScheduledInsightsGrabber:
|
|||||||
"""
|
"""
|
||||||
return await self.rate_limiter.execute_with_retry(func, *args, **kwargs)
|
return await self.rate_limiter.execute_with_retry(func, *args, **kwargs)
|
||||||
|
|
||||||
async def cache_account_metadata(self):
|
async def cache_account_metadata(self, account_id: str):
|
||||||
"""
|
"""
|
||||||
Cache ad account metadata to database.
|
Cache ad account metadata to database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account_id: Ad account ID to cache
|
||||||
|
|
||||||
This reduces API calls by storing rarely-changing account info.
|
This reduces API calls by storing rarely-changing account info.
|
||||||
"""
|
"""
|
||||||
print("Caching account metadata...")
|
|
||||||
|
|
||||||
# Get account details
|
# Get account details
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
account_fields = ['name', 'currency', 'timezone_name']
|
account_fields = ['name', 'currency', 'timezone_name']
|
||||||
account_data = await self._rate_limited_request(
|
account_data = await self._rate_limited_request(
|
||||||
self.ad_account.api_get,
|
ad_account.api_get,
|
||||||
fields=account_fields
|
fields=account_fields
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store in database
|
# Store in database
|
||||||
await self.db.upsert_ad_account(
|
await self.db.upsert_ad_account(
|
||||||
account_id=self.ad_account_id,
|
account_id=account_id,
|
||||||
account_name=account_data.get('name'),
|
account_name=account_data.get('name'),
|
||||||
currency=account_data.get('currency'),
|
currency=account_data.get('currency'),
|
||||||
timezone_name=account_data.get('timezone_name'),
|
timezone_name=account_data.get('timezone_name'),
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f" Account cached: {account_data.get('name')}")
|
print(f" Account cached: {account_data.get('name')} ({account_id})")
|
||||||
|
|
||||||
async def cache_campaigns_metadata(self, limit: int = 100):
|
async def cache_campaigns_metadata(self, account_id: str, limit: int = 100):
|
||||||
"""
|
"""
|
||||||
Cache campaign metadata to database.
|
Cache campaign metadata to database.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
account_id: Ad account ID
|
||||||
limit: Maximum number of campaigns to cache
|
limit: Maximum number of campaigns to cache
|
||||||
"""
|
"""
|
||||||
print("Caching campaign metadata...")
|
|
||||||
|
|
||||||
# Get campaigns
|
# Get campaigns
|
||||||
from facebook_business.adobjects.campaign import Campaign
|
from facebook_business.adobjects.campaign import Campaign
|
||||||
|
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
campaigns = await self._rate_limited_request(
|
campaigns = await self._rate_limited_request(
|
||||||
self.ad_account.get_campaigns,
|
ad_account.get_campaigns,
|
||||||
fields=[
|
fields=[
|
||||||
Campaign.Field.name,
|
Campaign.Field.name,
|
||||||
Campaign.Field.status,
|
Campaign.Field.status,
|
||||||
@@ -169,29 +201,29 @@ class ScheduledInsightsGrabber:
|
|||||||
for campaign in campaigns:
|
for campaign in campaigns:
|
||||||
await self.db.upsert_campaign(
|
await self.db.upsert_campaign(
|
||||||
campaign_id=campaign['id'],
|
campaign_id=campaign['id'],
|
||||||
account_id=self.ad_account_id,
|
account_id=account_id,
|
||||||
campaign_name=campaign.get('name', 'Unknown'),
|
campaign_name=campaign.get('name', 'Unknown'),
|
||||||
status=campaign.get('status'),
|
status=campaign.get('status'),
|
||||||
objective=campaign.get('objective'),
|
objective=campaign.get('objective'),
|
||||||
)
|
)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
print(f" {count} campaigns cached")
|
print(f" {count} campaigns cached for {account_id}")
|
||||||
|
|
||||||
async def cache_adsets_metadata(self, limit: int = 100):
|
async def cache_adsets_metadata(self, account_id: str, limit: int = 100):
|
||||||
"""
|
"""
|
||||||
Cache ad set metadata to database.
|
Cache ad set metadata to database.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
account_id: Ad account ID
|
||||||
limit: Maximum number of ad sets to cache
|
limit: Maximum number of ad sets to cache
|
||||||
"""
|
"""
|
||||||
print("Caching ad set metadata...")
|
|
||||||
|
|
||||||
# Get ad sets
|
# Get ad sets
|
||||||
from facebook_business.adobjects.adset import AdSet
|
from facebook_business.adobjects.adset import AdSet
|
||||||
|
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
adsets = await self._rate_limited_request(
|
adsets = await self._rate_limited_request(
|
||||||
self.ad_account.get_ad_sets,
|
ad_account.get_ad_sets,
|
||||||
fields=[
|
fields=[
|
||||||
AdSet.Field.name,
|
AdSet.Field.name,
|
||||||
AdSet.Field.campaign_id,
|
AdSet.Field.campaign_id,
|
||||||
@@ -210,17 +242,16 @@ class ScheduledInsightsGrabber:
|
|||||||
)
|
)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
print(f" {count} ad sets cached")
|
print(f" {count} ad sets cached for {account_id}")
|
||||||
|
|
||||||
async def grab_account_insights(self, date_preset: str = "today"):
|
async def grab_account_insights(self, account_id: str, date_preset: str = "today"):
|
||||||
"""
|
"""
|
||||||
Grab and store account-level insights.
|
Grab and store account-level insights.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
account_id: Ad account ID
|
||||||
date_preset: Meta date preset (default: 'today')
|
date_preset: Meta date preset (default: 'today')
|
||||||
"""
|
"""
|
||||||
print(f"Grabbing account insights ({date_preset})...")
|
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
AdsInsights.Field.impressions,
|
AdsInsights.Field.impressions,
|
||||||
AdsInsights.Field.clicks,
|
AdsInsights.Field.clicks,
|
||||||
@@ -240,34 +271,36 @@ class ScheduledInsightsGrabber:
|
|||||||
"level": "account",
|
"level": "account",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
insights = await self._rate_limited_request(
|
insights = await self._rate_limited_request(
|
||||||
self.ad_account.get_insights,
|
ad_account.get_insights,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
params=params,
|
params=params,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store insights
|
# Store insights
|
||||||
timestamp = datetime.now(timezone.utc)
|
timestamp = datetime.now(timezone.utc)
|
||||||
|
count = 0
|
||||||
for insight in insights:
|
for insight in insights:
|
||||||
await self.db.insert_account_insights(
|
await self.db.insert_account_insights(
|
||||||
time=timestamp,
|
time=timestamp,
|
||||||
account_id=self.ad_account_id,
|
account_id=account_id,
|
||||||
data=dict(insight),
|
data=dict(insight),
|
||||||
date_preset=date_preset,
|
date_preset=date_preset,
|
||||||
)
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
print(f" Account insights stored ({len(list(insights))} records)")
|
print(f" Account insights stored for {account_id} ({count} records)")
|
||||||
|
|
||||||
async def grab_campaign_insights(self, date_preset: str = "today", limit: int = 50):
|
async def grab_campaign_insights(self, account_id: str, date_preset: str = "today", limit: int = 50):
|
||||||
"""
|
"""
|
||||||
Grab and store campaign-level insights.
|
Grab and store campaign-level insights.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
account_id: Ad account ID
|
||||||
date_preset: Meta date preset
|
date_preset: Meta date preset
|
||||||
limit: Maximum number of campaigns
|
limit: Maximum number of campaigns
|
||||||
"""
|
"""
|
||||||
print(f"Grabbing campaign insights ({date_preset}, limit={limit})...")
|
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
AdsInsights.Field.campaign_id,
|
AdsInsights.Field.campaign_id,
|
||||||
AdsInsights.Field.campaign_name,
|
AdsInsights.Field.campaign_name,
|
||||||
@@ -287,8 +320,9 @@ class ScheduledInsightsGrabber:
|
|||||||
"limit": limit,
|
"limit": limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
insights = await self._rate_limited_request(
|
insights = await self._rate_limited_request(
|
||||||
self.ad_account.get_insights,
|
ad_account.get_insights,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
params=params,
|
params=params,
|
||||||
)
|
)
|
||||||
@@ -302,24 +336,23 @@ class ScheduledInsightsGrabber:
|
|||||||
await self.db.insert_campaign_insights(
|
await self.db.insert_campaign_insights(
|
||||||
time=timestamp,
|
time=timestamp,
|
||||||
campaign_id=campaign_id,
|
campaign_id=campaign_id,
|
||||||
account_id=self.ad_account_id,
|
account_id=account_id,
|
||||||
data=dict(insight),
|
data=dict(insight),
|
||||||
date_preset=date_preset,
|
date_preset=date_preset,
|
||||||
)
|
)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
print(f" Campaign insights stored ({count} records)")
|
print(f" Campaign insights stored for {account_id} ({count} records)")
|
||||||
|
|
||||||
async def grab_adset_insights(self, date_preset: str = "today", limit: int = 50):
|
async def grab_adset_insights(self, account_id: str, date_preset: str = "today", limit: int = 50):
|
||||||
"""
|
"""
|
||||||
Grab and store ad set level insights.
|
Grab and store ad set level insights.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
account_id: Ad account ID
|
||||||
date_preset: Meta date preset
|
date_preset: Meta date preset
|
||||||
limit: Maximum number of ad sets
|
limit: Maximum number of ad sets
|
||||||
"""
|
"""
|
||||||
print(f"Grabbing ad set insights ({date_preset}, limit={limit})...")
|
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
AdsInsights.Field.adset_id,
|
AdsInsights.Field.adset_id,
|
||||||
AdsInsights.Field.adset_name,
|
AdsInsights.Field.adset_name,
|
||||||
@@ -340,8 +373,9 @@ class ScheduledInsightsGrabber:
|
|||||||
"limit": limit,
|
"limit": limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
insights = await self._rate_limited_request(
|
insights = await self._rate_limited_request(
|
||||||
self.ad_account.get_insights,
|
ad_account.get_insights,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
params=params,
|
params=params,
|
||||||
)
|
)
|
||||||
@@ -357,17 +391,17 @@ class ScheduledInsightsGrabber:
|
|||||||
time=timestamp,
|
time=timestamp,
|
||||||
adset_id=adset_id,
|
adset_id=adset_id,
|
||||||
campaign_id=campaign_id,
|
campaign_id=campaign_id,
|
||||||
account_id=self.ad_account_id,
|
account_id=account_id,
|
||||||
data=dict(insight),
|
data=dict(insight),
|
||||||
date_preset=date_preset,
|
date_preset=date_preset,
|
||||||
)
|
)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
print(f" Ad set insights stored ({count} records)")
|
print(f" Ad set insights stored for {account_id} ({count} records)")
|
||||||
|
|
||||||
async def run_collection_cycle(self, cache_metadata: bool = True):
|
async def run_collection_cycle(self, cache_metadata: bool = True):
|
||||||
"""
|
"""
|
||||||
Run a single collection cycle.
|
Run a single collection cycle for all ad accounts.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cache_metadata: Whether to refresh metadata cache
|
cache_metadata: Whether to refresh metadata cache
|
||||||
@@ -375,19 +409,38 @@ class ScheduledInsightsGrabber:
|
|||||||
print("\n" + "="*60)
|
print("\n" + "="*60)
|
||||||
print(f"COLLECTION CYCLE - {datetime.now().isoformat()}")
|
print(f"COLLECTION CYCLE - {datetime.now().isoformat()}")
|
||||||
print("="*60)
|
print("="*60)
|
||||||
|
print(f"Processing {len(self.ad_account_ids)} ad account(s)")
|
||||||
|
print("="*60 + "\n")
|
||||||
|
|
||||||
# Refresh metadata cache if requested (do this less frequently)
|
# Loop through all ad accounts
|
||||||
if cache_metadata:
|
for i, account_id in enumerate(self.ad_account_ids, 1):
|
||||||
await self.cache_account_metadata()
|
print(f"\n[{i}/{len(self.ad_account_ids)}] Processing account: {account_id}")
|
||||||
await self.cache_campaigns_metadata(limit=100)
|
print("-" * 60)
|
||||||
await self.cache_adsets_metadata(limit=100)
|
|
||||||
|
|
||||||
# Grab insights (always use 'today' for scheduled collection)
|
try:
|
||||||
await self.grab_account_insights(date_preset="today")
|
# Refresh metadata cache if requested (do this less frequently)
|
||||||
await self.grab_campaign_insights(date_preset="today", limit=50)
|
if cache_metadata:
|
||||||
await self.grab_adset_insights(date_preset="today", limit=50)
|
print("Caching metadata...")
|
||||||
|
await self.cache_account_metadata(account_id)
|
||||||
|
await self.cache_campaigns_metadata(account_id, limit=100)
|
||||||
|
await self.cache_adsets_metadata(account_id, limit=100)
|
||||||
|
|
||||||
|
# Grab insights (always use 'today' for scheduled collection)
|
||||||
|
print("Grabbing insights...")
|
||||||
|
await self.grab_account_insights(account_id, date_preset="today")
|
||||||
|
await self.grab_campaign_insights(account_id, date_preset="today", limit=50)
|
||||||
|
await self.grab_adset_insights(account_id, date_preset="today", limit=50)
|
||||||
|
|
||||||
|
print(f"✓ Completed {account_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error processing {account_id}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
# Continue with next account
|
||||||
|
|
||||||
# Print rate limiter statistics
|
# Print rate limiter statistics
|
||||||
|
print("\n" + "-" * 60)
|
||||||
self.rate_limiter.print_stats()
|
self.rate_limiter.print_stats()
|
||||||
|
|
||||||
print("\n" + "="*60)
|
print("\n" + "="*60)
|
||||||
@@ -400,7 +453,7 @@ class ScheduledInsightsGrabber:
|
|||||||
refresh_metadata_every_n_cycles: int = 12,
|
refresh_metadata_every_n_cycles: int = 12,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Run scheduled data collection.
|
Run scheduled data collection for all accessible ad accounts.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
interval_hours: Hours between collection cycles (default: 2)
|
interval_hours: Hours between collection cycles (default: 2)
|
||||||
@@ -409,10 +462,6 @@ class ScheduledInsightsGrabber:
|
|||||||
print("\n" + "="*60)
|
print("\n" + "="*60)
|
||||||
print("SCHEDULED INSIGHTS GRABBER STARTED")
|
print("SCHEDULED INSIGHTS GRABBER STARTED")
|
||||||
print("="*60)
|
print("="*60)
|
||||||
print(f"Account: {self.ad_account_id}")
|
|
||||||
print(f"Collection interval: {interval_hours} hours")
|
|
||||||
print(f"Metadata refresh: every {refresh_metadata_every_n_cycles} cycles")
|
|
||||||
print("="*60 + "\n")
|
|
||||||
|
|
||||||
# Connect to database
|
# Connect to database
|
||||||
self.db = TimescaleDBClient()
|
self.db = TimescaleDBClient()
|
||||||
@@ -421,6 +470,19 @@ class ScheduledInsightsGrabber:
|
|||||||
# Initialize database schema (idempotent - safe to run multiple times)
|
# Initialize database schema (idempotent - safe to run multiple times)
|
||||||
await self.db.initialize_schema()
|
await self.db.initialize_schema()
|
||||||
|
|
||||||
|
# Load all accessible ad accounts
|
||||||
|
await self.load_ad_accounts()
|
||||||
|
|
||||||
|
if not self.ad_account_ids:
|
||||||
|
print("❌ No ad accounts found. Exiting.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"\nCollection interval: {interval_hours} hours")
|
||||||
|
print(f"Metadata refresh: every {refresh_metadata_every_n_cycles} cycles")
|
||||||
|
if self.max_accounts:
|
||||||
|
print(f"Max accounts: {self.max_accounts}")
|
||||||
|
print("="*60 + "\n")
|
||||||
|
|
||||||
cycle_count = 0
|
cycle_count = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -452,7 +514,9 @@ class ScheduledInsightsGrabber:
|
|||||||
async def async_main():
|
async def async_main():
|
||||||
"""Async main entry point for scheduled grabber."""
|
"""Async main entry point for scheduled grabber."""
|
||||||
try:
|
try:
|
||||||
grabber = ScheduledInsightsGrabber()
|
# Initialize with max_accounts=3 for conservative start
|
||||||
|
# Set max_accounts=None to process all accessible accounts
|
||||||
|
grabber = ScheduledInsightsGrabber(max_accounts=3)
|
||||||
|
|
||||||
# Run scheduled collection (every 2 hours)
|
# Run scheduled collection (every 2 hours)
|
||||||
await grabber.run_scheduled(
|
await grabber.run_scheduled(
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
Simple test script to initialize database and grab ad_accounts metadata.
|
Simple test script to initialize database and grab ad_accounts metadata.
|
||||||
This is useful for testing the database setup and verifying ad account access.
|
This is useful for testing the database setup and verifying ad account access.
|
||||||
|
Grabs ALL ad accounts accessible to the token.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from facebook_business.adobjects.adaccount import AdAccount
|
from facebook_business.adobjects.adaccount import AdAccount
|
||||||
|
from facebook_business.adobjects.user import User
|
||||||
from facebook_business.api import FacebookAdsApi
|
from facebook_business.api import FacebookAdsApi
|
||||||
|
|
||||||
from meta_api_grabber.database import TimescaleDBClient
|
from meta_api_grabber.database import TimescaleDBClient
|
||||||
@@ -20,18 +22,16 @@ async def test_ad_accounts():
|
|||||||
access_token = os.getenv("META_ACCESS_TOKEN")
|
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||||
app_secret = os.getenv("META_APP_SECRET")
|
app_secret = os.getenv("META_APP_SECRET")
|
||||||
app_id = os.getenv("META_APP_ID")
|
app_id = os.getenv("META_APP_ID")
|
||||||
ad_account_id = os.getenv("META_AD_ACCOUNT_ID")
|
|
||||||
|
|
||||||
if not all([access_token, app_secret, app_id, ad_account_id]):
|
if not all([access_token, app_secret, app_id]):
|
||||||
print("❌ Missing required environment variables")
|
print("❌ Missing required environment variables")
|
||||||
print(" Please ensure META_ACCESS_TOKEN, META_APP_SECRET, META_APP_ID,")
|
print(" Please ensure META_ACCESS_TOKEN, META_APP_SECRET, and META_APP_ID")
|
||||||
print(" and META_AD_ACCOUNT_ID are set in .env")
|
print(" are set in .env")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
print("="*60)
|
print("="*60)
|
||||||
print("AD ACCOUNT TEST")
|
print("AD ACCOUNTS TEST - GRABBING ALL ACCESSIBLE ACCOUNTS")
|
||||||
print("="*60)
|
print("="*60)
|
||||||
print(f"Account ID: {ad_account_id}")
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Initialize Facebook Ads API
|
# Initialize Facebook Ads API
|
||||||
@@ -51,56 +51,58 @@ async def test_ad_accounts():
|
|||||||
print("\nInitializing database schema...")
|
print("\nInitializing database schema...")
|
||||||
await db.initialize_schema()
|
await db.initialize_schema()
|
||||||
|
|
||||||
# Get ad account details from Meta API
|
# Get all ad accounts accessible to this token
|
||||||
print(f"\nFetching ad account details from Meta API...")
|
print("\nFetching all ad accounts accessible to this token...")
|
||||||
ad_account = AdAccount(ad_account_id)
|
me = User(fbid='me')
|
||||||
account_fields = ['name', 'currency', 'timezone_name', 'account_status']
|
account_fields = ['name', 'currency', 'timezone_name', 'account_status']
|
||||||
|
|
||||||
account_data = ad_account.api_get(fields=account_fields)
|
ad_accounts = me.get_ad_accounts(fields=account_fields)
|
||||||
|
|
||||||
print("\nAd Account Details:")
|
print(f"Found {len(ad_accounts)} ad account(s)\n")
|
||||||
print(f" ID: {ad_account_id}")
|
|
||||||
print(f" Name: {account_data.get('name', 'N/A')}")
|
|
||||||
print(f" Currency: {account_data.get('currency', 'N/A')}")
|
|
||||||
print(f" Timezone: {account_data.get('timezone_name', 'N/A')}")
|
|
||||||
print(f" Status: {account_data.get('account_status', 'N/A')}")
|
|
||||||
|
|
||||||
# Store in database
|
stored_count = 0
|
||||||
print("\nStoring ad account in database...")
|
for account in ad_accounts:
|
||||||
await db.upsert_ad_account(
|
account_id = account['id']
|
||||||
account_id=ad_account_id,
|
print(f"Ad Account {stored_count + 1}:")
|
||||||
account_name=account_data.get('name'),
|
print(f" ID: {account_id}")
|
||||||
currency=account_data.get('currency'),
|
print(f" Name: {account.get('name', 'N/A')}")
|
||||||
timezone_name=account_data.get('timezone_name'),
|
print(f" Currency: {account.get('currency', 'N/A')}")
|
||||||
)
|
print(f" Timezone: {account.get('timezone_name', 'N/A')}")
|
||||||
|
print(f" Status: {account.get('account_status', 'N/A')}")
|
||||||
|
|
||||||
print("✓ Ad account successfully stored in database")
|
# Store in database
|
||||||
|
await db.upsert_ad_account(
|
||||||
|
account_id=account_id,
|
||||||
|
account_name=account.get('name'),
|
||||||
|
currency=account.get('currency'),
|
||||||
|
timezone_name=account.get('timezone_name'),
|
||||||
|
)
|
||||||
|
print(f" ✓ Stored in database\n")
|
||||||
|
stored_count += 1
|
||||||
|
|
||||||
# Verify by querying the database
|
# Verify by querying the database
|
||||||
print("\nVerifying database storage...")
|
print("="*60)
|
||||||
|
print(f"Verifying database storage ({stored_count} account(s))...")
|
||||||
|
print("="*60)
|
||||||
async with db.pool.acquire() as conn:
|
async with db.pool.acquire() as conn:
|
||||||
row = await conn.fetchrow(
|
rows = await conn.fetch("SELECT * FROM ad_accounts ORDER BY account_name")
|
||||||
"SELECT * FROM ad_accounts WHERE account_id = $1",
|
if rows:
|
||||||
ad_account_id
|
print(f"\n✓ {len(rows)} ad account(s) found in database:\n")
|
||||||
)
|
for i, row in enumerate(rows, 1):
|
||||||
if row:
|
print(f"{i}. {row['account_name']} ({row['account_id']})")
|
||||||
print("✓ Ad account found in database:")
|
print(f" Currency: {row['currency']} | Timezone: {row['timezone_name']}")
|
||||||
print(f" Account ID: {row['account_id']}")
|
print(f" Updated: {row['updated_at']}\n")
|
||||||
print(f" Account Name: {row['account_name']}")
|
|
||||||
print(f" Currency: {row['currency']}")
|
|
||||||
print(f" Timezone: {row['timezone_name']}")
|
|
||||||
print(f" Created At: {row['created_at']}")
|
|
||||||
print(f" Updated At: {row['updated_at']}")
|
|
||||||
else:
|
else:
|
||||||
print("❌ Ad account not found in database")
|
print("❌ No ad accounts found in database")
|
||||||
|
|
||||||
print("\n" + "="*60)
|
print("="*60)
|
||||||
print("TEST COMPLETED SUCCESSFULLY")
|
print("TEST COMPLETED SUCCESSFULLY")
|
||||||
print("="*60)
|
print("="*60)
|
||||||
|
print(f"\n✓ Successfully grabbed and stored {stored_count} ad account(s)")
|
||||||
print("\nNext steps:")
|
print("\nNext steps:")
|
||||||
print("1. Check your database with: docker exec -it meta_api_grabber-timescaledb-1 psql -U meta_user -d meta_insights")
|
print("1. Check your database: docker exec meta_timescaledb psql -U meta_user -d meta_insights -c 'SELECT * FROM ad_accounts;'")
|
||||||
print("2. Query ad accounts: SELECT * FROM ad_accounts;")
|
print("2. Run scheduled grabber for all accounts: meta-scheduled")
|
||||||
print("3. Run full scheduled grabber: uv run python src/meta_api_grabber/scheduled_grabber.py")
|
print("3. The scheduled grabber will now process all these accounts!")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n❌ Error: {e}")
|
print(f"\n❌ Error: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user