ciyidogan commited on
Commit
e5d2ec0
ยท
verified ยท
1 Parent(s): a1c5ec0

Update api_executor.py

Browse files
Files changed (1) hide show
  1. 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) # Headers are always strings
222
- body = _render_json(api.body_template, session, api.name) # Body preserves types
 
 
 
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": api.timeout_seconds
 
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
- log_info(f"๐ŸŒ API call: {api.name} {api.method} {api.url} (attempt {attempt + 1}/{retry_count + 1})")
258
- log_info(f"๐Ÿ“‹ Request body: {json.dumps(body, ensure_ascii=False)}")
259
-
260
- response = requests.request(**request_params)
261
-
262
- # Handle 401 Unauthorized
263
- if response.status_code == 401 and api.auth and api.auth.enabled and attempt < retry_count:
264
- log_info(f"๐Ÿ”’ Got 401, refreshing token for {api.name}")
265
- _fetch_token(api, session) # Force new token
266
- headers = _render(api.headers, session, api.name) # Re-render headers
267
- request_params["headers"] = headers
268
- continue
269
-
270
- response.raise_for_status()
271
- log_info(f"โœ… API call successful: {api.name} ({response.status_code})")
272
-
273
- # Response mapping iลŸlemi
274
- if response.status_code in (200, 201, 202, 204) and hasattr(api, 'response_mappings') and api.response_mappings:
275
- try:
276
- if response.status_code != 204 and response.content:
277
- response_json = response.json()
278
-
279
- for mapping in api.response_mappings:
280
- var_name = mapping.get('variable_name')
281
- var_type = mapping.get('type', 'str')
282
- json_path = mapping.get('json_path')
283
-
284
- if not all([var_name, json_path]):
285
- continue
286
-
287
- # JSON path'ten deฤŸeri al
288
- value = response_json
289
- for path_part in json_path.split('.'):
290
- if isinstance(value, dict):
291
- value = value.get(path_part)
292
- if value is None:
293
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
- if value is not None:
296
- # Type conversion
297
- if var_type == 'int':
298
- value = int(value)
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
- # Session'a kaydet
310
- session.variables[var_name] = value
311
- log_info(f"๐Ÿ“ Mapped response value: {var_name} = {value}")
312
 
313
- except Exception as e:
314
- log_error("โš ๏ธ Response mapping error", e)
315
-
316
- return response
317
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  except requests.exceptions.Timeout as e:
319
  last_error = e
320
- log_warning(f"โฑ๏ธ API timeout for {api.name} (attempt {attempt + 1})")
 
 
 
 
 
321
 
322
  except requests.exceptions.RequestException as e:
323
  last_error = e
324
- log_error(f"โŒ API error for {api.name}", e)
 
 
 
 
 
 
 
 
 
 
 
 
 
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"โณ Waiting {backoff}s before retry...")
332
  time.sleep(backoff)
333
 
334
  # All retries failed
 
 
 
335
  if last_error:
336
  raise last_error
337
- raise requests.exceptions.RequestException(f"API call failed after {retry_count + 1} attempts")
 
 
 
 
 
 
 
 
 
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"