kollabor 0.4.9__py3-none-any.whl → 0.4.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,2150 @@
1
+ <!-- API Integration skill - integrate with external APIs reliably -->
2
+
3
+ api-integration mode: RELIABLE EXTERNAL SERVICE CONNECTIONS
4
+
5
+ when this skill is active, you follow disciplined API integration practices.
6
+ this is a comprehensive guide to integrating with REST and GraphQL APIs.
7
+
8
+
9
+ PHASE 0: ENVIRONMENT PREREQUISITES VERIFICATION
10
+
11
+ before integrating ANY external API, verify your environment is ready.
12
+
13
+
14
+ check http client library
15
+
16
+ <terminal>python -c "import requests; print(requests.__version__)"</terminal>
17
+
18
+ if requests not installed:
19
+ <terminal>pip install requests</terminal>
20
+
21
+
22
+ check async http client
23
+
24
+ <terminal>python -c "import httpx; print(httpx.__version__)"</terminal>
25
+
26
+ if httpx not installed:
27
+ <terminal>pip install httpx</terminal>
28
+
29
+
30
+ verify api credentials exist
31
+
32
+ <terminal>ls -la .env 2>/dev/null || echo "no .env file"</terminal>
33
+
34
+ <terminal>echo $API_KEY 2>/dev/null || echo "API_KEY not set"</terminal>
35
+
36
+ if credentials missing:
37
+ <create>
38
+ <file>.env.example</file>
39
+ <content>
40
+ # API Credentials
41
+ API_KEY=your_api_key_here
42
+ API_SECRET=your_api_secret_here
43
+ API_BASE_URL=https://api.example.com
44
+ </content>
45
+ </create>
46
+
47
+ remind user to create .env from .env.example
48
+
49
+
50
+ check for environment variable loader
51
+
52
+ <terminal>python -c "import dotenv; print('python-dotenv installed')" 2>/dev/null || echo "need python-dotenv"</terminal>
53
+
54
+ if not installed:
55
+ <terminal>pip install python-dotenv</terminal>
56
+
57
+
58
+ check project structure for api code
59
+
60
+ <terminal>ls -la src/api/ 2>/dev/null || ls -la core/api/ 2>/dev/null || echo "no api directory"</terminal>
61
+
62
+ <terminal>find . -name "*client*.py" -type f | grep -v __pycache__ | head -5</terminal>
63
+
64
+ understand existing api patterns before adding new integrations.
65
+
66
+
67
+ verify request/response validation tools
68
+
69
+ <terminal>python -c "import pydantic; print('pydantic available')" 2>/dev/null || echo "pydantic not installed"</terminal>
70
+
71
+ if not installed:
72
+ <terminal>pip install pydantic</terminal>
73
+
74
+
75
+ PHASE 1: API INTEGRATION FUNDAMENTALS
76
+
77
+
78
+ understand the api contract
79
+
80
+ before writing code, gather this information:
81
+
82
+ [ ] base URL (e.g., https://api.example.com/v1)
83
+ [ ] authentication method (API key, OAuth, JWT)
84
+ [ ] rate limits (requests per minute/hour)
85
+ [ ] available endpoints
86
+ [ ] request/response formats
87
+ [ ] error response format
88
+ [ ] pagination style
89
+ [ ] webhooks or streaming support
90
+
91
+ read the documentation. bookmark the reference.
92
+ save the openapi spec if available.
93
+
94
+
95
+ basic rest client structure
96
+
97
+ <create>
98
+ <file>src/api/base_client.py</file>
99
+ <content>
100
+ """Base API client with common functionality."""
101
+ from typing import Any, Dict, Optional
102
+ import requests
103
+ from requests.adapters import HTTPAdapter
104
+ from urllib3.util.retry import Retry
105
+
106
+
107
+ class BaseAPIClient:
108
+ """Base class for API clients."""
109
+
110
+ def __init__(
111
+ self,
112
+ base_url: str,
113
+ api_key: Optional[str] = None,
114
+ timeout: int = 30,
115
+ max_retries: int = 3
116
+ ):
117
+ self.base_url = base_url.rstrip("/")
118
+ self.api_key = api_key
119
+ self.timeout = timeout
120
+
121
+ # configure session with retry logic
122
+ self.session = requests.Session()
123
+ retry_strategy = Retry(
124
+ total=max_retries,
125
+ backoff_factor=1,
126
+ status_forcelist=[429, 500, 502, 503, 504],
127
+ allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"]
128
+ )
129
+ adapter = HTTPAdapter(max_retries=retry_strategy)
130
+ self.session.mount("http://", adapter)
131
+ self.session.mount("https://", adapter)
132
+
133
+ def _build_url(self, path: str) -> str:
134
+ """Build full URL from path."""
135
+ return f"{self.base_url}/{path.lstrip('/')}"
136
+
137
+ def _get_headers(self) -> Dict[str, str]:
138
+ """Build default headers."""
139
+ headers = {
140
+ "Content-Type": "application/json",
141
+ "Accept": "application/json"
142
+ }
143
+ if self.api_key:
144
+ headers["Authorization"] = f"Bearer {self.api_key}"
145
+ return headers
146
+ </content>
147
+ </create>
148
+
149
+
150
+ async rest client with httpx
151
+
152
+ <create>
153
+ <file>src/api/async_client.py</file>
154
+ <content>
155
+ """Async API client using httpx."""
156
+ from typing import Any, Dict, Optional
157
+ import httpx
158
+ import asyncio
159
+ from httpx import AsyncClient, Response, TimeoutException
160
+
161
+
162
+ class AsyncAPIClient:
163
+ """Async API client for high-performance integration."""
164
+
165
+ def __init__(
166
+ self,
167
+ base_url: str,
168
+ api_key: Optional[str] = None,
169
+ timeout: float = 30.0,
170
+ limits: Optional[httpx.Limits] = None
171
+ ):
172
+ self.base_url = base_url.rstrip("/")
173
+ self.api_key = api_key
174
+ self.timeout = timeout
175
+
176
+ # configure connection limits
177
+ if limits is None:
178
+ limits = httpx.Limits(
179
+ max_keepalive_connections=20,
180
+ max_connections=100,
181
+ keepalive_expiry=5.0
182
+ )
183
+
184
+ self._client: Optional[AsyncClient] = None
185
+ self._limits = limits
186
+
187
+ async def __aenter__(self):
188
+ """Enter context manager."""
189
+ await self.connect()
190
+ return self
191
+
192
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
193
+ """Exit context manager."""
194
+ await self.close()
195
+
196
+ async def connect(self):
197
+ """Initialize the async client."""
198
+ if self._client is None:
199
+ headers = {
200
+ "Content-Type": "application/json",
201
+ "Accept": "application/json"
202
+ }
203
+ if self.api_key:
204
+ headers["Authorization"] = f"Bearer {self.api_key}"
205
+
206
+ self._client = AsyncClient(
207
+ base_url=self.base_url,
208
+ headers=headers,
209
+ timeout=self.timeout,
210
+ limits=self._limits
211
+ )
212
+
213
+ async def close(self):
214
+ """Close the async client."""
215
+ if self._client:
216
+ await self._client.aclose()
217
+ self._client = None
218
+
219
+ def _ensure_connected(self):
220
+ """Ensure client is connected."""
221
+ if self._client is None:
222
+ raise RuntimeError("Client not connected. Use async with or call connect().")
223
+ </content>
224
+ </create>
225
+
226
+
227
+ PHASE 2: AUTHENTICATION PATTERNS
228
+
229
+
230
+ api key authentication
231
+
232
+ simplest form - key in header or query param:
233
+
234
+ <read><file>src/api/base_client.py</file></read>
235
+
236
+ <edit>
237
+ <file>src/api/base_client.py</file>
238
+ <find>
239
+ def _get_headers(self) -> Dict[str, str]:
240
+ """Build default headers."""
241
+ headers = {
242
+ "Content-Type": "application/json",
243
+ "Accept": "application/json"
244
+ }
245
+ if self.api_key:
246
+ headers["Authorization"] = f"Bearer {self.api_key}"
247
+ return headers
248
+ </find>
249
+ <replace>
250
+ def _get_headers(self) -> Dict[str, str]:
251
+ """Build default headers."""
252
+ headers = {
253
+ "Content-Type": "application/json",
254
+ "Accept": "application/json"
255
+ }
256
+ if self.api_key:
257
+ # common patterns: Bearer token, API key, or custom header
258
+ headers["Authorization"] = f"Bearer {self.api_key}"
259
+ # alternative: headers["X-API-Key"] = self.api_key
260
+ return headers
261
+ </replace>
262
+ </edit>
263
+
264
+
265
+ basic auth (username/password)
266
+
267
+ import requests
268
+ from requests.auth import HTTPBasicAuth
269
+
270
+ response = requests.get(
271
+ "https://api.example.com/endpoint",
272
+ auth=HTTPBasicAuth("username", "password")
273
+ )
274
+
275
+
276
+ oauth2 client credentials flow
277
+
278
+ <create>
279
+ <file>src/api/oauth_client.py</file>
280
+ <content>
281
+ """OAuth2 client credentials authentication."""
282
+ from typing import Optional
283
+ import time
284
+ import requests
285
+ from dataclasses import dataclass
286
+
287
+
288
+ @dataclass
289
+ class TokenResponse:
290
+ """OAuth token response."""
291
+ access_token: str
292
+ token_type: str
293
+ expires_in: int
294
+ refresh_token: Optional[str] = None
295
+
296
+ @property
297
+ def expires_at(self) -> float:
298
+ """Calculate expiration timestamp."""
299
+ return time.time() + self.expires_in - 60 # 1 minute buffer
300
+
301
+
302
+ class OAuth2Client:
303
+ """OAuth2 client with automatic token refresh."""
304
+
305
+ def __init__(
306
+ self,
307
+ token_url: str,
308
+ client_id: str,
309
+ client_secret: str,
310
+ scope: Optional[str] = None
311
+ ):
312
+ self.token_url = token_url
313
+ self.client_id = client_id
314
+ self.client_secret = client_secret
315
+ self.scope = scope
316
+ self._token: Optional[TokenResponse] = None
317
+
318
+ def get_token(self) -> str:
319
+ """Get valid access token, refreshing if needed."""
320
+ if self._token is None or time.time() >= self._token.expires_at:
321
+ self._fetch_token()
322
+ return self._token.access_token
323
+
324
+ def _fetch_token(self):
325
+ """Fetch new token from auth server."""
326
+ data = {
327
+ "grant_type": "client_credentials",
328
+ "client_id": self.client_id,
329
+ "client_secret": self.client_secret
330
+ }
331
+ if self.scope:
332
+ data["scope"] = self.scope
333
+
334
+ response = requests.post(self.token_url, data=data)
335
+ response.raise_for_status()
336
+
337
+ token_data = response.json()
338
+ self._token = TokenResponse(
339
+ access_token=token_data["access_token"],
340
+ token_type=token_data["token_type"],
341
+ expires_in=token_data["expires_in"],
342
+ refresh_token=token_data.get("refresh_token")
343
+ )
344
+ </content>
345
+ </create>
346
+
347
+
348
+ oauth2 authorization code flow
349
+
350
+ <create>
351
+ <file>src/api/auth_code_client.py</file>
352
+ <content>
353
+ """OAuth2 authorization code flow for user authentication."""
354
+ from typing import Optional
355
+ import uuid
356
+ from urllib.parse import urlencode
357
+ import webbrowser
358
+ from http.server import HTTPServer, BaseHTTPRequestHandler
359
+ import requests
360
+
361
+
362
+ class OAuth2AuthCodeFlow:
363
+ """Handle OAuth2 authorization code flow with local callback."""
364
+
365
+ def __init__(
366
+ self,
367
+ auth_url: str,
368
+ token_url: str,
369
+ client_id: str,
370
+ client_secret: str,
371
+ redirect_uri: str = "http://localhost:8080/callback",
372
+ scope: str = "openid profile email"
373
+ ):
374
+ self.auth_url = auth_url
375
+ self.token_url = token_url
376
+ self.client_id = client_id
377
+ self.client_secret = client_secret
378
+ self.redirect_uri = redirect_uri
379
+ self.scope = scope
380
+ self.state = str(uuid.uuid4())
381
+ self.auth_code: Optional[str] = None
382
+
383
+ def get_auth_url(self) -> str:
384
+ """Generate authorization URL."""
385
+ params = {
386
+ "response_type": "code",
387
+ "client_id": self.client_id,
388
+ "redirect_uri": self.redirect_uri,
389
+ "scope": self.scope,
390
+ "state": self.state
391
+ }
392
+ return f"{self.auth_url}?{urlencode(params)}"
393
+
394
+ def start_flow(self):
395
+ """Start authorization flow by opening browser."""
396
+ auth_url = self.get_auth_url()
397
+ print(f"Opening browser for authorization: {auth_url}")
398
+ webbrowser.open(auth_url)
399
+
400
+ # start local server to handle callback
401
+ self._start_callback_server()
402
+
403
+ def _start_callback_server(self):
404
+ """Start local HTTP server for callback."""
405
+ class CallbackHandler(BaseHTTPRequestHandler):
406
+ def __init__(self, parent, *args, **kwargs):
407
+ self.parent = parent
408
+ super().__init__(*args, **kwargs)
409
+
410
+ def do_GET(self):
411
+ if self.path.startswith("/callback"):
412
+ # parse query params
413
+ from urllib.parse import urlparse, parse_qs
414
+ query = parse_qs(urlparse(self.path).query)
415
+
416
+ code = query.get("code", [None])[0]
417
+ state = query.get("state", [None])[0]
418
+
419
+ if state == self.parent.state and code:
420
+ self.parent.auth_code = code
421
+ self.send_response(200)
422
+ self.end_headers()
423
+ self.wfile.write(b"Authorization successful! You can close this window.")
424
+ else:
425
+ self.send_response(400)
426
+ self.end_headers()
427
+ self.wfile.write(b"Authorization failed!")
428
+
429
+ def log_message(self, format, *args):
430
+ pass # suppress logs
431
+
432
+ server = HTTPServer(("localhost", 8080), lambda *args, **kwargs: CallbackHandler(self, *args, **kwargs))
433
+ print("Waiting for authorization callback on http://localhost:8080")
434
+ server.handle_request()
435
+
436
+ def exchange_code_for_token(self) -> dict:
437
+ """Exchange authorization code for access token."""
438
+ if not self.auth_code:
439
+ raise RuntimeError("No authorization code received")
440
+
441
+ data = {
442
+ "grant_type": "authorization_code",
443
+ "code": self.auth_code,
444
+ "redirect_uri": self.redirect_uri,
445
+ "client_id": self.client_id,
446
+ "client_secret": self.client_secret
447
+ }
448
+
449
+ response = requests.post(self.token_url, data=data)
450
+ response.raise_for_status()
451
+ return response.json()
452
+ </content>
453
+ </create>
454
+
455
+
456
+ jwt authentication
457
+
458
+ <create>
459
+ <file>src/api/jwt_auth.py</file>
460
+ <content>
461
+ """JWT authentication for API clients."""
462
+ from typing import Dict, Optional
463
+ import time
464
+ import jwt
465
+
466
+
467
+ class JWTAuth:
468
+ """JWT token generation and validation."""
469
+
470
+ def __init__(
471
+ self,
472
+ secret_key: str,
473
+ algorithm: str = "HS256",
474
+ issuer: Optional[str] = None,
475
+ audience: Optional[str] = None
476
+ ):
477
+ self.secret_key = secret_key
478
+ self.algorithm = algorithm
479
+ self.issuer = issuer
480
+ self.audience = audience
481
+
482
+ def generate_token(
483
+ self,
484
+ subject: str,
485
+ payload: Optional[Dict] = None,
486
+ expires_in: int = 3600
487
+ ) -> str:
488
+ """Generate a JWT token."""
489
+ now = int(time.time())
490
+
491
+ jwt_payload = {
492
+ "sub": subject,
493
+ "iat": now,
494
+ "exp": now + expires_in,
495
+ **(payload or {})
496
+ }
497
+
498
+ if self.issuer:
499
+ jwt_payload["iss"] = self.issuer
500
+ if self.audience:
501
+ jwt_payload["aud"] = self.audience
502
+
503
+ return jwt.encode(jwt_payload, self.secret_key, algorithm=self.algorithm)
504
+
505
+ def validate_token(self, token: str) -> Dict:
506
+ """Validate and decode a JWT token."""
507
+ try:
508
+ return jwt.decode(
509
+ token,
510
+ self.secret_key,
511
+ algorithms=[self.algorithm],
512
+ issuer=self.issuer,
513
+ audience=self.audience
514
+ )
515
+ except jwt.ExpiredSignatureError:
516
+ raise ValueError("Token has expired")
517
+ except jwt.InvalidTokenError as e:
518
+ raise ValueError(f"Invalid token: {e}")
519
+ </content>
520
+ </create>
521
+
522
+
523
+ PHASE 3: MAKING REQUESTS
524
+
525
+
526
+ get requests
527
+
528
+ <read><file>src/api/base_client.py</file></read>
529
+
530
+ <edit>
531
+ <file>src/api/base_client.py</file>
532
+ <find>
533
+ def _get_headers(self) -> Dict[str, str]:
534
+ """Build default headers."""
535
+ headers = {
536
+ "Content-Type": "application/json",
537
+ "Accept": "application/json"
538
+ }
539
+ if self.api_key:
540
+ # common patterns: Bearer token, API key, or custom header
541
+ headers["Authorization"] = f"Bearer {self.api_key}"
542
+ # alternative: headers["X-API-Key"] = self.api_key
543
+ return headers
544
+ </find>
545
+ <replace>
546
+ def _get_headers(self) -> Dict[str, str]:
547
+ """Build default headers."""
548
+ headers = {
549
+ "Content-Type": "application/json",
550
+ "Accept": "application/json"
551
+ }
552
+ if self.api_key:
553
+ headers["Authorization"] = f"Bearer {self.api_key}"
554
+ return headers
555
+
556
+ def get(self, path: str, params: Optional[Dict] = None) -> Response:
557
+ """Make GET request."""
558
+ url = self._build_url(path)
559
+ response = self.session.get(
560
+ url,
561
+ headers=self._get_headers(),
562
+ params=params,
563
+ timeout=self.timeout
564
+ )
565
+ response.raise_for_status()
566
+ return response
567
+
568
+ def post(self, path: str, data: Optional[Dict] = None, json: Optional[Dict] = None) -> Response:
569
+ """Make POST request."""
570
+ url = self._build_url(path)
571
+ response = self.session.post(
572
+ url,
573
+ headers=self._get_headers(),
574
+ data=data,
575
+ json=json,
576
+ timeout=self.timeout
577
+ )
578
+ response.raise_for_status()
579
+ return response
580
+
581
+ def put(self, path: str, data: Optional[Dict] = None, json: Optional[Dict] = None) -> Response:
582
+ """Make PUT request."""
583
+ url = self._build_url(path)
584
+ response = self.session.put(
585
+ url,
586
+ headers=self._get_headers(),
587
+ data=data,
588
+ json=json,
589
+ timeout=self.timeout
590
+ )
591
+ response.raise_for_status()
592
+ return response
593
+
594
+ def patch(self, path: str, data: Optional[Dict] = None, json: Optional[Dict] = None) -> Response:
595
+ """Make PATCH request."""
596
+ url = self._build_url(path)
597
+ response = self.session.patch(
598
+ url,
599
+ headers=self._get_headers(),
600
+ data=data,
601
+ json=json,
602
+ timeout=self.timeout
603
+ )
604
+ response.raise_for_status()
605
+ return response
606
+
607
+ def delete(self, path: str) -> Response:
608
+ """Make DELETE request."""
609
+ url = self._build_url(path)
610
+ response = self.session.delete(
611
+ url,
612
+ headers=self._get_headers(),
613
+ timeout=self.timeout
614
+ )
615
+ response.raise_for_status()
616
+ return response
617
+ </replace>
618
+ </edit>
619
+
620
+
621
+ add missing imports
622
+ <read><file>src/api/base_client.py</file></read>
623
+
624
+ <edit>
625
+ <file>src/api/base_client.py</file>
626
+ <find>
627
+ """Base API client with common functionality."""
628
+ from typing import Any, Dict, Optional
629
+ import requests
630
+ from requests.adapters import HTTPAdapter
631
+ from urllib3.util.retry import Retry
632
+ </find>
633
+ <replace>
634
+ """Base API client with common functionality."""
635
+ from typing import Any, Dict, Optional
636
+ import requests
637
+ from requests.adapters import HTTPAdapter
638
+ from requests.models import Response
639
+ from urllib3.util.retry import Retry
640
+ </replace>
641
+ </edit>
642
+
643
+
644
+ async requests
645
+
646
+ <read><file>src/api/async_client.py</file></read>
647
+
648
+ <edit>
649
+ <file>src/api/async_client.py</file>
650
+ <find>
651
+ def _ensure_connected(self):
652
+ """Ensure client is connected."""
653
+ if self._client is None:
654
+ raise RuntimeError("Client not connected. Use async with or call connect().")
655
+ </find>
656
+ <replace>
657
+ def _ensure_connected(self):
658
+ """Ensure client is connected."""
659
+ if self._client is None:
660
+ raise RuntimeError("Client not connected. Use async with or call connect().")
661
+
662
+ async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Response:
663
+ """Make async GET request."""
664
+ self._ensure_connected()
665
+ return await self._client.get(path, params=params)
666
+
667
+ async def post(
668
+ self,
669
+ path: str,
670
+ data: Optional[Dict[str, Any]] = None,
671
+ json: Optional[Dict[str, Any]] = None
672
+ ) -> Response:
673
+ """Make async POST request."""
674
+ self._ensure_connected()
675
+ return await self._client.post(path, data=data, json=json)
676
+
677
+ async def put(
678
+ self,
679
+ path: str,
680
+ data: Optional[Dict[str, Any]] = None,
681
+ json: Optional[Dict[str, Any]] = None
682
+ ) -> Response:
683
+ """Make async PUT request."""
684
+ self._ensure_connected()
685
+ return await self._client.put(path, data=data, json=json)
686
+
687
+ async def patch(
688
+ self,
689
+ path: str,
690
+ data: Optional[Dict[str, Any]] = None,
691
+ json: Optional[Dict[str, Any]] = None
692
+ ) -> Response:
693
+ """Make async PATCH request."""
694
+ self._ensure_connected()
695
+ return await self._client.patch(path, data=data, json=json)
696
+
697
+ async def delete(self, path: str) -> Response:
698
+ """Make async DELETE request."""
699
+ self._ensure_connected()
700
+ return await self._client.delete(path)
701
+ </replace>
702
+ </edit>
703
+
704
+
705
+ PHASE 4: ERROR HANDLING
706
+
707
+
708
+ custom exception hierarchy
709
+
710
+ <create>
711
+ <file>src/api/exceptions.py</file>
712
+ <content>
713
+ """API exception hierarchy for specific error handling."""
714
+
715
+
716
+ class APIError(Exception):
717
+ """Base exception for all API errors."""
718
+
719
+ def __init__(
720
+ self,
721
+ message: str,
722
+ status_code: Optional[int] = None,
723
+ response_data: Optional[dict] = None
724
+ ):
725
+ super().__init__(message)
726
+ self.status_code = status_code
727
+ self.response_data = response_data or {}
728
+
729
+
730
+ class AuthenticationError(APIError):
731
+ """Authentication failed - invalid credentials."""
732
+ pass
733
+
734
+
735
+ class AuthorizationError(APIError):
736
+ """Authorization failed - insufficient permissions."""
737
+ pass
738
+
739
+
740
+ class RateLimitError(APIError):
741
+ """Rate limit exceeded."""
742
+
743
+ def __init__(
744
+ self,
745
+ message: str,
746
+ retry_after: Optional[int] = None,
747
+ response_data: Optional[dict] = None
748
+ ):
749
+ super().__init__(message, status_code=429, response_data=response_data)
750
+ self.retry_after = retry_after
751
+
752
+
753
+ class ValidationError(APIError):
754
+ """Request validation failed (400)."""
755
+ pass
756
+
757
+
758
+ class NotFoundError(APIError):
759
+ """Resource not found (404)."""
760
+ pass
761
+
762
+
763
+ class ConflictError(APIError):
764
+ """Resource conflict (409)."""
765
+ pass
766
+
767
+
768
+ class ServerError(APIError):
769
+ """Server error (5xx)."""
770
+ pass
771
+
772
+
773
+ class TimeoutError(APIError):
774
+ """Request timed out."""
775
+ pass
776
+
777
+
778
+ class ConnectionError(APIError):
779
+ """Connection failed."""
780
+ pass
781
+ </content>
782
+ </create>
783
+
784
+
785
+ error handling middleware
786
+
787
+ <read><file>src/api/base_client.py</file></read>
788
+
789
+ <edit>
790
+ <file>src/api/base_client.py</file>
791
+ <find>
792
+ """Base API client with common functionality."""
793
+ from typing import Any, Dict, Optional
794
+ import requests
795
+ from requests.adapters import HTTPAdapter
796
+ from requests.models import Response
797
+ from urllib3.util.retry import Retry
798
+ </find>
799
+ <replace>
800
+ """Base API client with common functionality."""
801
+ from typing import Any, Dict, Optional
802
+ import requests
803
+ from requests.adapters import HTTPAdapter
804
+ from requests.models import Response
805
+ from urllib3.util.retry import Retry
806
+
807
+ from .exceptions import (
808
+ APIError,
809
+ AuthenticationError,
810
+ AuthorizationError,
811
+ RateLimitError,
812
+ ValidationError,
813
+ NotFoundError,
814
+ ConflictError,
815
+ ServerError
816
+ )
817
+ </replace>
818
+ </edit>
819
+
820
+ <edit>
821
+ <file>src/api/base_client.py</file>
822
+ <find>
823
+ def delete(self, path: str) -> Response:
824
+ """Make DELETE request."""
825
+ url = self._build_url(path)
826
+ response = self.session.delete(
827
+ url,
828
+ headers=self._get_headers(),
829
+ timeout=self.timeout
830
+ )
831
+ response.raise_for_status()
832
+ return response
833
+ </find>
834
+ <replace>
835
+ def delete(self, path: str) -> Response:
836
+ """Make DELETE request."""
837
+ url = self._build_url(path)
838
+ response = self.session.delete(
839
+ url,
840
+ headers=self._get_headers(),
841
+ timeout=self.timeout
842
+ )
843
+ self._handle_errors(response)
844
+ return response
845
+
846
+ def _handle_errors(self, response: Response):
847
+ """Handle API response errors with specific exceptions."""
848
+ if response.ok:
849
+ return
850
+
851
+ status_code = response.status_code
852
+
853
+ try:
854
+ error_data = response.json()
855
+ message = error_data.get("message", error_data.get("error", "Unknown error"))
856
+ except ValueError:
857
+ error_data = {}
858
+ message = response.text or "Unknown error"
859
+
860
+ if status_code == 401:
861
+ raise AuthenticationError(message, status_code, error_data)
862
+ elif status_code == 403:
863
+ raise AuthorizationError(message, status_code, error_data)
864
+ elif status_code == 404:
865
+ raise NotFoundError(message, status_code, error_data)
866
+ elif status_code == 409:
867
+ raise ConflictError(message, status_code, error_data)
868
+ elif status_code == 429:
869
+ retry_after = response.headers.get("Retry-After")
870
+ retry_after = int(retry_after) if retry_after else None
871
+ raise RateLimitError(message, retry_after, error_data)
872
+ elif 400 <= status_code < 500:
873
+ raise ValidationError(message, status_code, error_data)
874
+ elif 500 <= status_code < 600:
875
+ raise ServerError(message, status_code, error_data)
876
+ else:
877
+ raise APIError(message, status_code, error_data)
878
+ </replace>
879
+ </edit>
880
+
881
+
882
+ PHASE 5: RATE LIMITING
883
+
884
+
885
+ understanding rate limits
886
+
887
+ common rate limit types:
888
+ - requests per minute/hour
889
+ - concurrent connections
890
+ - burst allowance
891
+ - tiered limits (free vs paid)
892
+
893
+ check headers:
894
+ - X-RateLimit-Limit
895
+ - X-RateLimit-Remaining
896
+ - X-RateLimit-Reset
897
+ - Retry-After
898
+
899
+
900
+ token bucket rate limiter
901
+
902
+ <create>
903
+ <file>src/api/rate_limiter.py</file>
904
+ <content>
905
+ """Rate limiting for API requests."""
906
+ from typing import Optional
907
+ import time
908
+ from collections import deque
909
+ from threading import Lock
910
+
911
+
912
+ class TokenBucket:
913
+ """Token bucket rate limiter."""
914
+
915
+ def __init__(self, rate: float, capacity: int):
916
+ """
917
+ Args:
918
+ rate: tokens per second
919
+ capacity: bucket capacity
920
+ """
921
+ self.rate = rate
922
+ self.capacity = capacity
923
+ self.tokens = float(capacity)
924
+ self.last_update = time.time()
925
+ self._lock = Lock()
926
+
927
+ def _refill(self):
928
+ """Refill tokens based on elapsed time."""
929
+ now = time.time()
930
+ elapsed = now - self.last_update
931
+ self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
932
+ self.last_update = now
933
+
934
+ def acquire(self, tokens: float = 1.0) -> bool:
935
+ """Try to acquire tokens. Returns True if successful."""
936
+ with self._lock:
937
+ self._refill()
938
+ if self.tokens >= tokens:
939
+ self.tokens -= tokens
940
+ return True
941
+ return False
942
+
943
+ def wait_for_token(self, tokens: float = 1.0):
944
+ """Wait until tokens are available."""
945
+ while not self.acquire(tokens):
946
+ # calculate wait time
947
+ self._refill()
948
+ deficit = tokens - self.tokens
949
+ wait_time = deficit / self.rate
950
+ if wait_time > 0:
951
+ time.sleep(wait_time)
952
+
953
+
954
+ class SlidingWindowRateLimiter:
955
+ """Sliding window rate limiter."""
956
+
957
+ def __init__(self, max_requests: int, window_seconds: int):
958
+ """
959
+ Args:
960
+ max_requests: maximum requests in window
961
+ window_seconds: time window in seconds
962
+ """
963
+ self.max_requests = max_requests
964
+ self.window_seconds = window_seconds
965
+ self.requests = deque()
966
+ self._lock = Lock()
967
+
968
+ def _clean_old_requests(self):
969
+ """Remove requests outside the time window."""
970
+ now = time.time()
971
+ cutoff = now - self.window_seconds
972
+ while self.requests and self.requests[0] < cutoff:
973
+ self.requests.popleft()
974
+
975
+ def acquire(self) -> bool:
976
+ """Try to acquire a request slot."""
977
+ with self._lock:
978
+ self._clean_old_requests()
979
+ if len(self.requests) < self.max_requests:
980
+ self.requests.append(time.time())
981
+ return True
982
+ return False
983
+
984
+ def wait_for_slot(self):
985
+ """Wait until a request slot is available."""
986
+ while not self.acquire():
987
+ self._clean_old_requests()
988
+ if self.requests:
989
+ # wait until oldest request expires
990
+ oldest = self.requests[0]
991
+ wait_time = self.window_seconds - (time.time() - oldest)
992
+ if wait_time > 0:
993
+ time.sleep(wait_time)
994
+ </content>
995
+ </create>
996
+
997
+
998
+ adaptive rate limiting
999
+
1000
+ <create>
1001
+ <file>src/api/adaptive_limiter.py</file>
1002
+ <content>
1003
+ """Adaptive rate limiter that responds to server signals."""
1004
+ from typing import Optional
1005
+ import time
1006
+ from .rate_limiter import TokenBucket
1007
+ from .exceptions import RateLimitError
1008
+
1009
+
1010
+ class AdaptiveRateLimiter:
1011
+ """Rate limiter that adapts based on API responses."""
1012
+
1013
+ def __init__(
1014
+ self,
1015
+ initial_rate: float = 10.0,
1016
+ min_rate: float = 1.0,
1017
+ max_rate: float = 100.0
1018
+ ):
1019
+ self.initial_rate = initial_rate
1020
+ self.min_rate = min_rate
1021
+ self.max_rate = max_rate
1022
+ self.current_rate = initial_rate
1023
+ self.bucket = TokenBucket(rate=initial_rate, capacity=10)
1024
+ self.last_error_time: Optional[float] = None
1025
+ self.consecutive_errors = 0
1026
+
1027
+ def acquire(self) -> bool:
1028
+ """Acquire a token."""
1029
+ return self.bucket.acquire()
1030
+
1031
+ def wait_for_token(self):
1032
+ """Wait until token available."""
1033
+ self.bucket.wait_for_token()
1034
+
1035
+ def report_success(self):
1036
+ """Report successful request - can increase rate."""
1037
+ self.consecutive_errors = 0
1038
+
1039
+ # gradually increase rate back to initial
1040
+ if self.current_rate < self.initial_rate:
1041
+ self.current_rate = min(self.initial_rate, self.current_rate * 1.1)
1042
+ self.bucket = TokenBucket(rate=self.current_rate, capacity=10)
1043
+
1044
+ def report_rate_limit_error(self, error: RateLimitError):
1045
+ """Report rate limit error - decrease rate."""
1046
+ self.consecutive_errors += 1
1047
+ self.last_error_time = time.time()
1048
+
1049
+ # reduce rate based on consecutive errors
1050
+ reduction_factor = 0.5 ** self.consecutive_errors
1051
+ self.current_rate = max(
1052
+ self.min_rate,
1053
+ self.current_rate * reduction_factor
1054
+ )
1055
+ self.bucket = TokenBucket(rate=self.current_rate, capacity=10)
1056
+
1057
+ # respect retry-after if provided
1058
+ if error.retry_after:
1059
+ wait_time = error.retry_after
1060
+ else:
1061
+ wait_time = 2.0 ** self.consecutive_errors # exponential backoff
1062
+
1063
+ time.sleep(wait_time)
1064
+ </content>
1065
+ </create>
1066
+
1067
+
1068
+ PHASE 6: RETRY STRATEGIES
1069
+
1070
+
1071
+ exponential backoff
1072
+
1073
+ <create>
1074
+ <file>src/api/retry.py</file>
1075
+ <content>
1076
+ """Retry strategies for API calls."""
1077
+ from typing import Optional, Callable, Type, Tuple
1078
+ import time
1079
+ import random
1080
+
1081
+
1082
+ def calculate_backoff(
1083
+ attempt: int,
1084
+ base_delay: float = 1.0,
1085
+ max_delay: float = 60.0,
1086
+ exponential_base: float = 2.0,
1087
+ jitter: bool = True
1088
+ ) -> float:
1089
+ """Calculate exponential backoff delay."""
1090
+ delay = min(base_delay * (exponential_base ** attempt), max_delay)
1091
+
1092
+ if jitter:
1093
+ # add randomness to prevent thundering herd
1094
+ delay = delay * (0.5 + random.random() * 0.5)
1095
+
1096
+ return delay
1097
+
1098
+
1099
+ class RetryConfig:
1100
+ """Configuration for retry behavior."""
1101
+
1102
+ def __init__(
1103
+ self,
1104
+ max_attempts: int = 3,
1105
+ base_delay: float = 1.0,
1106
+ max_delay: float = 60.0,
1107
+ retryable_status_codes: Tuple[int, ...] = (429, 500, 502, 503, 504),
1108
+ retryable_exceptions: Tuple[Type[Exception], ...] = (
1109
+ ConnectionError,
1110
+ TimeoutError
1111
+ )
1112
+ ):
1113
+ self.max_attempts = max_attempts
1114
+ self.base_delay = base_delay
1115
+ self.max_delay = max_delay
1116
+ self.retryable_status_codes = retryable_status_codes
1117
+ self.retryable_exceptions = retryable_exceptions
1118
+
1119
+
1120
+ def retry_with_backoff(
1121
+ func: Callable,
1122
+ config: Optional[RetryConfig] = None,
1123
+ on_retry: Optional[Callable[[int, Exception], None]] = None
1124
+ ):
1125
+ """Decorator for retrying function calls with exponential backoff."""
1126
+
1127
+ if config is None:
1128
+ config = RetryConfig()
1129
+
1130
+ def wrapper(*args, **kwargs):
1131
+ last_exception = None
1132
+
1133
+ for attempt in range(config.max_attempts):
1134
+ try:
1135
+ return func(*args, **kwargs)
1136
+ except Exception as e:
1137
+ last_exception = e
1138
+
1139
+ # check if exception is retryable
1140
+ if not isinstance(e, config.retryable_exceptions):
1141
+ raise
1142
+
1143
+ # check if should retry
1144
+ if attempt < config.max_attempts - 1:
1145
+ delay = calculate_backoff(attempt, config.base_delay, config.max_delay)
1146
+
1147
+ if on_retry:
1148
+ on_retry(attempt + 1, e)
1149
+
1150
+ time.sleep(delay)
1151
+ else:
1152
+ raise
1153
+
1154
+ raise last_exception
1155
+
1156
+ return wrapper
1157
+ </content>
1158
+ </create>
1159
+
1160
+
1161
+ usage example
1162
+
1163
+ from src.api.retry import retry_with_backoff, RetryConfig
1164
+ from src.api.exceptions import ServerError
1165
+
1166
+ config = RetryConfig(
1167
+ max_attempts=5,
1168
+ base_delay=0.5,
1169
+ max_delay=30.0,
1170
+ retryable_status_codes=(429, 500, 502, 503, 504)
1171
+ )
1172
+
1173
+ @retry_with_backoff(config=config)
1174
+ def fetch_user_data(user_id: int):
1175
+ return client.get(f"/users/{user_id}")
1176
+
1177
+
1178
+ PHASE 7: PAGINATION
1179
+
1180
+
1181
+ cursor-based pagination
1182
+
1183
+ <create>
1184
+ <file>src/api/pagination.py</file>
1185
+ <content>
1186
+ """Pagination handling for API responses."""
1187
+ from typing import Iterator, List, Optional, TypeVar, Generic
1188
+
1189
+
1190
+ T = TypeVar("T")
1191
+
1192
+
1193
+ class CursorPage(Generic[T]):
1194
+ """Single page of cursor-paginated results."""
1195
+
1196
+ def __init__(
1197
+ self,
1198
+ items: List[T],
1199
+ next_cursor: Optional[str] = None,
1200
+ has_more: bool = False
1201
+ ):
1202
+ self.items = items
1203
+ self.next_cursor = next_cursor
1204
+ self.has_more = has_more
1205
+
1206
+
1207
+ class CursorPaginator(Generic[T]):
1208
+ """Iterator for cursor-based pagination."""
1209
+
1210
+ def __init__(self, fetch_function: callable, page_size: int = 100):
1211
+ """
1212
+ Args:
1213
+ fetch_function: callable that takes (cursor, limit) and returns CursorPage
1214
+ page_size: number of items per page
1215
+ """
1216
+ self.fetch_function = fetch_function
1217
+ self.page_size = page_size
1218
+
1219
+ def __iter__(self) -> Iterator[T]:
1220
+ """Iterate through all pages."""
1221
+ cursor = None
1222
+ while True:
1223
+ page = self.fetch_function(cursor=cursor, limit=self.page_size)
1224
+ yield from page.items
1225
+
1226
+ if not page.has_more or not page.next_cursor:
1227
+ break
1228
+ cursor = page.next_cursor
1229
+
1230
+ def get_all(self) -> List[T]:
1231
+ """Fetch all items as a list."""
1232
+ return list(self.__iter__())
1233
+
1234
+
1235
+ class OffsetPage(Generic[T]):
1236
+ """Single page of offset-based results."""
1237
+
1238
+ def __init__(
1239
+ self,
1240
+ items: List[T],
1241
+ total: int,
1242
+ offset: int,
1243
+ limit: int
1244
+ ):
1245
+ self.items = items
1246
+ self.total = total
1247
+ self.offset = offset
1248
+ self.limit = limit
1249
+
1250
+ @property
1251
+ def has_more(self) -> bool:
1252
+ """Check if more pages available."""
1253
+ return self.offset + self.limit < self.total
1254
+
1255
+
1256
+ class OffsetPaginator(Generic[T]):
1257
+ """Iterator for offset-based pagination."""
1258
+
1259
+ def __init__(
1260
+ self,
1261
+ fetch_function: callable,
1262
+ page_size: int = 100,
1263
+ starting_offset: int = 0
1264
+ ):
1265
+ """
1266
+ Args:
1267
+ fetch_function: callable that takes (offset, limit) and returns OffsetPage
1268
+ page_size: number of items per page
1269
+ starting_offset: initial offset
1270
+ """
1271
+ self.fetch_function = fetch_function
1272
+ self.page_size = page_size
1273
+ self.starting_offset = starting_offset
1274
+
1275
+ def __iter__(self) -> Iterator[T]:
1276
+ """Iterate through all pages."""
1277
+ offset = self.starting_offset
1278
+
1279
+ while True:
1280
+ page = self.fetch_function(offset=offset, limit=self.page_size)
1281
+ yield from page.items
1282
+
1283
+ if not page.has_more:
1284
+ break
1285
+ offset += self.page_size
1286
+
1287
+ def get_all(self) -> List[T]:
1288
+ """Fetch all items as a list."""
1289
+ return list(self.__iter__())
1290
+
1291
+ def page_at(self, page_number: int) -> List[T]:
1292
+ """Get items at specific page number (1-indexed)."""
1293
+ offset = (page_number - 1) * self.page_size
1294
+ page = self.fetch_function(offset=offset, limit=self.page_size)
1295
+ return page.items
1296
+ </content>
1297
+ </create>
1298
+
1299
+
1300
+ PHASE 8: RESPONSE VALIDATION
1301
+
1302
+
1303
+ pydantic models for validation
1304
+
1305
+ <create>
1306
+ <file>src/api/models.py</file>
1307
+ <content>
1308
+ """Pydantic models for API request/response validation."""
1309
+ from typing import List, Optional, Generic, TypeVar
1310
+ from datetime import datetime
1311
+ from pydantic import BaseModel, Field, validator
1312
+ from enum import Enum
1313
+
1314
+
1315
+ class UserRole(str, Enum):
1316
+ """User role enumeration."""
1317
+ ADMIN = "admin"
1318
+ USER = "user"
1319
+ GUEST = "guest"
1320
+
1321
+
1322
+ class User(BaseModel):
1323
+ """User model."""
1324
+ id: int = Field(..., description="Unique user identifier")
1325
+ name: str = Field(..., min_length=1, max_length=100)
1326
+ email: str = Field(..., regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
1327
+ role: UserRole = UserRole.USER
1328
+ created_at: datetime
1329
+ updated_at: Optional[datetime] = None
1330
+
1331
+ @validator("email")
1332
+ def email_must_be_lowercase(cls, v):
1333
+ """Ensure email is lowercase."""
1334
+ return v.lower()
1335
+
1336
+
1337
+ class CreateUserRequest(BaseModel):
1338
+ """Request model for creating user."""
1339
+ name: str = Field(..., min_length=1, max_length=100)
1340
+ email: str = Field(..., regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
1341
+ role: UserRole = UserRole.USER
1342
+ password: str = Field(..., min_length=8, max_length=100)
1343
+
1344
+
1345
+ class UpdateUserRequest(BaseModel):
1346
+ """Request model for updating user."""
1347
+ name: Optional[str] = Field(None, min_length=1, max_length=100)
1348
+ email: Optional[str] = Field(None, regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
1349
+ role: Optional[UserRole] = None
1350
+
1351
+
1352
+ class UserListResponse(BaseModel):
1353
+ """Response model for user list."""
1354
+ items: List[User]
1355
+ total: int
1356
+ page: int
1357
+ page_size: int
1358
+ has_more: bool
1359
+
1360
+
1361
+ class ErrorResponse(BaseModel):
1362
+ """Error response model."""
1363
+ error: str
1364
+ message: str
1365
+ details: Optional[dict] = None
1366
+ </content>
1367
+ </create>
1368
+
1369
+
1370
+ response parser
1371
+
1372
+ <create>
1373
+ <file>src/api/response_parser.py</file>
1374
+ <content>
1375
+ """Response parsing and validation."""
1376
+ from typing import TypeVar, Type, Optional
1377
+ from pydantic import BaseModel, ValidationError
1378
+ from .exceptions import APIError
1379
+
1380
+
1381
+ T = TypeVar("T", bound=BaseModel)
1382
+
1383
+
1384
+ class ResponseParser:
1385
+ """Parse and validate API responses."""
1386
+
1387
+ @staticmethod
1388
+ def parse(response_data: dict, model: Type[T]) -> T:
1389
+ """Parse response data into pydantic model."""
1390
+ try:
1391
+ return model(**response_data)
1392
+ except ValidationError as e:
1393
+ raise APIError(
1394
+ f"Response validation failed: {e}",
1395
+ response_data={"validation_errors": e.errors()}
1396
+ )
1397
+
1398
+ @staticmethod
1399
+ def parse_optional(response_data: Optional[dict], model: Type[T]) -> Optional[T]:
1400
+ """Parse optional response data."""
1401
+ if response_data is None:
1402
+ return None
1403
+ return ResponseParser.parse(response_data, model)
1404
+
1405
+ @staticmethod
1406
+ def parse_list(response_data: dict, items_key: str, model: Type[T]) -> list:
1407
+ """Parse response containing a list of items."""
1408
+ if items_key not in response_data:
1409
+ raise APIError(f"Response missing key: {items_key}")
1410
+
1411
+ items = response_data[items_key]
1412
+ if not isinstance(items, list):
1413
+ raise APIError(f"Expected list for key {items_key}, got {type(items)}")
1414
+
1415
+ result = []
1416
+ for item in items:
1417
+ try:
1418
+ result.append(model(**item))
1419
+ except ValidationError as e:
1420
+ raise APIError(f"Item validation failed: {e}")
1421
+ return result
1422
+ </content>
1423
+ </create>
1424
+
1425
+
1426
+ PHASE 9: CACHING STRATEGIES
1427
+
1428
+
1429
+ simple in-memory cache
1430
+
1431
+ <create>
1432
+ <file>src/api/cache.py</file>
1433
+ <content>
1434
+ """Caching for API responses."""
1435
+ from typing import Optional, Dict, Any, Callable
1436
+ from datetime import datetime, timedelta
1437
+ from functools import wraps
1438
+ from hashlib import sha256
1439
+ import json
1440
+
1441
+
1442
+ class CacheEntry:
1443
+ """Single cache entry."""
1444
+
1445
+ def __init__(self, value: Any, ttl_seconds: int):
1446
+ self.value = value
1447
+ self.expires_at = datetime.now() + timedelta(seconds=ttl_seconds)
1448
+
1449
+ @property
1450
+ def is_expired(self) -> bool:
1451
+ """Check if entry has expired."""
1452
+ return datetime.now() >= self.expires_at
1453
+
1454
+
1455
+ class MemoryCache:
1456
+ """Simple in-memory cache with TTL."""
1457
+
1458
+ def __init__(self):
1459
+ self._storage: Dict[str, CacheEntry] = {}
1460
+
1461
+ def get(self, key: str) -> Optional[Any]:
1462
+ """Get value from cache."""
1463
+ entry = self._storage.get(key)
1464
+ if entry is None:
1465
+ return None
1466
+ if entry.is_expired:
1467
+ del self._storage[key]
1468
+ return None
1469
+ return entry.value
1470
+
1471
+ def set(self, key: str, value: Any, ttl_seconds: int = 300):
1472
+ """Set value in cache."""
1473
+ self._storage[key] = CacheEntry(value, ttl_seconds)
1474
+
1475
+ def invalidate(self, key: str):
1476
+ """Invalidate cache entry."""
1477
+ self._storage.pop(key, None)
1478
+
1479
+ def clear(self):
1480
+ """Clear all cache entries."""
1481
+ self._storage.clear()
1482
+
1483
+ def cleanup_expired(self):
1484
+ """Remove all expired entries."""
1485
+ expired_keys = [
1486
+ k for k, v in self._storage.items()
1487
+ if v.is_expired
1488
+ ]
1489
+ for key in expired_keys:
1490
+ del self._storage[key]
1491
+
1492
+
1493
+ def cache_response(
1494
+ cache: MemoryCache,
1495
+ ttl_seconds: int = 300,
1496
+ key_prefix: str = ""
1497
+ ):
1498
+ """Decorator for caching API responses."""
1499
+
1500
+ def decorator(func: Callable) -> Callable:
1501
+ @wraps(func)
1502
+ def wrapper(*args, **kwargs):
1503
+ # generate cache key
1504
+ key_parts = [key_prefix]
1505
+ key_parts.extend(str(a) for a in args)
1506
+ key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
1507
+ cache_key = sha256("|".join(key_parts).encode()).hexdigest()
1508
+
1509
+ # try cache first
1510
+ cached = cache.get(cache_key)
1511
+ if cached is not None:
1512
+ return cached
1513
+
1514
+ # call function and cache result
1515
+ result = func(*args, **kwargs)
1516
+ cache.set(cache_key, result, ttl_seconds)
1517
+ return result
1518
+
1519
+ return wrapper
1520
+
1521
+ return decorator
1522
+ </content>
1523
+ </create>
1524
+
1525
+
1526
+ PHASE 10: GRAPHQL INTEGRATION
1527
+
1528
+
1529
+ graphql client
1530
+
1531
+ <create>
1532
+ <file>src/api/graphql_client.py</file>
1533
+ <content>
1534
+ """GraphQL API client."""
1535
+ from typing import Any, Dict, Optional, List
1536
+ import requests
1537
+ from .base_client import BaseAPIClient
1538
+ from .exceptions import APIError
1539
+
1540
+
1541
+ class GraphQLClient(BaseAPIClient):
1542
+ """Client for GraphQL APIs."""
1543
+
1544
+ def __init__(self, base_url: str, api_key: Optional[str] = None):
1545
+ super().__init__(base_url, api_key)
1546
+ # GraphQL typically doesn't use Accept: application/json
1547
+ # but some implementations do
1548
+
1549
+ def execute(
1550
+ self,
1551
+ query: str,
1552
+ variables: Optional[Dict[str, Any]] = None,
1553
+ operation_name: Optional[str] = None
1554
+ ) -> Dict[str, Any]:
1555
+ """Execute GraphQL query."""
1556
+ payload = {"query": query}
1557
+
1558
+ if variables:
1559
+ payload["variables"] = variables
1560
+ if operation_name:
1561
+ payload["operationName"] = operation_name
1562
+
1563
+ response = self.session.post(
1564
+ self._build_url(""),
1565
+ json=payload,
1566
+ headers=self._get_headers()
1567
+ )
1568
+ self._handle_errors(response)
1569
+
1570
+ data = response.json()
1571
+
1572
+ # check for GraphQL errors
1573
+ if "errors" in data:
1574
+ errors = data["errors"]
1575
+ messages = [e.get("message", str(e)) for e in errors]
1576
+ raise APIError(f"GraphQL errors: {', '.join(messages)}")
1577
+
1578
+ return data.get("data", {})
1579
+
1580
+ def query(
1581
+ self,
1582
+ query: str,
1583
+ variables: Optional[Dict[str, Any]] = None
1584
+ ) -> Dict[str, Any]:
1585
+ """Execute a GraphQL query."""
1586
+ return self.execute(query, variables)
1587
+
1588
+ def mutate(
1589
+ self,
1590
+ mutation: str,
1591
+ variables: Optional[Dict[str, Any]] = None
1592
+ ) -> Dict[str, Any]:
1593
+ """Execute a GraphQL mutation."""
1594
+ return self.execute(mutation, variables)
1595
+ </content>
1596
+ </create>
1597
+
1598
+
1599
+ graphql query builder
1600
+
1601
+ <create>
1602
+ <file>src/api/graphql_builder.py</file>
1603
+ <content>
1604
+ """GraphQL query builder for type-safe queries."""
1605
+ from typing import List, Optional, Dict, Any
1606
+
1607
+
1608
+ class GraphQLQueryBuilder:
1609
+ """Builder for constructing GraphQL queries."""
1610
+
1611
+ def __init__(self, operation_type: str = "query"):
1612
+ self.operation_type = operation_type
1613
+ self.name: Optional[str] = None
1614
+ self.fields: List[str] = []
1615
+ self.arguments: Dict[str, str] = {}
1616
+ self.fragments: List[str] = []
1617
+
1618
+ def name_op(self, name: str) -> "GraphQLQueryBuilder":
1619
+ """Set operation name."""
1620
+ self.name = name
1621
+ return self
1622
+
1623
+ def field(self, field_path: str) -> "GraphQLQueryBuilder":
1624
+ """Add a field to query."""
1625
+ self.fields.append(field_path)
1626
+ return self
1627
+
1628
+ def fields(self, *field_paths: str) -> "GraphQLQueryBuilder":
1629
+ """Add multiple fields."""
1630
+ self.fields.extend(field_paths)
1631
+ return self
1632
+
1633
+ def arg(self, key: str, value: Any) -> "GraphQLQueryBuilder":
1634
+ """Add argument to operation."""
1635
+ if isinstance(value, str):
1636
+ self.arguments[key] = f'"{value}"'
1637
+ elif isinstance(value, bool):
1638
+ self.arguments[key] = str(value).lower()
1639
+ elif value is None:
1640
+ self.arguments[key] = "null"
1641
+ else:
1642
+ self.arguments[key] = str(value)
1643
+ return self
1644
+
1645
+ def args(self, **kwargs: Any) -> "GraphQLQueryBuilder":
1646
+ """Add multiple arguments."""
1647
+ for key, value in kwargs.items():
1648
+ self.arg(key, value)
1649
+ return self
1650
+
1651
+ def fragment(self, fragment: str) -> "GraphQLQueryBuilder":
1652
+ """Add a fragment."""
1653
+ self.fragments.append(fragment)
1654
+ return self
1655
+
1656
+ def build(self) -> str:
1657
+ """Build the complete GraphQL query."""
1658
+ # operation declaration
1659
+ if self.name:
1660
+ args_str = ", ".join(f"${k}: {self._infer_type(v)}" for k, v in self.arguments.items())
1661
+ operation = f"{self.operation_type} {self.name}"
1662
+ if args_str:
1663
+ operation += f"({args_str})"
1664
+ else:
1665
+ operation = self.operation_type
1666
+
1667
+ # field arguments
1668
+ field_args = ""
1669
+ if self.arguments:
1670
+ field_args = "(" + ", ".join(f"{k}: ${k}" for k in self.arguments.keys()) + ")"
1671
+
1672
+ # selection set
1673
+ selection = "\n ".join(self.fields)
1674
+
1675
+ # combine
1676
+ query = f"{operation} {{{field_args}\n {selection}\n}}"
1677
+
1678
+ # add fragments
1679
+ if self.fragments:
1680
+ query += "\n\n" + "\n".join(self.fragments)
1681
+
1682
+ return query
1683
+
1684
+ def _infer_type(self, value: str) -> str:
1685
+ """Infer GraphQL type from formatted value."""
1686
+ if value.startswith('"'):
1687
+ return "String"
1688
+ if value == "true" or value == "false":
1689
+ return "Boolean"
1690
+ if value == "null":
1691
+ return "ID"
1692
+ if "." in value:
1693
+ return "Float"
1694
+ return "Int"
1695
+
1696
+
1697
+ def query(name: str) -> GraphQLQueryBuilder:
1698
+ """Start building a GraphQL query."""
1699
+ return GraphQLQueryBuilder("query").name_op(name)
1700
+
1701
+
1702
+ def mutation(name: str) -> GraphQLQueryBuilder:
1703
+ """Start building a GraphQL mutation."""
1704
+ return GraphQLQueryBuilder("mutation").name_op(name)
1705
+ </content>
1706
+ </create>
1707
+
1708
+
1709
+ PHASE 11: API TESTING
1710
+
1711
+
1712
+ testing with mock responses
1713
+
1714
+ <create>
1715
+ <file>tests/test_api_client.py</file>
1716
+ <content>
1717
+ """Tests for API client."""
1718
+ import pytest
1719
+ from unittest.mock import Mock, patch
1720
+ from src.api.base_client import BaseAPIClient
1721
+ from src.api.exceptions import NotFoundError, RateLimitError
1722
+
1723
+
1724
+ @pytest.fixture
1725
+ def mock_response():
1726
+ """Create mock response."""
1727
+ mock = Mock()
1728
+ mock.ok = True
1729
+ mock.status_code = 200
1730
+ mock.json.return_value = {"id": 1, "name": "Test"}
1731
+ return mock
1732
+
1733
+
1734
+ @pytest.fixture
1735
+ def client():
1736
+ """Create test client."""
1737
+ return BaseAPIClient(
1738
+ base_url="https://api.test.com",
1739
+ api_key="test_key"
1740
+ )
1741
+
1742
+
1743
+ def test_get_request_builds_correct_url(client, mock_response):
1744
+ """Test that GET builds correct URL."""
1745
+ with patch.object(client.session, "get", return_value=mock_response) as mock_get:
1746
+ client.get("/users/123")
1747
+
1748
+ mock_get.assert_called_once()
1749
+ called_url = mock_get.call_args[0][0]
1750
+ assert called_url == "https://api.test.com/users/123"
1751
+
1752
+
1753
+ def test_get_request_includes_auth_headers(client, mock_response):
1754
+ """Test that GET includes auth headers."""
1755
+ with patch.object(client.session, "get", return_value=mock_response) as mock_get:
1756
+ client.get("/users")
1757
+
1758
+ headers = mock_get.call_args[1]["headers"]
1759
+ assert "Authorization" in headers
1760
+ assert headers["Authorization"] == "Bearer test_key"
1761
+
1762
+
1763
+ def test_404_raises_not_found(client):
1764
+ """Test that 404 raises NotFoundError."""
1765
+ mock_resp = Mock()
1766
+ mock_resp.ok = False
1767
+ mock_resp.status_code = 404
1768
+ mock_resp.json.return_value = {"message": "Not found"}
1769
+
1770
+ with patch.object(client.session, "get", return_value=mock_resp):
1771
+ with pytest.raises(NotFoundError):
1772
+ client.get("/users/999")
1773
+
1774
+
1775
+ def test_429_raises_rate_limit_error(client):
1776
+ """Test that 429 raises RateLimitError."""
1777
+ mock_resp = Mock()
1778
+ mock_resp.ok = False
1779
+ mock_resp.status_code = 429
1780
+ mock_resp.headers = {"Retry-After": "60"}
1781
+ mock_resp.json.return_value = {"message": "Rate limit exceeded"}
1782
+
1783
+ with patch.object(client.session, "get", return_value=mock_resp):
1784
+ with pytest.raises(RateLimitError) as exc_info:
1785
+ client.get("/users")
1786
+
1787
+ assert exc_info.value.retry_after == 60
1788
+ </content>
1789
+ </create>
1790
+
1791
+
1792
+ PHASE 12: LOGGING AND MONITORING
1793
+
1794
+
1795
+ api client logging
1796
+
1797
+ <create>
1798
+ <file>src/api/logging.py</file>
1799
+ <content>
1800
+ """Logging configuration for API clients."""
1801
+ import logging
1802
+ import time
1803
+ from typing import Optional, Dict, Any
1804
+ from requests.models import Response, PreparedRequest
1805
+
1806
+
1807
+ class APILogger:
1808
+ """Structured logging for API calls."""
1809
+
1810
+ def __init__(self, name: str = "api"):
1811
+ self.logger = logging.getLogger(name)
1812
+
1813
+ def log_request(
1814
+ self,
1815
+ method: str,
1816
+ url: str,
1817
+ headers: Optional[Dict[str, str]] = None,
1818
+ body: Optional[Any] = None
1819
+ ):
1820
+ """Log outgoing request."""
1821
+ self.logger.debug(
1822
+ "API Request",
1823
+ extra={
1824
+ "event": "api_request",
1825
+ "method": method,
1826
+ "url": self._sanitize_url(url),
1827
+ "has_body": body is not None
1828
+ }
1829
+ )
1830
+
1831
+ def log_response(
1832
+ self,
1833
+ response: Response,
1834
+ duration_ms: float
1835
+ ):
1836
+ """Log received response."""
1837
+ self.logger.info(
1838
+ "API Response",
1839
+ extra={
1840
+ "event": "api_response",
1841
+ "status_code": response.status_code,
1842
+ "duration_ms": round(duration_ms, 2),
1843
+ "url": self._sanitize_url(str(response.url))
1844
+ }
1845
+ )
1846
+
1847
+ def log_error(
1848
+ self,
1849
+ error: Exception,
1850
+ duration_ms: Optional[float] = None
1851
+ ):
1852
+ """Log API error."""
1853
+ self.logger.error(
1854
+ "API Error",
1855
+ extra={
1856
+ "event": "api_error",
1857
+ "error_type": type(error).__name__,
1858
+ "error_message": str(error),
1859
+ "duration_ms": round(duration_ms, 2) if duration_ms else None
1860
+ },
1861
+ exc_info=error
1862
+ )
1863
+
1864
+ def _sanitize_url(self, url: str) -> str:
1865
+ """Remove sensitive parameters from URL."""
1866
+ # remove API keys, tokens, passwords from URL
1867
+ import re
1868
+ sanitized = re.sub(r'([?&](api_key|token|password)=)[^&]*', r'\1***', url)
1869
+ return sanitized
1870
+
1871
+
1872
+ class LoggedRequestMixin:
1873
+ """Mixin for adding logging to API clients."""
1874
+
1875
+ def __init__(self, *args, **kwargs):
1876
+ super().__init__(*args, **kwargs)
1877
+ self.logger = APILogger(f"api.{self.__class__.__name__}")
1878
+
1879
+ def _logged_request(self, method: str, *args, **kwargs):
1880
+ """Make request with logging."""
1881
+ import time
1882
+ start = time.time()
1883
+
1884
+ try:
1885
+ # log request
1886
+ self.logger.log_request(method, *args, **kwargs)
1887
+
1888
+ # make request
1889
+ response = super()._logged_request(method, *args, **kwargs)
1890
+
1891
+ # log response
1892
+ duration_ms = (time.time() - start) * 1000
1893
+ self.logger.log_response(response, duration_ms)
1894
+
1895
+ return response
1896
+
1897
+ except Exception as e:
1898
+ duration_ms = (time.time() - start) * 1000
1899
+ self.logger.log_error(e, duration_ms)
1900
+ raise
1901
+ </content>
1902
+ </create>
1903
+
1904
+
1905
+ PHASE 13: API DOCUMENTATION GENERATION
1906
+
1907
+
1908
+ openapi spec generator
1909
+
1910
+ <create>
1911
+ <file>src/api/openapi.py</file>
1912
+ <content>
1913
+ """Generate OpenAPI documentation for API clients."""
1914
+ from typing import Dict, Any, List, Optional
1915
+
1916
+
1917
+ class OpenAPIGenerator:
1918
+ """Generate OpenAPI specification from API client."""
1919
+
1920
+ def __init__(self, title: str, version: str = "1.0.0"):
1921
+ self.spec = {
1922
+ "openapi": "3.0.0",
1923
+ "info": {
1924
+ "title": title,
1925
+ "version": version
1926
+ },
1927
+ "servers": [],
1928
+ "paths": {},
1929
+ "components": {
1930
+ "schemas": {},
1931
+ "securitySchemes": {}
1932
+ }
1933
+ }
1934
+
1935
+ def add_server(self, url: str, description: Optional[str] = None):
1936
+ """Add server URL."""
1937
+ server = {"url": url}
1938
+ if description:
1939
+ server["description"] = description
1940
+ self.spec["servers"].append(server)
1941
+ return self
1942
+
1943
+ def add_path(
1944
+ self,
1945
+ path: str,
1946
+ method: str,
1947
+ summary: Optional[str] = None,
1948
+ description: Optional[str] = None,
1949
+ parameters: Optional[List[Dict]] = None,
1950
+ request_body: Optional[Dict] = None,
1951
+ responses: Optional[Dict[int, Dict]] = None,
1952
+ tags: Optional[List[str]] = None
1953
+ ):
1954
+ """Add path to specification."""
1955
+ if path not in self.spec["paths"]:
1956
+ self.spec["paths"][path] = {}
1957
+
1958
+ operation: Dict[str, Any] = {}
1959
+ if summary:
1960
+ operation["summary"] = summary
1961
+ if description:
1962
+ operation["description"] = description
1963
+ if parameters:
1964
+ operation["parameters"] = parameters
1965
+ if request_body:
1966
+ operation["requestBody"] = request_body
1967
+ if responses:
1968
+ operation["responses"] = responses
1969
+ if tags:
1970
+ operation["tags"] = tags
1971
+
1972
+ self.spec["paths"][path][method.lower()] = operation
1973
+ return self
1974
+
1975
+ def add_schema(self, name: str, schema: Dict[str, Any]):
1976
+ """Add schema to components."""
1977
+ self.spec["components"]["schemas"][name] = schema
1978
+ return self
1979
+
1980
+ def add_security_scheme(
1981
+ self,
1982
+ name: str,
1983
+ scheme_type: str,
1984
+ scheme: Optional[str] = None,
1985
+ bearer_format: Optional[str] = None
1986
+ ):
1987
+ """Add security scheme."""
1988
+ security_scheme: Dict[str, Any] = {"type": scheme_type}
1989
+ if scheme:
1990
+ security_scheme["scheme"] = scheme
1991
+ if bearer_format:
1992
+ security_scheme["bearerFormat"] = bearer_format
1993
+
1994
+ self.spec["components"]["securitySchemes"][name] = security_scheme
1995
+ return self
1996
+
1997
+ def generate(self) -> Dict[str, Any]:
1998
+ """Generate complete OpenAPI spec."""
1999
+ return self.spec
2000
+ </content>
2001
+ </create>
2002
+
2003
+
2004
+ PHASE 14: WEBHOOK HANDLING
2005
+
2006
+
2007
+ webhook signature verification
2008
+
2009
+ <create>
2010
+ <file>src/api/webhooks.py</file>
2011
+ <content>
2012
+ """Webhook signature verification and handling."""
2013
+ from typing import Optional, Callable
2014
+ from hashlib import hmac, sha256, sha512
2015
+ import json
2016
+
2017
+
2018
+ class WebhookVerifier:
2019
+ """Verify webhook signatures."""
2020
+
2021
+ def __init__(self, secret: str, header_name: str = "X-Signature"):
2022
+ self.secret = secret
2023
+ self.header_name = header_name
2024
+
2025
+ def verify(self, payload: bytes, signature: str) -> bool:
2026
+ """Verify webhook signature."""
2027
+ expected = self._compute_signature(payload)
2028
+ return hmac.compare_digest(expected, signature)
2029
+
2030
+ def _compute_signature(self, payload: bytes) -> str:
2031
+ """Compute HMAC signature."""
2032
+ mac = hmac.new(
2033
+ self.secret.encode(),
2034
+ payload,
2035
+ sha256
2036
+ )
2037
+ return f"sha256={mac.hexdigest()}"
2038
+
2039
+
2040
+ class WebhookHandler:
2041
+ """Handle incoming webhooks."""
2042
+
2043
+ def __init__(self, verifier: WebhookVerifier):
2044
+ self.verifier = verifier
2045
+ self.handlers: Dict[str, Callable] = {}
2046
+
2047
+ def on(self, event_type: str) -> Callable:
2048
+ """Decorator to register handler for event type."""
2049
+ def decorator(func: Callable):
2050
+ self.handlers[event_type] = func
2051
+ return func
2052
+ return decorator
2053
+
2054
+ def handle(self, payload: bytes, signature: str) -> Optional[Any]:
2055
+ """Handle incoming webhook."""
2056
+ if not self.verifier.verify(payload, signature):
2057
+ raise ValueError("Invalid webhook signature")
2058
+
2059
+ data = json.loads(payload)
2060
+ event_type = data.get("type") or data.get("event")
2061
+
2062
+ if event_type in self.handlers:
2063
+ return self.handlers[event_type](data)
2064
+
2065
+ return None
2066
+ </content>
2067
+ </create>
2068
+
2069
+
2070
+ PHASE 15: API INTEGRATION RULES
2071
+
2072
+
2073
+ while this skill is active, these rules are MANDATORY:
2074
+
2075
+ [1] ALWAYS implement rate limiting
2076
+ never assume the API can handle unlimited requests
2077
+ implement client-side limits even if server has limits
2078
+
2079
+ [2] NEVER hardcode API credentials
2080
+ use environment variables or secure vaults
2081
+ add .env to .gitignore immediately
2082
+
2083
+ [3] ALWAYS validate responses
2084
+ use pydantic models for type safety
2085
+ never trust API documentation alone
2086
+
2087
+ [4] IMPLEMENT retry logic with exponential backoff
2088
+ transient failures are common
2089
+ use jitter to prevent thundering herd
2090
+
2091
+ [5] LOG all API calls
2092
+ log request, response, duration
2093
+ sanitize sensitive data in logs
2094
+
2095
+ [6] HANDLE errors specifically
2096
+ catch specific exceptions, not generic Exception
2097
+ map API errors to domain errors
2098
+
2099
+ [7] USE async clients for high-volume operations
2100
+ httpx > requests for concurrent requests
2101
+ respect connection limits
2102
+
2103
+ [8] CACHE when appropriate
2104
+ cache GET requests that rarely change
2105
+ respect cache-control headers
2106
+
2107
+ [9] TIMEOUT every request
2108
+ never wait forever
2109
+ set reasonable defaults (30s for sync, 60s for async)
2110
+
2111
+ [10] WRITE tests for API integration
2112
+ mock responses in unit tests
2113
+ consider VCR for recording real responses
2114
+
2115
+
2116
+ FINAL REMINDERS
2117
+
2118
+
2119
+ api integration is about reliability
2120
+
2121
+ the best API integration is one that doesnt break.
2122
+ handle edge cases. handle failures. handle rate limits.
2123
+
2124
+
2125
+ documentation is your friend
2126
+
2127
+ read the docs. bookmark the reference.
2128
+ save the openapi spec if available.
2129
+ understand the errors before they happen.
2130
+
2131
+
2132
+ observability is non-negotiable
2133
+
2134
+ log everything. measure everything.
2135
+ you cant fix what you cant see.
2136
+
2137
+
2138
+ when the api fails
2139
+
2140
+ your application should degrade gracefully.
2141
+ show cached data. show a friendly error.
2142
+ never crash the whole app because one api failed.
2143
+
2144
+
2145
+ start simple, add complexity gradually
2146
+
2147
+ basic client first. then auth. then retries. then caching.
2148
+ each layer builds on the previous.
2149
+
2150
+ now go integrate some apis.