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
81 changes: 81 additions & 0 deletions asa_api_cli/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""App lookup 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,
get_client,
handle_api_error,
output_data,
print_warning,
spinner,
)

app = typer.Typer(help="Search the App Store for advertisable apps")

APP_COLUMNS = ["adam_id", "app_name", "developer_name", "countries"]


@app.command("search")
def search_apps(
query: Annotated[
str,
typer.Argument(help="Search term (app name or keyword)"),
],
own: Annotated[
bool,
typer.Option("--own", help="Only return apps owned by your org"),
] = False,
limit: Annotated[
int,
typer.Option("--limit", "-l", help="Maximum number of results"),
] = 50,
format: Annotated[
OutputFormat,
typer.Option("--format", "-f", help="Output format"),
] = OutputFormat.TABLE,
) -> None:
"""Search for iOS apps eligible for advertising.

Returns each app's adamId — the identifier you need to create campaigns.

Examples:
asa apps search "weather"
asa apps search "my app" --own
asa apps search photo --limit 10 --format json
"""
client = get_client()

try:
with client:
with spinner("Searching App Store..."):
results = client.apps.search(query=query, return_own_apps=own, limit=limit)

if not results:
print_warning("No apps found")
return

rows: list[dict[str, Any]] = [
{
"adam_id": app_info.adam_id,
"app_name": app_info.app_name,
"developer_name": app_info.developer_name or "-",
"countries": ", ".join(app_info.country_or_region_codes or []) or "-",
}
for app_info in results
]

output_data(
rows,
APP_COLUMNS,
format,
title=f"App search: {query}",
column_labels={"adam_id": "adam ID"},
)
except AppleSearchAdsError as e:
handle_api_error(e)
raise typer.Exit(EXIT_ERROR) from None
84 changes: 84 additions & 0 deletions asa_api_cli/countries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Supported country/region reference CLI commands."""

from typing import Annotated, Any

import typer
from asa_api_client.exceptions import AppleSearchAdsError
from asa_api_client.models import LanguageDetail

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

app = typer.Typer(help="List supported countries and regions")

COUNTRY_COLUMNS = ["country_or_region", "default_language", "supported_languages"]


def _language_label(detail: LanguageDetail | None) -> str:
"""Render a LanguageDetail as a short label, e.g. "English (en)"."""
if detail is None:
return "-"
if detail.language and detail.language_code:
return f"{detail.language} ({detail.language_code})"
return detail.language or detail.language_code or "-"


@app.command("list")
def list_countries(
codes: Annotated[
list[str] | None,
typer.Option("--code", "-c", help="Filter by ISO alpha-2 code (repeatable, e.g. -c US -c GB)"),
] = None,
format: Annotated[
OutputFormat,
typer.Option("--format", "-f", help="Output format"),
] = OutputFormat.TABLE,
) -> None:
"""List countries and regions supported for advertising.

Shows each region's default and supported ad languages.

Examples:
asa countries list
asa countries list --code US --code GB
asa countries list --format json
"""
client = get_client()

try:
with client:
with spinner("Fetching supported countries..."):
results = client.countries_or_regions.list(
countries_or_regions=[c.upper() for c in codes] if codes else None,
)

if not results:
print_warning("No countries found")
return

rows: list[dict[str, Any]] = [
{
"country_or_region": cr.country_or_region,
"default_language": _language_label(cr.default_language),
"supported_languages": ", ".join(_language_label(lang) for lang in cr.supported_languages or [])
or "-",
}
for cr in results
]

output_data(
rows,
COUNTRY_COLUMNS,
format,
title="Supported countries / regions",
)
except AppleSearchAdsError as e:
handle_api_error(e)
raise typer.Exit(EXIT_ERROR) from None
92 changes: 92 additions & 0 deletions asa_api_cli/geo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Geographic location lookup 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_warning,
spinner,
)

app = typer.Typer(help="Search geographic locations for targeting")

GEO_COLUMNS = ["id", "display_name", "entity", "country_or_region", "admin_area", "locality"]


@app.command("search")
def search_geo(
query: Annotated[
str,
typer.Argument(help="Place name to search for (e.g. California, London)"),
],
country: Annotated[
str,
typer.Option("--country", "-c", help="Country code to search within (e.g. US, GB)"),
],
entity: Annotated[
str | None,
typer.Option("--entity", "-e", help="Filter by entity type: Country, AdminArea, or Locality"),
] = None,
limit: Annotated[
int,
typer.Option("--limit", "-l", help="Maximum number of results"),
] = 50,
format: Annotated[
OutputFormat,
typer.Option("--format", "-f", help="Output format"),
] = OutputFormat.TABLE,
) -> None:
"""Search for geographic locations available for campaign targeting.

Each result's id can be used to target the location in a campaign.

Examples:
asa geo search California --country US
asa geo search London -c GB --entity Locality
asa geo search Bayern -c DE --format json
"""
client = get_client()

try:
with client:
with spinner("Searching locations..."):
results = client.geo.search(
query=query,
country_code=country.upper(),
entity=entity,
limit=limit,
)

if not results:
print_warning("No locations found")
return

rows: list[dict[str, Any]] = [
{
"id": loc.id,
"display_name": loc.display_name,
"entity": enum_value(loc.entity),
"country_or_region": loc.country_or_region or "-",
"admin_area": loc.admin_area or "-",
"locality": loc.locality or "-",
}
for loc in results
]

output_data(
rows,
GEO_COLUMNS,
format,
title=f"Locations in {country.upper()}: {query}",
)
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,9 +6,12 @@

from asa_api_cli import (
ad_groups,
apps,
auth,
brand,
campaigns,
countries,
geo,
impression_share,
keywords,
optimize,
Expand All @@ -29,6 +32,9 @@
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(keywords.app, name="keywords", help="Manage keywords")
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(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
30 changes: 30 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,33 @@ def test_auth_help() -> None:
assert result.exit_code == 0
assert "show" in result.stdout
assert "test" in result.stdout


def test_root_help_lists_lookup_commands() -> None:
"""Root help should advertise the v5 lookup command groups."""
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "apps" in result.stdout
assert "geo" in result.stdout
assert "countries" in result.stdout


def test_apps_help() -> None:
"""Test apps subcommand help."""
result = runner.invoke(app, ["apps", "--help"])
assert result.exit_code == 0
assert "search" in result.stdout


def test_geo_help() -> None:
"""Test geo subcommand help."""
result = runner.invoke(app, ["geo", "--help"])
assert result.exit_code == 0
assert "search" in result.stdout


def test_countries_help() -> None:
"""Test countries subcommand help."""
result = runner.invoke(app, ["countries", "--help"])
assert result.exit_code == 0
assert "list" in result.stdout
Loading