mcm-cli 0.45__py3-none-any.whl → 0.49__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: 0.45
3
+ Version: 0.49.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
@@ -32,30 +32,43 @@ Please contact the Moloco representative for more details.
32
32
 
33
33
  ## How to install
34
34
 
35
- Run the command from a terminal.
35
+ To install, run:
36
+ ```
37
+ pip install mcm-cli
38
+ ```
39
+
40
+ For installation from the source code, use:
36
41
  ```
37
42
  git clone https://github.com/moloco-mcm/mcm-cli.git && pip install mcm-cli
38
43
  ```
39
44
 
40
45
  ## How to upgrade
41
46
 
42
- Run the command from a terminal.
47
+ To upgrade, run:
48
+ ```
49
+ pip install --upgrade mcm-cli
50
+ ```
51
+
52
+ Or, if installed from the source code:
43
53
  ```
44
54
  git -C mcm-cli pull && pip install mcm-cli
45
55
  ```
46
56
 
47
57
  ## How to uninstall
48
58
 
49
- Run the command from a terminal.
59
+ To uninstall, run:
50
60
  ```
51
61
  pip uninstall mcm-cli
52
62
  ```
53
63
 
54
64
  ## How to use
55
- Run `$ mcm config init` to initialize the configuration. It saves the configuration to `~/.mcm/config.toml`.
56
-
57
- Use `--help` option to learn more of each command.
65
+ Initialize the configuration with:
66
+ ```
67
+ mcm config init
68
+ ```
69
+ This saves the configuration to `~/.mcm/config.toml`.
58
70
 
