mcm-cli 0.45__py3-none-any.whl → 0.471__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.471.dist-info}/METADATA +22 -8
- mcm_cli-0.471.dist-info/RECORD +21 -0
- mcmcli/__main__.py +3 -3
- mcmcli/command/account.py +461 -0
- mcmcli/data/account.py +59 -0
- mcmcli/data/account_user.py +50 -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.471.dist-info}/LICENSE +0 -0
- {mcm_cli-0.45.dist-info → mcm_cli-0.471.dist-info}/NOTICE +0 -0
- {mcm_cli-0.45.dist-info → mcm_cli-0.471.dist-info}/WHEEL +0 -0
- {mcm_cli-0.45.dist-info → mcm_cli-0.471.dist-info}/entry_points.txt +0 -0
- {mcm_cli-0.45.dist-info → mcm_cli-0.471.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.471
|
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,10 @@ $ 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
|
+
│ version Show the tool version │
|
71
85
|
│ wallet Wallet management │
|
72
86
|
╰──────────────────────────────────────────────────────────────────╯
|
73
87
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
mcmcli/__init__.py,sha256=-U6lMZ9_99IXAKwnqnYXYr6NcO6TSmG-kxewgvJjU4k,575
|
2
|
+
mcmcli/__main__.py,sha256=XCMJhGalbQkBsQrBBEcI_CN-3uPuMLdWYT6I8ROyITM,1357
|
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/wallet.py,sha256=TB3WrmniREsDk0ui20p3ha6OZMwA2wAJaxQ9QDGtKnA,8968
|
9
|
+
mcmcli/data/account.py,sha256=pe7tPapP6vlUD5D5L5Nh5k2bkWdYOK01Mpt5fBYFnJs,1782
|
10
|
+
mcmcli/data/account_user.py,sha256=27nQp52nMma5a3QszSJGqgq5Z0ivIb-nMZcZuhEgbEg,1328
|
11
|
+
mcmcli/data/error.py,sha256=d6xa_jTXumlA0EzXy2PJQ86ajBb0Pm90fss9R3LuHUc,1094
|
12
|
+
mcmcli/data/seller.py,sha256=40SA7QekM3a3svDrDYLo_QYJ68VUxDO0KeGejJMp4k4,1004
|
13
|
+
mcmcli/data/token.py,sha256=11wtyLHCAZHu0LVbNDPa-yipcL6lenxoYIKEI58VzFs,1744
|
14
|
+
mcmcli/data/wallet.py,sha256=W-CksF9SPqiv3jZg07Wy8ehVUP5Ot1Gbq2LEGNQCOC8,1906
|
15
|
+
mcm_cli-0.471.dist-info/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
|
16
|
+
mcm_cli-0.471.dist-info/METADATA,sha256=vWG49igEdd1cL-zRp4QbUOVb02qpsEQUwQfhFdpTWfI,2945
|
17
|
+
mcm_cli-0.471.dist-info/NOTICE,sha256=Ldnl2MjRaXPxcldUdbI2NTybq60XAa2LowRhFrRTuiI,76
|
18
|
+
mcm_cli-0.471.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
|
19
|
+
mcm_cli-0.471.dist-info/entry_points.txt,sha256=qTHAWZ-ejSiU4t11RYwtAU8ScqhQPDeMVTG9y4wMVLg,60
|
20
|
+
mcm_cli-0.471.dist-info/top_level.txt,sha256=sh7oqIaqLQlMtKHlxHHgpV2xGMrBMPFWpSp0C6nvJ_Y,7
|
21
|
+
mcm_cli-0.471.dist-info/RECORD,,
|
mcmcli/__main__.py
CHANGED
@@ -17,8 +17,7 @@
|
|
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
|
24
23
|
import mcmcli.command.wallet
|
@@ -32,8 +31,9 @@ def version():
|
|
32
31
|
"""
|
33
32
|
Show the tool version
|
34
33
|
"""
|
35
|
-
typer.echo(f"Version: mcm-cli v0.
|
34
|
+
typer.echo(f"Version: mcm-cli v0.471")
|
36
35
|
|
36
|
+
app.add_typer(mcmcli.command.account.app, name="account", help="Ad account management")
|
37
37
|
app.add_typer(mcmcli.command.auth.app, name="auth", help="Authentication management")
|
38
38
|
app.add_typer(mcmcli.command.config.app, name="config", help="Configurations")
|
39
39
|
app.add_typer(mcmcli.command.wallet.app, name="wallet", help="Wallet management")
|
@@ -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
|
+
|
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/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
|