mcm-cli 0.45__py3-none-any.whl → 0.49__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {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
|