@tikomni/skills 0.1.8 → 0.1.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikomni/skills",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "TikOmni skill installer CLI for structured social media crawling in Codex, Claude Code, and OpenClaw",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/mark-ly-wang/TikOmni-Skills#readme",
@@ -257,6 +257,13 @@ def _resolve_timeout_retry_backoff_ms() -> int:
257
257
  return max(0, min(backoff, 5000))
258
258
 
259
259
 
260
+ def resolve_timeout_retry_policy() -> Dict[str, int]:
261
+ return {
262
+ "max_retries": _resolve_timeout_retry_max(),
263
+ "backoff_ms": _resolve_timeout_retry_backoff_ms(),
264
+ }
265
+
266
+
260
267
  def _wait_rate_limit_slot(qps: float) -> int:
261
268
  global _NEXT_ALLOWED_TS
262
269
  interval_sec = 1.0 / max(qps, 0.1)
@@ -5,18 +5,26 @@ from __future__ import annotations
5
5
 
6
6
  import mimetypes
7
7
  import os
8
+ import socket
8
9
  import tempfile
10
+ import time
9
11
  import urllib.error
10
12
  import urllib.parse
11
13
  import urllib.request
12
14
  from pathlib import Path
13
- from typing import Any, Dict, Optional
15
+ from typing import Any, Dict, List, Optional
14
16
 
15
- from scripts.core.tikomni_common import DEFAULT_USER_AGENT, call_json_api, normalize_text
17
+ from scripts.core.tikomni_common import (
18
+ DEFAULT_USER_AGENT,
19
+ call_json_api,
20
+ normalize_text,
21
+ resolve_timeout_retry_policy,
22
+ )
16
23
 
17
24
  DEFAULT_U3_PROVIDER = "oss"
18
25
  DEFAULT_CONTENT_TYPE = "video/mp4"
19
26
  DOWNLOAD_CHUNK_SIZE = 1024 * 1024
27
+ TIMEOUT_LIKE_HTTP_STATUS_CODES = {408, 429, 502, 503, 504}
20
28
 
21
29
 
22
30
  def _safe_name_from_url(source_url: str) -> str:
@@ -135,6 +143,16 @@ def create_u3_upload(
135
143
  )
136
144
 
137
145
 
