API Client#
1. Calling External APIs#
Every real application talks to something external. A payment processor. A weather service. A shipping carrier. A CRM. You write the HTTP call, parse the response, handle timeouts, retry on failure, add auth headers.
Tina4's Api class makes outbound HTTP requests from your server-side code. It returns a consistent response format, handles auth in one line, and supports SSL and timeout configuration, with no extra libraries.
2. Basic Requests#
from tina4_python.api import Apiโapi = Api()โ# GETresult = api.get("https://api.example.com/products")โ# POST with JSON bodyresult = api.post("https://api.example.com/orders", { "product_id": 42, "quantity": 3})โ# PUTresult = api.put("https://api.example.com/orders/101", { "status": "shipped"})โ# PATCHresult = api.patch("https://api.example.com/orders/101", { "tracking_number": "1Z999AA10123456784"})โ# DELETEresult = api.delete("https://api.example.com/orders/101")Every method returns the same structure:
{ "http_code": 200, "body": { ... }, # Parsed JSON, or raw string if not JSON "headers": { ... }, # Response headers "error": None # None on success, error string on failure}3. Reading the Response#
from tina4_python.api import Apiโapi = Api()result = api.get("https://jsonplaceholder.typicode.com/posts/1")โif result["error"]: print(f"Request failed: {result['error']}")else: print(f"Status: {result['http_code']}") print(f"Title: {result['body']['title']}")Check result["error"] first. If it is None, the request succeeded. The HTTP status code is in result["http_code"]. The parsed response is in result["body"].
# In a route handlerfrom tina4_python.core.router import getfrom tina4_python.api import Apiโ@get("/api/posts/{post_id}")async def proxy_post(request, response): api = Api() post_id = request.params["post_id"]โ result = api.get(f"https://jsonplaceholder.typicode.com/posts/{post_id}")โ if result["error"]: return response({"error": result["error"]}, 502)โ if result["http_code"] == 404: return response({"error": "Post not found"}, 404)โ return response(result["body"])4. Authentication Headers#
Bearer Token#
from tina4_python.api import Apiโapi = Api(bearer_token="eyJhbGciOiJSUzI1NiJ9...")โresult = api.get("https://api.example.com/me")The Authorization: Bearer <token> header is sent with every request made by this Api instance.
Basic Authentication#
api = Api(username="api_user", password="secret123")โresult = api.get("https://api.example.com/data")Sends Authorization: Basic <base64(username:password)> with every request.
Custom Headers#
api = Api(headers={ "X-API-Key": "my-api-key-here", "X-Client-Version": "1.0.0", "Accept": "application/json"})โresult = api.get("https://api.example.com/data")Custom headers are merged with any authentication headers and sent with every request.
Mixing Auth and Custom Headers#
api = Api( bearer_token="eyJhbGciOiJSUzI1NiJ9...", headers={"X-Request-Source": "tina4-app"})5. SSL Verification and Timeouts#
# Disable SSL verification (dev only, never in production)api = Api(verify_ssl=False)โ# Set a 10-second timeoutapi = Api(timeout=10)โ# Bothapi = Api(verify_ssl=False, timeout=5)The default timeout is 30 seconds. The default SSL behaviour is to verify certificates (verify_ssl=True).
Never disable SSL verification in production. If an external API has a self-signed certificate, obtain the CA bundle and pass it explicitly, or ask the provider for their CA bundle.
6. Sending Query Parameters#
Pass query parameters as a dictionary to the params argument:
api = Api()โresult = api.get("https://api.example.com/products", params={ "category": "Electronics", "page": 1, "limit": 20, "in_stock": True})# Requests: GET /products?category=Electronics&page=1&limit=20&in_stock=True7. Real-World Patterns#
Payment Gateway#
import osfrom tina4_python.api import Apiโclass PaymentGateway: def __init__(self): self.api = Api( bearer_token=os.environ["PAYMENT_API_KEY"], timeout=15 ) self.base = "https://api.payment-provider.com/v1"โ def charge(self, amount_cents, currency, card_token): result = self.api.post(f"{self.base}/charges", { "amount": amount_cents, "currency": currency, "source": card_token })โ if result["error"]: return {"success": False, "error": result["error"]}โ if result["http_code"] not in (200, 201): return { "success": False, "error": result["body"].get("message", "Payment declined") }โ return { "success": True, "charge_id": result["body"]["id"], "status": result["body"]["status"] }โ def refund(self, charge_id, amount_cents=None): body = {"charge": charge_id} if amount_cents: body["amount"] = amount_centsโ result = self.api.post(f"{self.base}/refunds", body) return result["body"]Weather Service#
import osfrom tina4_python.api import Apiโclass WeatherService: def __init__(self): self.api = Api(timeout=5) self.api_key = os.environ["OPENWEATHER_API_KEY"] self.base = "https://api.openweathermap.org/data/2.5"โ def get_current(self, city): result = self.api.get(f"{self.base}/weather", params={ "q": city, "appid": self.api_key, "units": "metric" })โ if result["error"] or result["http_code"] != 200: return Noneโ data = result["body"] return { "city": data["name"], "temp_c": data["main"]["temp"], "description": data["weather"][0]["description"], "humidity": data["main"]["humidity"] }Usage in a route:
from tina4_python.core.router import getโweather = WeatherService()โ@get("/api/weather/{city}")async def get_weather(request, response): city = request.params["city"] data = weather.get_current(city)โ if data is None: return response({"error": "Weather data unavailable"}, 502)โ return response(data)8. Retry Logic#
For transient failures, add retry logic around Api calls:
import timefrom tina4_python.api import Apifrom tina4_python.debug import Logโdef api_get_with_retry(url, max_retries=3, backoff=1.0, **kwargs): api = Api(**kwargs) last_error = Noneโ for attempt in range(1, max_retries + 1): result = api.get(url)โ if not result["error"] and result["http_code"] < 500: return resultโ last_error = result["error"] or f"HTTP {result['http_code']}" Log.warning("API call failed, retrying", url=url, attempt=attempt, error=last_error)โ if attempt < max_retries: time.sleep(backoff * attempt)โ Log.error("API call failed after retries", url=url, error=last_error) return {"http_code": 0, "body": None, "headers": {}, "error": last_error}9. Exercise: Weather Dashboard API#
Build a route that fetches weather for multiple cities and returns a dashboard-ready response.
Requirements#
Create
GET /api/dashboard/weatherthat:- Accepts a
citiesquery parameter (comma-separated) - Calls a mock weather API for each city
- Returns aggregated results
- Returns 502 if any city fails
- Accepts a
- Use proper error checking on every API call
- Add a 5-second timeout and Bearer token auth
Test with:#
curl "http://localhost:7146/api/dashboard/weather?cities=London,Berlin,Tokyo"10. Solution#
Create src/routes/weather_dashboard.py:
from tina4_python.core.router import getfrom tina4_python.api import Apifrom tina4_python.debug import Logโ# Simulate a weather API with mock dataMOCK_WEATHER = { "london": {"city": "London", "temp_c": 12.3, "description": "Cloudy", "humidity": 78}, "berlin": {"city": "Berlin", "temp_c": 8.1, "description": "Clear", "humidity": 55}, "tokyo": {"city": "Tokyo", "temp_c": 18.7, "description": "Partly cloudy", "humidity": 65}, "paris": {"city": "Paris", "temp_c": 14.0, "description": "Rainy", "humidity": 82},}โdef fetch_weather(city_name): # In production this would be a real API call: # api = Api(bearer_token=os.environ["WEATHER_API_KEY"], timeout=5) # return api.get(f"https://api.weather.com/v1/current?city={city_name}")โ key = city_name.lower().strip() data = MOCK_WEATHER.get(key) if data is None: return {"http_code": 404, "body": None, "headers": {}, "error": f"City not found: {city_name}"} return {"http_code": 200, "body": data, "headers": {}, "error": None}โโ@get("/api/dashboard/weather")async def weather_dashboard(request, response): cities_param = request.params.get("cities", "")โ if not cities_param: return response({"error": "Provide at least one city via ?cities=City1,City2"}, 400)โ cities = [c.strip() for c in cities_param.split(",") if c.strip()] results = [] errors = []โ for city in cities: Log.debug("Fetching weather", city=city) result = fetch_weather(city)โ if result["error"]: Log.warning("Weather fetch failed", city=city, error=result["error"]) errors.append({"city": city, "error": result["error"]}) else: results.append(result["body"])โ if errors: return response({ "error": "One or more cities could not be fetched", "failed": errors, "succeeded": results }, 502)โ return response({ "cities": results, "count": len(results) })curl "http://localhost:7146/api/dashboard/weather?cities=London,Berlin,Tokyo"{ "cities": [ {"city": "London", "temp_c": 12.3, "description": "Cloudy", "humidity": 78}, {"city": "Berlin", "temp_c": 8.1, "description": "Clear", "humidity": 55}, {"city": "Tokyo", "temp_c": 18.7, "description": "Partly cloudy", "humidity": 65} ], "count": 3}11. Gotchas#
1. Not checking result["error"]#
Problem: Code accesses result["body"]["id"] but the request timed out, so body is None.
Fix: Always check result["error"] before accessing result["body"]. Treat any non-None error as a failure.
2. Using verify_ssl=False in production#
Problem: A man-in-the-middle intercepts requests to a payment gateway because SSL verification is disabled.
Fix: Only use verify_ssl=False against local services or during development. Never disable SSL for external APIs.
3. No timeout set for slow external APIs#
Problem: An external API hangs for 90 seconds. Your route handler blocks for 90 seconds. All workers are eventually held waiting.
Fix: Always set timeout to a sensible value (5-15 seconds for most external APIs). Return a 502 or 504 to the caller if the external API does not respond in time.
4. Hardcoding API keys#
Problem: api = Api(bearer_token="sk-live-abc123...") exposes the key in source control.
Fix: Read credentials from environment variables: bearer_token=os.environ["PAYMENT_API_KEY"]. Never hardcode secrets.