mcm-cli 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mcm-cli
3
- Version: 1.1.0
3
+ Version: 1.2.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,8 +1,8 @@
1
1
  mcmcli/__init__.py,sha256=-U6lMZ9_99IXAKwnqnYXYr6NcO6TSmG-kxewgvJjU4k,575
2
- mcmcli/__main__.py,sha256=9eAfYXz2reT3cOAgkqgCAuk169yB8KSla_rIF4oak6E,1616
2
+ mcmcli/__main__.py,sha256=G9B6mZCD2JJpk8nHDdexuKfNigm8WZeFJXhutUEmIUE,1616
3
3
  mcmcli/logging.py,sha256=xjRS5ey1ONx_d34qB1Fetb_SwPysoh2hzNDuNAaYYWQ,1739
4
- mcmcli/requests.py,sha256=50N_LiWIWr8-3EPs_yR9f4uEQf8rQ68s_FoMYRhgjzI,2343
5
- mcmcli/command/account.py,sha256=EtcYZsC5LL5ue-hKsZZl3_0oZ09nJdVi774yqiTykl8,14712
4
+ mcmcli/requests.py,sha256=ZoQULPpKdAvGtki25Jr6K2Xq2v213XLinzPHUeBi9wo,2601
5
+ mcmcli/command/account.py,sha256=kxJbBKYrw6OyCdaNZ0K0BEvKgZEAjj5azO-lNSeYLTM,25107
6
6
  mcmcli/command/admin.py,sha256=nJ7rm0nm0jHPobg0PjNHIWaWURTQu6QEUEUKIo__GO0,3010
7
7
  mcmcli/command/auth.py,sha256=QLdr_XFW5BVw9r4a7Kjj5BTBXpSux3AWI9eI03S8aiA,2480
8
8
  mcmcli/command/config.py,sha256=sdzge-l_Yi2P_TlTgSLqShcGyPCzpW3QJzctpIvc-g4,4195
@@ -12,13 +12,14 @@ mcmcli/data/account.py,sha256=pe7tPapP6vlUD5D5L5Nh5k2bkWdYOK01Mpt5fBYFnJs,1782
12
12
  mcmcli/data/account_user.py,sha256=27nQp52nMma5a3QszSJGqgq5Z0ivIb-nMZcZuhEgbEg,1328
13
13
  mcmcli/data/decision.py,sha256=bQ5j_PbPRSFa0sY5g9UVqdNzl_2epchcz1lHoDVuV90,2880
14
14
  mcmcli/data/error.py,sha256=d6xa_jTXumlA0EzXy2PJQ86ajBb0Pm90fss9R3LuHUc,1094
15
+ mcmcli/data/item.py,sha256=Z2xTRhU8T4vyJADO0l6-XPyQXvb9DX_OAjExhSXpW2A,1091
15
16
  mcmcli/data/seller.py,sha256=40SA7QekM3a3svDrDYLo_QYJ68VUxDO0KeGejJMp4k4,1004
16
17
  mcmcli/data/token.py,sha256=11wtyLHCAZHu0LVbNDPa-yipcL6lenxoYIKEI58VzFs,1744
17
18
  mcmcli/data/wallet.py,sha256=W-CksF9SPqiv3jZg07Wy8ehVUP5Ot1Gbq2LEGNQCOC8,1906
