google-ads-cli 0.0.1__py3-none-any.whl
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.
- google_ads_cli-0.0.1.dist-info/METADATA +212 -0
- google_ads_cli-0.0.1.dist-info/RECORD +28 -0
- google_ads_cli-0.0.1.dist-info/WHEEL +4 -0
- google_ads_cli-0.0.1.dist-info/entry_points.txt +2 -0
- google_ads_cli-0.0.1.dist-info/licenses/LICENSE +201 -0
- googleadscli/__init__.py +1 -0
- googleadscli/cli.py +81 -0
- googleadscli/client_factory.py +24 -0
- googleadscli/commands/__init__.py +0 -0
- googleadscli/commands/_common.py +26 -0
- googleadscli/commands/accounts.py +49 -0
- googleadscli/commands/auth.py +150 -0
- googleadscli/commands/call.py +76 -0
- googleadscli/commands/fields.py +94 -0
- googleadscli/commands/highlevel/__init__.py +18 -0
- googleadscli/commands/highlevel/ad.py +49 -0
- googleadscli/commands/highlevel/ad_group.py +46 -0
- googleadscli/commands/highlevel/budget.py +46 -0
- googleadscli/commands/highlevel/campaign.py +106 -0
- googleadscli/commands/highlevel/keyword.py +46 -0
- googleadscli/commands/mutate.py +53 -0
- googleadscli/commands/query.py +35 -0
- googleadscli/config.py +128 -0
- googleadscli/errors.py +81 -0
- googleadscli/formatting.py +55 -0
- googleadscli/gaql.py +37 -0
- googleadscli/proto_bridge.py +308 -0
- googleadscli/utils.py +22 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""`gads auth login|status` -- einmalige OAuth2-Einrichtung fuer nicht-interaktive Nutzung."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from googleadscli import client_factory, config, formatting
|
|
8
|
+
from googleadscli.commands._common import run_guarded
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(no_args_is_help=True, help="OAuth2-Einrichtung und Zugangspruefung")
|
|
11
|
+
|
|
12
|
+
SCOPES = ["https://www.googleapis.com/auth/adwords"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("login")
|
|
16
|
+
def login(
|
|
17
|
+
ctx: typer.Context,
|
|
18
|
+
client_id: str = typer.Option(..., "--client-id", envvar="GOOGLE_ADS_CLIENT_ID"),
|
|
19
|
+
client_secret: str = typer.Option(..., "--client-secret", envvar="GOOGLE_ADS_CLIENT_SECRET"),
|
|
20
|
+
developer_token: str = typer.Option(
|
|
21
|
+
None,
|
|
22
|
+
"--developer-token",
|
|
23
|
+
envvar="GOOGLE_ADS_DEVELOPER_TOKEN",
|
|
24
|
+
help="Optional: nicht noetig bei Cloud-managed Access ohne klassischen Developer Token",
|
|
25
|
+
),
|
|
26
|
+
login_customer_id: str = typer.Option(
|
|
27
|
+
None, "--login-customer-id", help="Optionale MCC-CID, die standardmaessig als login-customer-id gesetzt wird"
|
|
28
|
+
),
|
|
29
|
+
no_browser: bool = typer.Option(
|
|
30
|
+
False, "--no-browser", help="Auth-URL ausgeben statt Browser zu oeffnen (fuer SSH/Remote-Sessions)"
|
|
31
|
+
),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Fuehrt einmalig den interaktiven OAuth2-Consent-Flow aus und speichert den Refresh Token."""
|
|
34
|
+
|
|
35
|
+
def _run() -> None:
|
|
36
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
37
|
+
|
|
38
|
+
client_config = {
|
|
39
|
+
"installed": {
|
|
40
|
+
"client_id": client_id,
|
|
41
|
+
"client_secret": client_secret,
|
|
42
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
43
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
44
|
+
"redirect_uris": ["http://localhost"],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
flow = InstalledAppFlow.from_client_config(client_config, scopes=SCOPES)
|
|
48
|
+
credentials = flow.run_local_server(open_browser=not no_browser)
|
|
49
|
+
|
|
50
|
+
data = {
|
|
51
|
+
"client_id": client_id,
|
|
52
|
+
"client_secret": client_secret,
|
|
53
|
+
"refresh_token": credentials.refresh_token,
|
|
54
|
+
}
|
|
55
|
+
if developer_token:
|
|
56
|
+
data["developer_token"] = developer_token
|
|
57
|
+
if login_customer_id:
|
|
58
|
+
data["login_customer_id"] = login_customer_id.replace("-", "")
|
|
59
|
+
|
|
60
|
+
path = config.write_config_file(data)
|
|
61
|
+
formatting.render({"status": "ok", "config_path": str(path)}, fmt=ctx.obj["format"])
|
|
62
|
+
|
|
63
|
+
run_guarded(ctx, _run)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command("use-service-account")
|
|
67
|
+
def use_service_account(
|
|
68
|
+
ctx: typer.Context,
|
|
69
|
+
json_key_file_path: str = typer.Option(
|
|
70
|
+
...,
|
|
71
|
+
"--json-key-file-path",
|
|
72
|
+
help="Pfad zur Service-Account-JSON-Schluesseldatei aus der Google Cloud Console",
|
|
73
|
+
),
|
|
74
|
+
developer_token: str = typer.Option(
|
|
75
|
+
None,
|
|
76
|
+
"--developer-token",
|
|
77
|
+
envvar="GOOGLE_ADS_DEVELOPER_TOKEN",
|
|
78
|
+
help="Optional: nicht noetig bei Cloud-managed Access ohne klassischen Developer Token",
|
|
79
|
+
),
|
|
80
|
+
impersonated_email: str = typer.Option(
|
|
81
|
+
None,
|
|
82
|
+
"--impersonated-email",
|
|
83
|
+
help="Nur fuer Domain-Wide-Delegation: zu impersonierender Workspace-Nutzer. "
|
|
84
|
+
"Nicht noetig, wenn die client_email der Schluesseldatei direkt als Nutzer auf dem "
|
|
85
|
+
"Google-Ads-Konto/MCC hinterlegt wurde.",
|
|
86
|
+
),
|
|
87
|
+
login_customer_id: str = typer.Option(None, "--login-customer-id", help="Optionale MCC-CID"),
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Richtet Service-Account-basierte Authentifizierung ein (kein interaktiver Consent noetig).
|
|
90
|
+
|
|
91
|
+
Voraussetzung: die client_email aus der JSON-Schluesseldatei muss auf dem
|
|
92
|
+
Google-Ads-Konto (oder MCC) als Nutzer mit passendem Zugriffslevel hinterlegt sein
|
|
93
|
+
(Tools & Einstellungen > Zugriff und Sicherheit > Nutzer), es sei denn, es wird
|
|
94
|
+
stattdessen per --impersonated-email eine Domain-Wide-Delegation genutzt.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def _run() -> None:
|
|
98
|
+
import json as _json
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
|
|
101
|
+
key_path = Path(json_key_file_path).expanduser().resolve()
|
|
102
|
+
if not key_path.exists():
|
|
103
|
+
raise config.ConfigError(f"Service-Account-Schluesseldatei nicht gefunden: {key_path}")
|
|
104
|
+
key_data = _json.loads(key_path.read_text(encoding="utf-8"))
|
|
105
|
+
if key_data.get("type") != "service_account":
|
|
106
|
+
raise config.ConfigError(
|
|
107
|
+
f"Datei {key_path} ist kein Service-Account-Schluessel (type={key_data.get('type')!r})."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
data: dict = {"json_key_file_path": str(key_path)}
|
|
111
|
+
if developer_token:
|
|
112
|
+
data["developer_token"] = developer_token
|
|
113
|
+
if impersonated_email:
|
|
114
|
+
data["impersonated_email"] = impersonated_email
|
|
115
|
+
if login_customer_id:
|
|
116
|
+
data["login_customer_id"] = login_customer_id.replace("-", "")
|
|
117
|
+
|
|
118
|
+
path = config.write_config_file(data)
|
|
119
|
+
formatting.render(
|
|
120
|
+
{
|
|
121
|
+
"status": "ok",
|
|
122
|
+
"config_path": str(path),
|
|
123
|
+
"service_account_email": key_data.get("client_email"),
|
|
124
|
+
"hint": "Stelle sicher, dass diese client_email als Nutzer auf dem Google-Ads-Konto "
|
|
125
|
+
"hinterlegt ist, falls --impersonated-email nicht gesetzt wurde.",
|
|
126
|
+
},
|
|
127
|
+
fmt=ctx.obj["format"],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
run_guarded(ctx, _run)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command("status")
|
|
134
|
+
def status(ctx: typer.Context) -> None:
|
|
135
|
+
"""Prueft die aktuelle Konfiguration gegen die echte API (list_accessible_customers)."""
|
|
136
|
+
|
|
137
|
+
def _run() -> None:
|
|
138
|
+
client = client_factory.build_client(
|
|
139
|
+
config_path=ctx.obj["config_path"],
|
|
140
|
+
cli_overrides=ctx.obj["cli_overrides"],
|
|
141
|
+
version=ctx.obj["version"],
|
|
142
|
+
)
|
|
143
|
+
service = client.get_service("CustomerService", version=ctx.obj["version"])
|
|
144
|
+
response = service.list_accessible_customers()
|
|
145
|
+
formatting.render(
|
|
146
|
+
{"status": "ok", "accessible_customers": list(response.resource_names)},
|
|
147
|
+
fmt=ctx.obj["format"],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
run_guarded(ctx, _run)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""`gads call <Service> <Methode>` -- generischer Fallback fuer jede API-Methode.
|
|
2
|
+
|
|
3
|
+
Deckt alles ab, was kein regulaerer `mutate_*`-Aufruf ist: BatchJobService,
|
|
4
|
+
ConversionUploadService, OfflineUserDataJobService, KeywordPlanService,
|
|
5
|
+
ReachPlanService, GoogleAdsFieldService, CustomerService, Long-Running-
|
|
6
|
+
Operations, etc.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from googleadscli import formatting, proto_bridge
|
|
14
|
+
from googleadscli.commands import _common
|
|
15
|
+
from googleadscli.utils import load_json_arg
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register(app: typer.Typer) -> None:
|
|
19
|
+
@app.command("call")
|
|
20
|
+
def call_cmd(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
service: str = typer.Argument(..., help="Service-Name, z.B. 'BatchJobService'"),
|
|
23
|
+
method: str = typer.Argument(..., help="Methodenname (snake_case), z.B. 'mutate' oder 'run_batch_job'"),
|
|
24
|
+
request_json: str = typer.Option(
|
|
25
|
+
"{}", "--request-json", "-r", help="JSON-Objekt als Request-Payload; mit '@pfad.json' aus Datei laden"
|
|
26
|
+
),
|
|
27
|
+
no_wait: bool = typer.Option(
|
|
28
|
+
False, "--no-wait", help="Bei Long-Running-Operations nicht auf das Ergebnis warten"
|
|
29
|
+
),
|
|
30
|
+
timeout: float = typer.Option(
|
|
31
|
+
None, "--timeout", help="Timeout in Sekunden beim Warten auf eine Long-Running-Operation"
|
|
32
|
+
),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Ruft eine beliebige Service-Methode generisch auf (voller API-Fallback)."""
|
|
35
|
+
|
|
36
|
+
def _run() -> None:
|
|
37
|
+
payload = load_json_arg(request_json)
|
|
38
|
+
if payload is not None and not isinstance(payload, dict):
|
|
39
|
+
raise ValueError("--request-json muss ein JSON-Objekt sein.")
|
|
40
|
+
|
|
41
|
+
client = _common.build_client(ctx)
|
|
42
|
+
result = proto_bridge.invoke_call(
|
|
43
|
+
client,
|
|
44
|
+
service,
|
|
45
|
+
method,
|
|
46
|
+
payload,
|
|
47
|
+
wait_for_operation=not no_wait,
|
|
48
|
+
operation_timeout=timeout,
|
|
49
|
+
version=ctx.obj["version"],
|
|
50
|
+
)
|
|
51
|
+
formatting.render(result, fmt=ctx.obj["format"])
|
|
52
|
+
|
|
53
|
+
_common.run_guarded(ctx, _run)
|
|
54
|
+
|
|
55
|
+
@app.command("list-services")
|
|
56
|
+
def list_services_cmd(ctx: typer.Context) -> None:
|
|
57
|
+
"""Listet alle verfuegbaren Service-Namen der aktiven API-Version."""
|
|
58
|
+
|
|
59
|
+
def _run() -> None:
|
|
60
|
+
names = proto_bridge.list_service_names(version=ctx.obj["version"])
|
|
61
|
+
formatting.render(names, fmt=ctx.obj["format"])
|
|
62
|
+
|
|
63
|
+
_common.run_guarded(ctx, _run)
|
|
64
|
+
|
|
65
|
+
@app.command("list-methods")
|
|
66
|
+
def list_methods_cmd(
|
|
67
|
+
ctx: typer.Context, service: str = typer.Argument(..., help="Service-Name, z.B. 'CampaignService'")
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Listet alle aufrufbaren Methoden eines Service auf."""
|
|
70
|
+
|
|
71
|
+
def _run() -> None:
|
|
72
|
+
client = _common.build_client(ctx)
|
|
73
|
+
methods = proto_bridge.list_service_methods(client, service, version=ctx.obj["version"])
|
|
74
|
+
formatting.render(methods, fmt=ctx.obj["format"])
|
|
75
|
+
|
|
76
|
+
_common.run_guarded(ctx, _run)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""`gads fields <resource>` -- Feld-Metadaten der Google Ads API nachschlagen.
|
|
2
|
+
|
|
3
|
+
Duenner Komfort-Wrapper um GoogleAdsFieldService.SearchGoogleAdsFields (eine
|
|
4
|
+
GAQL-aehnliche Mini-Query-Sprache gegen den globalen Feld-Katalog der API,
|
|
5
|
+
kein Kundenkonto noetig). Beantwortet Fragen wie "welche Felder bietet
|
|
6
|
+
ad_group?" ohne dass man die Mini-Query-Syntax von Hand bauen muss.
|
|
7
|
+
|
|
8
|
+
Hinweis: Diese Metadaten sagen nur, was *selectable*/*filterable*/*sortable*
|
|
9
|
+
per GAQL ist -- nicht, was per `mutate` *schreibbar* oder mit welchem
|
|
10
|
+
advertising_channel_type kompatibel ist. Das laesst sich nur durch Lesen des
|
|
11
|
+
Resource-Protos (siehe README) oder durch tatsaechliches Ausprobieren
|
|
12
|
+
(--dry-run) herausfinden.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from googleadscli import formatting, proto_bridge
|
|
20
|
+
from googleadscli.commands._common import build_client, run_guarded
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(no_args_is_help=True, help="Feld-Metadaten der API nachschlagen (kein Kundenkonto noetig)")
|
|
23
|
+
|
|
24
|
+
_DEFAULT_SELECT = "name, category, data_type, selectable, filterable, sortable, is_repeated, enum_values"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command("list")
|
|
28
|
+
def list_fields(
|
|
29
|
+
ctx: typer.Context,
|
|
30
|
+
resource: str = typer.Argument(
|
|
31
|
+
..., help="Ressourcen- oder Feldname bzw. -praefix, z.B. 'ad_group' oder 'campaign.network_settings'"
|
|
32
|
+
),
|
|
33
|
+
category: str = typer.Option(
|
|
34
|
+
None,
|
|
35
|
+
"--category",
|
|
36
|
+
help="Nur diese Kategorie: RESOURCE|ATTRIBUTE|SEGMENT|METRIC (Default: alle)",
|
|
37
|
+
),
|
|
38
|
+
exact: bool = typer.Option(
|
|
39
|
+
False, "--exact", help="Nur exakten Feldnamen nachschlagen statt Praefix-Suche"
|
|
40
|
+
),
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Listet Feld-Metadaten (selectable/filterable/sortable/enum_values/...) fuer ein Praefix."""
|
|
43
|
+
|
|
44
|
+
def _run() -> None:
|
|
45
|
+
if exact:
|
|
46
|
+
where = f'name = "{resource}"'
|
|
47
|
+
else:
|
|
48
|
+
# Die Mini-Query-Sprache von GoogleAdsFieldService kennt kein OR,
|
|
49
|
+
# daher listet dies nur die Kindfelder (Praefix); der exakte
|
|
50
|
+
# Ressourcen-Eintrag selbst laesst sich per 'gads fields show' holen.
|
|
51
|
+
where = f'name LIKE "{resource}.%"'
|
|
52
|
+
if category:
|
|
53
|
+
where += f' AND category = "{category.upper()}"'
|
|
54
|
+
|
|
55
|
+
query = f"SELECT {_DEFAULT_SELECT} WHERE {where}"
|
|
56
|
+
|
|
57
|
+
client = build_client(ctx)
|
|
58
|
+
rows = proto_bridge.invoke_call(
|
|
59
|
+
client,
|
|
60
|
+
"GoogleAdsFieldService",
|
|
61
|
+
"search_google_ads_fields",
|
|
62
|
+
{"query": query},
|
|
63
|
+
version=ctx.obj["version"],
|
|
64
|
+
)
|
|
65
|
+
formatting.render(rows, fmt=ctx.obj["format"])
|
|
66
|
+
|
|
67
|
+
run_guarded(ctx, _run)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("show")
|
|
71
|
+
def show_field(
|
|
72
|
+
ctx: typer.Context,
|
|
73
|
+
name: str = typer.Argument(..., help="Exakter Feld- oder Ressourcenname, z.B. 'ad_group.status'"),
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Zeigt die vollstaendigen Metadaten (inkl. selectable_with) fuer genau einen Feld-/Ressourcennamen."""
|
|
76
|
+
|
|
77
|
+
def _run() -> None:
|
|
78
|
+
client = build_client(ctx)
|
|
79
|
+
rows = proto_bridge.invoke_call(
|
|
80
|
+
client,
|
|
81
|
+
"GoogleAdsFieldService",
|
|
82
|
+
"search_google_ads_fields",
|
|
83
|
+
{
|
|
84
|
+
"query": (
|
|
85
|
+
"SELECT name, category, data_type, selectable, filterable, sortable, "
|
|
86
|
+
"is_repeated, enum_values, selectable_with, attribute_resources, metrics, segments "
|
|
87
|
+
f'WHERE name = "{name}"'
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
version=ctx.obj["version"],
|
|
91
|
+
)
|
|
92
|
+
formatting.render(rows[0] if rows else {}, fmt=ctx.obj["format"])
|
|
93
|
+
|
|
94
|
+
run_guarded(ctx, _run)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Komfortbefehle fuer haeufige Workflows -- duenne Wrapper ueber `mutate`/`call`.
|
|
2
|
+
|
|
3
|
+
Vollstaendigkeit ist bereits durch die generischen Basisbefehle (query/mutate/
|
|
4
|
+
call) gegeben; diese Befehle dienen nur der Ergonomie.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from googleadscli.commands.highlevel import ad, ad_group, budget, campaign, keyword
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(no_args_is_help=True, help="Komfortbefehle fuer haeufige Workflows")
|
|
14
|
+
app.add_typer(budget.app, name="budget")
|
|
15
|
+
app.add_typer(campaign.app, name="campaign")
|
|
16
|
+
app.add_typer(ad_group.app, name="ad-group")
|
|
17
|
+
app.add_typer(keyword.app, name="keyword")
|
|
18
|
+
app.add_typer(ad.app, name="ad")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""`gads hl ad create-responsive-search-ad` -- Responsive Search Ad anlegen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from googleadscli import formatting, proto_bridge
|
|
8
|
+
from googleadscli.commands import _common
|
|
9
|
+
from googleadscli.utils import normalize_customer_id
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True, help="Anzeigen anlegen/verwalten")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("create-responsive-search-ad")
|
|
15
|
+
def create_responsive_search_ad(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
customer_id: str = typer.Option(..., "--customer-id", "-c"),
|
|
18
|
+
ad_group_resource_name: str = typer.Option(..., "--ad-group"),
|
|
19
|
+
final_url: str = typer.Option(..., "--final-url"),
|
|
20
|
+
headline: list[str] = typer.Option(..., "--headline", help="Mehrfach angebbar, min. 3 empfohlen"),
|
|
21
|
+
description: list[str] = typer.Option(..., "--description", help="Mehrfach angebbar, min. 2 empfohlen"),
|
|
22
|
+
status: str = typer.Option("PAUSED", "--status"),
|
|
23
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
24
|
+
) -> None:
|
|
25
|
+
def _run() -> None:
|
|
26
|
+
client = _common.build_client(ctx)
|
|
27
|
+
create_payload = {
|
|
28
|
+
"ad_group": ad_group_resource_name,
|
|
29
|
+
"status": status,
|
|
30
|
+
"ad": {
|
|
31
|
+
"final_urls": [final_url],
|
|
32
|
+
"responsive_search_ad": {
|
|
33
|
+
"headlines": [{"text": h} for h in headline],
|
|
34
|
+
"descriptions": [{"text": d} for d in description],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
operations = [{"create": create_payload}]
|
|
39
|
+
result = proto_bridge.run_mutate(
|
|
40
|
+
client,
|
|
41
|
+
"ad_group_ad",
|
|
42
|
+
normalize_customer_id(customer_id),
|
|
43
|
+
operations,
|
|
44
|
+
validate_only=dry_run,
|
|
45
|
+
version=ctx.obj["version"],
|
|
46
|
+
)
|
|
47
|
+
formatting.render(result, fmt=ctx.obj["format"])
|
|
48
|
+
|
|
49
|
+
_common.run_guarded(ctx, _run)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""`gads hl ad-group create` -- Ad Group anlegen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from googleadscli import formatting, proto_bridge
|
|
8
|
+
from googleadscli.commands import _common
|
|
9
|
+
from googleadscli.utils import normalize_customer_id
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True, help="Ad Groups anlegen/verwalten")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("create")
|
|
15
|
+
def create(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
customer_id: str = typer.Option(..., "--customer-id", "-c"),
|
|
18
|
+
name: str = typer.Option(..., "--name"),
|
|
19
|
+
campaign_resource_name: str = typer.Option(..., "--campaign", help="Resource-Name der Kampagne"),
|
|
20
|
+
status: str = typer.Option("PAUSED", "--status"),
|
|
21
|
+
ad_group_type: str = typer.Option("SEARCH_STANDARD", "--type", help="AdGroupTypeEnum-Wert"),
|
|
22
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
23
|
+
) -> None:
|
|
24
|
+
def _run() -> None:
|
|
25
|
+
client = _common.build_client(ctx)
|
|
26
|
+
operations = [
|
|
27
|
+
{
|
|
28
|
+
"create": {
|
|
29
|
+
"name": name,
|
|
30
|
+
"campaign": campaign_resource_name,
|
|
31
|
+
"status": status,
|
|
32
|
+
"type": ad_group_type,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
result = proto_bridge.run_mutate(
|
|
37
|
+
client,
|
|
38
|
+
"ad_group",
|
|
39
|
+
normalize_customer_id(customer_id),
|
|
40
|
+
operations,
|
|
41
|
+
validate_only=dry_run,
|
|
42
|
+
version=ctx.obj["version"],
|
|
43
|
+
)
|
|
44
|
+
formatting.render(result, fmt=ctx.obj["format"])
|
|
45
|
+
|
|
46
|
+
_common.run_guarded(ctx, _run)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""`gads hl budget create` -- CampaignBudget anlegen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from googleadscli import formatting, proto_bridge
|
|
8
|
+
from googleadscli.commands import _common
|
|
9
|
+
from googleadscli.utils import normalize_customer_id
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True, help="CampaignBudget anlegen/verwalten")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("create")
|
|
15
|
+
def create(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
customer_id: str = typer.Option(..., "--customer-id", "-c"),
|
|
18
|
+
name: str = typer.Option(..., "--name"),
|
|
19
|
+
amount_micros: int = typer.Option(..., "--amount-micros", help="Tagesbudget in Micros (1 EUR = 1_000_000)"),
|
|
20
|
+
delivery_method: str = typer.Option("STANDARD", "--delivery-method", help="STANDARD|ACCELERATED"),
|
|
21
|
+
explicitly_shared: bool = typer.Option(False, "--explicitly-shared"),
|
|
22
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
23
|
+
) -> None:
|
|
24
|
+
def _run() -> None:
|
|
25
|
+
client = _common.build_client(ctx)
|
|
26
|
+
operations = [
|
|
27
|
+
{
|
|
28
|
+
"create": {
|
|
29
|
+
"name": name,
|
|
30
|
+
"amount_micros": amount_micros,
|
|
31
|
+
"delivery_method": delivery_method,
|
|
32
|
+
"explicitly_shared": explicitly_shared,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
result = proto_bridge.run_mutate(
|
|
37
|
+
client,
|
|
38
|
+
"campaign_budget",
|
|
39
|
+
normalize_customer_id(customer_id),
|
|
40
|
+
operations,
|
|
41
|
+
validate_only=dry_run,
|
|
42
|
+
version=ctx.obj["version"],
|
|
43
|
+
)
|
|
44
|
+
formatting.render(result, fmt=ctx.obj["format"])
|
|
45
|
+
|
|
46
|
+
_common.run_guarded(ctx, _run)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""`gads hl campaign create|pause|enable|remove` -- Kampagnen verwalten."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from googleadscli import formatting, proto_bridge
|
|
8
|
+
from googleadscli.commands import _common
|
|
9
|
+
from googleadscli.utils import normalize_customer_id
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True, help="Kampagnen anlegen/verwalten")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("create")
|
|
15
|
+
def create(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
customer_id: str = typer.Option(..., "--customer-id", "-c"),
|
|
18
|
+
name: str = typer.Option(..., "--name"),
|
|
19
|
+
budget_resource_name: str = typer.Option(..., "--budget", help="Resource-Name des CampaignBudget"),
|
|
20
|
+
advertising_channel_type: str = typer.Option("SEARCH", "--channel-type", help="z.B. SEARCH, DISPLAY, PERFORMANCE_MAX"),
|
|
21
|
+
status: str = typer.Option("PAUSED", "--status", help="Standard PAUSED, um versehentliche Ausgaben zu vermeiden"),
|
|
22
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
23
|
+
) -> None:
|
|
24
|
+
def _run() -> None:
|
|
25
|
+
client = _common.build_client(ctx)
|
|
26
|
+
operations = [
|
|
27
|
+
{
|
|
28
|
+
"create": {
|
|
29
|
+
"name": name,
|
|
30
|
+
"campaign_budget": budget_resource_name,
|
|
31
|
+
"advertising_channel_type": advertising_channel_type,
|
|
32
|
+
"status": status,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
result = proto_bridge.run_mutate(
|
|
37
|
+
client,
|
|
38
|
+
"campaign",
|
|
39
|
+
normalize_customer_id(customer_id),
|
|
40
|
+
operations,
|
|
41
|
+
validate_only=dry_run,
|
|
42
|
+
version=ctx.obj["version"],
|
|
43
|
+
)
|
|
44
|
+
formatting.render(result, fmt=ctx.obj["format"])
|
|
45
|
+
|
|
46
|
+
_common.run_guarded(ctx, _run)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _set_status(ctx: typer.Context, customer_id: str, resource_name: str, status: str, dry_run: bool) -> None:
|
|
50
|
+
def _run() -> None:
|
|
51
|
+
client = _common.build_client(ctx)
|
|
52
|
+
operations = [{"update": {"resource_name": resource_name, "status": status}}]
|
|
53
|
+
result = proto_bridge.run_mutate(
|
|
54
|
+
client,
|
|
55
|
+
"campaign",
|
|
56
|
+
normalize_customer_id(customer_id),
|
|
57
|
+
operations,
|
|
58
|
+
validate_only=dry_run,
|
|
59
|
+
version=ctx.obj["version"],
|
|
60
|
+
)
|
|
61
|
+
formatting.render(result, fmt=ctx.obj["format"])
|
|
62
|
+
|
|
63
|
+
_common.run_guarded(ctx, _run)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command("pause")
|
|
67
|
+
def pause(
|
|
68
|
+
ctx: typer.Context,
|
|
69
|
+
customer_id: str = typer.Option(..., "--customer-id", "-c"),
|
|
70
|
+
resource_name: str = typer.Argument(..., help="Resource-Name der Kampagne"),
|
|
71
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
72
|
+
) -> None:
|
|
73
|
+
_set_status(ctx, customer_id, resource_name, "PAUSED", dry_run)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("enable")
|
|
77
|
+
def enable(
|
|
78
|
+
ctx: typer.Context,
|
|
79
|
+
customer_id: str = typer.Option(..., "--customer-id", "-c"),
|
|
80
|
+
resource_name: str = typer.Argument(..., help="Resource-Name der Kampagne"),
|
|
81
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
82
|
+
) -> None:
|
|
83
|
+
_set_status(ctx, customer_id, resource_name, "ENABLED", dry_run)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("remove")
|
|
87
|
+
def remove(
|
|
88
|
+
ctx: typer.Context,
|
|
89
|
+
customer_id: str = typer.Option(..., "--customer-id", "-c"),
|
|
90
|
+
resource_name: str = typer.Argument(..., help="Resource-Name der Kampagne"),
|
|
91
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
92
|
+
) -> None:
|
|
93
|
+
def _run() -> None:
|
|
94
|
+
client = _common.build_client(ctx)
|
|
95
|
+
operations = [{"remove": resource_name}]
|
|
96
|
+
result = proto_bridge.run_mutate(
|
|
97
|
+
client,
|
|
98
|
+
"campaign",
|
|
99
|
+
normalize_customer_id(customer_id),
|
|
100
|
+
operations,
|
|
101
|
+
validate_only=dry_run,
|
|
102
|
+
version=ctx.obj["version"],
|
|
103
|
+
)
|
|
104
|
+
formatting.render(result, fmt=ctx.obj["format"])
|
|
105
|
+
|
|
106
|
+
_common.run_guarded(ctx, _run)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""`gads hl keyword add` -- Keyword als AdGroupCriterion hinzufuegen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from googleadscli import formatting, proto_bridge
|
|
8
|
+
from googleadscli.commands import _common
|
|
9
|
+
from googleadscli.utils import normalize_customer_id
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True, help="Keywords hinzufuegen/verwalten")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("add")
|
|
15
|
+
def add(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
customer_id: str = typer.Option(..., "--customer-id", "-c"),
|
|
18
|
+
ad_group_resource_name: str = typer.Option(..., "--ad-group", help="Resource-Name der Ad Group"),
|
|
19
|
+
text: str = typer.Option(..., "--text", help="Keyword-Text"),
|
|
20
|
+
match_type: str = typer.Option("BROAD", "--match-type", help="EXACT|PHRASE|BROAD"),
|
|
21
|
+
status: str = typer.Option("ENABLED", "--status"),
|
|
22
|
+
cpc_bid_micros: int = typer.Option(None, "--cpc-bid-micros"),
|
|
23
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
24
|
+
) -> None:
|
|
25
|
+
def _run() -> None:
|
|
26
|
+
client = _common.build_client(ctx)
|
|
27
|
+
create_payload: dict = {
|
|
28
|
+
"ad_group": ad_group_resource_name,
|
|
29
|
+
"status": status,
|
|
30
|
+
"keyword": {"text": text, "match_type": match_type},
|
|
31
|
+
}
|
|
32
|
+
if cpc_bid_micros is not None:
|
|
33
|
+
create_payload["cpc_bid_micros"] = cpc_bid_micros
|
|
34
|
+
|
|
35
|
+
operations = [{"create": create_payload}]
|
|
36
|
+
result = proto_bridge.run_mutate(
|
|
37
|
+
client,
|
|
38
|
+
"ad_group_criterion",
|
|
39
|
+
normalize_customer_id(customer_id),
|
|
40
|
+
operations,
|
|
41
|
+
validate_only=dry_run,
|
|
42
|
+
version=ctx.obj["version"],
|
|
43
|
+
)
|
|
44
|
+
formatting.render(result, fmt=ctx.obj["format"])
|
|
45
|
+
|
|
46
|
+
_common.run_guarded(ctx, _run)
|