Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions asa_api_cli/ads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Ad (creative) read-only CLI commands."""

from typing import Annotated, Any

import typer
from asa_api_client.exceptions import AppleSearchAdsError

from asa_api_cli.utils import (
EXIT_ERROR,
OutputFormat,
enum_value,
get_client,
handle_api_error,
output_data,
print_result_panel,
print_warning,
spinner,
)

app = typer.Typer(help="View ads (creatives) within ad groups")

AD_COLUMNS = ["id", "name", "creative_type", "status", "serving_status", "creative_id"]


@app.command("list")
def list_ads(
campaign_id: Annotated[
int,
typer.Option("--campaign", "-c", help="Campaign ID"),
],
ad_group_id: Annotated[
int,
typer.Option("--ad-group", "-a", help="Ad group ID"),
],
limit: Annotated[
int,
typer.Option("--limit", "-l", help="Maximum number of results"),
] = 100,
format: Annotated[
OutputFormat,
typer.Option("--format", "-f", help="Output format"),
] = OutputFormat.TABLE,
) -> None:
"""List ads in an ad group.

Examples:
asa ads list --campaign 123 --ad-group 456
asa ads list -c 123 -a 456 --format json
"""
client = get_client()

try:
with client:
with spinner("Fetching ads..."):
result = client.campaigns(campaign_id).ad_groups(ad_group_id).ads.list(limit=limit)

if not result.data:
print_warning("No ads found")
return

rows: list[dict[str, Any]] = [
{
"id": ad.id,
"name": ad.name,
"creative_type": enum_value(ad.creative_type),
"status": enum_value(ad.status),
"serving_status": enum_value(ad.serving_status),
"creative_id": ad.creative_id or "-",
}
for ad in result.data
]

output_data(rows, AD_COLUMNS, format, title=f"Ads in ad group {ad_group_id}")
except AppleSearchAdsError as e:
handle_api_error(e)
raise typer.Exit(EXIT_ERROR) from None


@app.command("get")
def get_ad(
ad_id: Annotated[
int,
typer.Argument(help="Ad ID"),
],
campaign_id: Annotated[
int,
typer.Option("--campaign", "-c", help="Campaign ID"),
],
ad_group_id: Annotated[
int,
typer.Option("--ad-group", "-a", help="Ad group ID"),
],
) -> None:
"""Show details for a single ad.

Example:
asa ads get 789 --campaign 123 --ad-group 456
"""
client = get_client()

try:
with client:
with spinner("Fetching ad..."):
ad = client.campaigns(campaign_id).ad_groups(ad_group_id).ads.get(ad_id)

print_result_panel(
f"Ad {ad.id}",
{
"Name": ad.name,
"Creative type": enum_value(ad.creative_type),
"Status": enum_value(ad.status),
"Serving status": enum_value(ad.serving_status),
"Creative ID": str(ad.creative_id) if ad.creative_id else "-",
"Campaign ID": str(ad.campaign_id),
"Ad group ID": str(ad.ad_group_id),
},
)
except AppleSearchAdsError as e:
handle_api_error(e)
raise typer.Exit(EXIT_ERROR) from None
58 changes: 57 additions & 1 deletion asa_api_cli/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Authentication CLI commands."""

from pathlib import Path
from typing import Annotated
from typing import Annotated, Any