18
- mcm_cli-1.1.0.dist-info/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
19
- mcm_cli-1.1.0.dist-info/METADATA,sha256=zXrJD8pf59IjAeaqxJed60ROfk6-5eFQprXggZZSqOM,3018
20
- mcm_cli-1.1.0.dist-info/NOTICE,sha256=Ldnl2MjRaXPxcldUdbI2NTybq60XAa2LowRhFrRTuiI,76
21
- mcm_cli-1.1.0.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
22
- mcm_cli-1.1.0.dist-info/entry_points.txt,sha256=qTHAWZ-ejSiU4t11RYwtAU8ScqhQPDeMVTG9y4wMVLg,60
23
- mcm_cli-1.1.0.dist-info/top_level.txt,sha256=sh7oqIaqLQlMtKHlxHHgpV2xGMrBMPFWpSp0C6nvJ_Y,7
24
- mcm_cli-1.1.0.dist-info/RECORD,,
19
+ mcm_cli-1.2.0.dist-info/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
20
+ mcm_cli-1.2.0.dist-info/METADATA,sha256=7ewr0UPyJ6IqNvXOSL7Gci2BryiET3dEqfnuM1BYOxc,3018
21
+ mcm_cli-1.2.0.dist-info/NOTICE,sha256=Ldnl2MjRaXPxcldUdbI2NTybq60XAa2LowRhFrRTuiI,76
22
+ mcm_cli-1.2.0.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
23
+ mcm_cli-1.2.0.dist-info/entry_points.txt,sha256=qTHAWZ-ejSiU4t11RYwtAU8ScqhQPDeMVTG9y4wMVLg,60
24
+ mcm_cli-1.2.0.dist-info/top_level.txt,sha256=sh7oqIaqLQlMtKHlxHHgpV2xGMrBMPFWpSp0C6nvJ_Y,7
25
+ mcm_cli-1.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
mcmcli/__main__.py CHANGED
@@ -35,7 +35,7 @@ def version():
35
35
  """
36
36
  Show the tool version
37
37
  """
38
- typer.echo(f"Version: mcm-cli v1.1.0")
38
+ typer.echo(f"Version: mcm-cli v1.2.0")
39
39
 
40
40
  app.add_typer(mcmcli.command.account.app, name="account", help="Ad account management")
41
41
  app.add_typer(mcmcli.command.admin.app, name="admin", help="Platform administration")
mcmcli/command/account.py CHANGED
@@ -16,6 +16,7 @@ from enum import Enum
16
16
  from mcmcli.data.account import Account, AccountListWrapper
17
17
  from mcmcli.data.account_user import User, UserWrapper, UserListWrapper
18
18
  from mcmcli.data.error import Error
19
+ from mcmcli.data.item import ItemList, Item
19
20
  from mcmcli.data.seller import Seller, SellerListWrapper
20
21
  from mcmcli.requests import CurlString, api_request
21
22
  from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
