Skip to content

Commit 8b78c24

Browse files
✨ Add impression share reports API support
- Add custom_reports resource for impression share data - Add ImpressionShareReport, ImpressionShareReportRow models - Add ImpressionShareDateRange, ImpressionShareReportStatus enums - Create/get/list/delete impression share reports - Poll for async report completion with wait_for_report() - Convenience method get_impression_share() handles full workflow
1 parent d8d07dc commit 8b78c24

5 files changed

Lines changed: 734 additions & 4 deletions

File tree

asa_api_client/client.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from asa_api_client.auth import Authenticator
1414
from asa_api_client.logging import get_logger
1515
from asa_api_client.resources.campaigns import CampaignResource
16+
from asa_api_client.resources.custom_reports import CustomReportResource
1617
from asa_api_client.resources.reports import ReportResource
1718

1819
logger = get_logger(__name__)
@@ -34,6 +35,7 @@ class AppleSearchAdsClient:
3435
org_id: The Apple Search Ads organization ID.
3536
campaigns: Resource for managing campaigns.
3637
reports: Resource for generating reports.
38+
custom_reports: Resource for impression share reports.
3739
3840
Example:
3941
Basic usage::
@@ -134,6 +136,7 @@ def __init__(
134136
# Initialize resources
135137
self._campaigns = CampaignResource(self)
136138
self._reports = ReportResource(self)
139+
self._custom_reports = CustomReportResource(self)
137140

138141
logger.info(
139142
"AppleSearchAdsClient initialized for org_id=%d, base_url=%s",
@@ -261,6 +264,29 @@ def reports(self) -> ReportResource:
261264
"""
262265
return self._reports
263266

267+
@property
268+
def custom_reports(self) -> CustomReportResource:
269+
"""Get the custom reports resource for impression share data.
270+
271+
Returns:
272+
CustomReportResource for generating impression share reports.
273+
274+
Example:
275+
Get impression share report::
276+
277+
from datetime import date, timedelta
278+
279+
report = client.custom_reports.get_impression_share(
280+
start_date=date.today() - timedelta(days=7),
281+
end_date=date.today() - timedelta(days=1),
282+
)
283+
284+
for row in report.row:
285+
share = f"{row.low_impression_share}-{row.high_impression_share}%"
286+
print(f"{row.metadata.keyword}: {share}")
287+
"""
288+
return self._custom_reports
289+
264290
def close(self) -> None:
265291
"""Close the HTTP clients and release resources.
266292

asa_api_client/models/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
)
5353
from asa_api_client.models.reports import (
5454
GranularityType,
55+
ImpressionShareDateRange,
56+
ImpressionShareReport,
57+
ImpressionShareReportRequest,
58+
ImpressionShareReportRow,
59+
ImpressionShareReportStatus,
5560
ReportingRequest,
5661
ReportingResponse,
5762
ReportMetadata,
@@ -83,6 +88,11 @@
8388
"ConditionOperator",
8489
"CpaGoal",
8590
"GranularityType",
91+
"ImpressionShareDateRange",
92+
"ImpressionShareReport",
93+
"ImpressionShareReportRequest",
94+
"ImpressionShareReportRow",
95+
"ImpressionShareReportStatus",
8696
"Keyword",
8797
"KeywordCreate",
8898
"KeywordMatchType",

asa_api_client/models/reports.py

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,12 +340,17 @@ class ImpressionShareReportRow(BaseModel):
340340
"""A row in an impression share report.
341341
342342
Impression share shows how often your ads appeared compared
343-
to the total available impressions.
343+
to the total available impressions. Values are percentage ranges.
344344
345345
Attributes:
346-
metadata: Information about the entity.
347-
impression_share: Your share of impressions (0.0 to 1.0).
346+
metadata: Information about the entity (keyword, ad group, campaign).
347+
impression_share: Your share of impressions as a percentage range.
348+
low_impression_share: Low end of impression share range.
349+
high_impression_share: High end of impression share range.
348350
rank: Your rank compared to competitors.
351+
low_rank: Low end of rank range.
352+
high_rank: High end of rank range.
353+
search_popularity: Popularity of the search term (0-5 scale).
349354
"""
350355

351356
model_config = ConfigDict(populate_by_name=True, extra="ignore")
@@ -357,3 +362,130 @@ class ImpressionShareReportRow(BaseModel):
357362
rank: int | None = None
358363
low_rank: int | None = Field(default=None, alias="lowRank")
359364
high_rank: int | None = Field(default=None, alias="highRank")
365+
search_popularity: int | None = Field(default=None, alias="searchPopularity")
366+
367+
368+
class ImpressionShareDateRange(StrEnum):
369+
"""Date range options for weekly impression share reports."""
370+
371+
LAST_WEEK = "LAST_WEEK"
372+
LAST_2_WEEKS = "LAST_2_WEEKS"
373+
LAST_4_WEEKS = "LAST_4_WEEKS"
374+
375+
376+
class ImpressionShareReportStatus(StrEnum):
377+
"""Status of an impression share report."""
378+
379+
QUEUED = "QUEUED"
380+
RUNNING = "RUNNING"
381+
COMPLETED = "COMPLETED"
382+
FAILED = "FAILED"
383+
384+
385+
class ImpressionShareReportRequest(BaseModel):
386+
"""Request model for creating an impression share report.
387+
388+
Impression share reports are async - you create a report request,
389+
then poll for results.
390+
391+
Example:
392+
Create a daily impression share report::
393+
394+
request = ImpressionShareReportRequest(
395+
name="my_report",
396+
start_time=date(2024, 1, 1),
397+
end_time=date(2024, 1, 31),
398+
granularity=GranularityType.DAILY,
399+
)
400+
"""
401+
402+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
403+
404+
name: str = "impression_share_report"
405+
start_time: date = Field(alias="startTime")
406+
end_time: date = Field(alias="endTime")
407+
granularity: GranularityType = GranularityType.DAILY
408+
date_range: ImpressionShareDateRange | None = Field(default=None, alias="dateRange")
409+
selector: dict[str, Any] | None = None
410+
return_records_with_no_metrics: bool = Field(default=True, alias="returnRecordsWithNoMetrics")
411+
return_row_totals: bool = Field(default=False, alias="returnRowTotals")
412+
return_grand_totals: bool = Field(default=False, alias="returnGrandTotals")
413+
414+
415+
class ImpressionShareReport(BaseModel):
416+
"""An impression share report with metadata and rows.
417+
418+
Attributes:
419+
id: The report ID.
420+
name: The report name.
421+
state: Current state of the report (QUEUED, RUNNING, COMPLETED, FAILED).
422+
start_time: Report start date.
423+
end_time: Report end date.
424+
granularity: Time granularity (DAILY or WEEKLY).
425+
row: List of report rows with impression share data.
426+
"""
427+
428+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
429+
430+
id: int
431+
name: str | None = None
432+
state: ImpressionShareReportStatus | str = Field(alias="state")
433+
start_time: str | None = Field(default=None, alias="startTime")
434+
end_time: str | None = Field(default=None, alias="endTime")
435+
granularity: GranularityType | str | None = None
436+
date_range: str | None = Field(default=None, alias="dateRange")
437+
row: list[ImpressionShareReportRow] = Field(default_factory=list)
438+
grand_totals: GrandTotals | None = Field(default=None, alias="grandTotals")
439+
440+
@property
441+
def is_complete(self) -> bool:
442+
"""Check if the report has finished generating."""
443+
return self.state == ImpressionShareReportStatus.COMPLETED
444+
445+
@property
446+
def is_failed(self) -> bool:
447+
"""Check if the report generation failed."""
448+
return self.state == ImpressionShareReportStatus.FAILED
449+
450+
def to_dataframe(self) -> "pd.DataFrame":
451+
"""Convert impression share report to a pandas DataFrame.
452+
453+
Returns:
454+
A DataFrame with impression share data.
455+
456+
Raises:
457+
ImportError: If pandas is not installed.
458+
"""
459+
try:
460+
import pandas as pd
461+
except ImportError:
462+
raise ImportError(
463+
"pandas is required for to_dataframe(). "
464+
"Install with: pip install asa-api-client[pandas]"
465+
) from None
466+
467+
rows_data: list[dict[str, Any]] = []
468+
469+
for row in self.row:
470+
row_data: dict[str, Any] = {}
471+
472+
if row.metadata:
473+
meta_dict = row.metadata.model_dump(by_alias=False, exclude_none=True)
474+
if meta_dict.get("bid_amount"):
475+
row_data["bid"] = meta_dict["bid_amount"].get("amount")
476+
row_data["currency"] = meta_dict["bid_amount"].get("currency")
477+
del meta_dict["bid_amount"]
478+
row_data.update(meta_dict)
479+
480+
# Add impression share fields
481+
row_data["impression_share"] = row.impression_share
482+
row_data["low_impression_share"] = row.low_impression_share
483+
row_data["high_impression_share"] = row.high_impression_share
484+
row_data["rank"] = row.rank
485+
row_data["low_rank"] = row.low_rank
486+
row_data["high_rank"] = row.high_rank
487+
row_data["search_popularity"] = row.search_popularity
488+
489+
rows_data.append(row_data)
490+
491+
return pd.DataFrame(rows_data)

0 commit comments

Comments
 (0)