Spaces:
Running
Running
Update api_executor.py
Browse files- api_executor.py +155 -66
api_executor.py
CHANGED
@@ -8,6 +8,10 @@ from typing import Any, Dict, Optional, Union
|
|
8 |
from logger import log_info, log_error, log_warning, log_debug
|
9 |
from config_provider import ConfigProvider, APIConfig
|
10 |
from session import Session
|
|
|
|
|
|
|
|
|
11 |
|
12 |
_placeholder = re.compile(r"\{\{\s*([^\}]+?)\s*\}\}")
|
13 |
|
@@ -213,13 +217,17 @@ def _ensure_token(api: APIConfig, session: Session) -> None:
|
|
213 |
_fetch_token(api, session)
|
214 |
|
215 |
def call_api(api: APIConfig, session: Session) -> requests.Response:
|
216 |
-
"""Execute API call with automatic token management"""
|
|
|
217 |
# Ensure valid token
|
218 |
_ensure_token(api, session)
|
219 |
|
220 |
# Prepare request
|
221 |
-
headers = _render(api.headers, session, api.name)
|
222 |
-
body = _render_json(api.body_template, session, api.name)
|
|
|
|
|
|
|
223 |
|
224 |
# Handle proxy
|
225 |
proxies = None
|
@@ -235,7 +243,8 @@ def call_api(api: APIConfig, session: Session) -> requests.Response:
|
|
235 |
"method": api.method,
|
236 |
"url": str(api.url),
|
237 |
"headers": headers,
|
238 |
-
"timeout":
|
|
|
239 |
}
|
240 |
|
241 |
# Add body based on method
|
@@ -254,84 +263,164 @@ def call_api(api: APIConfig, session: Session) -> requests.Response:
|
|
254 |
|
255 |
for attempt in range(retry_count + 1):
|
256 |
try:
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
294 |
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
elif var_type == 'float':
|
300 |
-
value = float(value)
|
301 |
-
elif var_type == 'bool':
|
302 |
-
value = bool(value)
|
303 |
-
elif var_type == 'date':
|
304 |
-
# ISO format'ta sakla
|
305 |
-
value = str(value)
|
306 |
-
else: # str
|
307 |
-
value = str(value)
|
308 |
|
309 |
-
|
310 |
-
|
311 |
-
log_info(f"๐ Mapped response value: {var_name} = {value}")
|
312 |
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
318 |
except requests.exceptions.Timeout as e:
|
319 |
last_error = e
|
320 |
-
log_warning(
|
|
|
|
|
|
|
|
|
|
|
321 |
|
322 |
except requests.exceptions.RequestException as e:
|
323 |
last_error = e
|
324 |
-
log_error(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
326 |
# Retry backoff
|
327 |
if attempt < retry_count:
|
328 |
backoff = api.retry.backoff_seconds if api.retry else 2
|
329 |
if api.retry and api.retry.strategy == "exponential":
|
330 |
backoff = backoff * (2 ** attempt)
|
331 |
-
log_info(f"โณ
|
332 |
time.sleep(backoff)
|
333 |
|
334 |
# All retries failed
|
|
|
|
|
|
|
335 |
if last_error:
|
336 |
raise last_error
|
337 |
-
raise requests.exceptions.RequestException(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
from logger import log_info, log_error, log_warning, log_debug
|
9 |
from config_provider import ConfigProvider, APIConfig
|
10 |
from session import Session
|
11 |
+
import os
|
12 |
+
|
13 |
+
MAX_RESPONSE_SIZE = 10 * 1024 * 1024 # 10MB
|
14 |
+
DEFAULT_TIMEOUT = int(os.getenv("API_TIMEOUT_SECONDS", "30"))
|
15 |
|
16 |
_placeholder = re.compile(r"\{\{\s*([^\}]+?)\s*\}\}")
|
17 |
|
|
|
217 |
_fetch_token(api, session)
|
218 |
|
219 |
def call_api(api: APIConfig, session: Session) -> requests.Response:
|
220 |
+
"""Execute API call with automatic token management and better error handling"""
|
221 |
+
|
222 |
# Ensure valid token
|
223 |
_ensure_token(api, session)
|
224 |
|
225 |
# Prepare request
|
226 |
+
headers = _render(api.headers, session, api.name)
|
227 |
+
body = _render_json(api.body_template, session, api.name)
|
228 |
+
|
229 |
+
# Get timeout with fallback
|
230 |
+
timeout = api.timeout_seconds if api.timeout_seconds else DEFAULT_TIMEOUT
|
231 |
|
232 |
# Handle proxy
|
233 |
proxies = None
|
|
|
243 |
"method": api.method,
|
244 |
"url": str(api.url),
|
245 |
"headers": headers,
|
246 |
+
"timeout": timeout, # Use configured timeout
|
247 |
+
"stream": True # Enable streaming for large responses
|
248 |
}
|
249 |
|
250 |
# Add body based on method
|
|
|
263 |
|
264 |
for attempt in range(retry_count + 1):
|
265 |
try:
|
266 |
+
# Use LogTimer for performance tracking
|
267 |
+
with LogTimer(f"API call {api.name}", attempt=attempt + 1):
|
268 |
+
log_info(
|
269 |
+
f"๐ API call starting",
|
270 |
+
api=api.name,
|
271 |
+
method=api.method,
|
272 |
+
url=api.url,
|
273 |
+
attempt=f"{attempt + 1}/{retry_count + 1}",
|
274 |
+
timeout=timeout
|
275 |
+
)
|
276 |
+
|
277 |
+
if body:
|
278 |
+
log_debug(f"๐ Request body", body=json.dumps(body, ensure_ascii=False)[:500])
|
279 |
+
|
280 |
+
# Make request with streaming
|
281 |
+
response = requests.request(**request_params)
|
282 |
+
|
283 |
+
# Check response size from headers
|
284 |
+
content_length = response.headers.get('content-length')
|
285 |
+
if content_length and int(content_length) > MAX_RESPONSE_SIZE:
|
286 |
+
response.close()
|
287 |
+
raise ValueError(f"Response too large: {int(content_length)} bytes (max: {MAX_RESPONSE_SIZE})")
|
288 |
+
|
289 |
+
# Handle 401 Unauthorized
|
290 |
+
if response.status_code == 401 and api.auth and api.auth.enabled and attempt < retry_count:
|
291 |
+
log_warning(f"๐ Got 401, refreshing token", api=api.name)
|
292 |
+
_fetch_token(api, session)
|
293 |
+
headers = _render(api.headers, session, api.name)
|
294 |
+
request_params["headers"] = headers
|
295 |
+
response.close()
|
296 |
+
continue
|
297 |
+
|
298 |
+
# Read response with size limit
|
299 |
+
content_size = 0
|
300 |
+
chunks = []
|
301 |
+
|
302 |
+
for chunk in response.iter_content(chunk_size=8192):
|
303 |
+
chunks.append(chunk)
|
304 |
+
content_size += len(chunk)
|
305 |
+
|
306 |
+
if content_size > MAX_RESPONSE_SIZE:
|
307 |
+
response.close()
|
308 |
+
raise ValueError(f"Response exceeded size limit: {content_size} bytes")
|
309 |
+
|
310 |
+
# Reconstruct response content
|
311 |
+
response._content = b''.join(chunks)
|
312 |
+
response._content_consumed = True
|
313 |
+
|
314 |
+
# Check status
|
315 |
+
response.raise_for_status()
|
316 |
+
|
317 |
+
log_info(
|
318 |
+
f"โ
API call successful",
|
319 |
+
api=api.name,
|
320 |
+
status_code=response.status_code,
|
321 |
+
response_size=content_size,
|
322 |
+
duration_ms=f"{response.elapsed.total_seconds() * 1000:.2f}"
|
323 |
+
)
|
324 |
+
|
325 |
+
# Mevcut response mapping iลlemi korunacak
|
326 |
+
if response.status_code in (200, 201, 202, 204) and hasattr(api, 'response_mappings') and api.response_mappings:
|
327 |
+
try:
|
328 |
+
if response.status_code != 204 and response.content:
|
329 |
+
response_json = response.json()
|
330 |
|
331 |
+
for mapping in api.response_mappings:
|
332 |
+
var_name = mapping.get('variable_name')
|
333 |
+
var_type = mapping.get('type', 'str')
|
334 |
+
json_path = mapping.get('json_path')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
335 |
|
336 |
+
if not all([var_name, json_path]):
|
337 |
+
continue
|
|
|
338 |
|
339 |
+
# JSON path'ten deฤeri al
|
340 |
+
value = response_json
|
341 |
+
for path_part in json_path.split('.'):
|
342 |
+
if isinstance(value, dict):
|
343 |
+
value = value.get(path_part)
|
344 |
+
if value is None:
|
345 |
+
break
|
346 |
+
|
347 |
+
if value is not None:
|
348 |
+
# Type conversion
|
349 |
+
if var_type == 'int':
|
350 |
+
value = int(value)
|
351 |
+
elif var_type == 'float':
|
352 |
+
value = float(value)
|
353 |
+
elif var_type == 'bool':
|
354 |
+
value = bool(value)
|
355 |
+
elif var_type == 'date':
|
356 |
+
value = str(value)
|
357 |
+
else: # str
|
358 |
+
value = str(value)
|
359 |
+
|
360 |
+
# Session'a kaydet
|
361 |
+
session.variables[var_name] = value
|
362 |
+
log_info(f"๐ Mapped response", variable=var_name, value=value)
|
363 |
+
|
364 |
+
except Exception as e:
|
365 |
+
log_error("โ ๏ธ Response mapping error", error=str(e))
|
366 |
+
|
367 |
+
return response
|
368 |
+
|
369 |
except requests.exceptions.Timeout as e:
|
370 |
last_error = e
|
371 |
+
log_warning(
|
372 |
+
f"โฑ๏ธ API timeout",
|
373 |
+
api=api.name,
|
374 |
+
attempt=attempt + 1,
|
375 |
+
timeout=timeout
|
376 |
+
)
|
377 |
|
378 |
except requests.exceptions.RequestException as e:
|
379 |
last_error = e
|
380 |
+
log_error(
|
381 |
+
f"โ API request error",
|
382 |
+
api=api.name,
|
383 |
+
error=str(e),
|
384 |
+
attempt=attempt + 1
|
385 |
+
)
|
386 |
+
|
387 |
+
except ValueError as e: # Size limit exceeded
|
388 |
+
log_error(
|
389 |
+
f"โ Response size error",
|
390 |
+
api=api.name,
|
391 |
+
error=str(e)
|
392 |
+
)
|
393 |
+
raise # Don't retry for size errors
|
394 |
|
395 |
+
except Exception as e:
|
396 |
+
last_error = e
|
397 |
+
log_error(
|
398 |
+
f"โ Unexpected API error",
|
399 |
+
api=api.name,
|
400 |
+
error=str(e),
|
401 |
+
attempt=attempt + 1
|
402 |
+
)
|
403 |
+
|
404 |
# Retry backoff
|
405 |
if attempt < retry_count:
|
406 |
backoff = api.retry.backoff_seconds if api.retry else 2
|
407 |
if api.retry and api.retry.strategy == "exponential":
|
408 |
backoff = backoff * (2 ** attempt)
|
409 |
+
log_info(f"โณ Retry backoff", wait_seconds=backoff, next_attempt=attempt + 2)
|
410 |
time.sleep(backoff)
|
411 |
|
412 |
# All retries failed
|
413 |
+
error_msg = f"API call failed after {retry_count + 1} attempts"
|
414 |
+
log_error(error_msg, api=api.name, last_error=str(last_error))
|
415 |
+
|
416 |
if last_error:
|
417 |
raise last_error
|
418 |
+
raise requests.exceptions.RequestException(error_msg)
|
419 |
+
|
420 |
+
def format_size(size_bytes: int) -> str:
|
421 |
+
"""Format bytes to human readable format"""
|
422 |
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
423 |
+
if size_bytes < 1024.0:
|
424 |
+
return f"{size_bytes:.2f} {unit}"
|
425 |
+
size_bytes /= 1024.0
|
426 |
+
return f"{size_bytes:.2f} TB"
|