gmicloud 0.1.7__tar.gz → 0.1.10__tar.gz
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.
- {gmicloud-0.1.7 → gmicloud-0.1.10}/PKG-INFO +60 -33
- {gmicloud-0.1.7 → gmicloud-0.1.10}/README.md +59 -32
- gmicloud-0.1.10/gmicloud/_internal/_client/_auth_config.py +78 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_client/_http_client.py +7 -7
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_client/_iam_client.py +57 -38
- gmicloud-0.1.10/gmicloud/_internal/_client/_video_client.py +111 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_config.py +4 -3
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_enums.py +19 -1
- gmicloud-0.1.10/gmicloud/_internal/_exceptions.py +36 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_manager/_artifact_manager.py +27 -8
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_manager/_iam_manager.py +1 -1
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_manager/_task_manager.py +29 -0
- gmicloud-0.1.10/gmicloud/_internal/_manager/_video_manager.py +91 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_manager/serve_command_utils.py +10 -6
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_models.py +88 -3
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/client.py +15 -6
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud.egg-info/PKG-INFO +60 -33
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud.egg-info/SOURCES.txt +3 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/pyproject.toml +1 -1
- gmicloud-0.1.7/gmicloud/_internal/_exceptions.py +0 -19
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/__init__.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/__init__.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_client/__init__.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_client/_artifact_client.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_client/_decorator.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_client/_file_upload_client.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_client/_task_client.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_constants.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/_internal/_manager/__init__.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/tests/__init__.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/tests/test_artifacts.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/tests/test_tasks.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud/utils/uninstall_packages.py +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud.egg-info/dependency_links.txt +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/gmicloud.egg-info/top_level.txt +0 -0
- {gmicloud-0.1.7 → gmicloud-0.1.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: gmicloud
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.10
|
4
4
|
Summary: GMI Cloud Python SDK
|
5
5
|
Author-email: GMI <gmi@gmitec.net>
|
6
6
|
License: MIT
|
@@ -133,7 +133,22 @@ model_checkpoint_save_dir = "files/model_garden"
|
|
133
133
|
snapshot_download(repo_id=model_name, local_dir=model_checkpoint_save_dir)
|
134
134
|
```
|
135
135
|
|
136
|
-
|
136
|
+
#### Pre-downloaded models
|
137
|
+
```
|
138
|
+
"deepseek-ai/DeepSeek-R1"
|
139
|
+
"deepseek-ai/DeepSeek-V3-0324"
|
140
|
+
"deepseek-ai/DeepSeek-R1-Distill-Llama-70B"
|
141
|
+
"deepseek-ai/DeepSeek-R1-Distill-Llama-8B"
|
142
|
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B"
|
143
|
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"
|
144
|
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"
|
145
|
+
"meta-llama/Llama-3.3-70B-Instruct"
|
146
|
+
"meta-llama/Llama-4-Maverick-17B-128E-Instruct"
|
147
|
+
"meta-llama/Llama-4-Scout-17B-16E-Instruct"
|
148
|
+
"Qwen/QwQ-32B"
|
149
|
+
```
|
150
|
+
|
151
|
+
2. Find a template of specific vllm or SGLang version
|
137
152
|
|
138
153
|
```python
|
139
154
|
# export GMI_CLOUD_CLIENT_ID=<YOUR_CLIENT_ID>
|
@@ -158,55 +173,67 @@ picked_template_name = "gmi_sglang_0.4.5.post1"
|
|
158
173
|
serve_command = "python3 -m sglang.launch_server --model-path deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B --trust-remote-code --mem-fraction-static 0.8 --tp 2"
|
159
174
|
```
|
160
175
|
|
161
|
-
4. Create an artifact
|
176
|
+
4. Create an artifact. you can pass `pre_download_model` parameter. If you want custom model, upload model checkpoint to the artifactThe artifact can be reused to create inference tasks later. Artifact also suggests recommended resources for each inference server replica
|
162
177
|
|
163
178
|
```python
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
179
|
+
artifact_name = "artifact_hello_world"
|
180
|
+
artifact_id, recommended_replica_resources = cli.artifact_manager.create_artifact_for_serve_command_and_custom_model(
|
181
|
+
template_name=picked_template_name,
|
182
|
+
artifact_name=artifact_name,
|
183
|
+
serve_command=serve_command,
|
184
|
+
gpu_type="H100",
|
185
|
+
artifact_description="This is a test artifact",
|
186
|
+
pre_download_model=pick_pre_downloaded_model,
|
170
187
|
)
|
171
188
|
print(f"Created artifact {artifact_id} with recommended resources: {recommended_replica_resources}")
|
189
|
+
```
|
172
190
|
|
173
|
-
|
191
|
+
Alternatively, Upload a custom model checkpoint to artifact
|
192
|
+
```python
|
174
193
|
cli.artifact_manager.upload_model_files_to_artifact(artifact_id, model_checkpoint_save_dir)
|
194
|
+
|
195
|
+
# Maybe Wait 10 minutes for the artifact to be ready
|
196
|
+
time.sleep(10 * 60)
|
175
197
|
```
|
176
198
|
|
177
199
|
5. Create Inference task (defining min/max inference replica), start and wait
|
178
200
|
|
179
201
|
```python
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
scheduling_oneoff=OneOffScheduling(
|
190
|
-
trigger_timestamp=int(datetime.now().timestamp()),
|
191
|
-
min_replicas=1,
|
192
|
-
max_replicas=4,
|
193
|
-
)
|
194
|
-
),
|
195
|
-
),
|
196
|
-
)
|
197
|
-
task = cli.task_manager.create_task(new_task)
|
198
|
-
task_id = task.task_id
|
199
|
-
task = cli.task_manager.get_task(task_id)
|
202
|
+
# Create Task based on Artifact
|
203
|
+
new_task_id = cli.task_manager.create_task_from_artifact_id(artifact_id, recommended_replica_resources, TaskScheduling(
|
204
|
+
scheduling_oneoff=OneOffScheduling(
|
205
|
+
trigger_timestamp=int(datetime.now().timestamp()),
|
206
|
+
min_replicas=1,
|
207
|
+
max_replicas=4,
|
208
|
+
)
|
209
|
+
))
|
210
|
+
task = cli.task_manager.get_task(new_task_id)
|
200
211
|
print(f"Task created: {task.config.task_name}. You can check details at https://inference-engine.gmicloud.ai/user-console/task")
|
201
212
|
|
202
213
|
# Start Task and wait for it to be ready
|
203
|
-
cli.task_manager.start_task_and_wait(
|
214
|
+
cli.task_manager.start_task_and_wait(new_task_id)
|
204
215
|
```
|
205
216
|
|
206
|
-
6. Test with sample chat completion request
|
217
|
+
6. Test with sample chat completion request with OpenAI client
|
207
218
|
|
208
219
|
```python
|
209
|
-
|
220
|
+
pi_key = "<YOUR_API_KEY>"
|
221
|
+
endpoint_url = cli.task_manager.get_task_endpoint_url(new_task_id)
|
222
|
+
open_ai = OpenAI(
|
223
|
+
base_url=os.getenv("OPENAI_API_BASE", f"https://{endpoint_url}/serve/v1/"),
|
224
|
+
api_key=api_key
|
225
|
+
)
|
226
|
+
# Make a chat completion request using the new OpenAI client.
|
227
|
+
completion = open_ai.chat.completions.create(
|
228
|
+
model=picked_template_name,
|
229
|
+
messages=[
|
230
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
231
|
+
{"role": "user", "content": "Who are you?"},
|
232
|
+
],
|
233
|
+
max_tokens=500,
|
234
|
+
temperature=0.7
|
235
|
+
)
|
236
|
+
print(completion.choices[0].message.content)
|
210
237
|
```
|
211
238
|
|
212
239
|
|
@@ -121,7 +121,22 @@ model_checkpoint_save_dir = "files/model_garden"
|
|
121
121
|
snapshot_download(repo_id=model_name, local_dir=model_checkpoint_save_dir)
|
122
122
|
```
|
123
123
|
|
124
|
-
|
124
|
+
#### Pre-downloaded models
|
125
|
+
```
|
126
|
+
"deepseek-ai/DeepSeek-R1"
|
127
|
+
"deepseek-ai/DeepSeek-V3-0324"
|
128
|
+
"deepseek-ai/DeepSeek-R1-Distill-Llama-70B"
|
129
|
+
"deepseek-ai/DeepSeek-R1-Distill-Llama-8B"
|
130
|
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B"
|
131
|
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"
|
132
|
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"
|
133
|
+
"meta-llama/Llama-3.3-70B-Instruct"
|
134
|
+
"meta-llama/Llama-4-Maverick-17B-128E-Instruct"
|
135
|
+
"meta-llama/Llama-4-Scout-17B-16E-Instruct"
|
136
|
+
"Qwen/QwQ-32B"
|
137
|
+
```
|
138
|
+
|
139
|
+
2. Find a template of specific vllm or SGLang version
|
125
140
|
|
126
141
|
```python
|
127
142
|
# export GMI_CLOUD_CLIENT_ID=<YOUR_CLIENT_ID>
|
@@ -146,55 +161,67 @@ picked_template_name = "gmi_sglang_0.4.5.post1"
|
|
146
161
|
serve_command = "python3 -m sglang.launch_server --model-path deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B --trust-remote-code --mem-fraction-static 0.8 --tp 2"
|
147
162
|
```
|
148
163
|
|
149
|
-
4. Create an artifact
|
164
|
+
4. Create an artifact. you can pass `pre_download_model` parameter. If you want custom model, upload model checkpoint to the artifactThe artifact can be reused to create inference tasks later. Artifact also suggests recommended resources for each inference server replica
|
150
165
|
|
151
166
|
```python
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
167
|
+
artifact_name = "artifact_hello_world"
|
168
|
+
artifact_id, recommended_replica_resources = cli.artifact_manager.create_artifact_for_serve_command_and_custom_model(
|
169
|
+
template_name=picked_template_name,
|
170
|
+
artifact_name=artifact_name,
|
171
|
+
serve_command=serve_command,
|
172
|
+
gpu_type="H100",
|
173
|
+
artifact_description="This is a test artifact",
|
174
|
+
pre_download_model=pick_pre_downloaded_model,
|
158
175
|
)
|
159
176
|
print(f"Created artifact {artifact_id} with recommended resources: {recommended_replica_resources}")
|
177
|
+
```
|
160
178
|
|
161
|
-
|
179
|
+
Alternatively, Upload a custom model checkpoint to artifact
|
180
|
+
```python
|
162
181
|
cli.artifact_manager.upload_model_files_to_artifact(artifact_id, model_checkpoint_save_dir)
|
182
|
+
|
183
|
+
# Maybe Wait 10 minutes for the artifact to be ready
|
184
|
+
time.sleep(10 * 60)
|
163
185
|
```
|
164
186
|
|
165
187
|
5. Create Inference task (defining min/max inference replica), start and wait
|
166
188
|
|
167
189
|
```python
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
scheduling_oneoff=OneOffScheduling(
|
178
|
-
trigger_timestamp=int(datetime.now().timestamp()),
|
179
|
-
min_replicas=1,
|
180
|
-
max_replicas=4,
|
181
|
-
)
|
182
|
-
),
|
183
|
-
),
|
184
|
-
)
|
185
|
-
task = cli.task_manager.create_task(new_task)
|
186
|
-
task_id = task.task_id
|
187
|
-
task = cli.task_manager.get_task(task_id)
|
190
|
+
# Create Task based on Artifact
|
191
|
+
new_task_id = cli.task_manager.create_task_from_artifact_id(artifact_id, recommended_replica_resources, TaskScheduling(
|
192
|
+
scheduling_oneoff=OneOffScheduling(
|
193
|
+
trigger_timestamp=int(datetime.now().timestamp()),
|
194
|
+
min_replicas=1,
|
195
|
+
max_replicas=4,
|
196
|
+
)
|
197
|
+
))
|
198
|
+
task = cli.task_manager.get_task(new_task_id)
|
188
199
|
print(f"Task created: {task.config.task_name}. You can check details at https://inference-engine.gmicloud.ai/user-console/task")
|
189
200
|
|
190
201
|
# Start Task and wait for it to be ready
|
191
|
-
cli.task_manager.start_task_and_wait(
|
202
|
+
cli.task_manager.start_task_and_wait(new_task_id)
|
192
203
|
```
|
193
204
|
|
194
|
-
6. Test with sample chat completion request
|
205
|
+
6. Test with sample chat completion request with OpenAI client
|
195
206
|
|
196
207
|
```python
|
197
|
-
|
208
|
+
pi_key = "<YOUR_API_KEY>"
|
209
|
+
endpoint_url = cli.task_manager.get_task_endpoint_url(new_task_id)
|
210
|
+
open_ai = OpenAI(
|
211
|
+
base_url=os.getenv("OPENAI_API_BASE", f"https://{endpoint_url}/serve/v1/"),
|
212
|
+
api_key=api_key
|
213
|
+
)
|
214
|
+
# Make a chat completion request using the new OpenAI client.
|
215
|
+
completion = open_ai.chat.completions.create(
|
216
|
+
model=picked_template_name,
|
217
|
+
messages=[
|
218
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
219
|
+
{"role": "user", "content": "Who are you?"},
|
220
|
+
],
|
221
|
+
max_tokens=500,
|
222
|
+
temperature=0.7
|
223
|
+
)
|
224
|
+
print(completion.choices[0].message.content)
|
198
225
|
```
|
199
226
|
|
200
227
|
|
@@ -0,0 +1,78 @@
|
|
1
|
+
import os
|
2
|
+
import jwt
|
3
|
+
import time
|
4
|
+
import json
|
5
|
+
import logging
|
6
|
+
import threading
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
CONFIG_FILE_NAME = ".gmicloud.config.json"
|
12
|
+
|
13
|
+
# create the thread lock object56
|
14
|
+
lock = threading.Lock()
|
15
|
+
|
16
|
+
def _read_config_file()->dict|None:
|
17
|
+
"""Read the config file."""
|
18
|
+
base_dir = Path.home()
|
19
|
+
config_file_path =os.path.join(base_dir,CONFIG_FILE_NAME)
|
20
|
+
if not os.path.exists(config_file_path):
|
21
|
+
return None
|
22
|
+
with lock:
|
23
|
+
# open the config file, read mode with lock
|
24
|
+
with open(config_file_path,"r") as fr:
|
25
|
+
return json.loads(fr.read())
|
26
|
+
|
27
|
+
|
28
|
+
def _write_config_file(config_file_path:str,config_dic:dict)->None:
|
29
|
+
"""Write the config file."""
|
30
|
+
with lock:
|
31
|
+
# open the config file, write mode with lock
|
32
|
+
with open(config_file_path,"w") as fw:
|
33
|
+
# transform the config dictionary to JSON format and write it to the file
|
34
|
+
fw.write(json.dumps(config_dic))
|
35
|
+
|
36
|
+
|
37
|
+
def write_user_refresh_token_to_system_config(email:str,refresh_token:str)->bool:
|
38
|
+
"""Write the user refresh token to the system config file."""
|
39
|
+
base_dir = Path.home()
|
40
|
+
config_file_path = os.path.join(base_dir,CONFIG_FILE_NAME)
|
41
|
+
try:
|
42
|
+
# check the config file is exists. if not, create it, if yes, update the refresh token
|
43
|
+
if not os.path.exists(config_file_path):
|
44
|
+
config_dic = { email : {"refresh_token": refresh_token} }
|
45
|
+
_write_config_file(config_file_path,config_dic)
|
46
|
+
else:
|
47
|
+
config_dic = _read_config_file()
|
48
|
+
if not config_dic.get(email):
|
49
|
+
config_dic[email] = dict()
|
50
|
+
config_dic[email] = {"refresh_token": refresh_token}
|
51
|
+
_write_config_file(config_file_path,config_dic)
|
52
|
+
except Exception as e:
|
53
|
+
logger.error("write file wrong :", e)
|
54
|
+
return False
|
55
|
+
return True
|
56
|
+
|
57
|
+
|
58
|
+
def get_user_refresh_token_from_system_config(email:str)->str|None:
|
59
|
+
"""Get the user refresh token from the system config file."""
|
60
|
+
config_dic = _read_config_file()
|
61
|
+
if not config_dic or not config_dic.get(email):
|
62
|
+
return None
|
63
|
+
return config_dic[email]["refresh_token"]
|
64
|
+
|
65
|
+
|
66
|
+
def _parese_refresh_token(refresh_token:str)->dict:
|
67
|
+
"""Parse the refresh token."""
|
68
|
+
return jwt.decode(refresh_token, options={"verify_signature": False})
|
69
|
+
|
70
|
+
|
71
|
+
def is_refresh_token_expired(refresh_token:str)->bool:
|
72
|
+
"""Check the refresh token is expired. if expired, return True, else return False."""
|
73
|
+
try:
|
74
|
+
refresh_token_time = _parese_refresh_token(refresh_token)['exp']
|
75
|
+
except Exception as e:
|
76
|
+
logger.error("parse refresh token wrong :", e)
|
77
|
+
return True
|
78
|
+
return refresh_token_time < time.time()
|
@@ -57,28 +57,28 @@ class HTTPClient:
|
|
57
57
|
response = requests.request(method, url, params=params, json=data, headers=headers)
|
58
58
|
logger.debug(response.text)
|
59
59
|
if response.status_code == 401:
|
60
|
-
raise UnauthorizedError("Access token expired or invalid.")
|
60
|
+
raise UnauthorizedError(f"Unauthorized Error : {response.status_code} - Access token expired or invalid.")
|
61
61
|
elif response.status_code != 200 and response.status_code != 201:
|
62
|
-
if url.find("ie/artifact") != -1 or url.find("ie/task") != -1:
|
62
|
+
if url.find("ie/artifact") != -1 or url.find("ie/task") != -1 or url.find("ie/requestqueue") != -1:
|
63
63
|
error_message = response.json().get('error', 'Unknown error')
|
64
64
|
else:
|
65
65
|
error_message = response.json().get('message', 'Unknown error')
|
66
|
-
raise APIError(f"HTTP Request failed: {error_message}")
|
66
|
+
raise APIError(f"HTTP Request failed: {response.status_code} - {error_message}")
|
67
67
|
# Raise for HTTP errors
|
68
68
|
response.raise_for_status()
|
69
69
|
|
70
70
|
except requests.exceptions.RequestException as e:
|
71
|
-
raise APIError(f"HTTP Request failed: {str(e)}")
|
71
|
+
raise APIError(f"HTTP Request failed: {response.status_code} - {str(e)}")
|
72
72
|
except ValueError as e:
|
73
73
|
# Fallback if response JSON is invalid
|
74
|
-
raise APIError(f"Failed to parse JSON response: {response.text}")
|
74
|
+
raise APIError(f"Failed to parse JSON response: {response.status_code} - {response.text}")
|
75
75
|
|
76
76
|
if response.headers.get(CONTENT_TYPE_HEADER).find(JSON_CONTENT_TYPE) != -1:
|
77
77
|
return response.json()
|
78
78
|
elif response.headers.get(CONTENT_TYPE_HEADER).find(TEXT_CONTENT_TYPE) != -1:
|
79
|
-
raise APIError(f"Got text response: {response.text}")
|
79
|
+
raise APIError(f"Got text response: {response.status_code} - {response.text}")
|
80
80
|
else:
|
81
|
-
raise APIError(f"Unsupported content type: {response.headers.get(CONTENT_TYPE_HEADER)}")
|
81
|
+
raise APIError(f"Unsupported content type: {response.status_code} - {response.headers.get(CONTENT_TYPE_HEADER)}")
|
82
82
|
|
83
83
|
def post(self, endpoint, custom_headers=None, data=None):
|
84
84
|
"""
|
@@ -6,7 +6,11 @@ from ._http_client import HTTPClient
|
|
6
6
|
from .._config import IAM_SERVICE_BASE_URL
|
7
7
|
from .._models import *
|
8
8
|
from .._constants import CLIENT_ID_HEADER, AUTHORIZATION_HEADER
|
9
|
-
|
9
|
+
from ._auth_config import (
|
10
|
+
get_user_refresh_token_from_system_config,
|
11
|
+
write_user_refresh_token_to_system_config,
|
12
|
+
is_refresh_token_expired
|
13
|
+
)
|
10
14
|
logger = logging.getLogger(__name__)
|
11
15
|
|
12
16
|
|
@@ -38,42 +42,50 @@ class IAMClient:
|
|
38
42
|
Returns True if login is successful, otherwise False.
|
39
43
|
"""
|
40
44
|
try:
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
logger.error("Login failed: Received empty response from auth-tokens endpoint")
|
47
|
-
return False
|
48
|
-
|
49
|
-
auth_tokens_resp = AuthTokenResponse.model_validate(auth_tokens_result)
|
50
|
-
|
51
|
-
# Handle 2FA
|
52
|
-
if auth_tokens_resp.is2FARequired:
|
53
|
-
for attempt in range(3):
|
54
|
-
code = input(f"Attempt {attempt + 1}/3: Please enter the 2FA code: ")
|
55
|
-
create_session_req = CreateSessionRequest(
|
56
|
-
type="native", authToken=auth_tokens_resp.authToken, otpCode=code
|
57
|
-
)
|
58
|
-
try:
|
59
|
-
session_result = self.client.post("/me/sessions", custom_headers,
|
60
|
-
create_session_req.model_dump())
|
61
|
-
if session_result:
|
62
|
-
break
|
63
|
-
except RequestException:
|
64
|
-
logger.warning("Invalid 2FA code, please try again.")
|
65
|
-
if attempt == 2:
|
66
|
-
logger.error("Failed to create session after 3 incorrect 2FA attempts.")
|
67
|
-
return False
|
45
|
+
# Check config refresh token is available and is not expired, if yes ,refresh it
|
46
|
+
temp_refresh_token = get_user_refresh_token_from_system_config(self._email)
|
47
|
+
if temp_refresh_token and not is_refresh_token_expired(temp_refresh_token):
|
48
|
+
self._refresh_token = temp_refresh_token
|
49
|
+
self.refresh_token()
|
68
50
|
else:
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
51
|
+
custom_headers = {CLIENT_ID_HEADER: self._client_id}
|
52
|
+
req = AuthTokenRequest(email=self._email, password=self._password)
|
53
|
+
auth_tokens_result = self.client.post("/me/auth-tokens", custom_headers, req.model_dump())
|
54
|
+
|
55
|
+
if not auth_tokens_result:
|
56
|
+
logger.error("Login failed: Received empty response from auth-tokens endpoint")
|
57
|
+
return False
|
58
|
+
|
59
|
+
auth_tokens_resp = AuthTokenResponse.model_validate(auth_tokens_result)
|
60
|
+
|
61
|
+
# Handle 2FA
|
62
|
+
if auth_tokens_resp.is2FARequired:
|
63
|
+
for attempt in range(3):
|
64
|
+
code = input(f"Attempt {attempt + 1}/3: Please enter the 2FA code: ")
|
65
|
+
create_session_req = CreateSessionRequest(
|
66
|
+
type="native", authToken=auth_tokens_resp.authToken, otpCode=code
|
67
|
+
)
|
68
|
+
try:
|
69
|
+
session_result = self.client.post("/me/sessions", custom_headers,
|
70
|
+
create_session_req.model_dump())
|
71
|
+
if session_result:
|
72
|
+
break
|
73
|
+
except RequestException:
|
74
|
+
logger.warning("Invalid 2FA code, please try again.")
|
75
|
+
if attempt == 2:
|
76
|
+
logger.error("Failed to create session after 3 incorrect 2FA attempts.")
|
77
|
+
return False
|
78
|
+
else:
|
79
|
+
create_session_req = CreateSessionRequest(type="native", authToken=auth_tokens_resp.authToken,
|
80
|
+
otpCode=None)
|
81
|
+
session_result = self.client.post("/me/sessions", custom_headers, create_session_req.model_dump())
|
82
|
+
|
83
|
+
create_session_resp = CreateSessionResponse.model_validate(session_result)
|
84
|
+
|
85
|
+
self._access_token = create_session_resp.accessToken
|
86
|
+
self._refresh_token = create_session_resp.refreshToken
|
87
|
+
# first login write refresh token to system config
|
88
|
+
write_user_refresh_token_to_system_config(self._email,self._refresh_token)
|
77
89
|
self._user_id = self.parse_user_id()
|
78
90
|
|
79
91
|
# Fetch profile to get organization ID
|
@@ -96,7 +108,12 @@ class IAMClient:
|
|
96
108
|
"""
|
97
109
|
try:
|
98
110
|
custom_headers = {CLIENT_ID_HEADER: self._client_id}
|
99
|
-
|
111
|
+
try:
|
112
|
+
result = self.client.patch("/me/sessions", custom_headers, {"refreshToken": self._refresh_token})
|
113
|
+
except Exception as err:
|
114
|
+
logger.error(f"{str(err)}, please re-login.")
|
115
|
+
write_user_refresh_token_to_system_config(self._email,"")
|
116
|
+
return False
|
100
117
|
|
101
118
|
if not result:
|
102
119
|
logger.error("Failed to refresh token: Empty response received")
|
@@ -105,7 +122,9 @@ class IAMClient:
|
|
105
122
|
resp = CreateSessionResponse.model_validate(result)
|
106
123
|
self._access_token = resp.accessToken
|
107
124
|
self._refresh_token = resp.refreshToken
|
108
|
-
|
125
|
+
# the _refresh_token will be updated when call this function
|
126
|
+
# so write it to system config file for update the _refresh_token expired time
|
127
|
+
write_user_refresh_token_to_system_config(self._email,self._refresh_token)
|
109
128
|
return True
|
110
129
|
except (RequestException, ValueError) as e:
|
111
130
|
logger.error(f"Token refresh failed: {e}")
|
@@ -0,0 +1,111 @@
|
|
1
|
+
|
2
|
+
import logging
|
3
|
+
from requests.exceptions import RequestException
|
4
|
+
|
5
|
+
from ._http_client import HTTPClient
|
6
|
+
from ._decorator import handle_refresh_token
|
7
|
+
from ._iam_client import IAMClient
|
8
|
+
from .._config import IAM_SERVICE_BASE_URL
|
9
|
+
from .._models import *
|
10
|
+
from .._exceptions import formated_exception
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class VideoClient:
|
16
|
+
"""
|
17
|
+
A client for interacting with the video service API.
|
18
|
+
|
19
|
+
This client provides methods to retrieve, create, update, and stop video tasks
|
20
|
+
through HTTP calls to the video service.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, iam_client: IAMClient):
|
24
|
+
"""
|
25
|
+
Initializes the VideoClient with the given base URL for the video service.
|
26
|
+
"""
|
27
|
+
self.client = HTTPClient(IAM_SERVICE_BASE_URL+ "/ie/requestqueue")
|
28
|
+
self.iam_client = iam_client
|
29
|
+
|
30
|
+
|
31
|
+
@handle_refresh_token
|
32
|
+
def get_request_detail(self, request_id: str) -> GetRequestResponse | dict:
|
33
|
+
"""
|
34
|
+
Retrieves detailed information about a specific request by its ID. This endpoint requires authentication with a bearer token and only returns requests belonging to the authenticated organization.
|
35
|
+
|
36
|
+
:param request_id: The ID of the request to be retrieved.
|
37
|
+
:return: Details of the GetRequestResponse successfully retrieved
|
38
|
+
"""
|
39
|
+
try:
|
40
|
+
response = self.client.get(f"/requests/{request_id}", self.iam_client.get_custom_headers())
|
41
|
+
return GetRequestResponse.model_validate(response) if response else None
|
42
|
+
except Exception as e:
|
43
|
+
logger.error(f"An unexpected error occurred while retrieving request details for {request_id}: {e}")
|
44
|
+
return formated_exception(e)
|
45
|
+
|
46
|
+
|
47
|
+
@handle_refresh_token
|
48
|
+
def get_requests(self, model_id: str) -> List[GetRequestResponse] | dict:
|
49
|
+
"""
|
50
|
+
Retrieves a list of requests submitted by the authenticated user for a specific model. This endpoint requires authentication with a bearer token and filters results by the authenticated organization.
|
51
|
+
|
52
|
+
:param model_id: The ID of the model to be retrieved.
|
53
|
+
:return: List of GetRequestResponse successfully retrieved
|
54
|
+
"""
|
55
|
+
try:
|
56
|
+
response = self.client.get("/requests", self.iam_client.get_custom_headers(), {"model_id": model_id})
|
57
|
+
requests = response.get('requests', []) if response else []
|
58
|
+
return [GetRequestResponse.model_validate(req) for req in requests] if requests else None
|
59
|
+
except Exception as e:
|
60
|
+
logger.error(f"An unexpected error occurred while retrieving requests for model {model_id}: {e}")
|
61
|
+
return formated_exception(e)
|
62
|
+
|
63
|
+
|
64
|
+
@handle_refresh_token
|
65
|
+
def create_request(self, request: SubmitRequestRequest) -> SubmitRequestResponse | dict:
|
66
|
+
"""
|
67
|
+
Submits a new asynchronous request to process a specified model with provided parameters. This endpoint requires authentication with a bearer token.
|
68
|
+
|
69
|
+
:param request: The request data to be created by SubmitRequestRequest model.
|
70
|
+
:return: The created request data as SubmitRequestResponse model.
|
71
|
+
"""
|
72
|
+
try:
|
73
|
+
response = self.client.post("/requests", self.iam_client.get_custom_headers(), request.model_dump())
|
74
|
+
return SubmitRequestResponse.model_validate(response) if response else None
|
75
|
+
except Exception as e:
|
76
|
+
logger.error(f"An unexpected error occurred while creating a request: {e}")
|
77
|
+
return formated_exception(e)
|
78
|
+
|
79
|
+
|
80
|
+
@handle_refresh_token
|
81
|
+
def get_model_detail(self, model_id: str) -> GetModelResponse | dict:
|
82
|
+
"""
|
83
|
+
Retrieves detailed information about a specific model by its ID.
|
84
|
+
|
85
|
+
:param model_id: The ID of the model to be retrieved.
|
86
|
+
:return: Details of the GetModelResponse model successfully retrieved.
|
87
|
+
"""
|
88
|
+
try:
|
89
|
+
response = self.client.get(f"/models/{model_id}", self.iam_client.get_custom_headers())
|
90
|
+
return GetModelResponse.model_validate(response) if response else None
|
91
|
+
except Exception as e:
|
92
|
+
logger.error(f"An unexpected error occurred while retrieving model details for {model_id}: {e}")
|
93
|
+
return formated_exception(e)
|
94
|
+
|
95
|
+
|
96
|
+
@handle_refresh_token
|
97
|
+
def get_models(self) -> List[GetModelResponse] | dict:
|
98
|
+
"""
|
99
|
+
Retrieves a list of available models from the video service.
|
100
|
+
|
101
|
+
:return: A list of GetModelResponse model successfully retrieved.
|
102
|
+
"""
|
103
|
+
try:
|
104
|
+
response = self.client.get("/models", self.iam_client.get_custom_headers())
|
105
|
+
models = response.get('models', []) if response else []
|
106
|
+
return [GetModelResponse.model_validate(model) for model in models] if models else None
|
107
|
+
except Exception as e:
|
108
|
+
logger.error(f"An unexpected error occurred while retrieving models: {e}")
|
109
|
+
return formated_exception(e)
|
110
|
+
|
111
|
+
|
@@ -3,7 +3,8 @@
|
|
3
3
|
# TASK_SERVICE_BASE_URL = "https://ce-tot.gmicloud-dev.com/api/v1/ie/task"
|
4
4
|
# IAM_SERVICE_BASE_URL = "https://ce-tot.gmicloud-dev.com/api/v1"
|
5
5
|
|
6
|
+
|
6
7
|
# Prod environment
|
7
|
-
ARTIFACT_SERVICE_BASE_URL = "https://
|
8
|
-
TASK_SERVICE_BASE_URL = "https://
|
9
|
-
IAM_SERVICE_BASE_URL = "https://
|
8
|
+
ARTIFACT_SERVICE_BASE_URL = "https://console.gmicloud.ai/api/v1/ie/artifact"
|
9
|
+
TASK_SERVICE_BASE_URL = "https://console.gmicloud.ai/api/v1/ie/task"
|
10
|
+
IAM_SERVICE_BASE_URL = "https://console.gmicloud.ai/api/v1"
|
@@ -24,6 +24,7 @@ class TaskEndpointStatus(str, Enum):
|
|
24
24
|
UNREADY = "unready"
|
25
25
|
NEW = "new"
|
26
26
|
|
27
|
+
|
27
28
|
class TaskStatus(str, Enum):
|
28
29
|
IDLE = "idle"
|
29
30
|
STARTING = "starting"
|
@@ -32,7 +33,24 @@ class TaskStatus(str, Enum):
|
|
32
33
|
NEEDSTOP = "needstop"
|
33
34
|
ARCHIVED = "archived"
|
34
35
|
|
36
|
+
|
35
37
|
class ModelParameterType(str, Enum):
|
36
38
|
NUMERIC = "numeric"
|
37
39
|
TEXT = "text"
|
38
|
-
BOOLEAN = "boolean"
|
40
|
+
BOOLEAN = "boolean"
|
41
|
+
|
42
|
+
|
43
|
+
class RequestStatus(Enum):
|
44
|
+
CREATED = "created"
|
45
|
+
QUEUED = "queued"
|
46
|
+
DISPATCHED = "dispatched"
|
47
|
+
PROCESSING = "processing"
|
48
|
+
SUCCESS = "success"
|
49
|
+
FAILED = "failed"
|
50
|
+
CANCELLED = "cancelled"
|
51
|
+
|
52
|
+
|
53
|
+
class HostType(Enum):
|
54
|
+
DEFAULT = ""
|
55
|
+
INTERNAL = "internal"
|
56
|
+
EXTERNAL = "external"
|