71
+ For more details on each command, use the `--help` option.
59
72
  ```
60
73
  $ mcm --help
61
74
 
@@ -65,9 +78,11 @@ $ mcm --help
65
78
  │ --help Show this message and exit. │
66
79
  ╰──────────────────────────────────────────────────────────────────╯
67
80
  ╭─ Commands ───────────────────────────────────────────────────────╮
81
+ │ account Ad account management │
68
82
  │ auth Authentication management │
69
83
  │ config Configurations │
70
- version
84
+ decision Decision command
85
+ │ version Show the tool version │
71
86
  │ wallet Wallet management │
72
87
  ╰──────────────────────────────────────────────────────────────────╯
73
88
 
@@ -0,0 +1,23 @@
1
+ mcmcli/__init__.py,sha256=-U6lMZ9_99IXAKwnqnYXYr6NcO6TSmG-kxewgvJjU4k,575
2
+ mcmcli/__main__.py,sha256=RN_N--yPkndsO-Hya-yuQCrGY9JYFszO1kuJ6iFLw9c,1474
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
6
+ mcmcli/command/auth.py,sha256=QLdr_XFW5BVw9r4a7Kjj5BTBXpSux3AWI9eI03S8aiA,2480
7
+ mcmcli/command/config.py,sha256=sdzge-l_Yi2P_TlTgSLqShcGyPCzpW3QJzctpIvc-g4,4195
8
+ mcmcli/command/decision.py,sha256=Zjbmt71OVU-oL8Itt9O-SvwT9Lbxw-PAgRZaIgiXi-E,8411
9
+ mcmcli/command/wallet.py,sha256=TB3WrmniREsDk0ui20p3ha6OZMwA2wAJaxQ9QDGtKnA,8968
10
+ mcmcli/data/account.py,sha256=pe7tPapP6vlUD5D5L5Nh5k2bkWdYOK01Mpt5fBYFnJs,1782
11
+ mcmcli/data/account_user.py,sha256=27nQp52nMma5a3QszSJGqgq5Z0ivIb-nMZcZuhEgbEg,1328
12
+ mcmcli/data/decision.py,sha256=bQ5j_PbPRSFa0sY5g9UVqdNzl_2epchcz1lHoDVuV90,2880
13
+ mcmcli/data/error.py,sha256=d6xa_jTXumlA0EzXy2PJQ86ajBb0Pm90fss9R3LuHUc,1094
14
+ mcmcli/data/seller.py,sha256=40SA7QekM3a3svDrDYLo_QYJ68VUxDO0KeGejJMp4k4,1004
15
+ mcmcli/data/token.py,sha256=11wtyLHCAZHu0LVbNDPa-yipcL6lenxoYIKEI58VzFs,1744
16
+ mcmcli/data/wallet.py,sha256=W-CksF9SPqiv3jZg07Wy8ehVUP5Ot1Gbq2LEGNQCOC8,1906
17
+ mcm_cli-0.49.0.dist-info/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
18
+ mcm_cli-0.49.0.dist-info/METADATA,sha256=WkQSZEIyOkyjuChZpPHaEyToWLg6cTCrAzIEMmqB_hU,3019
19
+ mcm_cli-0.49.0.dist-info/NOTICE,sha256=Ldnl2MjRaXPxcldUdbI2NTybq60XAa2LowRhFrRTuiI,76
20
+ mcm_cli-0.49.0.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
21
+ mcm_cli-0.49.0.dist-info/entry_points.txt,sha256=qTHAWZ-ejSiU4t11RYwtAU8ScqhQPDeMVTG9y4wMVLg,60
22
+ mcm_cli-0.49.0.dist-info/top_level.txt,sha256=sh7oqIaqLQlMtKHlxHHgpV2xGMrBMPFWpSp0C6nvJ_Y,7
23
+ mcm_cli-0.49.0.dist-info/RECORD,,
mcmcli/__main__.py CHANGED
@@ -17,10 +17,10 @@
17
17
  """mcm cli entry point script."""
18
18
  # mcmcli/__main__.py
19
19
 
20
- from typing import Optional
21
-
20
+ import mcmcli.command.account
22
21
  import mcmcli.command.auth
23
22
  import mcmcli.command.config
23
+ import mcmcli.command.decision
24
24
  import mcmcli.command.wallet
25
25
  import mcmcli.logging
26
26
  import typer
@@ -32,10 +32,12 @@ def version():
32
32
  """
33
33
  Show the tool version
34
34
  """
35
- typer.echo(f"Version: mcm-cli v0.45")
35
+ typer.echo(f"Version: mcm-cli v0.49.0")
36
36
 
37
+ app.add_typer(mcmcli.command.account.app, name="account", help="Ad account management")
37
38
  app.add_typer(mcmcli.command.auth.app, name="auth", help="Authentication management")
38
39
  app.add_typer(mcmcli.command.config.app, name="config", help="Configurations")
40
+ app.add_typer(mcmcli.command.decision.app, name="decision", help="Decision command")
39
41
  app.add_typer(mcmcli.command.wallet.app, name="wallet", help="Wallet management")
40
42
 
41
43
  if __name__ == "__main__":
@@ -0,0 +1,461 @@
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 enum import Enum
16
+ from mcmcli.data.account import Account, AccountListWrapper
17
+ from mcmcli.data.account_user import User, UserWrapper, UserListWrapper
18
+ from mcmcli.data.error import Error
19
+ from mcmcli.data.seller import Seller, SellerListWrapper
20
+ from mcmcli.requests import CurlString, api_request
21
+ from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
22
+
23
+ import csv
24
+ import mcmcli.command.auth
25
+ import mcmcli.command.config
26
+ import mcmcli.logging
27
+ import mcmcli.requests
28
+ import sys
29
+ import time
30
+ import typer
31
+
32
+ MAX_NUM_ITEMS_PER_PAGE = 5000
33
+ T = TypeVar('T')
34
+
35
+ class UserRole(Enum):
36
+ AD_ACCOUNT_OWNER = "AD_ACCOUNT_OWNER"
37
+
38
+ app = typer.Typer(add_completion=False)
39
+
40
+
41
+ @app.command()
42
+ def bulk_invite_ad_account_owners(
43
+ csv_file: str = typer.Option(help="CSV file path that contains the list of ad account id, user email address, and the user name."),
44
+ dry_run: bool = typer.Option(True, help="Perform a dry run without sending emails."),
45
+ skip_csv_header: bool = typer.Option(False, help="Skip the first line of the CSV file."),
46
+ create_account: bool = typer.Option(False, help="Create the ad account if it doesn't exist."),
47
+ create_user: bool = typer.Option(False, help="Create the ad account user if it doesn't exist."),
48
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
49
+ ):
50
+ """
51
+ Send campaign manager invitation emails to ad account owners. Ensure the CSV file has three columns:
52
+ ad account ID, user email address, and user name. Extra columns are ignored.
53
+ """
54
+ csv_data = _read_csv_file(csv_file, skip_csv_header)
55
+ if csv_data is None:
56
+ return
57
+
58
+ a = _create_account_command(profile)
59
+ if a is None:
60
+ return
61
+
62
+ if dry_run:
63
+ print(f"Dry run initiated.")
64
+
65
+ #
66
+ # get the list of sellers
67
+ #
68
+ _, error, seller_dictionary = a.list_sellers()
69
+ if error:
70
+ print(f"ERROR: {error.message}", file=sys.stderr, flush=True)
71
+ return
72
+
73
+
74
+ print('Processing the command ', end='', file=sys.stderr, flush=True)
75
+ total_count = len(csv_data)
76
+ success_count = 0
77
+ account_creation_count = 0
78
+ for row in csv_data:
79
+ account_id = row[0].strip()
80
+ email_address = row[1].strip()
81
+ user_name = row[2].strip()
82
+ print('.', end='', file=sys.stderr, flush=True)
83
+
84
+ # Check if the ad account exists
85
+ if _lookup_dict(account_id, seller_dictionary) is None:
86
+ print(f"\nERROR: Could not find the ad account ID {account_id}", file=sys.stderr, flush=True)
87
+ continue
88
+
89
+ # Create the ad account if the seller doesn't have one yet and the CLI command tells to create the ad account.
90
+ seller_info = seller_dictionary[account_id]
91
+ if not seller_info.is_registered and create_account:
92
+ error, _ = a.create_account_with_retry(seller_info.id, seller_info.title, dry_run)
93
+ if error:
94
+ print(f"\nERROR: Failed to create the ad account {account_id}. {error.message}.", file=sys.stderr, flush=True)
95
+ continue
96
+ account_creation_count += 1
97
+
98
+ error = a.send_invitation_email_with_retry(account_id, email_address, user_name, create_user, dry_run)
99
+ if error:
100
+ print(f"\nERROR: Failed to send the mail for the ad account ID {account_id}. {error.message}.", file=sys.stderr, flush=True)
101
+ continue
102
+
103
+ success_count += 1
104
+
105
+ print(' Done', file=sys.stderr, flush=True)
106
+ if dry_run:
107
+ print(f"If this wasn't a dry-run, it would have sent {success_count} out of {total_count} emails and created {account_creation_count} new ad accounts.", flush=True)
108
+ else:
109
+ print(f'Sent {success_count} out of {total_count} emails, and created {account_creation_count} new ad accounts.', flush=True)
110
+ return
111
+
112
+
113
+ @app.command()
114
+ def bulk_check_user_registrations(
115
+ csv_file: str = typer.Option(help="CSV file path that contains the list of ad account id and user email address."),
116
+ skip_csv_header: bool = typer.Option(False, help="Skip the first line of the CSV file."),
117
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
118
+ ):
119
+ """
120
+ Check user status. Ensure the CSV file has two columns: ad account ID and user email address. Extra columns are ignored.
121
+ """
122
+ csv_data = _read_csv_file(csv_file, skip_csv_header)
123
+ if csv_data is None:
124
+ return
125
+
126
+ a = _create_account_command(profile)
127
+ if a is None:
128
+ return
129
+
130
+ _, error, account_dictionary = a.list_accounts(to_curl=False)
131
+ if error:
132
+ print(f"ERROR: {error.message}", file=sys.stderr, flush=True)
133
+ return
134
+
135
+ print('"Ad Account ID","Is Ad Account Exist","User Email","Is User Exist","User Role","User Status"')
136
+ for row in csv_data:
137
+ account_id = row[0].strip()
138
+ email_address = row[1].strip()
139
+ account = _lookup_dict(account_id, account_dictionary)
140
+ is_account_exist = "No Account Exists" if account is None else "Account Exists"
141
+
142
+ error, user_dictionary = a.list_account_users_with_retry(account_id)
143
+ if error:
144
+ print(f"\nERROR: {error.message}", file=sys.stderr, flush=True)
145
+ return
146
+
147
+ user = _lookup_dict(email_address, user_dictionary)
148
+ is_user_exist = "No User Exist" if user is None else "User Exists"
149
+ user_role = "" if user is None else user.role
150
+ user_status = "" if user is None else user.status
151
+ print(f'"{account_id}","{is_account_exist}","{email_address}","{is_user_exist}","{user_role}","{user_status}"')
152
+ return
153
+
154
+
155
+ class AccountCommand:
156
+ def __init__(
157
+ self,
158
+ profile,
159
+ auth_command: mcmcli.command.auth.AuthCommand,
160
+ token
161
+ ):
162
+ self.config = mcmcli.command.config.get_config(profile)
163
+ if (self.config is None):
164
+ print(f"ERROR: Failed to load the CLI profile", file=sys.stderr, flush=True)
165
+ sys.exit()
166
+
167
+ self.profile = profile
168
+ self.auth_command = auth_command
169
+ self.api_base_url = f"{self.config['management_api_hostname']}/rmp/mgmt/v1/platforms/{self.config['platform_id']}"
170
+ self.headers = {
171
+ "accept": "application/json",
172
+ "content-type": "application/json",
173
+ "Authorization": f"Bearer {token}"
174
+ }
175
+
176
+
177
+ def refresh_token(
178
+ self,
179
+ ) -> None:
180
+ _, error, token = self.auth_command.get_token()
181
+ if error:
182
+ print(f"ERROR: {error.message}", file=sys.stderr, flush=True)
183
+ return
184
+ self.headers["Authorization"] = f"Bearer {token.token}"
185
+
186
+
187
+ def retry_with_token_refresh(
188
+ self,
189
+ operation: Callable[[], Tuple[Optional[Error], Any]],
190
+ max_retries: int = 3,
191
+ delay: int = 1,
192
+ ) -> Tuple[
193
+ Optional[Error],
194
+ Any
195
+ ]:
196
+ retries = 0
197
+ while retries < max_retries:
198
+ error, result = operation()
199
+ if error and error.code == 16:
200
+ print(f"\nERROR: authentication token expired. Retrying...", file=sys.stderr, flush=True)
201
+ self.refresh_token()
202
+ retries += 1
203
+ time.sleep(delay)
204
+ continue
205
+ return error, result
206
+ return Error(code=16, message="Failed to regain an authentication token"), None
207
+
208
+
209
+ def send_invitation_email(
210
+ self,
211
+ account_id: str,
212
+ email_address: str,
213
+ user_name: str,
214
+ create_user: bool,
215
+ ) -> Optional[Error]:
216
+
217
+ error, user_dictionary = self.list_account_users(account_id)
218
+ if error:
219
+ return error
220
+
221
+ user = _lookup_dict(email_address, user_dictionary)
222
+ if user is None:
223
+ if not create_user:
224
+ _msg = f"\nERROR: The ad account {account_id} exists. But we could not find the user with email {email_address}"
225
+ print(_msg, file=sys.stderr, flush=True)
226
+ return Error(code=1, message = _msg)
227
+
228
+ _, error, _ = self.invite_user(account_id, email_address, user_name, to_curl=False)
229
+ return error
230
+
231
+ #
232
+ # The ad account and the user exists. We can just send the password reset email
233
+ #
234
+ _api_url = f"{self.api_base_url}/users/{user.id}/password-reset-tokens"
235
+ _, error, _ = api_request('POST', False, _api_url, self.headers)
236
+ return error
237
+
238
+ def send_invitation_email_with_retry(
239
+ self,
240
+ account_id: str,
241
+ email_address: str,
242
+ user_name: str,
243
+ create_user: bool,
244
+ dry_run: bool,
245
+ ) -> Optional[Error]:
246
+ if dry_run:
247
+ return None
248
+
249
+ error, _ = self.retry_with_token_refresh(
250
+ lambda: (
251
+ self.send_invitation_email(account_id, email_address, user_name, create_user),
252
+ None,
253
+ ),
254
+ )
255
+ return error
256
+
257
+
258
+ def invite_user(
259
+ self,
260
+ account_id,
261
+ user_email,
262
+ user_name,
263
+ role = UserRole.AD_ACCOUNT_OWNER,
264
+ to_curl = True,
265
+ ) -> tuple[
266
+ Optional[CurlString],
267
+ Optional[Error],
268
+ Optional[User]
269
+ ]:
270
+ _api_url = f"{self.api_base_url}/ad-accounts/{account_id}/users"
271
+ _payload = {
272
+ "user": {
273
+ "ad_account_id": account_id,
274
+ "email": user_email,
275
+ "name": user_name,
276
+ "role": role.value,
277
+ }
278
+ }
279
+ curl, error, json_obj = api_request('POST', to_curl, _api_url, self.headers, _payload)
280
+ if curl:
281
+ return curl, None, None
282
+ if error:
283
+ return None, error, None
284
+
285
+ ret = UserWrapper(**json_obj).user
286
+ return None, None, ret
287
+
288
+ def create_account(
289
+ self,
290
+ account_id,
291
+ account_name,
292
+ to_curl
293
+ ) -> tuple[
294
+ Optional[CurlString],
295
+ Optional[Error],
296
+ Optional[Account]
297
+ ]:
298
+ _api_url = f"{self.api_base_url}/ad-accounts"
299
+ _payload = {
300
+ "ad_account": {
301
+ "id": account_id,
302
+ "title": account_name
303
+ }
304
+ }
305
+
306
+ curl, error, json_obj = api_request('POST', to_curl, _api_url, self.headers, _payload)
307
+ if curl:
308
+ return curl, None, None
309
+ if error:
310
+ return None, error, None
311
+
312
+ account_info = Account(**json_obj['ad_account'])
313
+ return None, None, account_info
314
+
315
+
316
+ def create_account_with_retry(
317
+ self,
318
+ account_id: str,
319
+ account_name: str,
320
+ dry_run: bool,
321
+ ) -> tuple[
322
+ Optional[Error],
323
+ Optional[Account],
324
+ ]:
325
+ if dry_run:
326
+ #print(f"\nDRY RUN: create_account({account_id}, {account_name})")
327
+ return None, None
328
+
329
+ retries = 0
330
+ delay = 1
331
+ max_retries = 3
332
+ while retries < max_retries:
333
+ _, error, account = self.create_account(account_id, account_name, to_curl=False)
334
+ if error and error.code == 16:
335
+ print(f"\nERROR: authentication token expired. Retrying...", file=sys.stderr, flush=True)
336
+ self.refresh_token()
337
+ retries += 1
338
+ time.sleep(delay)
339
+ continue
340
+ return error, account
341
+ return Error(code=16, message="Failed to regain an authentication token"), None
342
+
343
+
344
+ def list_accounts(
345
+ self,
346
+ to_curl=False
347
+ ) -> tuple[
348
+ Optional[CurlString],
349
+ Optional[Error],
350
+ Dict[str, Account],
351
+ ]:
352
+ _api_url = f"{self.api_base_url}/ad-accounts"
353
+
354
+ curl, error, json_obj = api_request('GET', to_curl, _api_url, self.headers)
355
+ if curl:
356
+ return curl, None, {}
357
+ if error:
358
+ return None, error, {}
359
+
360
+ account_list = AccountListWrapper(**json_obj).ad_accounts
361
+
362
+ accounts = {}
363
+ for x in account_list:
364
+ accounts[x.id] = x
365
+ return None, None, accounts
366
+
367
+
368
+ def list_sellers(
369
+ self,
370
+ ) -> tuple[
371
+ Optional[CurlString],
372
+ Optional[Error],
373
+ Dict[str, Seller],
374
+ ]:
375
+ _api_url = f"{self.api_base_url}/sellers"
376
+
377
+ curl, error, json_obj = api_request('GET', False, _api_url, self.headers)
378
+ if curl:
379
+ return curl, None, {}
380
+ if error:
381
+ return None, error, {}
382
+
383
+ seller_list = SellerListWrapper(**json_obj).sellers
384
+
385
+ sellers = {}
386
+ for x in seller_list:
387
+ sellers[x.id] = x
388
+ return None, None, sellers
389
+
390
+
391
+ def list_account_users(
392
+ self,
393
+ account_id: str,
394
+ ) -> tuple [
395
+ Optional[Error],
396
+ Dict[str, User],
397
+ ]:
398
+ _api_url = f"{self.api_base_url}/ad-accounts/{account_id}/users"
399
+
400
+ _, error, json_obj = api_request('GET', False, _api_url, self.headers)
401
+ if error:
402
+ return error, {}
403
+ user_list = UserListWrapper(**json_obj).users
404
+
405
+ users = {}
406
+ for x in user_list:
407
+ users[x.email] = x
408
+
409
+ return None, users
410
+
411
+ def list_account_users_with_retry(
412
+ self,
413
+ account_id: str
414
+ ) -> tuple [
415
+ Optional[Error],
416
+ Dict[str, User],
417
+ ]:
418
+ return self.retry_with_token_refresh(
419
+ lambda: self.list_account_users(account_id),
420
+ )
421
+
422
+
423
+
424
+ #
425
+ # Helper functions
426
+ #
427
+ def _create_account_command(
428
+ profile: str
429
+ ) -> Optional[AccountCommand]:
430
+ auth = mcmcli.command.auth.AuthCommand(profile)
431
+ _, error, token = auth.get_token()
432
+ if error:
433
+ print(f"ERROR: {error.message}", file=sys.stderr, flush=True)
434
+ return None
435
+ return AccountCommand(profile, auth, token.token)
436
+
437
+
438
+ def _read_csv_file(
439
+ csv_file: str,
440
+ skip_csv_header: bool,
441
+ ) -> Optional[list[list]]:
442
+ try:
443
+ with open(csv_file, mode='r', newline='') as file:
444
+ csv_reader = csv.reader(file)
445
+ if skip_csv_header:
446
+ next(csv_reader)
447
+ return list(csv_reader)
448
+ except Exception as e:
449
+ print(f"ERROR: {e}", file=sys.stderr, flush=True)
450
+ return None
451
+
452
+
453
+ def _lookup_dict(
454
+ id: str,
455
+ dictionary: Dict[str, T],
456
+ ) -> Optional[T]:
457
+ if id in dictionary:
458
+ return dictionary[id]
459
+ return None
460
+
461
+
@@ -0,0 +1,265 @@
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 mcmcli.data.error import Error
16
+ from mcmcli.data.decision import DecidedCreative, DecidedCreativeBulkList, DecidedItemList
17
+ from mcmcli.requests import CurlString, api_request
18
+ from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
19
+
20
+ import json
21
+ import mcmcli.command.auth
22
+ import mcmcli.command.config
23
+ import mcmcli.logging
24
+ import sys
25
+ import typer
26
+
27
+ MAX_NUM_ITEMS_PER_PAGE = 5000
28
+
29
+ app = typer.Typer(add_completion=False)
30
+
31
+ @app.command()
32
+ def decide_items(
33
+ inventory_id: str = typer.Option(help="Ad inventory ID"),
34
+ num_items: int = typer.Option(help="Number of items requested for the inventory."),
35
+ items: str = typer.Option(None, help="The main item ids of the page. For example, homepage inventories don't have any main items, and product-detail-page inventories have one main item."),
36
+ location_filter: str = typer.Option(None, help="Location filter value"),
37
+ to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
38
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
39
+ ):
40
+ """
41
+ Request item decision by auction.
42
+ """
43
+ d = DecisionCommand(profile)
44
+
45
+ curl, error, ret = d.decide_items(inventory_id, num_items, items, location_filter, to_curl)
46
+ if to_curl:
47
+ print(curl)
48
+ return
49
+ if error:
50
+ print(f"ERROR: {error.message}")
51
+ return
52
+ if ret is None:
53
+ print(f"ERROR: Unknown error")
54
+ return
55
+
56
+ print(ret.model_dump_json())
57
+ return
58
+
59
+
60
+ @app.command()
61
+ def decide_creative(
62
+ inventory_id: str = typer.Option(help="Ad inventory ID"),
63
+ items: str = typer.Option(None, help="The main item ids of the page. For example, homepage inventories don't have any main items, and product-detail-page inventories have one main item."),
64
+ location_filter: str = typer.Option(None, help="Location filter value"),
65
+ to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
66
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
67
+ ):
68
+ """
69
+ Request item decision by creative auction.
70
+ """
71
+ d = DecisionCommand(profile)
72
+
73
+ curl, error, ret = d.decide_creative(inventory_id, items, location_filter, to_curl)
74
+ if to_curl:
75
+ print(curl)
76
+ return
77
+ if error:
78
+ print(f"ERROR: {error.message}")
79
+ return
80
+ if ret is None:
81
+ print(f"ERROR: Unknown error")
82
+ return
83
+
84
+ print(ret.model_dump_json())
85
+ return
86
+
87
+
88
+ @app.command()
89
+ def decide_creative_bulk(
90
+ inventory_id_list: str = typer.Option(help="Ad inventory IDs separated by comma(,)"),
91
+ items: str = typer.Option(None, help="The main item ids of the page. For example, homepage inventories don't have any main items, and product-detail-page inventories have one main item."),
92
+ location_filter: str = typer.Option(None, help="Location filter value"),
93
+ to_curl: bool = typer.Option(False, help="Generate the curl command instead of executing it."),
94
+ profile: str = typer.Option("default", help="profile name of the MCM CLI."),
95
+ ):
96
+ """
97
+ Request item decision by creative auction for multiple inventories.
98
+ """
99
+ d = DecisionCommand(profile)
100
+
101
+ curl, error, ret = d.decide_creative_bulk(inventory_id_list, items, location_filter, to_curl)
102
+ if to_curl:
103
+ print(curl)
104
+ return
105
+ if error:
106
+ print(f"ERROR: {error.message}")
107
+ return
108
+ if ret is None:
109
+ print(f"ERROR: Unknown error")
110
+ return
111
+
112
+ print(ret.model_dump_json())
113
+ return
114
+
115
+
116
+
117
+ class DecisionCommand:
118
+ def __init__(
119
+ self,
120
+ profile,
121
+ ):
122
+ self.config = mcmcli.command.config.get_config(profile)
123
+ if (self.config is None):
124
+ print(f"ERROR: Failed to load the CLI profile", file=sys.stderr, flush=True)
125
+ sys.exit()
126
+
127
+ self.profile = profile
128
+ self.api_base_url = f"{self.config['decision_api_hostname']}/rmp/decision/v1/platforms/{self.config['platform_id']}"
129
+ self.headers = {
130
+ "accept": "application/json",
131
+ "content-type": "application/json",
132
+ "x-api-key": self.config['decision_api_key']
133
+ }
134
+
135
+ def decide_items(
136
+ self,
137
+ inventory_id,
138
+ num_items,
139
+ items,
140
+ location_filter,
141
+ to_curl,
142
+ ) -> tuple[
143
+ Optional[CurlString],
144
+ Optional[Error],
145
+ Optional[DecidedItemList],
146
+ ]:
147
+ _api_url = f"{self.api_base_url}/auction"
148
+ _payload = {
149
+ "request_id": "request-1",
150
+ "inventory": {
151
+ "inventory_id": inventory_id,
152
+ "num_items": num_items
153
+ },
154
+ "user": {
155
+ "user_id": "user-1"
156
+ },
157
+ "device": {
158
+ "persistent_id": "persistent-device-1"
159
+ },
160
+ }
161
+ if items:
162
+ _payload["inventory"]["items"] = items.split(',')
163
+
164
+ if location_filter:
165
+ _payload["filtering"] = {
166
+ "location": {
167
+ "locations": location_filter.split(',')
168
+ }
169
+ }
170
+
171
+ curl, error, json_obj = api_request('POST', to_curl, _api_url, self.headers, _payload)
172
+ if curl:
173
+ return curl, None, None
174
+ if error:
175
+ return None, error, None
176
+
177
+ decided_items = DecidedItemList(**json_obj)
178
+ return None, None, decided_items
179
+
180
+
181
+ def decide_creative(
182
+ self,
183
+ inventory_id,
184
+ items,
185
+ location_filter,
186
+ to_curl
187
+ ) -> tuple[
188
+ Optional[CurlString],
189
+ Optional[Error],
190
+ Optional[DecidedCreative],
191
+ ]:
192
+ _api_url = f"{self.api_base_url}/creative-auction"
193
+ _payload = {
194
+ "request_id": "request-1",
195
+ "inventory": {
196
+ "inventory_id": inventory_id
197
+ },
198
+ "user": {
199
+ "user_id": "user-1"
200
+ },
201
+ "device": {
202
+ "persistent_id": "persistent-device-1"
203
+ },
204
+ }
205
+ if items:
206
+ _payload["inventory"]["items"] = items.split(',')
207
+
208
+ if location_filter:
209
+ _payload["filtering"] = {
210
+ "location": {
211
+ "locations": location_filter.split(',')
212
+ }
213
+ }
214
+
215
+ curl, error, json_obj = api_request('POST', to_curl, _api_url, self.headers, _payload)
216
+ if curl:
217
+ return curl, None, None
218
+ if error:
219
+ return None, error, None
220
+
221
+ decided_creative = DecidedCreative(**json_obj)
222
+ return None, None, decided_creative
223
+
224
+
225
+ def decide_creative_bulk(
226
+ self,
227
+ inventory_id_list,
228
+ items,
229
+ location_filter,
230
+ to_curl
231
+ ) -> tuple[
232
+ Optional[CurlString],
233
+ Optional[Error],
234
+ Optional[DecidedCreativeBulkList],
235
+ ]:
236
+ _api_url = f"{self.api_base_url}/creative-auction-bulk"
237
+ _payload = {
238
+ "request_id": "request-1",
239
+ "inventories": list(map(lambda x: { "inventory_id": x }, inventory_id_list.split(','))),
240
+ "user": {
241
+ "user_id": "user-1"
242
+ },
243
+ "device": {
244
+ "persistent_id": "persistent-device-1"
245
+ },
246
+ }
247
+ if items:
248
+ for inventory in _payload["inventories"]:
249
+ inventory["items"] = items.split(',')
250
+
251
+ if location_filter:
252
+ _payload["filtering"] = {
253
+ "location": {
254
+ "locations": location_filter.split(',')
255
+ }
256
+ }
257
+
258
+ curl, error, json_obj = api_request('POST', to_curl, _api_url, self.headers, _payload)
259
+ if curl:
260
+ return curl, None, None
261
+ if error:
262
+ return None, error, None
263
+
264
+ return None, None, DecidedCreativeBulkList(**json_obj)
265
+
mcmcli/data/account.py ADDED
@@ -0,0 +1,59 @@
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
+
17
+ #
18
+ # API error response dataclasses
19
+ #
20
+ # list-accounts command's example response is as below:
21
+ #
22
+ # {
23
+ # "ad_accounts": [
24
+ # {
25
+ # "id": "0d139a1b-abcd-11ee-b5f8-12f12be0eb51",
26
+ # "title": "Smoke Distribution Inc.",
27
+ # "timezone": "America/New_York",
28
+ # "state_info": {
29
+ # "ad_account_id": "0d139a1b-abcd-11ee-b5f8-12f12be0eb51",
30
+ # "state": "ACTIVE",
31
+ # "state_case": "INIT_BY_PLATFORM",
32
+ # "state_updated_at": "2023-10-23T12:43:31.542660Z",
33
+ # "updated_at": "2023-10-23T12:43:31.542660Z"
34
+ # },
35
+ # "available_features": [],
36
+ # "created_at": "2023-10-23T12:43:31.542660Z",
37
+ # "updated_at": "2023-10-23T12:43:31.542660Z"
38
+ # }
39
+ # ]
40
+ # }
41
+
42
+ class State(BaseModel):
43
+ ad_account_id: str
44
+ state: str
45
+ state_case: str
46
+ state_updated_at: str
47
+ updated_at: str
48
+
49
+ class Account(BaseModel):
50
+ id: str
51
+ title: str
52
+ timezone: str
53
+ state_info: State
54
+ # available_features: []
55
+ created_at: str
56
+ updated_at: str
57
+
58
+ class AccountListWrapper(BaseModel):
59
+ ad_accounts: list[Account]
@@ -0,0 +1,50 @@
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
+
17
+ #
18
+ # Ad account users response example is as below:f
19
+ #
20
+ # {
21
+ # "users": [
22
+ # {
23
+ # "ad_account_id": "1234",
24
+ # "id": "CEdyiC4f1yyyySGEOGj7",
25
+ # "email": "user@example.com",
26
+ # "name": "John Doe",
27
+ # "role": "AD_ACCOUNT_OWNER",
28
+ # "status": "NOT_REGISTERED",
29
+ # "created_at": "2024-06-06T14:29:42Z",
30
+ # "updated_at": "2024-06-06T14:29:42Z"
31
+ # }
32
+ # ]
33
+ # }
34
+
35
+ class User(BaseModel):
36
+ ad_account_id: str
37
+ id: str
38
+ email: str
39
+ name: str
40
+ role: str
41
+ status: str
42
+ created_at: str
43
+ updated_at: str
44
+
45
+ class UserListWrapper(BaseModel):
46
+ users: list[User]
47
+
48
+ class UserWrapper(BaseModel):
49
+ user: User
50
+
@@ -0,0 +1,104 @@
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
+ # Item decision API's response example:
22
+ #
23
+ # {
24
+ # "request_id": "request-1",
25
+ # "decided_items": [
26
+ # {
27
+ # "item_id": "1111833",
28
+ # "auction_result": {
29
+ # "ad_account_id": "2444",
30
+ # "campaign_id": "zWHhpyNbzYcy5FAy",
31
+ # "win_price": {
32
+ # "currency": "USD",
33
+ # "amount_micro": "500000000"
34
+ # },
35
+ # "campaign_text_entry": ""
36
+ # },
37
+ # "imp_trackers": [
38
+ # "https://myplatform-evt.rmp-api.moloco.com/t/i/MYPLATFORM_TEST?source=2X0op"
39
+ # ],
40
+ # "click_trackers": [
41
+ # "https://myplatform-evt.rmp-api.moloco.com/t/c/MYPLATFORM_TEST?source=2X0opp"
42
+ # ],
43
+ # "track_id": "2X0op"
44
+ # }
45
+ # ]
46
+ # }
47
+
48
+ class MicroPrice(BaseModel):
49
+ currency: str
50
+ amount_micro: str
51
+
52
+ class AuctionResult(BaseModel):
53
+ ad_account_id: str
54
+ campaign_id: str
55
+ win_price: Optional[MicroPrice]
56
+ campaign_text_entry: Optional[str]
57
+
58
+ class DecidedItem(BaseModel):
59
+ item_id: str
60
+ auction_result: Optional[AuctionResult]
61
+ imp_trackers: list[str]
62
+ click_trackers: list[str]
63
+ track_id: Optional[str]
64
+
65
+ class DecidedItemList(BaseModel):
66
+ request_id: str
67
+ decided_items: list[DecidedItem]
68
+
69
+ class CreativeBanner(BaseModel):
70
+ creative_id: str
71
+ image_url: str
72
+ imp_trackers: list[str]
73
+ click_trackers: list[str]
74
+
75
+ class CreativeItem(BaseModel):
76
+ item_id: str
77
+ imp_trackers: list[str]
78
+ click_trackers: list[str]
79
+
80
+ class LandingUrl(BaseModel):
81
+ id: str
82
+ url: str
83
+
84
+ class DecidedCreative(BaseModel):
85
+ request_id: str
86
+ auction_result: Optional[AuctionResult]
87
+ banner: Optional[CreativeBanner]
88
+ items: list[CreativeItem]
89
+ landing_url:Optional[LandingUrl]
90
+
91
+ class CreativeBannerWrapper(BaseModel):
92
+ banner: CreativeBanner
93
+
94
+ class DecidedCreativeBulk(BaseModel):
95
+ inventory_id: str
96
+ auction_result: Optional[AuctionResult]
97
+ creatives: list[CreativeBannerWrapper]
98
+ items: list[DecidedItem]
99
+ landing_url: Optional[LandingUrl]
100
+
101
+ class DecidedCreativeBulkList(BaseModel):
102
+ request_id: str
103
+ results: list[DecidedCreativeBulk]
104
+
mcmcli/data/seller.py ADDED
@@ -0,0 +1,38 @@
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
+
17
+ #
18
+ # API error response dataclasses
19
+ #
20
+ # list-accounts command's example response is as below:
21
+ #
22
+ # {
23
+ # "sellers": [
24
+ # {
25
+ # "id": "4450",
26
+ # "title": "1 All Good",
27
+ # "is_registered": false
28
+ # }
29
+ # ]
30
+ # }
31
+
32
+ class Seller(BaseModel):
33
+ id: str
34
+ title: str
35
+ is_registered: bool
36
+
37
+ class SellerListWrapper(BaseModel):
38
+ sellers: list[Seller]
mcmcli/data/wallet.py CHANGED
@@ -24,7 +24,7 @@ from pydantic import BaseModel
24
24
  # {
25
25
  # "id": "TjNwMMlONkaMqNyn",
26
26
  # "title": "",
27
- # "ad_account_id": "side_bar_1-ME",
27
+ # "ad_account_id": "1234",
28
28
  # "type": "PRE_PAYMENT",
29
29
  # "accounts": [
30
30
  # {
@@ -1,17 +0,0 @@
1
- mcmcli/__init__.py,sha256=-U6lMZ9_99IXAKwnqnYXYr6NcO6TSmG-kxewgvJjU4k,575
2
- mcmcli/__main__.py,sha256=Mil8isMhqc3nzQyTDIfC8q3bgmM8jDHaIKtDupyj_xw,1267
3
- mcmcli/logging.py,sha256=xjRS5ey1ONx_d34qB1Fetb_SwPysoh2hzNDuNAaYYWQ,1739
4
- mcmcli/requests.py,sha256=50N_LiWIWr8-3EPs_yR9f4uEQf8rQ68s_FoMYRhgjzI,2343
5
- mcmcli/command/auth.py,sha256=QLdr_XFW5BVw9r4a7Kjj5BTBXpSux3AWI9eI03S8aiA,2480
6
- mcmcli/command/config.py,sha256=sdzge-l_Yi2P_TlTgSLqShcGyPCzpW3QJzctpIvc-g4,4195
7
- mcmcli/command/wallet.py,sha256=TB3WrmniREsDk0ui20p3ha6OZMwA2wAJaxQ9QDGtKnA,8968
8
- mcmcli/data/error.py,sha256=d6xa_jTXumlA0EzXy2PJQ86ajBb0Pm90fss9R3LuHUc,1094
9
- mcmcli/data/token.py,sha256=11wtyLHCAZHu0LVbNDPa-yipcL6lenxoYIKEI58VzFs,1744
10
- mcmcli/data/wallet.py,sha256=HvFJsRsPNRw-n_lwm6kAKlUxQbtnft97QFkHiS3StjY,1915
11
- mcm_cli-0.45.dist-info/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
12
- mcm_cli-0.45.dist-info/METADATA,sha256=4G0mNs2xqIgMeE3GkIoDDNiFxFIrl2hSrrWDFE9zkKA,2755
13
- mcm_cli-0.45.dist-info/NOTICE,sha256=Ldnl2MjRaXPxcldUdbI2NTybq60XAa2LowRhFrRTuiI,76
14
- mcm_cli-0.45.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
15
- mcm_cli-0.45.dist-info/entry_points.txt,sha256=qTHAWZ-ejSiU4t11RYwtAU8ScqhQPDeMVTG9y4wMVLg,60
16
- mcm_cli-0.45.dist-info/top_level.txt,sha256=sh7oqIaqLQlMtKHlxHHgpV2xGMrBMPFWpSp0C6nvJ_Y,7
17
- mcm_cli-0.45.dist-info/RECORD,,