146
+ def _is_timeout_like_upload_error(status_code: Optional[int], error_reason: Optional[str]) -> bool:
147
+ if isinstance(status_code, (int, float)) and int(status_code) in TIMEOUT_LIKE_HTTP_STATUS_CODES:
148
+ return True
149
+
150
+ reason = str(error_reason or "").strip().lower()
151
+ if not reason:
152
+ return False
153
+ return any(token in reason for token in ("timeout", "timed out", "deadline exceeded"))
154
+
155
+
138
156
  def upload_file_to_presigned_url(
139
157
  *,
140
158
  upload_url: str,
@@ -147,35 +165,130 @@ def upload_file_to_presigned_url(
147
165
  try:
148
166
  with open(file_path, "rb") as handle:
149
167
  data = handle.read()
150
-
151
- headers = {
152
- "Content-Type": content_type or DEFAULT_CONTENT_TYPE,
153
- "User-Agent": os.getenv("TIKOMNI_HTTP_USER_AGENT", DEFAULT_USER_AGENT),
168
+ except Exception as error:
169
+ return {
170
+ "ok": False,
171
+ "status_code": None,
172
+ "error_reason": f"u3_upload_failed:{normalize_text(error)}",
173
+ "retry_attempt": 0,
174
+ "timeout_retry_max": 0,
175
+ "timeout_retry_exhausted": False,
176
+ "retry_chain": [],
154
177
  }
155
- if isinstance(upload_headers, dict):
156
- for key, value in upload_headers.items():
157
- header_key = str(key).strip()
158
- if not header_key:
159
- continue
160
- headers[header_key] = str(value)
161
-
162
- request = urllib.request.Request(
163
- upload_url,
164
- data=data,
165
- headers=headers,
166
- method=(upload_method or "PUT").upper(),
178
+
179
+ headers = {
180
+ "Content-Type": content_type or DEFAULT_CONTENT_TYPE,
181
+ "User-Agent": os.getenv("TIKOMNI_HTTP_USER_AGENT", DEFAULT_USER_AGENT),
182
+ }
183
+ if isinstance(upload_headers, dict):
184
+ for key, value in upload_headers.items():
185
+ header_key = str(key).strip()
186
+ if not header_key:
187
+ continue
188
+ headers[header_key] = str(value)
189
+
190
+ retry_policy = resolve_timeout_retry_policy()
191
+ timeout_retry_max = int(retry_policy.get("max_retries", 0) or 0)
192
+ retry_backoff_ms = int(retry_policy.get("backoff_ms", 0) or 0)
193
+ max_attempts = 1 + timeout_retry_max
194
+ retry_chain: List[Dict[str, Any]] = []
195
+ last_result: Dict[str, Any] = {
196
+ "ok": False,
197
+ "status_code": None,
198
+ "error_reason": "u3_upload_failed:unknown",
199
+ }
200
+
201
+ for attempt in range(1, max_attempts + 1):
202
+ if attempt > 1 and retry_backoff_ms > 0:
203
+ sleep_ms = retry_backoff_ms * (2 ** (attempt - 2))
204
+ time.sleep(sleep_ms / 1000.0)
205
+
206
+ try:
207
+ request = urllib.request.Request(
208
+ upload_url,
209
+ data=data,
210
+ headers=headers,
211
+ method=(upload_method or "PUT").upper(),
212
+ )
213
+ with urllib.request.urlopen(request, timeout=max(timeout_ms / 1000.0, 1.0)) as response:
214
+ status_code = response.getcode()
215
+ result: Dict[str, Any] = {
216
+ "ok": 200 <= int(status_code) < 300,
217
+ "status_code": status_code,
218
+ "error_reason": None if 200 <= int(status_code) < 300 else f"u3_upload_http_{status_code}",
219
+ }
220
+ except urllib.error.HTTPError as error:
221
+ result = {
222
+ "ok": False,
223
+ "status_code": error.code,
224
+ "error_reason": f"u3_upload_http_{error.code}",
225
+ }
226
+ except urllib.error.URLError as error:
227
+ reason_obj = getattr(error, "reason", error)
228
+ reason_text = normalize_text(reason_obj)
229
+ result = {
230
+ "ok": False,
231
+ "status_code": None,
232
+ "error_reason": f"u3_upload_failed:{reason_text or 'network_error'}",
233
+ "_timeout_like": isinstance(reason_obj, socket.timeout)
234
+ or _is_timeout_like_upload_error(status_code=None, error_reason=reason_text),
235
+ }
236
+ except (TimeoutError, socket.timeout) as error:
237
+ result = {
238
+ "ok": False,
239
+ "status_code": None,
240
+ "error_reason": f"u3_upload_failed:{normalize_text(error) or 'timeout'}",
241
+ "_timeout_like": True,
242
+ }
243
+ except Exception as error:
244
+ reason_text = normalize_text(error)
245
+ result = {
246
+ "ok": False,
247
+ "status_code": None,
248
+ "error_reason": f"u3_upload_failed:{reason_text or 'unknown'}",
249
+ "_timeout_like": _is_timeout_like_upload_error(status_code=None, error_reason=reason_text),
250
+ }
251
+
252
+ if result.get("ok"):
253
+ result["retry_attempt"] = max(0, attempt - 1)
254
+ result["timeout_retry_max"] = timeout_retry_max
255
+ result["timeout_retry_exhausted"] = False
256
+ result["retry_chain"] = retry_chain
257
+ return result
258
+
259
+ timeout_like = bool(
260
+ result.pop(
261
+ "_timeout_like",
262
+ _is_timeout_like_upload_error(
263
+ status_code=result.get("status_code"),
264
+ error_reason=result.get("error_reason"),
265
+ ),
266
+ )
167
267
  )
168
- with urllib.request.urlopen(request, timeout=max(timeout_ms / 1000.0, 1.0)) as response:
169
- status_code = response.getcode()
170
- return {
171
- "ok": 200 <= int(status_code) < 300,
172
- "status_code": status_code,
173
- "error_reason": None if 200 <= int(status_code) < 300 else f"u3_upload_http_{status_code}",
268
+ retry_chain.append(
269
+ {
270
+ "attempt": attempt,
271
+ "status_code": result.get("status_code"),
272
+ "error_reason": result.get("error_reason"),
273
+ "timeout_like": timeout_like,
174
274
  }
175
- except urllib.error.HTTPError as error:
176
- return {"ok": False, "status_code": error.code, "error_reason": f"u3_upload_http_{error.code}"}
177
- except Exception as error:
178
- return {"ok": False, "status_code": None, "error_reason": f"u3_upload_failed:{normalize_text(error)}"}
275
+ )
276
+ last_result = dict(result)
277
+
278
+ if timeout_like and attempt < max_attempts:
279
+ continue
280
+
281
+ last_result["retry_attempt"] = max(0, attempt - 1)
282
+ last_result["timeout_retry_max"] = timeout_retry_max
283
+ last_result["timeout_retry_exhausted"] = bool(timeout_like and attempt >= max_attempts)
284
+ last_result["retry_chain"] = retry_chain
285
+ return last_result
286
+
287
+ last_result["retry_attempt"] = timeout_retry_max
288
+ last_result["timeout_retry_max"] = timeout_retry_max
289
+ last_result["timeout_retry_exhausted"] = True
290
+ last_result["retry_chain"] = retry_chain
291
+ return last_result
179
292
 
180
293
 
181
294
  def complete_u3_upload(
@@ -284,6 +397,11 @@ def run_u3_public_url_fallback(
284
397
  "ok": bool(upload_response.get("ok")),
285
398
  "status_code": upload_response.get("status_code"),
286
399
  "error_reason": upload_response.get("error_reason"),
400
+ "retry_attempt": upload_response.get("retry_attempt", 0),
401
+ "retry_count": len(upload_response.get("retry_chain") or []),
402
+ "timeout_retry_max": upload_response.get("timeout_retry_max", 0),
403
+ "timeout_retry_exhausted": bool(upload_response.get("timeout_retry_exhausted")),
404
+ "retry_chain": upload_response.get("retry_chain") or [],
287
405
  }
288
406
  )
289
407
  if not upload_response.get("ok"):