import typer
from asa_api_client import AppleSearchAdsClient, Settings
Expand All @@ -12,8 +12,12 @@
from asa_api_cli.utils import (
EXIT_ERROR,
EXIT_USAGE,
OutputFormat,
console,
error_console,
get_client,
handle_api_error,
output_data,
print_error,
print_info,
print_result_panel,
Expand Down Expand Up @@ -200,3 +204,55 @@ def show_config(
msg = err["msg"]
error_console.print(f" [error]ASA_{str(field).upper()}:[/error] {msg}")
raise typer.Exit(EXIT_USAGE) from None


ORG_COLUMNS = ["org_id", "org_name", "currency", "role_names", "time_zone"]


@app.command("orgs")
def list_orgs(
format: Annotated[
OutputFormat,
typer.Option("--format", "-f", help="Output format"),
] = OutputFormat.TABLE,
) -> None:
"""List organizations your credentials can access.

Shows each org's ID (use as ASA_ORG_ID), name, currency, and your roles.

Examples:
asa auth orgs
asa auth orgs --format json
"""
client = get_client()

try:
with client:
with spinner("Fetching accessible organizations..."):
acls = client.acls.list()

if not acls:
print_warning("No organizations found for these credentials")
return

rows: list[dict[str, Any]] = [
{
"org_id": acl.org_id,
"org_name": acl.org_name,
"currency": acl.currency or "-",
"role_names": ", ".join(acl.role_names) or "-",
"time_zone": acl.time_zone or "-",
}
for acl in acls
]

output_data(
rows,
ORG_COLUMNS,
format,
title="Accessible organizations",
column_labels={"org_id": "Org ID", "time_zone": "Time Zone"},
)
except AppleSearchAdsError as e:
handle_api_error(e)
raise typer.Exit(EXIT_ERROR) from None
114 changes: 114 additions & 0 deletions asa_api_cli/budget_orders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Budget order CLI commands."""

from typing import Annotated, Any

import typer
from asa_api_client.exceptions import AppleSearchAdsError

from asa_api_cli.utils import (
EXIT_ERROR,
OutputFormat,
enum_value,
format_money,
get_client,
handle_api_error,
output_data,
print_result_panel,
print_warning,
spinner,
)

app = typer.Typer(help="View budget orders")

BUDGET_ORDER_COLUMNS = ["id", "name", "status", "budget", "order_number", "start_date", "end_date"]


def _date_str(value: object) -> str:
"""Render a datetime/date-ish value as an ISO date, or '-'."""
if value is None:
return "-"
return str(value).split("T")[0].split(" ")[0]


@app.command("list")
def list_budget_orders(
limit: Annotated[
int,
typer.Option("--limit", "-l", help="Maximum number of results"),
] = 100,
format: Annotated[
OutputFormat,
typer.Option("--format", "-f", help="Output format"),
] = OutputFormat.TABLE,
) -> None:
"""List budget orders.

Examples:
asa budget-orders list
asa budget-orders list --format json
"""
client = get_client()

try:
with client:
with spinner("Fetching budget orders..."):
result = client.budget_orders.list(limit=limit)

if not result.data:
print_warning("No budget orders found")
return

rows: list[dict[str, Any]] = [
{
"id": bo.id,
"name": bo.name or "-",
"status": enum_value(bo.status) if bo.status else "-",
"budget": format_money(bo.budget.amount, bo.budget.currency) if bo.budget else "-",
"order_number": bo.order_number or "-",
"start_date": _date_str(bo.start_date),
"end_date": _date_str(bo.end_date),
}
for bo in result.data
]

output_data(rows, BUDGET_ORDER_COLUMNS, format, title="Budget orders")
except AppleSearchAdsError as e:
handle_api_error(e)
raise typer.Exit(EXIT_ERROR) from None


@app.command("get")
def get_budget_order(
budget_order_id: Annotated[
int,
typer.Argument(help="Budget order ID"),
],
) -> None:
"""Show details for a single budget order.

Example:
asa budget-orders get 123456
"""
client = get_client()

try:
with client:
with spinner("Fetching budget order..."):
bo = client.budget_orders.get(budget_order_id)

print_result_panel(
f"Budget order {bo.id}",
{
"Name": bo.name or "-",
"Status": enum_value(bo.status) if bo.status else "-",
"Budget": format_money(bo.budget.amount, bo.budget.currency) if bo.budget else "-",
"Order number": bo.order_number or "-",
"Start date": _date_str(bo.start_date),
"End date": _date_str(bo.end_date),
"Client name": bo.client_name or "-",
"Billing email": bo.billing_email or "-",
},
)
except AppleSearchAdsError as e:
handle_api_error(e)
raise typer.Exit(EXIT_ERROR) from None
6 changes: 6 additions & 0 deletions asa_api_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@

from asa_api_cli import (
ad_groups,
ads,
apps,
auth,
brand,
budget_orders,
campaigns,
countries,
geo,
impression_share,
keywords,
optimize,
product_pages,
reports,
translate,
)
Expand All @@ -31,10 +34,13 @@
app.add_typer(brand.app, name="brand", help="Create brand protection campaigns")
app.add_typer(campaigns.app, name="campaigns", help="Manage campaigns")
app.add_typer(ad_groups.app, name="ad-groups", help="Manage ad groups")
app.add_typer(ads.app, name="ads", help="View ads (creatives) within ad groups")
app.add_typer(keywords.app, name="keywords", help="Manage keywords")
app.add_typer(product_pages.app, name="product-pages", help="View custom product pages (CPP)")
app.add_typer(apps.app, name="apps", help="Search the App Store for advertisable apps")
app.add_typer(geo.app, name="geo", help="Search geographic locations for targeting")
app.add_typer(countries.app, name="countries", help="List supported countries and regions")
app.add_typer(budget_orders.app, name="budget-orders", help="View budget orders")
app.add_typer(reports.app, name="reports", help="Generate reports")
app.add_typer(optimize.app, name="optimize", help="Optimization tools")
app.add_typer(impression_share.app, name="impression-share", help="Impression share analysis")
Expand Down
Loading
Loading