ciyidogan commited on
Commit
8682228
·
verified ·
1 Parent(s): ebb2256

Delete api_executor.py

Browse files
Files changed (1) hide show
  1. api_executor.py +0 -426
api_executor.py DELETED
@@ -1,426 +0,0 @@
1
- """
2
- Flare – API Executor (v2.0 · session-aware token management)
3
- """
4
-
5
- from __future__ import annotations
6
- import json, re, time, requests
7
- from typing import Any, Dict, Optional, Union
8
- from utils.logger import log_info, log_error, log_warning, log_debug, LogTimer
9
- from config.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
-
18
- def _get_variable_value(session: Session, var_path: str) -> Any:
19
- cfg = ConfigProvider.get()
20
-
21
- """Get variable value with proper type from session"""
22
- if var_path.startswith("variables."):
23
- var_name = var_path.split(".", 1)[1]
24
- return session.variables.get(var_name)
25
- elif var_path.startswith("auth_tokens."):
26
- parts = var_path.split(".")
27
- if len(parts) >= 3:
28
- token_api = parts[1]
29
- token_field = parts[2]
30
- token_data = session._auth_tokens.get(token_api, {})
31
- return token_data.get(token_field)
32
- elif var_path.startswith("config."):
33
- attr_name = var_path.split(".", 1)[1]
34
- return getattr(cfg.global_config, attr_name, None)
35
- return None
36
-
37
- def _render_value(value: Any) -> Union[str, int, float, bool, None]:
38
- """Convert value to appropriate JSON type"""
39
- if value is None:
40
- return None
41
- elif isinstance(value, bool):
42
- return value
43
- elif isinstance(value, (int, float)):
44
- return value
45
- elif isinstance(value, str):
46
- # Check if it's a number string
47
- if value.isdigit():
48
- return int(value)
49
- try:
50
- return float(value)
51
- except ValueError:
52
- pass
53
- # Check if it's a boolean string
54
- if value.lower() in ('true', 'false'):
55
- return value.lower() == 'true'
56
- # Return as string
57
- return value
58
- else:
59
- return str(value)
60
-
61
- def _render_json(obj: Any, session: Session, api_name: str) -> Any:
62
- """Render JSON preserving types"""
63
- if isinstance(obj, str):
64
- # Check if entire string is a template
65
- template_match = _placeholder.fullmatch(obj.strip())
66
- if template_match:
67
- # This is a pure template like {{variables.pnr}}
68
- var_path = template_match.group(1).strip()
69
- value = _get_variable_value(session, var_path)
70
- return _render_value(value)
71
- else:
72
- # String with embedded templates or regular string
73
- def replacer(match):
74
- var_path = match.group(1).strip()
75
- value = _get_variable_value(session, var_path)
76
- return str(value) if value is not None else ""
77
-
78
- return _placeholder.sub(replacer, obj)
79
-
80
- elif isinstance(obj, dict):
81
- return {k: _render_json(v, session, api_name) for k, v in obj.items()}
82
-
83
- elif isinstance(obj, list):
84
- return [_render_json(v, session, api_name) for v in obj]
85
-
86
- else:
87
- # Return as-is for numbers, booleans, None
88
- return obj
89
-
90
- def _render(obj: Any, session: Session, api_name: str) -> Any:
91
- """Render template with session variables and tokens"""
92
- # For headers and other string-only contexts
93
- if isinstance(obj, str):
94
- def replacer(match):
95
- var_path = match.group(1).strip()
96
- value = _get_variable_value(session, var_path)
97
- return str(value) if value is not None else ""
98
-
99
- return _placeholder.sub(replacer, obj)
100
-
101
- elif isinstance(obj, dict):
102
- return {k: _render(v, session, api_name) for k, v in obj.items()}
103
-
104
- elif isinstance(obj, list):
105
- return [_render(v, session, api_name) for v in obj]
106
-
107
- return obj
108
-
109
- def _fetch_token(api: APIConfig, session: Session) -> None:
110
- """Fetch new auth token"""
111
- if not api.auth or not api.auth.enabled:
112
- return
113
-
114
- log_info(f"🔑 Fetching token for {api.name}")
115
-
116
- try:
117
- # Use _render_json for body to preserve types
118
- body = _render_json(api.auth.token_request_body, session, api.name)
119
- headers = {"Content-Type": "application/json"}
120
-
121
- response = requests.post(
122
- str(api.auth.token_endpoint),
123
- json=body,
124
- headers=headers,
125
- timeout=api.timeout_seconds
126
- )
127
- response.raise_for_status()
128
-
129
- json_data = response.json()
130
-
131
- # Extract token using path
132
- token = json_data
133
- for path_part in api.auth.response_token_path.split("."):
134
- token = token.get(path_part)
135
- if token is None:
136
- raise ValueError(f"Token path {api.auth.response_token_path} not found in response")
137
-
138
- # Store in session
139
- session._auth_tokens[api.name] = {
140
- "token": token,
141
- "expiry": time.time() + 3500, # ~1 hour
142
- "refresh_token": json_data.get("refresh_token")
143
- }
144
-
145
- log_info(f"✅ Token obtained for {api.name}")
146
-
147
- except Exception as e:
148
- log_error(f"❌ Token fetch failed for {api.name}", e)
149
- raise
150
-
151
- def _refresh_token(api: APIConfig, session: Session) -> bool:
152
- """Refresh existing token"""
153
- if not api.auth or not api.auth.token_refresh_endpoint:
154
- return False
155
-
156
- token_info = session._auth_tokens.get(api.name, {})
157
- if not token_info.get("refresh_token"):
158
- return False
159
-
160
- log_info(f"🔄 Refreshing token for {api.name}")
161
-
162
- try:
163
- body = _render_json(api.auth.token_refresh_body or {}, session, api.name)
164
- body["refresh_token"] = token_info["refresh_token"]
165
-
166
- response = requests.post(
167
- str(api.auth.token_refresh_endpoint),
168
- json=body,
169
- timeout=api.timeout_seconds
170
- )
171
- response.raise_for_status()
172
-
173
- json_data = response.json()
174
-
175
- # Extract new token
176
- token = json_data
177
- for path_part in api.auth.response_token_path.split("."):
178
- token = token.get(path_part)
179
- if token is None:
180
- raise ValueError(f"Token path {api.auth.response_token_path} not found in refresh response")
181
-
182
- # Update session
183
- session._auth_tokens[api.name] = {
184
- "token": token,
185
- "expiry": time.time() + 3500,
186
- "refresh_token": json_data.get("refresh_token", token_info["refresh_token"])
187
- }
188
-
189
- log_info(f"✅ Token refreshed for {api.name}")
190
- return True
191
-
192
- except Exception as e:
193
- log_error(f"❌ Token refresh failed for {api.name}", e)
194
- return False
195
-
196
- def _ensure_token(api: APIConfig, session: Session) -> None:
197
- """Ensure valid token exists for API"""
198
- if not api.auth or not api.auth.enabled:
199
- return
200
-
201
- token_info = session._auth_tokens.get(api.name)
202
-
203
- # No token yet
204
- if not token_info:
205
- _fetch_token(api, session)
206
- return
207
-
208
- # Token still valid
209
- if token_info.get("expiry", 0) > time.time():
210
- return
211
-
212
- # Try refresh first
213
- if _refresh_token(api, session):
214
- return
215
-
216
- # Refresh failed, get new token
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
234
- if api.proxy:
235
- if isinstance(api.proxy, str):
236
- proxies = {"http": api.proxy, "https": api.proxy}
237
- elif hasattr(api.proxy, "enabled") and api.proxy.enabled:
238
- proxy_url = str(api.proxy.url)
239
- proxies = {"http": proxy_url, "https": proxy_url}
240
-
241
- # Prepare request parameters
242
- request_params = {
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
251
- if api.method in ("POST", "PUT", "PATCH"):
252
- request_params["json"] = body
253
- elif api.method == "GET" and body:
254
- request_params["params"] = body
255
-
256
- if proxies:
257
- request_params["proxies"] = proxies
258
-
259
- # Execute with retry
260
- retry_count = api.retry.retry_count if api.retry else 0
261
- last_error = None
262
- response = None
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"