mcm-cli 1.5.1__py3-none-any.whl → 1.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mcm-cli
3
- Version: 1.5.1
3
+ Version: 1.6.1
4
4
  Summary: A command-line interface for Moloco Commerde Media
5
5
  Home-page: https://github.com/moloco-mcm/mcm-cli
6
6
  Author: Moloco MCM Team
@@ -1,18 +1,18 @@
1
1
  mcmcli/__init__.py,sha256=-U6lMZ9_99IXAKwnqnYXYr6NcO6TSmG-kxewgvJjU4k,575
2
- mcmcli/__main__.py,sha256=a4A9mxOQMsB-6DK_5mABC-ersAhxnVFjqQ-u-TZ99hY,1844
2
+ mcmcli/__main__.py,sha256=8Bwhb-VptKSRYlH1LwgGgKZ8pzdc51zSunv9d2cPI0k,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=wh6WU19dCw0NBTp521g5wK6_0GraviWZxxahIcSlD44,12364
8
+ mcmcli/command/campaign.py,sha256=eHE_i1lRlUuU1GeVMwXkJgBU_zOgU-b9Wo0SYC9TK7M,15444
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=jwExzdEHLanhYTfR4-rkL3mGheROoRi3bkGnD-5nKCg,3496
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.5.1.dist-info/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
27
- mcm_cli-1.5.1.dist-info/METADATA,sha256=mnhJv3rIBrVgGmBCGcUl9ACei2q4QVHhL34PKgxq2H4,3056
28
- mcm_cli-1.5.1.dist-info/NOTICE,sha256=Ldnl2MjRaXPxcldUdbI2NTybq60XAa2LowRhFrRTuiI,76
29
- mcm_cli-1.5.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
30
- mcm_cli-1.5.1.dist-info/entry_points.txt,sha256=qTHAWZ-ejSiU4t11RYwtAU8ScqhQPDeMVTG9y4wMVLg,60
31
- mcm_cli-1.5.1.dist-info/top_level.txt,sha256=sh7oqIaqLQlMtKHlxHHgpV2xGMrBMPFWpSp0C6nvJ_Y,7
32
- mcm_cli-1.5.1.dist-info/RECORD,,
26
+ mcm_cli-1.6.1.dist-info/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
27
+ mcm_cli-1.6.1.dist-info/METADATA,sha256=dgSb4q2UwtTILG4P54BcMsiglP27lmXi_kHUwkNcc2E,3056
28
+ mcm_cli-1.6.1.dist-info/NOTICE,sha256=Ldnl2MjRaXPxcldUdbI2NTybq60XAa2LowRhFrRTuiI,76
29
+ mcm_cli-1.6.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
30
+ mcm_cli-1.6.1.dist-info/entry_points.txt,sha256=qTHAWZ-ejSiU4t11RYwtAU8ScqhQPDeMVTG9y4wMVLg,60
31
+ mcm_cli-1.6.1.dist-info/top_level.txt,sha256=sh7oqIaqLQlMtKHlxHHgpV2xGMrBMPFWpSp0C6nvJ_Y,7
32
+ mcm_cli-1.6.1.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.5.1")
40
+ typer.echo(f"Version: mcm-cli v1.6.1")
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")
@@ -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
- to_json: bool = typer.Option(False, help="Print raw output in json"),
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,84 @@ def read_campaign(
86
86
  if error:
87
87
  print(f"ERROR: {error.message}")
88
88
  return
89
- if to_json:
90
- json_dumps = c.model_dump_json()
91
- print(json_dumps)
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: float = 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
- print(f"Ad Account ID = {c.ad_account_id}")
95
- print(f"Campaign ID = {c.id}")
96
- print(f"Campaign title = {c.title}")
97
- print(f"Ad Type = {c.ad_type}")
98
- print(f"Campaign begins at {c.schedule.start}")
99
- print(f"Campaign ends at {c.schedule.end}")
100
- print(f"Budget = {int(c.budget.amount.amount_micro) / 1000000} {c.budget.amount.currency} {c.budget.period}")
101
- print(f"Goal = {c.goal.model_dump()}")
102
- print(f"Registered items = {c.catalog_item_ids}")
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
+ # Remove 'daily_budget' as it's unnecessary
152
+ c.root.pop("daily_budget", None)
153
+
154
+ _, error, c = command.update_campaign(c)
155
+ if error:
156
+ print(f"ERROR: {error.message}", file=sys.stderr, flush=True)
157
+ return
103
158
 
159
+ print(f'Successfully updated campaign ID: {campaign_id} for ad account ID: {account_id}')
104
160
  return
105
161
 
106
162
  @app.command()
107
163
  def archive_campaign(
108
164
  account_id: str = typer.Option(help="Ad account ID"),
109
165
  campaign_id: str = typer.Option(help="Campaign ID"),
110
- profile: str = typer.Option("default", help="Profile name of the MCM CLI."),
166
+ profile: str = typer.Option("default", help="Profile Name The MCM CLI configuration profile to use."),
111
167
  ):
112
168
  """
113
169
  Turn off (pause and disable) the campaign and archive it.
@@ -150,7 +206,7 @@ def list_campaign_items(
150
206
  campaign_id: str = typer.Option(help="Campaign ID"),
151
207
  to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
152
208
  to_json: bool = typer.Option(False, help="Print raw output in json"),
153
- profile: str = "default",
209
+ profile: str = typer.Option("default", help="Profile Name – The MCM CLI configuration profile to use."),
154
210
  ):
155
211
  """
156
212
  List all the items of a given campaign.
@@ -186,7 +242,7 @@ def add_items_to_campaign(
186
242
  item_ids: str = typer.Option(help="Item IDs to add separated by comma(,) like 'p123,p456"),
187
243
  to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
188
244
  to_json: bool = typer.Option(False, help="Print raw output in json"),
189
- profile: str = "default",
245
+ profile: str = typer.Option("default", help="Profile Name – The MCM CLI configuration profile to use."),
190
246
  ):
191
247
  """
192
248
  Add the item IDs to the given campaign of the account
@@ -252,7 +308,7 @@ class CampaignCommand:
252
308
  self.headers[auth_header_name] = auth_header_value
253
309
 
254
310
 
255
- def read_campaign(self, account_id, campaign_id, to_curl=False) -> tuple[CurlString, Error, Campaign]:
311
+ def read_campaign(self, account_id, campaign_id, to_curl=False) -> tuple[CurlString, Error, AnyCampaign]:
256
312
  _api_url = f"{self.api_base_url}/ad-accounts/{account_id}/campaigns/{campaign_id}"
257
313
  curl, error, json_obj = api_request('GET', to_curl, _api_url, self.headers)
258
314
  if curl:
@@ -260,14 +316,14 @@ class CampaignCommand:
260
316
  if error:
261
317
  return None, error, None
262
318
 
263
- campaign = Campaign(**json_obj['campaign'])
319
+ campaign = AnyCampaign(**json_obj['campaign'])
264
320
  return None, None, campaign
265
321
 
266
- def update_campaign(self, campaign: Campaign, to_curl=False) -> tuple[CurlString, Error, Campaign]:
322
+ def update_campaign(self, campaign: AnyCampaign, to_curl=False) -> tuple[CurlString, Error, AnyCampaign]:
267
323
  if campaign is None:
268
324
  return Error(code=0, message="invalid campaign info"), None
269
325
 
270
- _api_url = f"{self.api_base_url}/ad-accounts/{campaign.ad_account_id}/campaigns/{campaign.id}"
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 = Campaign(**json_obj['campaign'])
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