agentberlin 0.166.0__tar.gz → 0.167.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.
- {agentberlin-0.166.0 → agentberlin-0.167.0}/PKG-INFO +91 -1
- {agentberlin-0.166.0 → agentberlin-0.167.0}/README.md +90 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/pyproject.toml +1 -1
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/__init__.py +1 -1
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/_http.py +1 -1
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/client.py +8 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/__init__.py +16 -0
- agentberlin-0.167.0/src/agentberlin/models/linkedin_ads.py +128 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/readme.yaml +88 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/__init__.py +2 -0
- agentberlin-0.167.0/src/agentberlin/resources/linkedin_ads.py +183 -0
- agentberlin-0.167.0/tests/test_linkedin_ads.py +200 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/.gitignore +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/LICENSE +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/cli.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/config.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/exceptions.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/ai_mentions.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/ai_search.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/amplitude.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/analytics.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/backlinks.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/bing_search.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/bing_webmaster.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/brand.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/crawls.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/email_marketing.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/ga4.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/google_ads.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/google_maps.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/google_search.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/google_trends.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/gsc.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/linkedin.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/market.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/microsoft_ads.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/openai_ads.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/pagespeed.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/posthog.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/reddit.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/models/search.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/py.typed +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/ai_mentions.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/ai_search.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/amplitude.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/backlinks.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/bing_search.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/bing_webmaster.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/brand.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/crawls.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/email_marketing.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/ga4.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/google_ads.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/google_maps.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/google_search.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/google_trends.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/gsc.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/keywords.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/linkedin.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/market.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/microsoft_ads.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/openai_ads.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/pages.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/pagespeed.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/posthog.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/resources/reddit.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/session.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/src/agentberlin/utils.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/__init__.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/conftest.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_ai_mentions.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_ai_search.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_backlinks.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_cli_brand.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_cli_findings.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_cli_report.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_cli_sdk_docs.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_cli_workflow.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_cli_workflow_validate.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_client.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_crawls.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_email_marketing.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_exceptions.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_google_ads.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_gsc.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_linkedin.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_market.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_models.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_openai_ads.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_pages.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_reddit.py +0 -0
- {agentberlin-0.166.0 → agentberlin-0.167.0}/tests/test_retry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentberlin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.167.0
|
|
4
4
|
Summary: Python SDK for Agent Berlin - AI-powered SEO and AEO automation
|
|
5
5
|
Project-URL: Homepage, https://agentberlin.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.agentberlin.ai/sdk/python
|
|
@@ -77,6 +77,7 @@ client = AgentBerlin()
|
|
|
77
77
|
- **google_ads**: `search_terms()`, `keywords()`, `campaigns()`, `ad_groups()`, `landing_pages()`, `coverage()` - Google Ads performance reports (read-only, nightly-synced warehouse; cost values in micros)
|
|
78
78
|
- **openai_ads**: `account()`, `campaigns()`, `ad_groups()`, `ads()`, `list_campaigns()`, `record_hint_set()`, `list_hint_sets()` - OpenAI (ChatGPT) Ads performance insights read live from the connected Advertiser API key (read-only; six metrics only; no keywords/search terms; insights money in account currency, not micros), plus recording/recall of generated ad-group context_hints for history
|
|
79
79
|
- **microsoft_ads**: `search_terms()`, `keywords()`, `campaigns()`, `ad_groups()`, `landing_pages()`, `share_of_voice()`, `coverage()` - Microsoft Advertising performance reports (read-only, nightly-synced warehouse; money values are decimal, not micros; share-of-voice keeps ~6 months of history)
|
|
80
|
+
- **linkedin_ads**: `account()`, `campaign_groups()`, `campaigns()`, `creatives()`, `list_campaigns()` - LinkedIn Ads (paid social) performance insights read live from the connected ad account via the /adAnalytics finder (read-only; no keywords/search terms; `cost_in_local_currency` in account currency, not micros)
|
|
80
81
|
|
|
81
82
|
## API Reference
|
|
82
83
|
|
|
@@ -3597,3 +3598,92 @@ cov = client.microsoft_ads.coverage(
|
|
|
3597
3598
|
)
|
|
3598
3599
|
print(f"Search terms cover {cov.coverage.coverage_ratio:.0%} of spend")
|
|
3599
3600
|
```
|
|
3601
|
+
|
|
3602
|
+
### linkedin_ads
|
|
3603
|
+
|
|
3604
|
+
LinkedIn Ads (paid social) performance for the project's connected ad account: account, campaign-group, campaign, and creative insights, plus a campaign listing. Read-only — the SDK never mutates campaigns or any other account state. Data is read LIVE via LinkedIn's /adAnalytics finder on every call (no warehouse, so no `data_as_of` staleness). This is paid SOCIAL, not paid search: there are NO keywords and NO search terms. Money (`cost_in_local_currency`) is a DECIMAL in the account currency in `meta.currency_code` — not micros, so no division is needed; all IDs are URNs kept as strings.
|
|
3605
|
+
|
|
3606
|
+
|
|
3607
|
+
#### client.linkedin_ads.account()
|
|
3608
|
+
|
|
3609
|
+
Get account-level performance totals (one row per time bucket).
|
|
3610
|
+
|
|
3611
|
+
Signature:
|
|
3612
|
+
```python
|
|
3613
|
+
result = client.linkedin_ads.account(
|
|
3614
|
+
start_date="2026-06-01", # Optional: YYYY-MM-DD (default 30 days ago)
|
|
3615
|
+
end_date="2026-06-30", # Optional: YYYY-MM-DD (default today)
|
|
3616
|
+
granularity="daily", # Optional: daily (default) | monthly | yearly | all
|
|
3617
|
+
)
|
|
3618
|
+
```
|
|
3619
|
+
|
|
3620
|
+
Returns LinkedInAdsInsightsResponse with:
|
|
3621
|
+
- rows - List of LinkedInAdsInsightRow objects
|
|
3622
|
+
- row_count - Number of rows returned
|
|
3623
|
+
- meta - LinkedInAdsMeta (source, account_id, currency_code)
|
|
3624
|
+
|
|
3625
|
+
Each LinkedInAdsInsightRow has:
|
|
3626
|
+
- pivot_value - URN of the pivoted entity (empty at account level)
|
|
3627
|
+
- start / end - Time-bucket bounds (YYYY-MM-DD)
|
|
3628
|
+
- impressions / clicks
|
|
3629
|
+
- cost_in_local_currency / cost_in_usd - decimal, not micros
|
|
3630
|
+
- conversions / one_click_leads / likes / comments / shares / follows
|
|
3631
|
+
|
|
3632
|
+
#### client.linkedin_ads.campaign_groups()
|
|
3633
|
+
|
|
3634
|
+
Per-campaign-group performance (one row per group per time bucket). Same
|
|
3635
|
+
args and shape as `account()`; each row's `pivot_value` is the campaign
|
|
3636
|
+
group URN.
|
|
3637
|
+
|
|
3638
|
+
#### client.linkedin_ads.campaigns()
|
|
3639
|
+
|
|
3640
|
+
Per-campaign performance (one row per campaign per time bucket). Same
|
|
3641
|
+
args and shape as `account()`; each row's `pivot_value` is the campaign
|
|
3642
|
+
URN. Pair with `list_campaigns()` for names, budgets, and type.
|
|
3643
|
+
|
|
3644
|
+
Example:
|
|
3645
|
+
```python
|
|
3646
|
+
report = client.linkedin_ads.campaigns(
|
|
3647
|
+
start_date="2026-06-01",
|
|
3648
|
+
end_date="2026-06-30",
|
|
3649
|
+
)
|
|
3650
|
+
for row in report.rows:
|
|
3651
|
+
print(f"{row.pivot_value}: {row.clicks} clicks, "
|
|
3652
|
+
f"{row.cost_in_local_currency:.2f} {report.meta.currency_code}")
|
|
3653
|
+
```
|
|
3654
|
+
|
|
3655
|
+
#### client.linkedin_ads.creatives()
|
|
3656
|
+
|
|
3657
|
+
Per-creative performance (one row per creative per time bucket). Same
|
|
3658
|
+
args and shape as `account()`; each row's `pivot_value` is the creative
|
|
3659
|
+
URN.
|
|
3660
|
+
|
|
3661
|
+
#### client.linkedin_ads.list_campaigns()
|
|
3662
|
+
|
|
3663
|
+
List the connected account's campaigns with metadata for interpreting
|
|
3664
|
+
the insight rows.
|
|
3665
|
+
|
|
3666
|
+
Signature:
|
|
3667
|
+
```python
|
|
3668
|
+
result = client.linkedin_ads.list_campaigns()
|
|
3669
|
+
```
|
|
3670
|
+
|
|
3671
|
+
Returns LinkedInAdsCampaignsResponse with:
|
|
3672
|
+
- rows - List of LinkedInAdsCampaignRow objects
|
|
3673
|
+
- row_count - Number of campaigns returned
|
|
3674
|
+
- meta - LinkedInAdsMeta (source, account_id, currency_code)
|
|
3675
|
+
|
|
3676
|
+
Each LinkedInAdsCampaignRow has:
|
|
3677
|
+
- id / name / status
|
|
3678
|
+
- type - SPONSORED_UPDATES | TEXT_AD | SPONSORED_INMAILS | DYNAMIC
|
|
3679
|
+
- cost_type - CPC | CPM | CPV
|
|
3680
|
+
- daily_budget / total_budget / unit_cost - decimal-string money objects
|
|
3681
|
+
in the account currency (amount + currency_code)
|
|
3682
|
+
- campaign_group - the parent campaign group URN
|
|
3683
|
+
- audience_expansion_enabled / run_schedule / targeting_criteria
|
|
3684
|
+
|
|
3685
|
+
Example:
|
|
3686
|
+
```python
|
|
3687
|
+
for c in client.linkedin_ads.list_campaigns().rows:
|
|
3688
|
+
print(f"{c.name} [{c.status}] type={c.type}")
|
|
3689
|
+
```
|
|
@@ -41,6 +41,7 @@ client = AgentBerlin()
|
|
|
41
41
|
- **google_ads**: `search_terms()`, `keywords()`, `campaigns()`, `ad_groups()`, `landing_pages()`, `coverage()` - Google Ads performance reports (read-only, nightly-synced warehouse; cost values in micros)
|
|
42
42
|
- **openai_ads**: `account()`, `campaigns()`, `ad_groups()`, `ads()`, `list_campaigns()`, `record_hint_set()`, `list_hint_sets()` - OpenAI (ChatGPT) Ads performance insights read live from the connected Advertiser API key (read-only; six metrics only; no keywords/search terms; insights money in account currency, not micros), plus recording/recall of generated ad-group context_hints for history
|
|
43
43
|
- **microsoft_ads**: `search_terms()`, `keywords()`, `campaigns()`, `ad_groups()`, `landing_pages()`, `share_of_voice()`, `coverage()` - Microsoft Advertising performance reports (read-only, nightly-synced warehouse; money values are decimal, not micros; share-of-voice keeps ~6 months of history)
|
|
44
|
+
- **linkedin_ads**: `account()`, `campaign_groups()`, `campaigns()`, `creatives()`, `list_campaigns()` - LinkedIn Ads (paid social) performance insights read live from the connected ad account via the /adAnalytics finder (read-only; no keywords/search terms; `cost_in_local_currency` in account currency, not micros)
|
|
44
45
|
|
|
45
46
|
## API Reference
|
|
46
47
|
|
|
@@ -3561,3 +3562,92 @@ cov = client.microsoft_ads.coverage(
|
|
|
3561
3562
|
)
|
|
3562
3563
|
print(f"Search terms cover {cov.coverage.coverage_ratio:.0%} of spend")
|
|
3563
3564
|
```
|
|
3565
|
+
|
|
3566
|
+
### linkedin_ads
|
|
3567
|
+
|
|
3568
|
+
LinkedIn Ads (paid social) performance for the project's connected ad account: account, campaign-group, campaign, and creative insights, plus a campaign listing. Read-only — the SDK never mutates campaigns or any other account state. Data is read LIVE via LinkedIn's /adAnalytics finder on every call (no warehouse, so no `data_as_of` staleness). This is paid SOCIAL, not paid search: there are NO keywords and NO search terms. Money (`cost_in_local_currency`) is a DECIMAL in the account currency in `meta.currency_code` — not micros, so no division is needed; all IDs are URNs kept as strings.
|
|
3569
|
+
|
|
3570
|
+
|
|
3571
|
+
#### client.linkedin_ads.account()
|
|
3572
|
+
|
|
3573
|
+
Get account-level performance totals (one row per time bucket).
|
|
3574
|
+
|
|
3575
|
+
Signature:
|
|
3576
|
+
```python
|
|
3577
|
+
result = client.linkedin_ads.account(
|
|
3578
|
+
start_date="2026-06-01", # Optional: YYYY-MM-DD (default 30 days ago)
|
|
3579
|
+
end_date="2026-06-30", # Optional: YYYY-MM-DD (default today)
|
|
3580
|
+
granularity="daily", # Optional: daily (default) | monthly | yearly | all
|
|
3581
|
+
)
|
|
3582
|
+
```
|
|
3583
|
+
|
|
3584
|
+
Returns LinkedInAdsInsightsResponse with:
|
|
3585
|
+
- rows - List of LinkedInAdsInsightRow objects
|
|
3586
|
+
- row_count - Number of rows returned
|
|
3587
|
+
- meta - LinkedInAdsMeta (source, account_id, currency_code)
|
|
3588
|
+
|
|
3589
|
+
Each LinkedInAdsInsightRow has:
|
|
3590
|
+
- pivot_value - URN of the pivoted entity (empty at account level)
|
|
3591
|
+
- start / end - Time-bucket bounds (YYYY-MM-DD)
|
|
3592
|
+
- impressions / clicks
|
|
3593
|
+
- cost_in_local_currency / cost_in_usd - decimal, not micros
|
|
3594
|
+
- conversions / one_click_leads / likes / comments / shares / follows
|
|
3595
|
+
|
|
3596
|
+
#### client.linkedin_ads.campaign_groups()
|
|
3597
|
+
|
|
3598
|
+
Per-campaign-group performance (one row per group per time bucket). Same
|
|
3599
|
+
args and shape as `account()`; each row's `pivot_value` is the campaign
|
|
3600
|
+
group URN.
|
|
3601
|
+
|
|
3602
|
+
#### client.linkedin_ads.campaigns()
|
|
3603
|
+
|
|
3604
|
+
Per-campaign performance (one row per campaign per time bucket). Same
|
|
3605
|
+
args and shape as `account()`; each row's `pivot_value` is the campaign
|
|
3606
|
+
URN. Pair with `list_campaigns()` for names, budgets, and type.
|
|
3607
|
+
|
|
3608
|
+
Example:
|
|
3609
|
+
```python
|
|
3610
|
+
report = client.linkedin_ads.campaigns(
|
|
3611
|
+
start_date="2026-06-01",
|
|
3612
|
+
end_date="2026-06-30",
|
|
3613
|
+
)
|
|
3614
|
+
for row in report.rows:
|
|
3615
|
+
print(f"{row.pivot_value}: {row.clicks} clicks, "
|
|
3616
|
+
f"{row.cost_in_local_currency:.2f} {report.meta.currency_code}")
|
|
3617
|
+
```
|
|
3618
|
+
|
|
3619
|
+
#### client.linkedin_ads.creatives()
|
|
3620
|
+
|
|
3621
|
+
Per-creative performance (one row per creative per time bucket). Same
|
|
3622
|
+
args and shape as `account()`; each row's `pivot_value` is the creative
|
|
3623
|
+
URN.
|
|
3624
|
+
|
|
3625
|
+
#### client.linkedin_ads.list_campaigns()
|
|
3626
|
+
|
|
3627
|
+
List the connected account's campaigns with metadata for interpreting
|
|
3628
|
+
the insight rows.
|
|
3629
|
+
|
|
3630
|
+
Signature:
|
|
3631
|
+
```python
|
|
3632
|
+
result = client.linkedin_ads.list_campaigns()
|
|
3633
|
+
```
|
|
3634
|
+
|
|
3635
|
+
Returns LinkedInAdsCampaignsResponse with:
|
|
3636
|
+
- rows - List of LinkedInAdsCampaignRow objects
|
|
3637
|
+
- row_count - Number of campaigns returned
|
|
3638
|
+
- meta - LinkedInAdsMeta (source, account_id, currency_code)
|
|
3639
|
+
|
|
3640
|
+
Each LinkedInAdsCampaignRow has:
|
|
3641
|
+
- id / name / status
|
|
3642
|
+
- type - SPONSORED_UPDATES | TEXT_AD | SPONSORED_INMAILS | DYNAMIC
|
|
3643
|
+
- cost_type - CPC | CPM | CPV
|
|
3644
|
+
- daily_budget / total_budget / unit_cost - decimal-string money objects
|
|
3645
|
+
in the account currency (amount + currency_code)
|
|
3646
|
+
- campaign_group - the parent campaign group URN
|
|
3647
|
+
- audience_expansion_enabled / run_schedule / targeting_criteria
|
|
3648
|
+
|
|
3649
|
+
Example:
|
|
3650
|
+
```python
|
|
3651
|
+
for c in client.linkedin_ads.list_campaigns().rows:
|
|
3652
|
+
print(f"{c.name} [{c.status}] type={c.type}")
|
|
3653
|
+
```
|
|
@@ -42,6 +42,7 @@ from .resources.google_trends import GoogleTrendsResource
|
|
|
42
42
|
from .resources.gsc import GSCResource
|
|
43
43
|
from .resources.keywords import KeywordsResource
|
|
44
44
|
from .resources.linkedin import LinkedInResource
|
|
45
|
+
from .resources.linkedin_ads import LinkedInAdsResource
|
|
45
46
|
from .resources.market import MarketResource
|
|
46
47
|
from .resources.microsoft_ads import MicrosoftAdsResource
|
|
47
48
|
from .resources.pages import PagesResource
|
|
@@ -139,6 +140,12 @@ class AgentBerlin:
|
|
|
139
140
|
coverage metric, served from Berlin's nightly-synced warehouse.
|
|
140
141
|
Money values are decimal in `meta.currency_code` (not micros);
|
|
141
142
|
share-of-voice keeps only ~6 months of history.
|
|
143
|
+
linkedin_ads: LinkedIn Ads (paid social) performance resource
|
|
144
|
+
(read-only) — account/campaign-group/campaign/creative insights
|
|
145
|
+
plus a campaign listing, read live from the connected ad account
|
|
146
|
+
via the /adAnalytics finder. No keywords or search terms;
|
|
147
|
+
`cost_in_local_currency` is decimal in `meta.currency_code`, not
|
|
148
|
+
micros.
|
|
142
149
|
"""
|
|
143
150
|
|
|
144
151
|
def __init__(
|
|
@@ -220,6 +227,7 @@ class AgentBerlin:
|
|
|
220
227
|
self.google_ads = GoogleAdsResource(self._http)
|
|
221
228
|
self.openai_ads = OpenAIAdsResource(self._http)
|
|
222
229
|
self.microsoft_ads = MicrosoftAdsResource(self._http)
|
|
230
|
+
self.linkedin_ads = LinkedInAdsResource(self._http)
|
|
223
231
|
|
|
224
232
|
def __repr__(self) -> str:
|
|
225
233
|
return f"AgentBerlin(base_url='{self._config.base_url}')"
|
|
@@ -144,6 +144,15 @@ from .openai_ads import (
|
|
|
144
144
|
OpenAIAdsInsightsResponse,
|
|
145
145
|
OpenAIAdsMeta,
|
|
146
146
|
)
|
|
147
|
+
from .linkedin_ads import (
|
|
148
|
+
LinkedInAdsCampaignRow,
|
|
149
|
+
LinkedInAdsCampaignsResponse,
|
|
150
|
+
LinkedInAdsInsightRow,
|
|
151
|
+
LinkedInAdsInsightsResponse,
|
|
152
|
+
LinkedInAdsMeta,
|
|
153
|
+
LinkedInAdsMoney,
|
|
154
|
+
LinkedInAdsRunSchedule,
|
|
155
|
+
)
|
|
147
156
|
from .google_maps import (
|
|
148
157
|
GoogleMapsPlace,
|
|
149
158
|
GoogleMapsReview,
|
|
@@ -409,6 +418,13 @@ __all__ = [
|
|
|
409
418
|
"OpenAIAdsInsightRow",
|
|
410
419
|
"OpenAIAdsInsightsResponse",
|
|
411
420
|
"OpenAIAdsMeta",
|
|
421
|
+
"LinkedInAdsCampaignRow",
|
|
422
|
+
"LinkedInAdsCampaignsResponse",
|
|
423
|
+
"LinkedInAdsInsightRow",
|
|
424
|
+
"LinkedInAdsInsightsResponse",
|
|
425
|
+
"LinkedInAdsMeta",
|
|
426
|
+
"LinkedInAdsMoney",
|
|
427
|
+
"LinkedInAdsRunSchedule",
|
|
412
428
|
# Google Maps
|
|
413
429
|
"GoogleMapsPlace",
|
|
414
430
|
"GoogleMapsReview",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Pydantic models for LinkedIn Ads (Marketing API) responses.
|
|
2
|
+
|
|
3
|
+
LinkedIn Ads reads are served LIVE from the project's connected ad account via
|
|
4
|
+
the /adAnalytics finder (there is no warehouse — the finder is synchronous, and
|
|
5
|
+
LinkedIn is outside the SEM warehouse plan). Insights money
|
|
6
|
+
(``cost_in_local_currency``) is a decimal in the account currency
|
|
7
|
+
(``meta.currency_code``), NOT micros. Campaign budgets are decimal-string money
|
|
8
|
+
objects, also not micros.
|
|
9
|
+
|
|
10
|
+
LinkedIn Ads is paid social: there are no keywords and no search terms. Pivots
|
|
11
|
+
are account / campaign_group / campaign / creative, and ids are URNs kept as
|
|
12
|
+
opaque strings.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Any, List, Optional
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LinkedInAdsMeta(BaseModel):
|
|
21
|
+
"""Metadata attached to every LinkedIn Ads response.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
source: Where the data came from — always "linkedin_ads".
|
|
25
|
+
account_id: The connected sponsored ad-account id (numeric).
|
|
26
|
+
currency_code: Account currency. Insights ``cost_in_local_currency`` is
|
|
27
|
+
in this currency (not micros).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
source: str
|
|
31
|
+
account_id: Optional[str] = None
|
|
32
|
+
currency_code: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LinkedInAdsMoney(BaseModel):
|
|
36
|
+
"""A LinkedIn money amount — a decimal string in ``currency_code`` (not
|
|
37
|
+
micros)."""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
40
|
+
|
|
41
|
+
amount: Optional[str] = None
|
|
42
|
+
currency_code: Optional[str] = Field(default=None, alias="currencyCode")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LinkedInAdsRunSchedule(BaseModel):
|
|
46
|
+
"""An entity's flight window (epoch-millis). ``end`` of 0/None means
|
|
47
|
+
run-until-budget-spent."""
|
|
48
|
+
|
|
49
|
+
start: Optional[int] = None
|
|
50
|
+
end: Optional[int] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LinkedInAdsInsightRow(BaseModel):
|
|
54
|
+
"""One /adAnalytics row at the requested pivot.
|
|
55
|
+
|
|
56
|
+
``pivot_value`` is the URN of the pivoted entity (campaign / creative / ...),
|
|
57
|
+
empty at account pivot. ``start``/``end`` bound the time bucket (YYYY-MM-DD).
|
|
58
|
+
Money is in the account currency, not micros.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
pivot_value: Optional[str] = None
|
|
62
|
+
start: Optional[str] = None
|
|
63
|
+
end: Optional[str] = None
|
|
64
|
+
impressions: int = 0
|
|
65
|
+
clicks: int = 0
|
|
66
|
+
cost_in_local_currency: float = 0.0
|
|
67
|
+
cost_in_usd: float = 0.0
|
|
68
|
+
conversions: float = 0.0
|
|
69
|
+
one_click_leads: float = 0.0
|
|
70
|
+
likes: int = 0
|
|
71
|
+
comments: int = 0
|
|
72
|
+
shares: int = 0
|
|
73
|
+
follows: int = 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LinkedInAdsInsightsResponse(BaseModel):
|
|
77
|
+
"""Response from the insights endpoint.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
rows: One row per entity at the requested pivot (per time bucket).
|
|
81
|
+
row_count: Number of rows returned.
|
|
82
|
+
meta: Account metadata (see LinkedInAdsMeta). Data is live — there is no
|
|
83
|
+
``data_as_of`` staleness stamp.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
rows: List[LinkedInAdsInsightRow] = Field(default_factory=list)
|
|
87
|
+
row_count: int = 0
|
|
88
|
+
meta: LinkedInAdsMeta
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class LinkedInAdsCampaignRow(BaseModel):
|
|
92
|
+
"""One campaign in the connected ad account.
|
|
93
|
+
|
|
94
|
+
``type`` is the LinkedIn campaign type (SPONSORED_UPDATES, TEXT_AD, ...);
|
|
95
|
+
``cost_type`` is CPC / CPM / CPV. Budgets are decimal-string money objects
|
|
96
|
+
in the account currency. ``targeting_criteria`` is the raw Rest.li targeting
|
|
97
|
+
object (kept opaque).
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
id: str
|
|
101
|
+
account: Optional[str] = None
|
|
102
|
+
campaign_group: Optional[str] = None
|
|
103
|
+
name: str
|
|
104
|
+
status: str
|
|
105
|
+
type: Optional[str] = None
|
|
106
|
+
cost_type: Optional[str] = None
|
|
107
|
+
format: Optional[str] = None
|
|
108
|
+
objective_type: Optional[str] = None
|
|
109
|
+
daily_budget: Optional[LinkedInAdsMoney] = None
|
|
110
|
+
total_budget: Optional[LinkedInAdsMoney] = None
|
|
111
|
+
unit_cost: Optional[LinkedInAdsMoney] = None
|
|
112
|
+
audience_expansion_enabled: bool = False
|
|
113
|
+
run_schedule: LinkedInAdsRunSchedule = Field(default_factory=LinkedInAdsRunSchedule)
|
|
114
|
+
targeting_criteria: Optional[Any] = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class LinkedInAdsCampaignsResponse(BaseModel):
|
|
118
|
+
"""Response from the campaigns list endpoint.
|
|
119
|
+
|
|
120
|
+
Attributes:
|
|
121
|
+
rows: The account's campaigns.
|
|
122
|
+
row_count: Number of campaigns returned.
|
|
123
|
+
meta: Account metadata (see LinkedInAdsMeta).
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
rows: List[LinkedInAdsCampaignRow] = Field(default_factory=list)
|
|
127
|
+
row_count: int = 0
|
|
128
|
+
meta: LinkedInAdsMeta
|
|
@@ -51,6 +51,7 @@ introduction: |
|
|
|
51
51
|
- **google_ads**: `search_terms()`, `keywords()`, `campaigns()`, `ad_groups()`, `landing_pages()`, `coverage()` - Google Ads performance reports (read-only, nightly-synced warehouse; cost values in micros)
|
|
52
52
|
- **openai_ads**: `account()`, `campaigns()`, `ad_groups()`, `ads()`, `list_campaigns()`, `record_hint_set()`, `list_hint_sets()` - OpenAI (ChatGPT) Ads performance insights read live from the connected Advertiser API key (read-only; six metrics only; no keywords/search terms; insights money in account currency, not micros), plus recording/recall of generated ad-group context_hints for history
|
|
53
53
|
- **microsoft_ads**: `search_terms()`, `keywords()`, `campaigns()`, `ad_groups()`, `landing_pages()`, `share_of_voice()`, `coverage()` - Microsoft Advertising performance reports (read-only, nightly-synced warehouse; money values are decimal, not micros; share-of-voice keeps ~6 months of history)
|
|
54
|
+
- **linkedin_ads**: `account()`, `campaign_groups()`, `campaigns()`, `creatives()`, `list_campaigns()` - LinkedIn Ads (paid social) performance insights read live from the connected ad account via the /adAnalytics finder (read-only; no keywords/search terms; `cost_in_local_currency` in account currency, not micros)
|
|
54
55
|
|
|
55
56
|
resources:
|
|
56
57
|
ga4:
|
|
@@ -3492,3 +3493,90 @@ resources:
|
|
|
3492
3493
|
)
|
|
3493
3494
|
print(f"Search terms cover {cov.coverage.coverage_ratio:.0%} of spend")
|
|
3494
3495
|
```
|
|
3496
|
+
|
|
3497
|
+
linkedin_ads:
|
|
3498
|
+
description: >
|
|
3499
|
+
LinkedIn Ads (paid social) performance for the project's connected ad
|
|
3500
|
+
account: account, campaign-group, campaign, and creative insights, plus a
|
|
3501
|
+
campaign listing. Read-only — the SDK never mutates campaigns or any other
|
|
3502
|
+
account state. Data is read LIVE via LinkedIn's /adAnalytics finder on
|
|
3503
|
+
every call (no warehouse, so no `data_as_of` staleness). This is paid
|
|
3504
|
+
SOCIAL, not paid search: there are NO keywords and NO search terms. Money
|
|
3505
|
+
(`cost_in_local_currency`) is a DECIMAL in the account currency in
|
|
3506
|
+
`meta.currency_code` — not micros, so no division is needed; all IDs are
|
|
3507
|
+
URNs kept as strings.
|
|
3508
|
+
methods:
|
|
3509
|
+
account: |
|
|
3510
|
+
Get account-level performance totals (one row per time bucket).
|
|
3511
|
+
|
|
3512
|
+
Signature:
|
|
3513
|
+
```python
|
|
3514
|
+
result = client.linkedin_ads.account(
|
|
3515
|
+
start_date="2026-06-01", # Optional: YYYY-MM-DD (default 30 days ago)
|
|
3516
|
+
end_date="2026-06-30", # Optional: YYYY-MM-DD (default today)
|
|
3517
|
+
granularity="daily", # Optional: daily (default) | monthly | yearly | all
|
|
3518
|
+
)
|
|
3519
|
+
```
|
|
3520
|
+
|
|
3521
|
+
Returns LinkedInAdsInsightsResponse with:
|
|
3522
|
+
- rows - List of LinkedInAdsInsightRow objects
|
|
3523
|
+
- row_count - Number of rows returned
|
|
3524
|
+
- meta - LinkedInAdsMeta (source, account_id, currency_code)
|
|
3525
|
+
|
|
3526
|
+
Each LinkedInAdsInsightRow has:
|
|
3527
|
+
- pivot_value - URN of the pivoted entity (empty at account level)
|
|
3528
|
+
- start / end - Time-bucket bounds (YYYY-MM-DD)
|
|
3529
|
+
- impressions / clicks
|
|
3530
|
+
- cost_in_local_currency / cost_in_usd - decimal, not micros
|
|
3531
|
+
- conversions / one_click_leads / likes / comments / shares / follows
|
|
3532
|
+
campaign_groups: |
|
|
3533
|
+
Per-campaign-group performance (one row per group per time bucket). Same
|
|
3534
|
+
args and shape as `account()`; each row's `pivot_value` is the campaign
|
|
3535
|
+
group URN.
|
|
3536
|
+
campaigns: |
|
|
3537
|
+
Per-campaign performance (one row per campaign per time bucket). Same
|
|
3538
|
+
args and shape as `account()`; each row's `pivot_value` is the campaign
|
|
3539
|
+
URN. Pair with `list_campaigns()` for names, budgets, and type.
|
|
3540
|
+
|
|
3541
|
+
Example:
|
|
3542
|
+
```python
|
|
3543
|
+
report = client.linkedin_ads.campaigns(
|
|
3544
|
+
start_date="2026-06-01",
|
|
3545
|
+
end_date="2026-06-30",
|
|
3546
|
+
)
|
|
3547
|
+
for row in report.rows:
|
|
3548
|
+
print(f"{row.pivot_value}: {row.clicks} clicks, "
|
|
3549
|
+
f"{row.cost_in_local_currency:.2f} {report.meta.currency_code}")
|
|
3550
|
+
```
|
|
3551
|
+
creatives: |
|
|
3552
|
+
Per-creative performance (one row per creative per time bucket). Same
|
|
3553
|
+
args and shape as `account()`; each row's `pivot_value` is the creative
|
|
3554
|
+
URN.
|
|
3555
|
+
list_campaigns: |
|
|
3556
|
+
List the connected account's campaigns with metadata for interpreting
|
|
3557
|
+
the insight rows.
|
|
3558
|
+
|
|
3559
|
+
Signature:
|
|
3560
|
+
```python
|
|
3561
|
+
result = client.linkedin_ads.list_campaigns()
|
|
3562
|
+
```
|
|
3563
|
+
|
|
3564
|
+
Returns LinkedInAdsCampaignsResponse with:
|
|
3565
|
+
- rows - List of LinkedInAdsCampaignRow objects
|
|
3566
|
+
- row_count - Number of campaigns returned
|
|
3567
|
+
- meta - LinkedInAdsMeta (source, account_id, currency_code)
|
|
3568
|
+
|
|
3569
|
+
Each LinkedInAdsCampaignRow has:
|
|
3570
|
+
- id / name / status
|
|
3571
|
+
- type - SPONSORED_UPDATES | TEXT_AD | SPONSORED_INMAILS | DYNAMIC
|
|
3572
|
+
- cost_type - CPC | CPM | CPV
|
|
3573
|
+
- daily_budget / total_budget / unit_cost - decimal-string money objects
|
|
3574
|
+
in the account currency (amount + currency_code)
|
|
3575
|
+
- campaign_group - the parent campaign group URN
|
|
3576
|
+
- audience_expansion_enabled / run_schedule / targeting_criteria
|
|
3577
|
+
|
|
3578
|
+
Example:
|
|
3579
|
+
```python
|
|
3580
|
+
for c in client.linkedin_ads.list_campaigns().rows:
|
|
3581
|
+
print(f"{c.name} [{c.status}] type={c.type}")
|
|
3582
|
+
```
|
|
@@ -20,6 +20,7 @@ from .linkedin import LinkedInResource
|
|
|
20
20
|
from .market import MarketResource
|
|
21
21
|
from .openai_ads import OpenAIAdsResource
|
|
22
22
|
from .microsoft_ads import MicrosoftAdsResource
|
|
23
|
+
from .linkedin_ads import LinkedInAdsResource
|
|
23
24
|
from .pages import PagesResource
|
|
24
25
|
from .pagespeed import PageSpeedResource
|
|
25
26
|
from .posthog import PostHogResource
|
|
@@ -46,6 +47,7 @@ __all__ = [
|
|
|
46
47
|
"MarketResource",
|
|
47
48
|
"OpenAIAdsResource",
|
|
48
49
|
"MicrosoftAdsResource",
|
|
50
|
+
"LinkedInAdsResource",
|
|
49
51
|
"PagesResource",
|
|
50
52
|
"PageSpeedResource",
|
|
51
53
|
"PostHogResource",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""LinkedIn Ads (Marketing API) resource for Agent Berlin SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from .._http import HTTPClient
|
|
6
|
+
from ..models.linkedin_ads import (
|
|
7
|
+
LinkedInAdsCampaignsResponse,
|
|
8
|
+
LinkedInAdsInsightsResponse,
|
|
9
|
+
)
|
|
10
|
+
from ..utils import get_project_domain
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LinkedInAdsResource:
|
|
14
|
+
"""Resource for LinkedIn Ads (paid social) performance data (read-only).
|
|
15
|
+
|
|
16
|
+
Serves campaign performance for the project's connected LinkedIn ad account.
|
|
17
|
+
Data is read LIVE from the customer's own connected account via the
|
|
18
|
+
/adAnalytics finder on each call — there is no warehouse, because the finder
|
|
19
|
+
is synchronous. This resource is read-only: the SDK never mutates campaigns
|
|
20
|
+
or any account state.
|
|
21
|
+
|
|
22
|
+
LinkedIn Ads is paid social — there are NO keywords and NO search terms.
|
|
23
|
+
Targeting is job title / function / seniority / industry / geo facets, and
|
|
24
|
+
reporting money (``cost_in_local_currency``) is a decimal in the account
|
|
25
|
+
currency reported in ``meta.currency_code`` (NOT micros).
|
|
26
|
+
|
|
27
|
+
The insights methods mirror the platform's pivots: ``account()``,
|
|
28
|
+
``campaign_groups()``, ``campaigns()``, and ``creatives()``. Use
|
|
29
|
+
``list_campaigns()`` to fetch campaign metadata (names, statuses, budgets,
|
|
30
|
+
type) for interpreting the insight rows.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
# Last 30 days of per-campaign performance
|
|
34
|
+
report = client.linkedin_ads.campaigns()
|
|
35
|
+
for row in report.rows:
|
|
36
|
+
print(f"{row.pivot_value}: {row.clicks} clicks, "
|
|
37
|
+
f"{row.cost_in_local_currency:.2f} {report.meta.currency_code}")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
41
|
+
self._http = http
|
|
42
|
+
|
|
43
|
+
def _insights(
|
|
44
|
+
self,
|
|
45
|
+
level: str,
|
|
46
|
+
start_date: Optional[str],
|
|
47
|
+
end_date: Optional[str],
|
|
48
|
+
granularity: Optional[str],
|
|
49
|
+
) -> LinkedInAdsInsightsResponse:
|
|
50
|
+
payload: Dict[str, Any] = {
|
|
51
|
+
"project_domain": get_project_domain(),
|
|
52
|
+
"level": level,
|
|
53
|
+
}
|
|
54
|
+
if start_date is not None:
|
|
55
|
+
payload["start_date"] = start_date
|
|
56
|
+
if end_date is not None:
|
|
57
|
+
payload["end_date"] = end_date
|
|
58
|
+
if granularity is not None:
|
|
59
|
+
payload["granularity"] = granularity
|
|
60
|
+
data = self._http.post("/linkedin-ads/insights", json=payload)
|
|
61
|
+
return LinkedInAdsInsightsResponse.model_validate(data)
|
|
62
|
+
|
|
63
|
+
def account(
|
|
64
|
+
self,
|
|
65
|
+
start_date: Optional[str] = None,
|
|
66
|
+
end_date: Optional[str] = None,
|
|
67
|
+
*,
|
|
68
|
+
granularity: Optional[str] = None,
|
|
69
|
+
) -> LinkedInAdsInsightsResponse:
|
|
70
|
+
"""Get account-level performance totals (one row per time bucket).
|
|
71
|
+
|
|
72
|
+
Read live from the connected LinkedIn ad account.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
start_date: Start date in YYYY-MM-DD format. Defaults to 30 days ago.
|
|
76
|
+
end_date: End date in YYYY-MM-DD format. Defaults to today.
|
|
77
|
+
granularity: Time bucket — daily (default), monthly, yearly, or all.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
LinkedInAdsInsightsResponse with rows (impressions, clicks,
|
|
81
|
+
cost_in_local_currency, conversions, engagement), row_count, and
|
|
82
|
+
meta. Money is in ``meta.currency_code``, not micros.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
AgentBerlinNotFoundError: If no LinkedIn Ads integration is connected
|
|
86
|
+
for the domain.
|
|
87
|
+
AgentBerlinAPIError: If the request is invalid or access was revoked
|
|
88
|
+
(reconnect the integration).
|
|
89
|
+
"""
|
|
90
|
+
return self._insights("account", start_date, end_date, granularity)
|
|
91
|
+
|
|
92
|
+
def campaign_groups(
|
|
93
|
+
self,
|
|
94
|
+
start_date: Optional[str] = None,
|
|
95
|
+
end_date: Optional[str] = None,
|
|
96
|
+
*,
|
|
97
|
+
granularity: Optional[str] = None,
|
|
98
|
+
) -> LinkedInAdsInsightsResponse:
|
|
99
|
+
"""Get per-campaign-group performance (one row per group per bucket).
|
|
100
|
+
|
|
101
|
+
Read live. Each row's ``pivot_value`` is the campaign group URN.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
start_date: Start date in YYYY-MM-DD format. Defaults to 30 days ago.
|
|
105
|
+
end_date: End date in YYYY-MM-DD format. Defaults to today.
|
|
106
|
+
granularity: Time bucket — daily (default), monthly, yearly, or all.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
LinkedInAdsInsightsResponse with per-campaign-group rows, row_count,
|
|
110
|
+
and meta.
|
|
111
|
+
"""
|
|
112
|
+
return self._insights("campaign_group", start_date, end_date, granularity)
|
|
113
|
+
|
|
114
|
+
def campaigns(
|
|
115
|
+
self,
|
|
116
|
+
start_date: Optional[str] = None,
|
|
117
|
+
end_date: Optional[str] = None,
|
|
118
|
+
*,
|
|
119
|
+
granularity: Optional[str] = None,
|
|
120
|
+
) -> LinkedInAdsInsightsResponse:
|
|
121
|
+
"""Get per-campaign performance (one row per campaign per time bucket).
|
|
122
|
+
|
|
123
|
+
Read live. Each row's ``pivot_value`` is the campaign URN; use
|
|
124
|
+
``list_campaigns()`` for budgets and type.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
start_date: Start date in YYYY-MM-DD format. Defaults to 30 days ago.
|
|
128
|
+
end_date: End date in YYYY-MM-DD format. Defaults to today.
|
|
129
|
+
granularity: Time bucket — daily (default), monthly, yearly, or all.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
LinkedInAdsInsightsResponse with per-campaign rows, row_count, and
|
|
133
|
+
meta. Money is in ``meta.currency_code``, not micros.
|
|
134
|
+
"""
|
|
135
|
+
return self._insights("campaign", start_date, end_date, granularity)
|
|
136
|
+
|
|
137
|
+
def creatives(
|
|
138
|
+
self,
|
|
139
|
+
start_date: Optional[str] = None,
|
|
140
|
+
end_date: Optional[str] = None,
|
|
141
|
+
*,
|
|
142
|
+
granularity: Optional[str] = None,
|
|
143
|
+
) -> LinkedInAdsInsightsResponse:
|
|
144
|
+
"""Get per-creative performance (one row per creative per time bucket).
|
|
145
|
+
|
|
146
|
+
Read live. Each row's ``pivot_value`` is the creative URN.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
start_date: Start date in YYYY-MM-DD format. Defaults to 30 days ago.
|
|
150
|
+
end_date: End date in YYYY-MM-DD format. Defaults to today.
|
|
151
|
+
granularity: Time bucket — daily (default), monthly, yearly, or all.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
LinkedInAdsInsightsResponse with per-creative rows, row_count, and
|
|
155
|
+
meta.
|
|
156
|
+
"""
|
|
157
|
+
return self._insights("creative", start_date, end_date, granularity)
|
|
158
|
+
|
|
159
|
+
def list_campaigns(self) -> LinkedInAdsCampaignsResponse:
|
|
160
|
+
"""List the connected account's campaigns with their metadata.
|
|
161
|
+
|
|
162
|
+
Read live. Returns names, statuses, type, cost type, and budgets — the
|
|
163
|
+
context for interpreting the insights rows (which carry only pivot URNs
|
|
164
|
+
and metrics).
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
LinkedInAdsCampaignsResponse with campaign rows (id, name, status,
|
|
168
|
+
type, cost_type, budgets, ...), row_count, and meta. Budgets are
|
|
169
|
+
decimal-string money objects in ``meta.currency_code``.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
AgentBerlinNotFoundError: If no LinkedIn Ads integration is connected
|
|
173
|
+
for the domain.
|
|
174
|
+
AgentBerlinAPIError: If access was revoked (reconnect the
|
|
175
|
+
integration).
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
for c in client.linkedin_ads.list_campaigns().rows:
|
|
179
|
+
print(f"{c.name} [{c.status}] type={c.type}")
|
|
180
|
+
"""
|
|
181
|
+
payload = {"project_domain": get_project_domain()}
|
|
182
|
+
data = self._http.post("/linkedin-ads/campaigns", json=payload)
|
|
183
|
+
return LinkedInAdsCampaignsResponse.model_validate(data)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Tests for LinkedIn Ads (paid social) resource and models."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from agentberlin.models import (
|
|
9
|
+
LinkedInAdsCampaignRow,
|
|
10
|
+
LinkedInAdsCampaignsResponse,
|
|
11
|
+
LinkedInAdsInsightRow,
|
|
12
|
+
LinkedInAdsInsightsResponse,
|
|
13
|
+
LinkedInAdsMeta,
|
|
14
|
+
LinkedInAdsMoney,
|
|
15
|
+
)
|
|
16
|
+
from agentberlin.resources.linkedin_ads import LinkedInAdsResource
|
|
17
|
+
|
|
18
|
+
META = {
|
|
19
|
+
"source": "linkedin_ads",
|
|
20
|
+
"account_id": "512345678",
|
|
21
|
+
"currency_code": "USD",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestLinkedInAdsModels:
|
|
26
|
+
"""Tests for LinkedIn Ads Pydantic models."""
|
|
27
|
+
|
|
28
|
+
def test_meta(self):
|
|
29
|
+
meta = LinkedInAdsMeta.model_validate(META)
|
|
30
|
+
assert meta.source == "linkedin_ads"
|
|
31
|
+
assert meta.account_id == "512345678"
|
|
32
|
+
assert meta.currency_code == "USD"
|
|
33
|
+
|
|
34
|
+
def test_meta_minimal(self):
|
|
35
|
+
"""Only source is required; the rest default to None."""
|
|
36
|
+
meta = LinkedInAdsMeta.model_validate({"source": "linkedin_ads"})
|
|
37
|
+
assert meta.source == "linkedin_ads"
|
|
38
|
+
assert meta.account_id is None
|
|
39
|
+
assert meta.currency_code is None
|
|
40
|
+
|
|
41
|
+
def test_insight_row_full(self):
|
|
42
|
+
row = LinkedInAdsInsightRow.model_validate(
|
|
43
|
+
{
|
|
44
|
+
"pivot_value": "urn:li:sponsoredCampaign:777",
|
|
45
|
+
"start": "2026-07-01",
|
|
46
|
+
"end": "2026-07-01",
|
|
47
|
+
"impressions": 1000,
|
|
48
|
+
"clicks": 25,
|
|
49
|
+
"cost_in_local_currency": 12.5,
|
|
50
|
+
"cost_in_usd": 12.5,
|
|
51
|
+
"conversions": 3,
|
|
52
|
+
"one_click_leads": 1,
|
|
53
|
+
"likes": 4,
|
|
54
|
+
"comments": 2,
|
|
55
|
+
"shares": 1,
|
|
56
|
+
"follows": 0,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
assert row.pivot_value == "urn:li:sponsoredCampaign:777"
|
|
60
|
+
assert row.impressions == 1000
|
|
61
|
+
assert row.cost_in_local_currency == 12.5
|
|
62
|
+
assert row.conversions == 3
|
|
63
|
+
|
|
64
|
+
def test_insight_row_minimal(self):
|
|
65
|
+
"""Metrics default to 0 and identity fields to None when omitted."""
|
|
66
|
+
row = LinkedInAdsInsightRow.model_validate({})
|
|
67
|
+
assert row.pivot_value is None
|
|
68
|
+
assert row.impressions == 0
|
|
69
|
+
assert row.cost_in_local_currency == 0.0
|
|
70
|
+
|
|
71
|
+
def test_insights_response_empty_rows(self):
|
|
72
|
+
resp = LinkedInAdsInsightsResponse.model_validate({"row_count": 0, "meta": META})
|
|
73
|
+
assert resp.rows == []
|
|
74
|
+
assert resp.row_count == 0
|
|
75
|
+
assert resp.meta.source == "linkedin_ads"
|
|
76
|
+
|
|
77
|
+
def test_campaign_row_with_money(self):
|
|
78
|
+
row = LinkedInAdsCampaignRow.model_validate(
|
|
79
|
+
{
|
|
80
|
+
"id": "500001",
|
|
81
|
+
"name": "Q3 Brand",
|
|
82
|
+
"status": "ACTIVE",
|
|
83
|
+
"type": "SPONSORED_UPDATES",
|
|
84
|
+
"cost_type": "CPC",
|
|
85
|
+
"daily_budget": {"amount": "50", "currencyCode": "USD"},
|
|
86
|
+
"campaign_group": "urn:li:sponsoredCampaignGroup:9001",
|
|
87
|
+
"audience_expansion_enabled": True,
|
|
88
|
+
"run_schedule": {"start": 1700000000000},
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
assert row.id == "500001"
|
|
92
|
+
assert row.type == "SPONSORED_UPDATES"
|
|
93
|
+
assert isinstance(row.daily_budget, LinkedInAdsMoney)
|
|
94
|
+
assert row.daily_budget.amount == "50"
|
|
95
|
+
# currencyCode (camelCase from the Go wire shape) maps onto currency_code.
|
|
96
|
+
assert row.daily_budget.currency_code == "USD"
|
|
97
|
+
assert row.audience_expansion_enabled is True
|
|
98
|
+
assert row.run_schedule.start == 1700000000000
|
|
99
|
+
|
|
100
|
+
def test_campaign_row_defaults(self):
|
|
101
|
+
row = LinkedInAdsCampaignRow.model_validate({"id": "2", "name": "X", "status": "DRAFT"})
|
|
102
|
+
assert row.daily_budget is None
|
|
103
|
+
assert row.audience_expansion_enabled is False
|
|
104
|
+
assert row.run_schedule.start is None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestLinkedInAdsResource:
|
|
108
|
+
"""Tests for LinkedInAdsResource methods."""
|
|
109
|
+
|
|
110
|
+
@pytest.fixture
|
|
111
|
+
def mock_http(self):
|
|
112
|
+
return MagicMock()
|
|
113
|
+
|
|
114
|
+
@pytest.fixture
|
|
115
|
+
def linkedin_ads_resource(self, mock_http):
|
|
116
|
+
return LinkedInAdsResource(mock_http)
|
|
117
|
+
|
|
118
|
+
@pytest.fixture(autouse=True)
|
|
119
|
+
def mock_project_domain(self):
|
|
120
|
+
with patch.dict(os.environ, {"PROJECT_DOMAIN": "example.com"}):
|
|
121
|
+
yield
|
|
122
|
+
|
|
123
|
+
def _insights_payload(self, mock_http):
|
|
124
|
+
mock_http.post.return_value = {
|
|
125
|
+
"rows": [
|
|
126
|
+
{
|
|
127
|
+
"pivot_value": "urn:li:sponsoredCampaign:777",
|
|
128
|
+
"impressions": 1000,
|
|
129
|
+
"clicks": 25,
|
|
130
|
+
"cost_in_local_currency": 12.5,
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"row_count": 1,
|
|
134
|
+
"meta": META,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def test_account_minimal_omits_optional_params(self, linkedin_ads_resource, mock_http):
|
|
138
|
+
"""No dates/granularity => only project_domain + level sent."""
|
|
139
|
+
self._insights_payload(mock_http)
|
|
140
|
+
|
|
141
|
+
result = linkedin_ads_resource.account()
|
|
142
|
+
|
|
143
|
+
mock_http.post.assert_called_once_with(
|
|
144
|
+
"/linkedin-ads/insights",
|
|
145
|
+
json={"project_domain": "example.com", "level": "account"},
|
|
146
|
+
)
|
|
147
|
+
assert len(result.rows) == 1
|
|
148
|
+
assert result.rows[0].pivot_value == "urn:li:sponsoredCampaign:777"
|
|
149
|
+
assert result.meta.currency_code == "USD"
|
|
150
|
+
|
|
151
|
+
def test_campaigns_with_all_params(self, linkedin_ads_resource, mock_http):
|
|
152
|
+
self._insights_payload(mock_http)
|
|
153
|
+
|
|
154
|
+
linkedin_ads_resource.campaigns(
|
|
155
|
+
start_date="2026-07-01",
|
|
156
|
+
end_date="2026-07-31",
|
|
157
|
+
granularity="daily",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
mock_http.post.assert_called_once_with(
|
|
161
|
+
"/linkedin-ads/insights",
|
|
162
|
+
json={
|
|
163
|
+
"project_domain": "example.com",
|
|
164
|
+
"level": "campaign",
|
|
165
|
+
"start_date": "2026-07-01",
|
|
166
|
+
"end_date": "2026-07-31",
|
|
167
|
+
"granularity": "daily",
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def test_pivot_levels(self, linkedin_ads_resource, mock_http):
|
|
172
|
+
"""Each insights method sends its own pivot level."""
|
|
173
|
+
for method, level in [
|
|
174
|
+
(linkedin_ads_resource.account, "account"),
|
|
175
|
+
(linkedin_ads_resource.campaign_groups, "campaign_group"),
|
|
176
|
+
(linkedin_ads_resource.campaigns, "campaign"),
|
|
177
|
+
(linkedin_ads_resource.creatives, "creative"),
|
|
178
|
+
]:
|
|
179
|
+
self._insights_payload(mock_http)
|
|
180
|
+
mock_http.post.reset_mock()
|
|
181
|
+
method()
|
|
182
|
+
_, kwargs = mock_http.post.call_args
|
|
183
|
+
assert kwargs["json"]["level"] == level
|
|
184
|
+
|
|
185
|
+
def test_list_campaigns(self, linkedin_ads_resource, mock_http):
|
|
186
|
+
mock_http.post.return_value = {
|
|
187
|
+
"rows": [{"id": "1", "name": "C", "status": "ACTIVE", "type": "SPONSORED_UPDATES"}],
|
|
188
|
+
"row_count": 1,
|
|
189
|
+
"meta": META,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
result = linkedin_ads_resource.list_campaigns()
|
|
193
|
+
|
|
194
|
+
mock_http.post.assert_called_once_with(
|
|
195
|
+
"/linkedin-ads/campaigns",
|
|
196
|
+
json={"project_domain": "example.com"},
|
|
197
|
+
)
|
|
198
|
+
assert isinstance(result, LinkedInAdsCampaignsResponse)
|
|
199
|
+
assert result.rows[0].name == "C"
|
|
200
|
+
assert result.rows[0].type == "SPONSORED_UPDATES"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|