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.
- {mcm_cli-0.45.dist-info → mcm_cli-0.49.0.dist-info}/METADATA +23 -8
- mcm_cli-0.49.0.dist-info/RECORD +23 -0
- mcmcli/__main__.py +5 -3
- mcmcli/command/account.py +461 -0
- mcmcli/command/decision.py +265 -0
- mcmcli/data/account.py +59 -0
- mcmcli/data/account_user.py +50 -0
- mcmcli/data/decision.py +104 -0
- mcmcli/data/seller.py +38 -0
- mcmcli/data/wallet.py +1 -1
- mcm_cli-0.45.dist-info/RECORD +0 -17
- {mcm_cli-0.45.dist-info → mcm_cli-0.49.0.dist-info}/LICENSE +0 -0
- {mcm_cli-0.45.dist-info → mcm_cli-0.49.0.dist-info}/NOTICE +0 -0
- {mcm_cli-0.45.dist-info → mcm_cli-0.49.0.dist-info}/WHEEL +0 -0
- {mcm_cli-0.45.dist-info → mcm_cli-0.49.0.dist-info}/entry_points.txt +0 -0
- {mcm_cli-0.45.dist-info → mcm_cli-0.49.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: mcm-cli
|
3
|
-
Version: 0.
|
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
|
-
|
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
|
-
|
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
|
-
|
59
|
+
To uninstall, run:
|
50
60
|
```
|
51
61
|
pip uninstall mcm-cli
|
52
62
|
```
|
53
63
|
|
54
64
|
## How to use
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
│
|
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
|
-
|
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.
|
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
|
+
|
mcmcli/data/decision.py
ADDED
@@ -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
mcm_cli-0.45.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|