mcm-cli 1.1.0__tar.gz → 1.2.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. {mcm_cli-1.1.0/mcm_cli.egg-info → mcm_cli-1.2.0}/PKG-INFO +1 -1
  2. {mcm_cli-1.1.0 → mcm_cli-1.2.0/mcm_cli.egg-info}/PKG-INFO +1 -1
  3. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcm_cli.egg-info/SOURCES.txt +1 -0
  4. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/__main__.py +1 -1
  5. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/command/account.py +333 -0
  6. mcm_cli-1.2.0/mcmcli/data/item.py +42 -0
  7. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/requests.py +11 -0
  8. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/setup.py +1 -1
  9. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/LICENSE +0 -0
  10. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/NOTICE +0 -0
  11. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/README.md +0 -0
  12. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcm_cli.egg-info/dependency_links.txt +0 -0
  13. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcm_cli.egg-info/entry_points.txt +0 -0
  14. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcm_cli.egg-info/requires.txt +0 -0
  15. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcm_cli.egg-info/top_level.txt +0 -0
  16. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/__init__.py +0 -0
  17. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/command/admin.py +0 -0
  18. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/command/auth.py +0 -0
  19. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/command/config.py +0 -0
  20. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/command/decision.py +0 -0
  21. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/command/wallet.py +0 -0
  22. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/data/account.py +0 -0
  23. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/data/account_user.py +0 -0
  24. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/data/decision.py +0 -0
  25. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/data/error.py +0 -0
  26. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/data/seller.py +0 -0
  27. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/data/token.py +0 -0
  28. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/data/wallet.py +0 -0
  29. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/mcmcli/logging.py +0 -0
  30. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/setup.cfg +0 -0
  31. {mcm_cli-1.1.0 → mcm_cli-1.2.0}/tests/test_config.py +0 -0
@@ -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,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
@@ -22,6 +22,7 @@ mcmcli/data/account.py
22
22
  mcmcli/data/account_user.py
23
23
  mcmcli/data/decision.py
24
24
  mcmcli/data/error.py
25
+ mcmcli/data/item.py
25
26
  mcmcli/data/seller.py
26
27
  mcmcli/data/token.py
27
28
  mcmcli/data/wallet.py
@@ -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")
@@ -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
@@ -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
@@ -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':
@@ -18,7 +18,7 @@ from setuptools import setup, find_packages
18
18
 
19
19
  setup(
20
20
  name='mcm-cli',
21
- version='1.1.0',
21
+ version='1.2.0',
22
22
  description='A command-line interface for Moloco Commerde Media',
23
23
  long_description=open('README.md').read(),
24
24
  long_description_content_type='text/markdown',
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes