hakiapi 2.0.2__tar.gz → 2.1.2__tar.gz

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 (42) hide show
  1. hakiapi-2.1.2/PKG-INFO +423 -0
  2. hakiapi-2.1.2/README.md +396 -0
  3. hakiapi-2.1.2/hakiapi/clients/github.py +322 -0
  4. hakiapi-2.1.2/hakiapi/core/async_base_client.py +252 -0
  5. hakiapi-2.1.2/hakiapi.egg-info/PKG-INFO +423 -0
  6. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi.egg-info/SOURCES.txt +2 -0
  7. {hakiapi-2.0.2 → hakiapi-2.1.2}/pyproject.toml +1 -1
  8. hakiapi-2.1.2/tests/test_async_base_client.py +316 -0
  9. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_github.py +253 -0
  10. hakiapi-2.0.2/PKG-INFO +0 -300
  11. hakiapi-2.0.2/README.md +0 -273
  12. hakiapi-2.0.2/hakiapi/clients/github.py +0 -103
  13. hakiapi-2.0.2/hakiapi.egg-info/PKG-INFO +0 -300
  14. {hakiapi-2.0.2 → hakiapi-2.1.2}/LICENSE +0 -0
  15. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/__init__.py +0 -0
  16. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/clients/__init__.py +0 -0
  17. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/clients/gmail.py +0 -0
  18. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/clients/google_calendar.py +0 -0
  19. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/__init__.py +0 -0
  20. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/auth.py +0 -0
  21. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/base_client.py +0 -0
  22. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/exceptions.py +0 -0
  23. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/oauth/__init__.py +0 -0
  24. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/oauth/google.py +0 -0
  25. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/oauth/refresh.py +0 -0
  26. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/oauth/token_store.py +0 -0
  27. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/paginator.py +0 -0
  28. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/retry.py +0 -0
  29. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi.egg-info/dependency_links.txt +0 -0
  30. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi.egg-info/requires.txt +0 -0
  31. {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi.egg-info/top_level.txt +0 -0
  32. {hakiapi-2.0.2 → hakiapi-2.1.2}/setup.cfg +0 -0
  33. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_auth.py +0 -0
  34. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_base_client.py +0 -0
  35. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_exceptions.py +0 -0
  36. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_gmail.py +0 -0
  37. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_google.py +0 -0
  38. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_google_calender.py +0 -0
  39. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_paginator.py +0 -0
  40. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_refresh.py +0 -0
  41. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_retry.py +0 -0
  42. {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_token_store.py +0 -0
hakiapi-2.1.2/PKG-INFO ADDED
@@ -0,0 +1,423 @@
1
+ Metadata-Version: 2.4
2
+ Name: hakiapi
3
+ Version: 2.1.2
4
+ Summary: A modern Python framework for building clean, typed, and extensible API clients.
5
+ Author: Gugilla Aakash
6
+ Author-email: gugillaaakash6@gmail.com
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/Gugilla-Aakash/hakiapi
9
+ Project-URL: Source Code, https://github.com/Gugilla-Aakash/hakiapi
10
+ Project-URL: Bug Tracker, https://github.com/Gugilla-Aakash/hakiapi/issues
11
+ Keywords: api-client,retry,pagination,rest-api,http
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Intended Audience :: Developers
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests>=2.32.0
21
+ Requires-Dist: urllib3>=1.26.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest; extra == "dev"
24
+ Requires-Dist: responses; extra == "dev"
25
+ Requires-Dist: ruff; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ <div align="center">
29
+
30
+ # 🚀 HakiAPI
31
+
32
+ ### Build production-grade Python API SDKs — not boilerplate.
33
+
34
+ Authentication · OAuth 2.0 · Retries · Pagination · Typed Exceptions
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/hakiapi?style=for-the-badge)](https://pypi.org/project/hakiapi/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/hakiapi?style=for-the-badge)](https://pypi.org/project/hakiapi/)
38
+ [![License](https://img.shields.io/github/license/Gugilla-Aakash/hakiapi?style=for-the-badge)](LICENSE)
39
+ [![Tests](https://img.shields.io/badge/tests-300_passing-success?style=for-the-badge)](#testing)
40
+ [![Typing](https://img.shields.io/badge/typing-fully_typed-blue?style=for-the-badge)](#features)
41
+ [![Downloads](https://img.shields.io/pypi/dm/hakiapi?style=for-the-badge)](https://pypistats.org/packages/hakiapi)
42
+
43
+ **Stop rewriting authentication, retries, and pagination for every API client you build.**
44
+
45
+ [Installation](#installation) • [Quick Start](#quick-start) • [Features](#features) • [Core Concepts](#core-concepts) • [Bundled Clients](#bundled-clients) • [Create Your Own Client](#create-your-own-client) • [Architecture](#architecture--project-structure) • [Roadmap](#roadmap)
46
+
47
+ </div>
48
+
49
+ ---
50
+
51
+ ## Why HakiAPI?
52
+
53
+ Every API client grows the same infrastructure, in the same order. You start with a simple HTTP call. Then you add authentication. Then retries. Then pagination. Then timeout and exception handling. A month later, you've rebuilt the same plumbing you already wrote for the last five projects.
54
+
55
+ HakiAPI extracts all of that into one reusable core (`BaseAPIClient`), so every client you build on top of it inherits the same battle-tested behavior automatically. Instead of writing infrastructure, you write endpoint logic.
56
+
57
+ ### Raw `requests` vs. HakiAPI
58
+
59
+ | | Raw `requests` | HakiAPI |
60
+ |---|---|---|
61
+ | **OAuth 2.0 Flow** | Hand-roll the consent URL, spin up a redirect server, parse the callback yourself | `GoogleOAuthFlow` builds the consent URL, opens the browser, catches the redirect on `localhost`, verifies CSRF `state`, and exchanges the code for you |
62
+ | **Token Persistence** | Read/write a JSON file yourself and hope nothing corrupts it mid-write | `FileTokenStore` writes atomically (temp file + `os.replace`) with `0600` permissions |
63
+ | **Retry Logic** | Wire up your own `urllib3.Retry` + `HTTPAdapter` | Built into every `BaseAPIClient` session with exponential backoff on `429/500/502/503/504` |
64
+ | **Static Auth** | Reimplement Bearer/HMAC/API-key headers per project | 5 reusable `AuthBase` strategies, drop-in |
65
+ | **Error Handling** | Manually branch on `response.status_code` everywhere | Raised as a typed, catchable exception hierarchy carrying `status_code` and the original `response` |
66
+ | **Pagination** | Write a custom `while` loop per API's pagination style | `paginate()` auto-detects Link-header, `data`/`meta.next_token`, `messages`/`nextPageToken`, and `items`/`nextPageToken` styles, yielding lazily |
67
+
68
+ ---
69
+
70
+ ## ✨ Features
71
+
72
+ | Feature | Details |
73
+ |---|---|
74
+ | 🔐 **Interactive OAuth 2.0 Flow** | `GoogleOAuthFlow` drives Google's Authorization Code flow end-to-end: builds the consent URL, opens the system browser, boots a one-shot local `HTTPServer` to catch the redirect, validates the CSRF `state` token, and exchanges the code for tokens. |
75
+ | 🔁 **Manual Token Refresh** | `refresh_access_token()` exchanges a stored `refresh_token` for a new `access_token` without user interaction, and wipes the token store automatically if Google reports the grant as revoked. |
76
+ | 🗄️ **Atomic Token Vault** | `FileTokenStore` persists tokens to a local JSON file by writing to a temp file and swapping it in with `os.replace()`, so a crash mid-write can never leave a corrupted token file. The file is `chmod 0600`. |
77
+ | 🔐 **Multiple Auth Strategies** | `BearerTokenAuth`, `HeaderApiKeyAuth`, `QueryApiKeyAuth`, `HmacAuth` (SHA-256 request signing), and `OAuth2Auth` for wiring a `GoogleOAuthFlow` directly into a `requests.Session`. |
78
+ | 🔁 **Automatic Retries** | `create_retry_adapter()` mounts an `HTTPAdapter` with exponential backoff on `429/500/502/503/504` onto every `BaseAPIClient` session, deferring status handling to HakiAPI's own exceptions via `raise_on_status=False`. |
79
+ | 📄 **Smart Pagination** | `paginate()` auto-detects GitHub-style `Link` headers, Twitter-style `meta.next_token`, Gmail-style `messages` + `nextPageToken`, and Calendar-style `items` + `nextPageToken` — all as one lazy generator. |
80
+ | ⚠️ **Typed Exceptions** | `RateLimitError` (with `retry_after`), `AuthenticationError` (401/403), `ClientError` (4xx), `ServerError` (5xx), `RequestTimeoutError` — all inherit from `HakiAPIError`, which carries `status_code` and the original `response`. |
81
+ | 📦 **Ready-to-use Clients** | `GitHubClient` (REST + GraphQL), `GmailClient`, `GoogleCalendarClient` — out of the box. |
82
+
83
+ ---
84
+
85
+ ## Installation
86
+
87
+ ```bash
88
+ pip install hakiapi
89
+ ```
90
+
91
+ Requires **Python 3.10+**. Core dependencies are `requests>=2.32.0` and `urllib3>=1.26.0`.
92
+
93
+ ---
94
+
95
+ ## Quick Start
96
+
97
+ ### 1. Interactive OAuth 2.0 (Google Calendar)
98
+
99
+ `GoogleOAuthFlow.get_token()` checks the `TokenStore` first. If a valid, non-expired token is already saved, it's returned immediately. Otherwise it opens your browser, runs the full consent flow, and persists the result:
100
+
101
+ ```python
102
+ import os
103
+ from dotenv import load_dotenv
104
+ from hakiapi.clients.google_calendar import GoogleCalendarClient
105
+ from hakiapi.core.oauth.google import GoogleOAuthFlow
106
+ from hakiapi.core.oauth.token_store import FileTokenStore
107
+
108
+ # Load variables from your .env file into os.environ
109
+ load_dotenv()
110
+
111
+ # 1. Set up the flow and the token vault
112
+ oauth_flow = GoogleOAuthFlow(
113
+ client_id=os.environ["GOOGLE_CLIENT_ID"],
114
+ client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
115
+ scopes=["https://www.googleapis.com/auth/calendar.readonly"],
116
+ store=FileTokenStore("my_secure_token.json"),
117
+ redirect_port=8765, # must match an authorized redirect URI in Google Cloud Console
118
+ )
119
+
120
+ # 2. get_token() returns the cached token if it's still valid,
121
+ # otherwise it opens the browser and runs the full consent flow.
122
+ token = oauth_flow.get_token()
123
+
124
+ # 3. Initialize your client with the raw access token
125
+ with GoogleCalendarClient(token=token.access_token) as calendar:
126
+ for event in calendar.events.upcoming(max_results=3):
127
+ print(event.get("summary"))
128
+ ```
129
+
130
+ > **Note:** `get_token()` does **not** silently refresh an expired token — it re-runs the interactive consent flow when the stored token is missing or expired. If you want silent, non-interactive refreshes using a saved `refresh_token`, call `refresh_access_token()` from `hakiapi.core.oauth.refresh` explicitly (see [OAuth 2.0](#oauth-20) below).
131
+
132
+ ### 2. Automatic Pagination (GitHub)
133
+
134
+ Forget page numbers, `while` loops, and manually checking for a `next` page. `paginate()` follows Link headers automatically and yields lazily:
135
+
136
+ ```python
137
+ from hakiapi.clients.github import GitHubClient
138
+
139
+ with GitHubClient() as github:
140
+ # Lazily walks every page of the user's public repos
141
+ for repo in github.get_all_user_repos("torvalds"):
142
+ print(repo["name"])
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Core Concepts
148
+
149
+ ### Exception Handling
150
+
151
+ Every exception raised by `BaseAPIClient._request()` inherits from `HakiAPIError`, carrying `status_code` and the original `response` object:
152
+
153
+ ```python
154
+ from hakiapi.core.exceptions import AuthenticationError, RateLimitError, ServerError
155
+
156
+ try:
157
+ github.get_user("torvalds")
158
+ except RateLimitError as e:
159
+ print(f"Rate limited — retry after {e.retry_after}s")
160
+ except AuthenticationError:
161
+ print("Invalid credentials.")
162
+ except ServerError:
163
+ print("GitHub is currently unavailable.")
164
+ ```
165
+
166
+ | Exception | Raised when | Extra attributes |
167
+ |---|---|---|
168
+ | `RateLimitError` | HTTP `429` | `retry_after` — parsed from the `Retry-After` header, if present |
169
+ | `AuthenticationError` | HTTP `401` / `403` | `auth_method` |
170
+ | `ClientError` | Any other `4xx` | — |
171
+ | `ServerError` | Any `5xx` | — |
172
+ | `RequestTimeoutError` | The request times out at the network level (no HTTP response was ever received) | `timeout_duration` |
173
+
174
+ ### Authentication Strategies (`core/auth.py`)
175
+
176
+ All strategies implement `requests.auth.AuthBase`, so they drop straight into `BaseAPIClient(auth=...)`:
177
+
178
+ ```python
179
+ from hakiapi.core.auth import BearerTokenAuth, HeaderApiKeyAuth, QueryApiKeyAuth, HmacAuth
180
+ ```
181
+
182
+ - **`BearerTokenAuth(token)`** — sets `Authorization: Bearer <token>`.
183
+ - **`HeaderApiKeyAuth(header_name, api_key)`** — injects the key under a custom header.
184
+ - **`QueryApiKeyAuth(param_name, api_key)`** — appends the key as a query parameter, preserving any existing query string.
185
+ - **`HmacAuth(api_key, secret_key, ...)`** — signs each request with HMAC-SHA256 over `METHOD\nPATH\nTIMESTAMP\nBODY` (newline-delimited to prevent field-collision signature forgery), sending the key, timestamp, and signature as headers. Raises `TypeError` for streaming bodies, which aren't supported.
186
+ - **`OAuth2Auth(flow)`** — wraps any object exposing `get_token()` (like `GoogleOAuthFlow`) and injects a fresh `Authorization: Bearer` header on every request.
187
+
188
+ ### Retry Engine (`core/retry.py`)
189
+
190
+ `create_retry_adapter()` builds an `HTTPAdapter` backed by `urllib3.util.Retry`:
191
+
192
+ - **3 retries by default**, with an exponential `backoff_factor` of `1.0`.
193
+ - Retries on `429, 500, 502, 503, 504` by default (configurable via `status_forcelist`).
194
+ - `raise_on_status=False` — `urllib3` never raises on its own; HakiAPI's typed exceptions handle the final failure.
195
+ - Mounted on both `http://` and `https://` for every `BaseAPIClient` session automatically.
196
+
197
+ ### Smart Pagination (`core/paginator.py`)
198
+
199
+ `paginate(client, endpoint, max_pages=None, **kwargs)` is a generator that keeps requesting pages until it runs out, detecting the item list and the "next page" signal from the response shape:
200
+
201
+ | Response shape | Items key | Next-page signal |
202
+ |---|---|---|
203
+ | Raw JSON list | the list itself | `Link` response header (`rel="next"`) |
204
+ | `{"data": [...], "meta": {...}}` (Twitter/X-style) | `data` | `meta.next_token` |
205
+ | `{"messages": [...], "nextPageToken": ...}` (Gmail-style) | `messages` | `nextPageToken` |
206
+ | `{"items": [...], "nextPageToken": ...}` (Calendar-style) | `items` | `nextPageToken` |
207
+ | `{"resultSizeEstimate": 0}` (Gmail empty result) | — | stops cleanly, no error |
208
+
209
+ Any other shape raises `ValueError("Unexpected pagination response: ...")`. Pass `max_pages` to cap how many pages are fetched.
210
+
211
+ ### OAuth 2.0 (`core/oauth/`)
212
+
213
+ The OAuth engine is split into three independent pieces:
214
+
215
+ - **`token_store.py`** — `OAuthToken` (a dataclass with `access_token`, `refresh_token`, `expires_at`, `scopes`, and an `is_expired` property with a 30-second leeway buffer) and the `TokenStore` abstract base class. `FileTokenStore` is the concrete implementation: it serializes tokens to JSON, writes atomically via a temp file + `os.replace()`, and sets `0600` permissions on the file.
216
+ - **`google.py`** — `GoogleOAuthFlow` drives the full interactive Authorization Code flow: builds the consent URL (`access_type=offline`, `prompt=consent` to force a refresh token on every run), opens it with `webbrowser.open()`, boots a one-shot `http.server.HTTPServer` on `localhost:<redirect_port>` to catch the redirect, validates the CSRF `state` parameter, and exchanges the authorization code for tokens via a direct POST to Google's token endpoint. Raises `OAuthFlowError` on denial, timeout, a `state` mismatch, or a failed exchange.
217
+ - **`refresh.py`** — `refresh_access_token(token, client_id, client_secret, store)` is a standalone function that exchanges a saved `refresh_token` for a new `access_token` without opening a browser. If Google rejects the refresh (revoked/invalid grant), it calls `store.delete_token()` so the next `get_token()` call cleanly falls back to the interactive flow.
218
+
219
+ These three pieces are intentionally decoupled — `GoogleOAuthFlow.get_token()` only checks expiry and re-runs the interactive flow if needed; wiring in silent refreshes via `refresh_access_token()` is left to the caller (or to `OAuth2Auth`, once you build that logic into your own `flow` object).
220
+
221
+ ---
222
+
223
+ ## Bundled Clients
224
+
225
+ ### `GitHubClient` — REST + GraphQL
226
+
227
+ ```python
228
+ from hakiapi.clients.github import GitHubClient
229
+
230
+ with GitHubClient(token="ghp_...") as gh: # token is optional for public endpoints
231
+ gh.get_user("torvalds")
232
+ gh.search_users("location:hyderabad")
233
+ gh.get_all_search_users("python") # auto-paginated generator
234
+ gh.get_user_repos("torvalds") # single page
235
+ gh.get_all_user_repos("torvalds") # auto-paginated generator
236
+ gh.get_repo_languages("torvalds", "linux")
237
+ gh.get_aggregate_user_languages("torvalds") # sums languages across every repo
238
+ gh.get_user_authored_activity("torvalds") # recent authored PRs + issues
239
+ gh.execute_graphql(query, variables={...}) # raises HakiAPIError on GraphQL-level errors
240
+ gh.get_user_contributions("torvalds", from_date="2025-01-01T00:00:00Z")
241
+ ```
242
+
243
+ `get_aggregate_user_languages()` walks every repository returned by `get_all_user_repos()` and silently skips any repo whose language lookup raises `HakiAPIError`, so one broken/empty repo doesn't fail the whole aggregation.
244
+
245
+ #### GraphQL Engine
246
+
247
+ `GitHubClient` isn't purely REST — it also ships a GraphQL execution layer on top of the same `BaseAPIClient` infrastructure, so GraphQL calls get the same retries, timeout handling, and auth as everything else.
248
+
249
+ ```python
250
+ from hakiapi.clients.github import GitHubClient
251
+
252
+ with GitHubClient(token="ghp_...") as gh:
253
+ data = gh.execute_graphql(
254
+ """
255
+ query($login: String!) {
256
+ user(login: $login) {
257
+ name
258
+ bio
259
+ }
260
+ }
261
+ """,
262
+ variables={"login": "torvalds"},
263
+ )
264
+ print(data["user"]["name"])
265
+ ```
266
+
267
+ - **`execute_graphql(query, variables=None, **kwargs)`** — the low-level engine. It `POST`s `{"query": ..., "variables": ...}` to GitHub's `/graphql` endpoint and unwraps the response. GraphQL is notorious for returning HTTP `200 OK` even when the query itself failed, with the real error buried in the response body — `execute_graphql` checks for an `"errors"` key in the payload and raises a `HakiAPIError` joining every message it finds, instead of letting a broken query silently return `None`. On success it returns just the `"data"` portion of the payload.
268
+ - **`get_user_contributions(username, from_date=None, to_date=None, **kwargs)`** — a ready-made query built on top of `execute_graphql`. It fetches a user's `contributionsCollection`: total contributions, commit contributions, issue contributions, and pull-request contributions, optionally scoped to a date range. `from_date`/`to_date` must be ISO 8601 strings (e.g. `"2025-01-01T00:00:00Z"`).
269
+
270
+ ### `GmailClient` — resource-based routing
271
+
272
+ ```python
273
+ from hakiapi import GmailClient
274
+
275
+ with GmailClient(token=access_token) as gmail:
276
+ gmail.profile.get() # users/{id}/profile
277
+ gmail.labels.list() # users/{id}/labels
278
+ gmail.messages.get(message_id) # a single message
279
+ gmail.messages.list(max_pages=2) # auto-paginated generator
280
+ gmail.messages.search("is:unread") # auto-paginated generator with a query
281
+ gmail.messages.send({"raw": base64_rfc2822_string})
282
+ ```
283
+
284
+ ### `GoogleCalendarClient` — resource-based routing
285
+
286
+ ```python
287
+ from hakiapi import GoogleCalendarClient
288
+
289
+ with GoogleCalendarClient(token=access_token) as cal:
290
+ cal.calendars.list(max_pages=1)
291
+ cal.events.get(event_id)
292
+ cal.events.list(calendar_id="primary")
293
+ cal.events.today() # midnight-to-midnight UTC, recurring events expanded
294
+ cal.events.upcoming(max_results=5) # next N events from now, single page
295
+ cal.events.create({"summary": "...", "start": {...}, "end": {...}})
296
+ cal.events.delete(event_id)
297
+ ```
298
+
299
+ `today()` and `upcoming()` both auto-fill `timeMin`/`timeMax`, set `singleEvents=True` to expand recurring events, and sort by `startTime` — you only pass the calendar ID.
300
+
301
+ ---
302
+
303
+ ## Create Your Own Client
304
+
305
+ Subclass `BaseAPIClient`, point it at a base URL, and define your endpoints as plain methods. Authentication, retries, timeout handling, and typed exceptions are inherited automatically:
306
+
307
+ ```python
308
+ from hakiapi import BaseAPIClient
309
+
310
+ class WeatherClient(BaseAPIClient):
311
+ def __init__(self, **kwargs):
312
+ super().__init__(base_url="https://api.open-meteo.com/v1", **kwargs)
313
+
314
+ def get_weather(self, latitude: float, longitude: float):
315
+ return self.get(
316
+ "forecast",
317
+ params={
318
+ "latitude": latitude,
319
+ "longitude": longitude,
320
+ "current_weather": True,
321
+ },
322
+ )
323
+
324
+ if __name__ == "__main__":
325
+ # Hyderabad, Telangana, India
326
+ with WeatherClient() as client:
327
+ weather = client.get_weather(latitude=17.385, longitude=78.4867)
328
+ print(weather["current_weather"])
329
+ ```
330
+
331
+ `BaseAPIClient` exposes `get`, `post`, `put`, `patch`, and `delete`, all routed through `_request()`, which handles the retry-mounted session, timeout errors, status-code-to-exception mapping, and JSON/text response parsing (falling back to `response.text` if the body isn't valid JSON). Pass `raw_response=True` to get the raw `requests.Response` instead — this is what `paginate()` uses internally to read the `Link` header.
332
+
333
+ ---
334
+
335
+ ## Architecture & Project Structure
336
+
337
+ ```text
338
+ hakiapi/
339
+ ├── core/
340
+ │ ├── oauth/
341
+ │ │ ├── google.py # GoogleOAuthFlow — interactive Authorization Code flow
342
+ │ │ ├── refresh.py # refresh_access_token() — silent refresh via refresh_token
343
+ │ │ └── token_store.py # OAuthToken, TokenStore (ABC), FileTokenStore
344
+ │ ├── auth.py # BearerTokenAuth, HeaderApiKeyAuth, QueryApiKeyAuth, HmacAuth, OAuth2Auth
345
+ │ ├── retry.py # create_retry_adapter() — exponential-backoff HTTPAdapter factory
346
+ │ ├── paginator.py # paginate() — Link-header + token-based pagination
347
+ │ ├── base_client.py # BaseAPIClient — session, retries, exception mapping
348
+ │ └── exceptions.py # HakiAPIError hierarchy
349
+ │
350
+ └── clients/
351
+ ├── github.py # GitHubClient — REST + GraphQL
352
+ ├── gmail.py # GmailClient — profile / labels / messages resources
353
+ └── google_calendar.py # GoogleCalendarClient — calendars / events resources
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Design Principles
359
+
360
+ * Infrastructure should be written once.
361
+ * API clients should remain lightweight.
362
+ * Explicit is better than magical.
363
+ * Strong typing improves maintainability.
364
+ * Production readiness should be the default, not an afterthought.
365
+ * Developer experience matters as much as correctness.
366
+
367
+ ---
368
+
369
+ ## Testing
370
+
371
+ ```bash
372
+ pip install hakiapi[dev]
373
+ pytest
374
+ ```
375
+
376
+ * ✅ **300 tests passing**
377
+ * ✅ Core framework covered: `auth`, `retry`, `paginator`, `base_client`, `exceptions`
378
+ * ✅ Full OAuth 2.0 engine covered: `google.py` (interactive flow) and `refresh.py` (silent refresh), fully mocked
379
+ * ✅ `FileTokenStore` atomic-write behavior covered
380
+ * ✅ `GitHubClient`, `GmailClient`, `GoogleCalendarClient` covered
381
+
382
+ ---
383
+
384
+ ## Roadmap
385
+
386
+ **Completed**
387
+
388
+ * [x] Base API framework (`BaseAPIClient`)
389
+ * [x] Authentication strategies (Bearer, Header API Key, Query API Key, HMAC, OAuth2)
390
+ * [x] Retry engine with exponential backoff
391
+ * [x] Automatic pagination (Link header, `data`/`meta`, `messages`/`items` + token styles)
392
+ * [x] Typed exception hierarchy
393
+ * [x] Interactive Google OAuth 2.0 flow (local redirect interceptor, CSRF-protected)
394
+ * [x] Atomic `FileTokenStore` and standalone silent-refresh routine
395
+ * [x] `GitHubClient`, `GmailClient`, `GoogleCalendarClient`
396
+
397
+ **Planned**
398
+
399
+ * [ ] Stripe client
400
+ * [ ] Twitter/X client
401
+ * [ ] Wire automatic silent refresh into `GoogleOAuthFlow.get_token()`
402
+ * [ ] Async client (`httpx`-based)
403
+ * [ ] Plugin system
404
+
405
+ ---
406
+
407
+ ## Contributing
408
+
409
+ Contributions are welcome — bug fixes, documentation, tests, or new clients. Please open an issue before proposing major changes so we can discuss the approach first.
410
+
411
+ ---
412
+
413
+ ## License
414
+
415
+ MIT License — see [LICENSE](LICENSE) for details.
416
+
417
+ ---
418
+
419
+ ### ⭐ If HakiAPI saved you from rewriting the same API client for the tenth time, consider giving it a star.
420
+
421
+ It helps more developers discover the project and motivates future development.
422
+
423
+ Built with ❤️ by **Gugilla Aakash**