mcm-cli 1.5.0__py3-none-any.whl → 1.6.0__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.
- {mcm_cli-1.5.0.dist-info → mcm_cli-1.6.0.dist-info}/METADATA +1 -1
- {mcm_cli-1.5.0.dist-info → mcm_cli-1.6.0.dist-info}/RECORD +10 -10
- mcmcli/__main__.py +1 -1
- mcmcli/command/campaign.py +80 -24
- mcmcli/data/campaign.py +62 -3
- {mcm_cli-1.5.0.dist-info → mcm_cli-1.6.0.dist-info}/LICENSE +0 -0
- {mcm_cli-1.5.0.dist-info → mcm_cli-1.6.0.dist-info}/NOTICE +0 -0
- {mcm_cli-1.5.0.dist-info → mcm_cli-1.6.0.dist-info}/WHEEL +0 -0
- {mcm_cli-1.5.0.dist-info → mcm_cli-1.6.0.dist-info}/entry_points.txt +0 -0
- {mcm_cli-1.5.0.dist-info → mcm_cli-1.6.0.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,18 @@
|
|
1
1
|
mcmcli/__init__.py,sha256=-U6lMZ9_99IXAKwnqnYXYr6NcO6TSmG-kxewgvJjU4k,575
|
2
|
-
mcmcli/__main__.py,sha256=
|
2
|
+
mcmcli/__main__.py,sha256=wmwII11VcYr77xnuEbOpnQyLfb2J9lPzMDta_D3djIc,1844
|
3
3
|
mcmcli/logging.py,sha256=xjRS5ey1ONx_d34qB1Fetb_SwPysoh2hzNDuNAaYYWQ,1739
|
4
4
|
mcmcli/requests.py,sha256=IuySBQ8P_GoGF3f_TRysfgQNOhi2n9M84WK_eRXnoEU,2945
|
5
5
|
mcmcli/command/account.py,sha256=FWXmzOLj4rVLVLEv-w0eDVlQVrkONvR1UewZbcTDgE4,24994
|
6
6
|
mcmcli/command/admin.py,sha256=2GC0B3oqQsT5N5mHUyezgZvzPkB4-oGtddguOWfdLJs,15980
|
7
7
|
mcmcli/command/auth.py,sha256=Ak7ZNEskWPpMoeTJcbYlEpDBgzxn8N33Q2dNf67SsCs,2926
|
8
|
-
mcmcli/command/campaign.py,sha256=
|
8
|
+
mcmcli/command/campaign.py,sha256=gjzCDdmwp4BlXZ8PnckkBr_JE2o6ZLFR8XKwhGIHY1o,15457
|
9
9
|
mcmcli/command/config.py,sha256=08C5ftAvdvpQ26Z329LqhP8AxTI629LS7Ou6glzrRgw,4396
|
10
10
|
mcmcli/command/decision.py,sha256=iLvEDEa2k0LAgoXrOvcdn-HcFRo90E8Dxht_Ls9EGCY,8690
|
11
11
|
mcmcli/command/report.py,sha256=N8IMyDZ5QpY11-KkZG-n5_ZzEeh-ME5s2s3wrAKEGjY,5387
|
12
12
|
mcmcli/command/wallet.py,sha256=vG2rg7tPwGsV9YB7cpvkiocbqtB1reqXn7dmolFmzD4,11536
|
13
13
|
mcmcli/data/account.py,sha256=pe7tPapP6vlUD5D5L5Nh5k2bkWdYOK01Mpt5fBYFnJs,1782
|
14
14
|
mcmcli/data/account_user.py,sha256=27nQp52nMma5a3QszSJGqgq5Z0ivIb-nMZcZuhEgbEg,1328
|
15
|
-
mcmcli/data/campaign.py,sha256=
|
15
|
+
mcmcli/data/campaign.py,sha256=pZDeSkIMWQhcgi2seDpD8xeSP3E78-_7fV5f-ObvfW4,3557
|
16
16
|
mcmcli/data/decision.py,sha256=bQ5j_PbPRSFa0sY5g9UVqdNzl_2epchcz1lHoDVuV90,2880
|
17
17
|
mcmcli/data/error.py,sha256=d6xa_jTXumlA0EzXy2PJQ86ajBb0Pm90fss9R3LuHUc,1094
|
18
18
|
mcmcli/data/item.py,sha256=Z2xTRhU8T4vyJADO0l6-XPyQXvb9DX_OAjExhSXpW2A,1091
|
@@ -23,10 +23,10 @@ mcmcli/data/seller.py,sha256=40SA7QekM3a3svDrDYLo_QYJ68VUxDO0KeGejJMp4k4,1004
|
|
23
23
|
mcmcli/data/token.py,sha256=11wtyLHCAZHu0LVbNDPa-yipcL6lenxoYIKEI58VzFs,1744
|
24
24
|
mcmcli/data/user_join_request.py,sha256=lXMO2hE_VpRg0JofVrYAVM82S-RLFkPrZk8-drvhoDI,1251
|
25
25
|
mcmcli/data/wallet.py,sha256=eMUi8N0vJdg_E10TPhSPoZkZtmIG7gHyqgabQ8C5Lg8,3217
|
26
|
-
mcm_cli-1.
|
27
|
-
mcm_cli-1.
|
28
|
-
mcm_cli-1.
|
29
|
-
mcm_cli-1.
|
30
|
-
mcm_cli-1.
|
31
|
-
mcm_cli-1.
|
32
|
-
mcm_cli-1.
|
26
|
+
mcm_cli-1.6.0.dist-info/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
|
27
|
+
mcm_cli-1.6.0.dist-info/METADATA,sha256=n3ektTooOWWE_kxjpN7D_CEOJha7LX-GJIeng4rKYKk,3056
|
28
|
+
mcm_cli-1.6.0.dist-info/NOTICE,sha256=Ldnl2MjRaXPxcldUdbI2NTybq60XAa2LowRhFrRTuiI,76
|
29
|
+
mcm_cli-1.6.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
30
|
+
mcm_cli-1.6.0.dist-info/entry_points.txt,sha256=qTHAWZ-ejSiU4t11RYwtAU8ScqhQPDeMVTG9y4wMVLg,60
|
31
|
+
mcm_cli-1.6.0.dist-info/top_level.txt,sha256=sh7oqIaqLQlMtKHlxHHgpV2xGMrBMPFWpSp0C6nvJ_Y,7
|
32
|
+
mcm_cli-1.6.0.dist-info/RECORD,,
|
mcmcli/__main__.py
CHANGED
@@ -37,7 +37,7 @@ def version():
|
|
37
37
|
"""
|
38
38
|
Show the tool version
|
39
39
|
"""
|
40
|
-
typer.echo(f"Version: mcm-cli v1.
|
40
|
+
typer.echo(f"Version: mcm-cli v1.6.0")
|
41
41
|
|
42
42
|
app.add_typer(mcmcli.command.account.app, name="account", help="Ad account management")
|
43
43
|
app.add_typer(mcmcli.command.admin.app, name="admin", help="Platform administration")
|
mcmcli/command/campaign.py
CHANGED
@@ -11,8 +11,9 @@
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
|
+
from enum import Enum
|
14
15
|
from mcmcli.command.auth import AuthCommand, AuthHeaderName, AuthHeaderValue
|
15
|
-
from mcmcli.data.campaign import Campaign, CampaignList
|
16
|
+
from mcmcli.data.campaign import AnyCampaign, Campaign, CampaignList
|
16
17
|
from mcmcli.data.error import Error
|
17
18
|
from mcmcli.data.item import Item, ItemList
|
18
19
|
from mcmcli.requests import CurlString, api_request
|
@@ -37,7 +38,7 @@ def list_campaigns(
|
|
37
38
|
account_id: str = typer.Option(help="Ad account ID"),
|
38
39
|
to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
|
39
40
|
to_json: bool = typer.Option(False, help="Print raw output in json"),
|
40
|
-
profile: str = "default",
|
41
|
+
profile: str = typer.Option("default", help="Profile Name – The MCM CLI configuration profile to use."),
|
41
42
|
):
|
42
43
|
"""
|
43
44
|
List all the campaigns of an ad account.
|
@@ -69,8 +70,7 @@ def read_campaign(
|
|
69
70
|
account_id: str = typer.Option(help="Ad account ID"),
|
70
71
|
campaign_id: str = typer.Option(help="Campaign ID"),
|
71
72
|
to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
|
72
|
-
|
73
|
-
profile: str = "default",
|
73
|
+
profile: str = typer.Option("default", help="Profile Name – The MCM CLI configuration profile to use."),
|
74
74
|
):
|
75
75
|
"""
|
76
76
|
Read the campaign information
|
@@ -86,28 +86,81 @@ def read_campaign(
|
|
86
86
|
if error:
|
87
87
|
print(f"ERROR: {error.message}")
|
88
88
|
return
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
|
90
|
+
json_dumps = c.model_dump_json()
|
91
|
+
print(json_dumps)
|
92
|
+
return
|
93
|
+
|
94
|
+
class BudgetPeriod(Enum):
|
95
|
+
DAILY = "DAILY"
|
96
|
+
WEEKLY = 'WEEKLY'
|
97
|
+
|
98
|
+
def validate_budget_period_amount(budget_period: BudgetPeriod = None, budget_amount: int = None):
|
99
|
+
if (budget_period is None) != (budget_amount is None): # XOR logic: One is given, the other isn't
|
100
|
+
raise typer.BadParameter("Both --budget-period and --budget-amount mube be provided together.")
|
101
|
+
|
102
|
+
def validate_exclusive_params(target_roas: int = None, target_cpc: float = None):
|
103
|
+
if target_roas is not None and target_cpc is not None:
|
104
|
+
raise typer.BadParameter("You can only provide one of --target_roas or --target-cpc, not both.")
|
105
|
+
|
106
|
+
@app.command()
|
107
|
+
def update_campaign(
|
108
|
+
account_id: str = typer.Option(help="Ad account ID"),
|
109
|
+
campaign_id: str = typer.Option(help="Campaign ID"),
|
110
|
+
target_roas: int = typer.Option(None, help='Target ROAS (%) – Applies only to campaigns with the Optimize ROAS goal type.'),
|
111
|
+
target_cpc: float = typer.Option(None, help='Target CPC – Set in the platform currency. Applies only to campaigns with the Manual CPC goal type.'),
|
112
|
+
budget_period: BudgetPeriod = typer.Option(None, help='Budget Period – Choose between DAILY or WEEKLY.'),
|
113
|
+
budget_amount: int = typer.Option(None, help='Budget Amount – Set the spending limit for the chosen period.'),
|
114
|
+
profile: str = typer.Option("default", help="Profile Name – The MCM CLI configuration profile to use."),
|
115
|
+
):
|
116
|
+
"""
|
117
|
+
Update the campaign information
|
118
|
+
"""
|
119
|
+
command = _create_campaign_command(profile)
|
120
|
+
if command is None:
|
92
121
|
return
|
122
|
+
|
123
|
+
_, error, c = command.read_campaign(account_id, campaign_id)
|
124
|
+
if error:
|
125
|
+
print(f"ERROR: {error.message}", file=sys.stderr, flush=True)
|
126
|
+
return
|
127
|
+
|
128
|
+
if budget_period is not None or budget_amount is not None:
|
129
|
+
validate_budget_period_amount(budget_period, budget_amount)
|
93
130
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
131
|
+
if target_roas is not None or target_cpc is not None:
|
132
|
+
validate_exclusive_params(target_roas, target_cpc)
|
133
|
+
|
134
|
+
if budget_period is not None:
|
135
|
+
budget = c.root.get('budget')
|
136
|
+
budget['period'] = budget_period.value
|
137
|
+
budget['amount']['amount_micro'] = int(budget_amount * 1_000_000)
|
138
|
+
|
139
|
+
if target_roas is not None:
|
140
|
+
goal = c.root.get('goal')
|
141
|
+
if goal and goal.get('type') != 'OPTIMIZE_ROAS':
|
142
|
+
raise typer.BadParameter(f"The campaign goal type is not OPTIMIZE_ROAS and cannot update the target ROAS. Ad Account ID = {account_id}, Campaign ID = {campaign_id}")
|
143
|
+
goal['optimize_roas']['target_roas'] = target_roas
|
144
|
+
|
145
|
+
if target_cpc is not None:
|
146
|
+
goal = c.root.get('goal')
|
147
|
+
if goal and goal.get('type') != 'FIXED_CPC':
|
148
|
+
raise typer.BadParameter(f"The campaign goal type is not FIXED_CPC and cannot update the target CPC. Ad Account ID = {account_id}, Campaign ID = {campaign_id}")
|
149
|
+
goal['optimize_fixed_cpc']['target_cpc']['amount_micro'] = int(target_cpc * 1_000_000)
|
150
|
+
|
151
|
+
_, error, c = command.update_campaign(c)
|
152
|
+
if error:
|
153
|
+
print(f"ERROR: {error.message}", file=sys.stderr, flush=True)
|
154
|
+
return
|
103
155
|
|
156
|
+
print(f'Successfully updated campaign ID: {campaign_id} for ad account ID: {account_id}')
|
104
157
|
return
|
105
158
|
|
106
159
|
@app.command()
|
107
160
|
def archive_campaign(
|
108
161
|
account_id: str = typer.Option(help="Ad account ID"),
|
109
162
|
campaign_id: str = typer.Option(help="Campaign ID"),
|
110
|
-
profile: str = typer.Option("default", help="Profile
|
163
|
+
profile: str = typer.Option("default", help="Profile Name – The MCM CLI configuration profile to use."),
|
111
164
|
):
|
112
165
|
"""
|
113
166
|
Turn off (pause and disable) the campaign and archive it.
|
@@ -150,7 +203,7 @@ def list_campaign_items(
|
|
150
203
|
campaign_id: str = typer.Option(help="Campaign ID"),
|
151
204
|
to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
|
152
205
|
to_json: bool = typer.Option(False, help="Print raw output in json"),
|
153
|
-
profile: str = "default",
|
206
|
+
profile: str = typer.Option("default", help="Profile Name – The MCM CLI configuration profile to use."),
|
154
207
|
):
|
155
208
|
"""
|
156
209
|
List all the items of a given campaign.
|
@@ -186,7 +239,7 @@ def add_items_to_campaign(
|
|
186
239
|
item_ids: str = typer.Option(help="Item IDs to add separated by comma(,) like 'p123,p456"),
|
187
240
|
to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
|
188
241
|
to_json: bool = typer.Option(False, help="Print raw output in json"),
|
189
|
-
profile: str = "default",
|
242
|
+
profile: str = typer.Option("default", help="Profile Name – The MCM CLI configuration profile to use."),
|
190
243
|
):
|
191
244
|
"""
|
192
245
|
Add the item IDs to the given campaign of the account
|
@@ -252,7 +305,7 @@ class CampaignCommand:
|
|
252
305
|
self.headers[auth_header_name] = auth_header_value
|
253
306
|
|
254
307
|
|
255
|
-
def read_campaign(self, account_id, campaign_id, to_curl=False) -> tuple[CurlString, Error,
|
308
|
+
def read_campaign(self, account_id, campaign_id, to_curl=False) -> tuple[CurlString, Error, AnyCampaign]:
|
256
309
|
_api_url = f"{self.api_base_url}/ad-accounts/{account_id}/campaigns/{campaign_id}"
|
257
310
|
curl, error, json_obj = api_request('GET', to_curl, _api_url, self.headers)
|
258
311
|
if curl:
|
@@ -260,14 +313,17 @@ class CampaignCommand:
|
|
260
313
|
if error:
|
261
314
|
return None, error, None
|
262
315
|
|
263
|
-
campaign =
|
316
|
+
campaign = AnyCampaign(**json_obj['campaign'])
|
264
317
|
return None, None, campaign
|
265
318
|
|
266
|
-
def update_campaign(self, campaign:
|
319
|
+
def update_campaign(self, campaign: AnyCampaign, to_curl=False) -> tuple[CurlString, Error, AnyCampaign]:
|
267
320
|
if campaign is None:
|
268
321
|
return Error(code=0, message="invalid campaign info"), None
|
269
322
|
|
270
|
-
|
323
|
+
# Remove 'daily_budget' as it's unnecessary
|
324
|
+
campaign.root.pop("daily_budget", None)
|
325
|
+
|
326
|
+
_api_url = f"{self.api_base_url}/ad-accounts/{campaign.root['ad_account_id']}/campaigns/{campaign.root['id']}"
|
271
327
|
_payload = {
|
272
328
|
"campaign": campaign.model_dump()
|
273
329
|
}
|
@@ -277,7 +333,7 @@ class CampaignCommand:
|
|
277
333
|
if error:
|
278
334
|
return None, error, None
|
279
335
|
|
280
|
-
c =
|
336
|
+
c = AnyCampaign(**json_obj['campaign'])
|
281
337
|
return None, None, c
|
282
338
|
|
283
339
|
def list_campaigns(self, account_id, to_curl=False) -> tuple[CurlString, Error, list[Campaign]]:
|
mcmcli/data/campaign.py
CHANGED
@@ -12,8 +12,11 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
from pydantic import BaseModel
|
16
|
-
from typing import Optional
|
15
|
+
from pydantic import BaseModel, RootModel
|
16
|
+
from typing import Any, Optional
|
17
|
+
|
18
|
+
class AnyCampaign(RootModel[Any]):
|
19
|
+
pass
|
17
20
|
|
18
21
|
#
|
19
22
|
# API response dataclasses
|
@@ -24,7 +27,7 @@ class MicroPrice(BaseModel):
|
|
24
27
|
|
25
28
|
class Schedule(BaseModel):
|
26
29
|
start: str
|
27
|
-
end: Optional[str]
|
30
|
+
end: Optional[str] = None
|
28
31
|
|
29
32
|
class Budget(BaseModel):
|
30
33
|
period: str
|
@@ -58,6 +61,56 @@ class Targeting(BaseModel):
|
|
58
61
|
placement_setting: Optional[str]
|
59
62
|
audience_setting: Optional[AudienceSetting]
|
60
63
|
|
64
|
+
class TargetInventoryDimension(BaseModel):
|
65
|
+
width: int
|
66
|
+
height: int
|
67
|
+
|
68
|
+
class BannerAsset(BaseModel):
|
69
|
+
creative_id: str
|
70
|
+
image_url: str
|
71
|
+
width: int
|
72
|
+
height: int
|
73
|
+
target_inventory_dimension: TargetInventoryDimension
|
74
|
+
|
75
|
+
class LogoAsset(BaseModel):
|
76
|
+
creative_id: str
|
77
|
+
image_url: str
|
78
|
+
|
79
|
+
class HeadlineAsset(BaseModel):
|
80
|
+
text: str
|
81
|
+
|
82
|
+
class CTAAsset(BaseModel):
|
83
|
+
text: str
|
84
|
+
|
85
|
+
class ReviewInformation(BaseModel):
|
86
|
+
status: str
|
87
|
+
rejection_reason: str
|
88
|
+
updated_at: str
|
89
|
+
|
90
|
+
class Asset(BaseModel):
|
91
|
+
id: str
|
92
|
+
banner: BannerAsset
|
93
|
+
logo: LogoAsset
|
94
|
+
headline: HeadlineAsset
|
95
|
+
cta: CTAAsset
|
96
|
+
review_information: ReviewInformation
|
97
|
+
created_at: str
|
98
|
+
updated_at: str
|
99
|
+
|
100
|
+
class CustomURLSetting(BaseModel):
|
101
|
+
url: str
|
102
|
+
|
103
|
+
class LandPage(BaseModel):
|
104
|
+
type: str
|
105
|
+
custom_url_setting: CustomURLSetting
|
106
|
+
id: str
|
107
|
+
review_information: ReviewInformation
|
108
|
+
created_at: str
|
109
|
+
updated_at: str
|
110
|
+
|
111
|
+
class ItemSelection(BaseModel):
|
112
|
+
type: str
|
113
|
+
|
61
114
|
class Campaign(BaseModel):
|
62
115
|
id: str
|
63
116
|
title: str
|
@@ -80,6 +133,12 @@ class Campaign(BaseModel):
|
|
80
133
|
updated_at: str
|
81
134
|
audience_types: list[str]
|
82
135
|
offsite_setting: Optional[str]
|
136
|
+
item_selection: Optional[ItemSelection]
|
137
|
+
assets: Optional[list[Asset]]
|
138
|
+
landing_pages: Optional[list[LandPage]]
|
139
|
+
landing_url_suffix: Optional[str]
|
140
|
+
catalog_brand_id: Optional[str]
|
141
|
+
catalog_category: Optional[str]
|
83
142
|
|
84
143
|
class CampaignList(BaseModel):
|
85
144
|
campaigns: list[Campaign]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|