mcm-cli 1.5.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mcm-cli
3
- Version: 1.5.1
3
+ Version: 1.6.0
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=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=wh6WU19dCw0NBTp521g5wK6_0GraviWZxxahIcSlD44,12364
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=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.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.5.1")
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")
@@ -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,81 @@ 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: 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
- 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
+ _, 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 name of the MCM CLI."),
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, Campaign]:
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 = Campaign(**json_obj['campaign'])
316
+ campaign = AnyCampaign(**json_obj['campaign'])
264
317
  return None, None, campaign
265
318
 
266
- def update_campaign(self, campaign: Campaign, to_curl=False) -> tuple[CurlString, Error, 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
- _api_url = f"{self.api_base_url}/ad-accounts/{campaign.ad_account_id}/campaigns/{campaign.id}"
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 = 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