@@ -95,6 +96,16 @@ def bulk_invite_ad_account_owners(
95
96
  continue
96
97
  account_creation_count += 1
97
98
 
99
+ #
100
+ # TODO: investigate if this step is necessary
101
+ # Activate the ad account
102
+ #
103
+ #if account:
104
+ # error = a.activate_account_with_retry(account)
105
+ # if error:
106
+ # print(f"\nERROR: Failed to activate the ad account {account_id}. {error.message}.", file=sys.stderr, flush=True)
107
+ # continue
108
+
98
109
  error = a.send_invitation_email_with_retry(account_id, email_address, user_name, create_user, dry_run)
99
110
  if error:
100
111
  print(f"\nERROR: Failed to send the mail for the ad account ID {account_id}. {error.message}.", file=sys.stderr, flush=True)
@@ -151,6 +162,187 @@ def bulk_check_user_registrations(
151
162
  print(f'"{account_id}","{is_account_exist}","{email_address}","{is_user_exist}","{user_role}","{user_status}"')
152
163
  return
153
164
 
165
+ @app.command()
166
+ def delete_user(
167
+ account_id: str = typer.Option(help="Ad account ID"),
168
+ user_email: str = typer.Option(help="User's email address"),
169
+ email_notification: bool = typer.Option(True, help="Send the email notification to the user"),
170
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
171
+ ):
172
+ """
173
+ Delete the email user. It will send the notification email to the user by default.
174
+ Please use the `--no-email-notification` option if you want to skip it.
175
+ """
176
+ a = _create_account_command(profile)
177
+ if a is None:
178
+ return
179
+
180
+ err, users = a.list_account_users(account_id)
181
+ if err:
182
+ print(f"ERROR: {err}", file=sys.stderr, flush=True)
183
+ sys.exit()
184
+ if user_email not in users:
185
+ print(f"ERROR: The email user {user_email} was not found in the ad account {account_id}.", file=sys.stderr, flush=True)
186
+ sys.exit()
187
+
188
+ user_id = users[user_email].id
189
+ err = a.delete_user(account_id, user_id, email_notification)
190
+ if err:
191
+ print(f"ERROR: {err}", file=sys.stderr, flush=True)
192
+ sys.exit()
193
+
194
+ print(f'Successfully deleted the email user {user_email} from the ad account ID {account_id}.')
195
+
196
+
197
+ @app.command()
198
+ def invite_user(
199
+ account_id: str = typer.Option(help="Ad account ID"),
200
+ user_email: str = typer.Option(help="User's email address"),
201
+ user_name: str = typer.Option(help="User's name"),
202
+ user_role: UserRole = typer.Option(UserRole.AD_ACCOUNT_OWNER.value, help="User Role"),
203
+ to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
204
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
205
+ ):
206
+ """
207
+ Invite a user as an ad account owner. The process creates the ad account if it doesn’t exist
208
+ and sends an invitation email to the user.
209
+ """
210
+ a = _create_account_command(profile)
211
+ if a is None:
212
+ return
213
+
214
+ #
215
+ # get the list of sellers
216
+ #
217
+ _, error, seller_dictionary = a.list_sellers()
218
+ if error:
219
+ print(f"ERROR: {error.message}")
220
+ sys.exit()
221
+
222
+ seller = _lookup_dict(account_id, seller_dictionary)
223
+ if seller is None:
224
+ print(f"ERROR: Could not find the ad account ID {account_id}")
225
+ sys.exit()
226
+
227
+ #
228
+ # create the ad account first
229
+ #
230
+ _, error, _ = a.create_account(seller.id, seller.title, to_curl=False)
231
+ if error and error.code != 6:
232
+ # error code 6 means the ad account already exists; we can ignore it.
233
+ print(f"ERROR: {error.message}")
234
+ sys.exit()
235
+
236
+ # We can assume that the ad account exists at this line, because we checked it above.
237
+
238
+ #
239
+ # invite the user
240
+ #
241
+ curl, error, user_join_request = a.invite_user(account_id, user_email, user_name, user_role, to_curl)
242
+ if to_curl:
243
+ print(curl)
244
+ elif error:
245
+ print(f"ERROR: {error.message}")
246
+ sys.exit()
247
+ else:
248
+ print(user_join_request)
249
+
250
+
251
+ @app.command()
252
+ def create_account(
253
+ account_id: str = typer.Option(help="Ad account ID"),
254
+ account_name: str = typer.Option(help="Ad account name"),
255
+ to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
256
+ to_json: bool = typer.Option(False, help="Print raw output in json"),
257
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
258
+ ):
259
+ """
260
+ Create a new ad account.
261
+ """
262
+ a = _create_account_command(profile)
263
+ if a is None:
264
+ return
265
+
266
+ curl, error, account_info = a.create_account(account_id, account_name, to_curl)
267
+ if to_curl:
268
+ print(curl)
269
+ sys.exit()
270
+ if error:
271
+ print(f"ERROR: {error.message}")
272
+ sys.exit()
273
+ if account_info is None:
274
+ print(f"ERROR: account information is None, which is not supposed to be.")
275
+ sys.exit()
276
+
277
+ if to_json:
278
+ print(account_info.model_dump_json())
279
+ sys.exit()
280
+
281
+ print(f"""Ad account ID {account_info.id} ("{account_info.title}") has been created.""")
282
+
283
+
284
+ @app.command()
285
+ def list_accounts(
286
+ to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
287
+ to_json: bool = typer.Option(False, help="Print raw output in json"),
288
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
289
+ ):
290
+ """
291
+ List all ad accounts on the platform.
292
+ """
293
+ a = _create_account_command(profile)
294
+ if a is None:
295
+ return
296
+
297
+ curl, error, accounts = a.list_accounts(to_curl)
298
+ if to_curl:
299
+ print(curl)
300
+ return
301
+ if error:
302
+ print(f"ERROR: {error.message}")
303
+ return
304
+ if to_json:
305
+ json_dumps = [x.model_dump_json() for x in accounts.values()]
306
+ print(f"[{','.join(json_dumps)}]")
307
+ return
308
+
309
+ print("Ad Account ID, State, Ad Account Title")
310
+ for a in accounts.values():
311
+ print(f"{a.id}, {a.state_info.state}, {a.title}")
312
+
313
+ return
314
+
315
+ @app.command()
316
+ def list_account_items(
317
+ account_id: str = typer.Option(help="Ad account ID"),
318
+ to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
319
+ to_json: bool = typer.Option(False, help="Print raw output in json"),
320
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
321
+ ):
322
+ """
323
+ List all items for a given ad account.
324
+ """
325
+ a = _create_account_command(profile)
326
+ if a is None:
327
+ return
328
+
329
+ curl, error, items = a.list_account_items(account_id, to_curl)
330
+
331
+ if to_curl:
332
+ print(curl)
333
+ sys.exit()
334
+ if error:
335
+ print(f"ERROR: {error.message}")
336
+ sys.exit()
337
+ if to_json:
338
+ json_dumps = [x.model_dump_json() for x in items]
339
+ print(f"[{','.join(json_dumps)}]")
340
+ sys.exit()
341
+
342
+ print("Ad Account ID, Item ID, Is Active, Created At, Item Title")
343
+ for i in items:
344
+ print(f"{account_id}, {i.id}, {i.is_active}, {i.created_timestamp}, {i.title}")
345
+
154
346
 
155
347
  class AccountCommand:
156
348
  def __init__(
@@ -255,6 +447,21 @@ class AccountCommand:
255
447
  return error
256
448
 
257
449
 
450
+ def delete_user(
451
+ self,
452
+ account_id,
453
+ user_id,
454
+ send_email_notification,
455
+ ) -> Optional[Error]:
456
+ _api_url = f"{self.api_base_url}/ad-accounts/{account_id}/users/{user_id}?reason=UserDeleted"
457
+ if not send_email_notification:
458
+ _api_url += "&bypass_notification=true"
459
+
460
+ _, error, _ = api_request('DELETE', False, _api_url, self.headers)
461
+
462
+ return error
463
+
464
+
258
465
  def invite_user(
259
466
  self,
260
467
  account_id,
@@ -285,6 +492,50 @@ class AccountCommand:
285
492
  ret = UserWrapper(**json_obj).user
286
493
  return None, None, ret
287
494
 
495
+ def activate_account(
496
+ self,
497
+ account: Account,
498
+ ) -> Optional[Error]:
499
+ account.state_info.state = "ACTIVE"
500
+ account.state_info.state_case = "ACTIVATED"
501
+
502
+ _api_url = f"{self.api_base_url}/ad-accounts/{account.id}"
503
+ _payload = {
504
+ "ad_account": account.model_dump_json()
505
+ }
506
+ _, error, _ = api_request('POST', False, _api_url, self.headers, _payload)
507
+ if error:
508
+ return error
509
+
510
+ _api_url = f"{self.api_base_url}/ad_accounts/{account.id}/state-info"
511
+ _payload = account.state_info.model_dump_json()
512
+ _, error, _ = api_request('POST', False, _api_url, self.headers, _payload)
513
+ if error:
514
+ return error
515
+
516
+ return None
517
+
518
+
519
+
520
+ def activate_account_with_retry(
521
+ self,
522
+ account: Account,
523
+ ) -> Optional[Error]:
524
+ retries = 0
525
+ delay = 1
526
+ max_retries = 3
527
+ while retries < max_retries:
528
+ error = self.activate_account(account)
529
+ if error and error.code == 16:
530
+ print(f"\nERROR: authentication token expired. Retrying...", file=sys.stderr, flush=True)
531
+ self.refresh_token()
532
+ retries += 1
533
+ time.sleep(delay)
534
+ continue
535
+ return error
536
+ return Error(code=16, message="Failed to regain an authentication token")
537
+
538
+
288
539
  def create_account(
289
540
  self,
290
541
  account_id,
@@ -420,6 +671,88 @@ class AccountCommand:
420
671
  )
421
672
 
422
673
 
674
+ def list_account_items(
675
+ self,
676
+ account_id,
677
+ to_curl=False
678
+ ) -> tuple[
679
+ Optional[CurlString],
680
+ Optional[Error],
681
+ list[Item]
682
+ ]:
683
+ _api_url = f"{self.api_base_url}/ad-accounts/{account_id}/items"
684
+ _payload = {
685
+ "ad_account_id": account_id,
686
+ "search_keyword":[],
687
+ "order_by": [{
688
+ "column": "ID",
689
+ "direction": "ASC"
690
+ }],
691
+ "filter": [
692
+ {
693
+ "column": "IS_ACTIVE",
694
+ "filter_operator": "EQ",
695
+ "value": "true or false",
696
+ }
697
+ ],
698
+ "page_index": 1,
699
+ "page_size": MAX_NUM_ITEMS_PER_PAGE,
700
+ }
701
+
702
+ #
703
+ # Get active items
704
+ #
705
+ curl, error, active_items = self.list_account_items_(to_curl, _api_url, self.headers, _payload, True)
706
+ if curl:
707
+ return curl, None, []
708
+ if error:
709
+ return None, error, []
710
+ #
711
+ # Get inactive items
712
+ #
713
+ _, error, inactive_items = self.list_account_items_(to_curl, _api_url, self.headers, _payload, False)
714
+ if error:
715
+ return None, error, []
716
+
717
+ return None, None, active_items + inactive_items
718
+
719
+
720
+ def list_account_items_(
721
+ self,
722
+ to_curl,
723
+ _api_url,
724
+ _headers,
725
+ _payload,
726
+ _is_active
727
+ ) -> tuple[
728
+ Optional[CurlString],
729
+ Optional[Error],
730
+ list[Item]
731
+ ]:
732
+ items = []
733
+ num_items = 0
734
+ page_index = 1
735
+ while True:
736
+ _payload["page_index"] = page_index
737
+ _payload["filter"][0]["value"] = "true" if _is_active else "false"
738
+
739
+ curl, error, json_obj = api_request('POST', to_curl, _api_url, _headers, _payload)
740
+ if curl:
741
+ return curl, None, []
742
+ if error:
743
+ return None, error, []
744
+
745
+ item_group = ItemList(**json_obj)
746
+ items += item_group.rows
747
+ num_items += len(item_group.rows)
748
+
749
+ if num_items >= item_group.num_counts:
750
+ break
751
+ page_index += 1
752
+
753
+ return None, None, items
754
+
755
+
423
756
 
424
757
  #
425
758
  # Helper functions
mcmcli/data/item.py ADDED
@@ -0,0 +1,42 @@
1
+ # Copyright 2023 Moloco, Inc
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from pydantic import BaseModel
16
+ from typing import Optional
17
+
18
+ #
19
+ # API response dataclasses
20
+ #
21
+ class Price(BaseModel):
22
+ currency: str
23
+ amount: float
24
+
25
+ class Item(BaseModel):
26
+ id: str
27
+ title: str
28
+ price: Price
29
+ sale_price: Price
30
+ link: str
31
+ image_link: str
32
+ category: str
33
+ review_count: str
34
+ rating_score: float
35
+ is_active: bool
36
+ is_new: Optional[bool]
37
+ created_timestamp: str
38
+
39
+ class ItemList(BaseModel):
40
+ rows: list[Item]
41
+ num_counts: int
42
+ created_timestamp_filter: str
mcmcli/requests.py CHANGED
@@ -41,6 +41,15 @@ def get(url, headers):
41
41
  except Exception as e:
42
42
  return e, None
43
43
 
44
+
45
+ def delete(url, headers):
46
+ try:
47
+ res = requests.delete(url, headers=headers)
48
+ return None, json.loads(res.text)
49
+ except Exception as e:
50
+ return e, None
51
+
52
+
44
53
  def post(url, headers, payload):
45
54
  try:
46
55
  res = requests.post(url, headers=headers, json=payload)
@@ -62,6 +71,8 @@ def api_request(method, to_curl, url, headers, payload=None) -> tuple[CurlString
62
71
 
63
72
  if method == 'GET':
64
73
  error, json_obj = get(url, headers)
74
+ elif method == 'DELETE':
75
+ error, json_obj = delete(url, headers)
65
76
  elif method == 'POST':
66
77
  error, json_obj = post(url, headers, payload)
67
78
  elif method == 'PUT':