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.
- hakiapi-2.1.2/PKG-INFO +423 -0
- hakiapi-2.1.2/README.md +396 -0
- hakiapi-2.1.2/hakiapi/clients/github.py +322 -0
- hakiapi-2.1.2/hakiapi/core/async_base_client.py +252 -0
- hakiapi-2.1.2/hakiapi.egg-info/PKG-INFO +423 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi.egg-info/SOURCES.txt +2 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/pyproject.toml +1 -1
- hakiapi-2.1.2/tests/test_async_base_client.py +316 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_github.py +253 -0
- hakiapi-2.0.2/PKG-INFO +0 -300
- hakiapi-2.0.2/README.md +0 -273
- hakiapi-2.0.2/hakiapi/clients/github.py +0 -103
- hakiapi-2.0.2/hakiapi.egg-info/PKG-INFO +0 -300
- {hakiapi-2.0.2 → hakiapi-2.1.2}/LICENSE +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/__init__.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/clients/__init__.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/clients/gmail.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/clients/google_calendar.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/__init__.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/auth.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/base_client.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/exceptions.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/oauth/__init__.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/oauth/google.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/oauth/refresh.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/oauth/token_store.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/paginator.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi/core/retry.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi.egg-info/dependency_links.txt +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi.egg-info/requires.txt +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/hakiapi.egg-info/top_level.txt +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/setup.cfg +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_auth.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_base_client.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_exceptions.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_gmail.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_google.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_google_calender.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_paginator.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_refresh.py +0 -0
- {hakiapi-2.0.2 → hakiapi-2.1.2}/tests/test_retry.py +0 -0
- {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
|
+
[](https://pypi.org/project/hakiapi/)
|
|
37
|
+
[](https://pypi.org/project/hakiapi/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
[](#testing)
|
|
40
|
+
[](#features)
|
|
41
|
+
[](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**
|