231 lines
10 KiB
Markdown
231 lines
10 KiB
Markdown
---
|
|
title: "Designing a Zero-Dependency Python SDK for REST APIs with Automatic Retries & Rate-Limit Backoff"
|
|
description: "A comprehensive developer guide to creating lightweight, zero-dependency Python SDKs using standard library urllib, dataclasses, and exponential jitter backoff."
|
|
tags: ["python", "architecture", "api", "sdk"]
|
|
canonical_url: "https://greenlenspro.com/"
|
|
cover_image: "https://greenlenspro.com/images/blog/zero-dependency-python-sdk.jpg"
|
|
---
|
|
|
|
# Designing a Zero-Dependency Python SDK for REST APIs with Automatic Retries & Rate-Limit Backoff
|
|
|
|
When releasing a developer SDK or API client library in Python, the instinct of many developers is to immediately install `requests`, `httpx`, or `pydantic`. While these libraries are outstanding for standalone applications, including them as transitive dependencies in an SDK package can create major dependency conflicts (dependency hell) for downstream users.
|
|
|
|
If your SDK forces version pins on `urllib3`, `certifi`, or `pydantic`, it can break environments in production data pipelines, CLI tools, or AWS Lambda serverless functions where strict dependency trees exist.
|
|
|
|
In this guide, we'll examine the design of `greenlens-python`—a zero-dependency Python SDK built for high performance, zero external bloat, and maximum compatibility. We'll implement a clean HTTP transport layer using Python's standard `urllib.request`, native `dataclasses`, and an exponential backoff algorithm with jitter for handling rate limits (`HTTP 429`).
|
|
|
|
---
|
|
|
|
## 1. Why Zero Dependencies Matter for API SDKs
|
|
|
|
Building an SDK (`app pflanzen erkennen` / plant identifier API) with zero third-party dependencies offers critical production benefits:
|
|
|
|
1. **Instant Installation & Zero Overhead:** Installation takes milliseconds (`pip install greenlens`), without downloading megabytes of transitives.
|
|
2. **Zero Security Vulnerability Cascades:** Fewer third-party dependencies mean fewer Dependabot alerts and supply chain risks.
|
|
3. **AWS Lambda & Edge Compatibility:** Minimal footprint fits easily under tight package size constraints.
|
|
4. **Universal Version Compatibility:** Runs seamlessly on Python 3.8+ without version mismatch conflicts.
|
|
|
|
---
|
|
|
|
## 2. Architecting the Core HTTP Transport Component
|
|
|
|
Instead of relying on third-party HTTP libraries, Python's standard `urllib.request` library provides robust networking tools when paired with custom context managers and JSON serialization.
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Client Application] --> B[GreenLens API Client SDK]
|
|
B --> C[Request Builder & Serializer]
|
|
C --> D[Standard `urllib.request` Transport]
|
|
D -->|HTTP Request| E[Remote REST API Endpoint]
|
|
E -->|HTTP 429 Rate Limit| F[Exponential Backoff & Jitter Evaluator]
|
|
F -->|Wait & Retry| D
|
|
E -->|HTTP 200 Success| G[Response Deserializer & Dataclass]
|
|
G --> A
|
|
```
|
|
|
|
### The `GreenLensClient` Implementation
|
|
|
|
Below is a complete, production-ready Python client implementation without a single external dependency:
|
|
|
|
```python
|
|
# greenlens/client.py
|
|
import json
|
|
import time
|
|
import random
|
|
import urllib.request
|
|
import urllib.error
|
|
from dataclasses import dataclass
|
|
from typing import Dict, Any, Optional, List
|
|
|
|
class GreenLensAPIError(Exception):
|
|
"""Base exception for API communication errors."""
|
|
def __init__(self, message: str, status_code: Optional[int] = None):
|
|
super().__init__(message)
|
|
self.status_code = status_code
|
|
|
|
|
|
@dataclass
|
|
class SymptomMatch:
|
|
name: str
|
|
confidence: float
|
|
severity: str
|
|
|
|
|
|
@dataclass
|
|
class PlantDiagnosticResponse:
|
|
species: str
|
|
health_score: int
|
|
symptoms: List[SymptomMatch]
|
|
recommended_action: str
|
|
|
|
|
|
class GreenLensClient:
|
|
"""
|
|
Zero-dependency Python SDK for the GreenLens Plant Recognition & Diagnostic API.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
api_key: str,
|
|
base_url: str = "https://greenlenspro.com/v1",
|
|
max_retries: int = 3,
|
|
backoff_factor: float = 1.5
|
|
):
|
|
self.api_key = api_key
|
|
self.base_url = base_url.rstrip("/")
|
|
self.max_retries = max_retries
|
|
self.backoff_factor = backoff_factor
|
|
|
|
def _build_headers() -> Dict[str, str]:
|
|
return {
|
|
"Authorization": f"Bearer {self.api_key}",
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "GreenLens-Python-SDK/1.0.0"
|
|
}
|
|
|
|
def _execute_request(self, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
data = json.dumps(payload).encode("utf-8")
|
|
headers = self._build_headers()
|
|
|
|
for attempt in range(self.max_retries + 1):
|
|
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10.0) as response:
|
|
res_body = response.read().decode("utf-8")
|
|
return json.loads(res_body)
|
|
|
|
except urllib.error.HTTPError as e:
|
|
# Handle Rate Limiting (429) or Transient Server Errors (502, 503)
|
|
if e.code in (429, 502, 503) and attempt < self.max_retries:
|
|
sleep_time = (self.backoff_factor ** attempt) + random.uniform(0.1, 0.5)
|
|
time.sleep(sleep_time)
|
|
continue
|
|
|
|
error_body = e.read().decode("utf-8") if e.fp else str(e)
|
|
raise GreenLensAPIError(
|
|
f"API Request Failed: {e.reason} ({error_body})",
|
|
status_code=e.code
|
|
)
|
|
|
|
except urllib.error.URLError as e:
|
|
if attempt < self.max_retries:
|
|
time.sleep((self.backoff_factor ** attempt))
|
|
continue
|
|
raise GreenLensAPIError(f"Network Connection Failed: {e.reason}")
|
|
|
|
raise GreenLensAPIError("Max retries exceeded.")
|
|
|
|
def scan_image(self, image_url: str) -> PlantDiagnosticResponse:
|
|
"""
|
|
Scan a plant image by URL to identify species and diagnose symptoms.
|
|
Hits POST {base_url}/scan, i.e. https://greenlenspro.com/v1/scan.
|
|
"""
|
|
raw_data = self._execute_request("scan", {"image_url": image_url})
|
|
|
|
symptoms = [
|
|
SymptomMatch(
|
|
name=s["name"],
|
|
confidence=s["confidence"],
|
|
severity=s["severity"]
|
|
)
|
|
for s in raw_data.get("symptoms", [])
|
|
]
|
|
|
|
return PlantDiagnosticResponse(
|
|
species=raw_data.get("species", "Unknown"),
|
|
health_score=raw_data.get("health_score", 100),
|
|
symptoms=symptoms,
|
|
recommended_action=raw_data.get("recommended_action", "")
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Implementing Exponential Backoff with Jitter
|
|
|
|
When building high-volume developer APIs (`pflanzen app`), rate limits (`HTTP 429`) will inevitably occur. Re-trying requests immediately in a loop can cause a **thundering herd problem** that degrades your API servers.
|
|
|
|
Adding randomized **Jitter** prevents retrying clients from synchronizing their retry spikes:
|
|
|
|
$$\text{Sleep Time} = (\text{Backoff Factor}^{\text{Attempt}}) + \text{UniformRandom}(0.1, 0.5)$$
|
|
|
|
```python
|
|
# Rate Limit Retry Execution Timeline Demonstration
|
|
# Attempt 0: Direct Execution (No delay)
|
|
# Attempt 1: Fail (429) -> Wait ~1.65 seconds (1.5^1 + jitter)
|
|
# Attempt 2: Fail (429) -> Wait ~2.55 seconds (1.5^2 + jitter)
|
|
# Attempt 3: Fail (429) -> Wait ~3.88 seconds (1.5^3 + jitter)
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Usage Example & Developer DX
|
|
|
|
Because our SDK leverages native `dataclasses`, Python IDEs like PyCharm and VSCode provide complete auto-completion without requiring third-party plugins:
|
|
|
|
```python
|
|
# example_usage.py
|
|
from greenlens import GreenLensClient
|
|
|
|
# Initialize Client
|
|
client = GreenLensClient(api_key="gl_live_998124712894")
|
|
|
|
try:
|
|
print("Initiating Plant Recognition Scan...")
|
|
result = client.scan_image("https://example.com/monstera-leaf.jpg")
|
|
|
|
print(f"Detected Species: {result.species}")
|
|
print(f"Health Score: {result.health_score}/100")
|
|
|
|
for symptom in result.symptoms:
|
|
print(f" - Symptom: {symptom.name} ({symptom.confidence * 100:.1f}%)")
|
|
|
|
except Exception as e:
|
|
print(f"Diagnosis Failed: {e}")
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Package Overhead & Install Latency: What You're Actually Trading Off
|
|
|
|
It's worth being honest about what "zero-dependency" buys you instead of quoting precise numbers that will vary by machine, network, and pip cache state. Directionally, the tradeoffs look like this:
|
|
|
|
| SDK Architecture | Relative Package Size | Transitive Dependencies | Relative Cold Install Time |
|
|
|---|---|---|---|
|
|
| Heavy SDK (`requests` + `pydantic` + `urllib3`) | Noticeably larger — pulls in a chain of transitive wheels | Several (varies by pinned versions) | Slower — more packages to resolve and download |
|
|
| Zero-Dep SDK (`greenlens-python`) | Minimal — a handful of `.py` files, no wheels beyond the stdlib | None | Fast — effectively just copying source |
|
|
|
|
The real win isn't shaving off a few seconds of `pip install` time; it's avoiding version-resolution conflicts in downstream projects that already pin `urllib3`, `certifi`, or `pydantic` to specific versions for unrelated reasons. If your SDK has zero third-party dependencies, it can never be the thing that breaks someone else's dependency graph. Run your own benchmark with `time pip install` in a clean virtualenv if you want numbers specific to your environment — don't trust any blog post's install-latency claims, including this one.
|
|
|
|
---
|
|
|
|
## Summary & Best Practices
|
|
|
|
1. **Avoid Heavy Dependencies in SDKs:** Restrict third-party packages in developer libraries unless strictly necessary.
|
|
2. **Use Standard Library Networking:** Python's `urllib.request` can handle authentication, SSL validation, timeouts, and headers cleanly.
|
|
3. **Always Add Jitter to Retries:** Randomize backoff delays to prevent synchronized client traffic surges.
|
|
4. **Expose Typed Dataclasses:** Return strongly typed objects instead of raw `dict` structures for superior developer experience (`app pflanzen erkennen`).
|
|
|
|
To integrate AI plant recognition into your Python applications, check out the official [GreenLens API Platform](https://greenlenspro.com/).
|