hakiapi 2.0.0__tar.gz → 2.0.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.0.2/PKG-INFO +300 -0
- hakiapi-2.0.2/README.md +273 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/clients/github.py +33 -4
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/paginator.py +8 -4
- hakiapi-2.0.2/hakiapi.egg-info/PKG-INFO +300 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/pyproject.toml +1 -1
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_github.py +73 -10
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_paginator.py +1 -1
- hakiapi-2.0.0/PKG-INFO +0 -428
- hakiapi-2.0.0/README.md +0 -401
- hakiapi-2.0.0/hakiapi.egg-info/PKG-INFO +0 -428
- {hakiapi-2.0.0 → hakiapi-2.0.2}/LICENSE +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/__init__.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/clients/__init__.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/clients/gmail.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/clients/google_calendar.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/__init__.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/auth.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/base_client.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/exceptions.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/oauth/__init__.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/oauth/google.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/oauth/refresh.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/oauth/token_store.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi/core/retry.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi.egg-info/SOURCES.txt +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi.egg-info/dependency_links.txt +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi.egg-info/requires.txt +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/hakiapi.egg-info/top_level.txt +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/setup.cfg +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_auth.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_base_client.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_exceptions.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_gmail.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_google.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_google_calender.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_refresh.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_retry.py +0 -0
- {hakiapi-2.0.0 → hakiapi-2.0.2}/tests/test_token_store.py +0 -0
hakiapi-2.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hakiapi
|
|
3
|
+
Version: 2.0.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 · Self-Healing 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) • [Architecture](#architecture) • [Create Your Own Client](#create-your-own-client) • [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, so every client you build on top of it inherits enterprise-grade behavior automatically. Instead of writing infrastructure, you write business logic.
|
|
56
|
+
|
|
57
|
+
### Without HakiAPI vs. with HakiAPI
|
|
58
|
+
|
|
59
|
+
| | Raw `requests` | HakiAPI |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| **OAuth 2.0 Flow** | Copy-paste URLs, manually spin up servers, hand-roll refreshes | Fully automated local interceptor + atomic token vault + auto-refresh |
|
|
62
|
+
| **Retry Logic** | Write your own `urllib3.Retry` + `HTTPAdapter` wiring | Built into `BaseAPIClient` with exponential backoff on 429/50x |
|
|
63
|
+
| **Static Auth** | Reimplement Bearer/HMAC/API keys per project | 4 reusable `AuthBase` strategies, drop-in |
|
|
64
|
+
| **Error Handling** | Manually check `response.status_code` everywhere | Raised as statically-typed, catchable exceptions |
|
|
65
|
+
| **Pagination** | Write a custom `while` loop per API's pagination style | Auto-detects Link-header & cursor pagination, iterated lazily |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ✨ Features
|
|
70
|
+
|
|
71
|
+
| Feature | Details |
|
|
72
|
+
|---|---|
|
|
73
|
+
| 🔄 **Self-Healing OAuth 2.0** | Interactive local server interceptor, CSRF-protected browser flow, and background token refreshing. |
|
|
74
|
+
| 🗄️ **Atomic Token Vault** | `FileTokenStore` guarantees zero state corruption during concurrent writes using secure temp-file swapping. |
|
|
75
|
+
| 🔐 **Multiple Auth Strategies** | `BearerTokenAuth`, `HeaderApiKeyAuth`, `QueryApiKeyAuth`, `HmacAuth`. |
|
|
76
|
+
| 🔁 **Automatic Retries** | Exponential backoff on `429/500/502/503/504`, mounted transparently on every session. |
|
|
77
|
+
| 📄 **Smart Pagination** | Auto-detects Link-header (GitHub-style) and cursor/token (`meta.next_token`) pagination. |
|
|
78
|
+
| ⚠️ **Typed Exceptions** | `RateLimitError`, `AuthenticationError`, `ClientError`, `ServerError`, `RequestTimeoutError`. |
|
|
79
|
+
| 📦 **Ready-to-use Clients** | GitHub, Gmail, GoogleCalendar out of the box. |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install hakiapi
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Requires **Python 3.10+**.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Quick Start
|
|
95
|
+
|
|
96
|
+
### 1. The Full-Cycle OAuth 2.0 Engine (Google Calendar)
|
|
97
|
+
|
|
98
|
+
Stop forcing users to copy and paste authorization codes. HakiAPI handles the complete browser flow, securely saves the state to an atomic file vault, and **automatically refreshes expired tokens in the background.**
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import os
|
|
102
|
+
from hakiapi.clients.google_calendar import GoogleCalendarClient
|
|
103
|
+
from hakiapi.core.oauth import GoogleOAuthFlow, FileTokenStore
|
|
104
|
+
|
|
105
|
+
# 1. Set up the secure vault and flow logic
|
|
106
|
+
oauth_flow = GoogleOAuthFlow(
|
|
107
|
+
client_id=os.environ.get("GOOGLE_CLIENT_ID"),
|
|
108
|
+
client_secret=os.environ.get("GOOGLE_CLIENT_SECRET"),
|
|
109
|
+
scopes=["https://www.googleapis.com/auth/calendar.readonly"],
|
|
110
|
+
store=FileTokenStore("my_secure_token.json"),
|
|
111
|
+
redirect_port=8765,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# 2. .get_token() does the heavy lifting:
|
|
115
|
+
# It loads the token if valid, silently refreshes if expired,
|
|
116
|
+
# or opens the browser and spins up a local interceptor if missing!
|
|
117
|
+
token = oauth_flow.get_token()
|
|
118
|
+
|
|
119
|
+
# 3. Initialize your client
|
|
120
|
+
with GoogleCalendarClient(token=token.access_token) as calendar:
|
|
121
|
+
for event in calendar.events.upcoming(max_results=3):
|
|
122
|
+
print(event.get("summary"))
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 2. Automatic Pagination (GitHub)
|
|
127
|
+
|
|
128
|
+
Forget page numbers, `while` loops, and manually checking for a `next` page. HakiAPI follows Link headers and cursor pagination automatically, lazily yielding results:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from hakiapi.clients.github import GitHubClient
|
|
132
|
+
|
|
133
|
+
with GitHubClient() as github:
|
|
134
|
+
# Lazily iterates through every repository, handling all HTTP requests behind the scenes
|
|
135
|
+
for repo in github.get_all_user_repos("torvalds"):
|
|
136
|
+
print(repo["name"])
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Exception Handling
|
|
143
|
+
|
|
144
|
+
Never check HTTP status codes manually again. Every exception inherits from `HakiAPIError`, carrying the `status_code` and original `response` object.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from hakiapi.core.exceptions import AuthenticationError, RateLimitError, ServerError
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
github.get_user("torvalds")
|
|
151
|
+
except RateLimitError as e:
|
|
152
|
+
print(f"Rate limited — retry after {e.retry_after}s")
|
|
153
|
+
except AuthenticationError:
|
|
154
|
+
print("Invalid credentials.")
|
|
155
|
+
except ServerError:
|
|
156
|
+
print("GitHub is currently unavailable.")
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Architecture & Project Structure
|
|
163
|
+
|
|
164
|
+
HakiAPI is designed with strict separation of concerns. Every service client inherits production-ready infrastructure automatically.
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
hakiapi/
|
|
168
|
+
├── core/
|
|
169
|
+
│ ├── oauth/ # The Self-Healing OAuth 2.0 Engine
|
|
170
|
+
│ │ ├── google.py # Local server interceptor & CSRF protection
|
|
171
|
+
│ │ ├── token_store.py # Atomic file vault for token persistence
|
|
172
|
+
│ │ └── refresh.py # Background token refresh logic
|
|
173
|
+
│ ├── auth.py # Static auth strategies (Bearer, HMAC, API Keys)
|
|
174
|
+
│ ├── retry.py # Exponential-backoff HTTPAdapter factory
|
|
175
|
+
│ ├── paginator.py # Link-header + cursor/token pagination
|
|
176
|
+
│ ├── base_client.py # Request lifecycle & session management
|
|
177
|
+
│ └── exceptions.py # Typed exception hierarchy
|
|
178
|
+
│
|
|
179
|
+
└── clients/
|
|
180
|
+
├── github.py # GitHub API implementation
|
|
181
|
+
├── gmail.py # Gmail API implementation
|
|
182
|
+
└── google_calendar.py # Google Calendar implementation
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Create Your Own Client
|
|
189
|
+
|
|
190
|
+
Creating a new SDK is intentionally simple: subclass `BaseAPIClient`, point it at a base URL, and define your endpoints as plain methods.
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from hakiapi import BaseAPIClient
|
|
194
|
+
|
|
195
|
+
class WeatherClient(BaseAPIClient):
|
|
196
|
+
def __init__(self, **kwargs):
|
|
197
|
+
super().__init__(base_url="https://api.open-meteo.com/v1", **kwargs)
|
|
198
|
+
|
|
199
|
+
def get_weather(self, latitude: float, longitude: float):
|
|
200
|
+
return self.get(
|
|
201
|
+
"forecast",
|
|
202
|
+
params={
|
|
203
|
+
"latitude": latitude,
|
|
204
|
+
"longitude": longitude,
|
|
205
|
+
"current_weather": True,
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
# Hyderabad, Telangana, India
|
|
211
|
+
with WeatherClient() as client:
|
|
212
|
+
weather = client.get_weather(latitude=17.385, longitude=78.4867)
|
|
213
|
+
print(weather["current_weather"])
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Output
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"time": "2026-07-24T22:00",
|
|
222
|
+
"interval": 900,
|
|
223
|
+
"temperature": 30.6,
|
|
224
|
+
"windspeed": 13.9,
|
|
225
|
+
"winddirection": 271,
|
|
226
|
+
"is_day": 0,
|
|
227
|
+
"weathercode": 51
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
*Authentication, retries, pagination, sessions, and exceptions are entirely inherited — you only write the endpoint logic.*
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Design Principles
|
|
237
|
+
|
|
238
|
+
* Infrastructure should be written once.
|
|
239
|
+
* API clients should remain lightweight.
|
|
240
|
+
* Explicit is better than magical.
|
|
241
|
+
* Strong typing improves maintainability.
|
|
242
|
+
* Production readiness should be the default, not an afterthought.
|
|
243
|
+
* Developer experience matters as much as correctness.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Testing
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
pip install hakiapi[dev]
|
|
251
|
+
pytest
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
* ✅ **280+ tests passing**
|
|
256
|
+
* ✅ Core framework covered (auth, retry, paginator, base client, exceptions)
|
|
257
|
+
* ✅ Full OAuth 2.0 engine mocked and covered
|
|
258
|
+
* ✅ Atomic file vault concurrency tested
|
|
259
|
+
* ✅ Client implementations covered
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Roadmap
|
|
264
|
+
|
|
265
|
+
**Completed**
|
|
266
|
+
|
|
267
|
+
* [x] Base API framework (`BaseAPIClient`)
|
|
268
|
+
* [x] Authentication system (Bearer, Header, Query, HMAC)
|
|
269
|
+
* [x] Retry engine with exponential backoff
|
|
270
|
+
* [x] Automatic pagination (Link header + cursor/token)
|
|
271
|
+
* [x] Typed exception hierarchy
|
|
272
|
+
* [x] Full-cycle OAuth 2.0 Engine (Local server, Vault, Auto-Refresh)
|
|
273
|
+
* [x] GitHub, Gmail, & Google Calendar clients
|
|
274
|
+
|
|
275
|
+
**Planned**
|
|
276
|
+
|
|
277
|
+
* [ ] Stripe client
|
|
278
|
+
* [ ] Twitter/X client
|
|
279
|
+
* [ ] Async client (`httpx`-based)
|
|
280
|
+
* [ ] Plugin system
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Contributing
|
|
285
|
+
|
|
286
|
+
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.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT License — see [LICENSE](https://www.google.com/search?q=LICENSE) for details.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### ⭐ If HakiAPI saved you from rewriting the same API client for the tenth time, consider giving it a star.
|
|
297
|
+
|
|
298
|
+
It helps more developers discover the project and motivates future development.
|
|
299
|
+
|
|
300
|
+
Built with ❤️ by **Gugilla Aakash**
|
hakiapi-2.0.2/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🚀 HakiAPI
|
|
4
|
+
|
|
5
|
+
### Build production-grade Python API SDKs — not boilerplate.
|
|
6
|
+
|
|
7
|
+
Authentication · Self-Healing OAuth 2.0 · Retries · Pagination · Typed Exceptions
|
|
8
|
+
|
|
9
|
+
[](https://pypi.org/project/hakiapi/)
|
|
10
|
+
[](https://pypi.org/project/hakiapi/)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](#testing)
|
|
13
|
+
[](#features)
|
|
14
|
+
[](https://pypistats.org/packages/hakiapi)
|
|
15
|
+
|
|
16
|
+
**Stop rewriting authentication, retries, and pagination for every API client you build.**
|
|
17
|
+
|
|
18
|
+
[Installation](#installation) • [Quick Start](#quick-start) • [Features](#features) • [Architecture](#architecture) • [Create Your Own Client](#create-your-own-client) • [Roadmap](#roadmap)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Why HakiAPI?
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
HakiAPI extracts all of that into one reusable core, so every client you build on top of it inherits enterprise-grade behavior automatically. Instead of writing infrastructure, you write business logic.
|
|
29
|
+
|
|
30
|
+
### Without HakiAPI vs. with HakiAPI
|
|
31
|
+
|
|
32
|
+
| | Raw `requests` | HakiAPI |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| **OAuth 2.0 Flow** | Copy-paste URLs, manually spin up servers, hand-roll refreshes | Fully automated local interceptor + atomic token vault + auto-refresh |
|
|
35
|
+
| **Retry Logic** | Write your own `urllib3.Retry` + `HTTPAdapter` wiring | Built into `BaseAPIClient` with exponential backoff on 429/50x |
|
|
36
|
+
| **Static Auth** | Reimplement Bearer/HMAC/API keys per project | 4 reusable `AuthBase` strategies, drop-in |
|
|
37
|
+
| **Error Handling** | Manually check `response.status_code` everywhere | Raised as statically-typed, catchable exceptions |
|
|
38
|
+
| **Pagination** | Write a custom `while` loop per API's pagination style | Auto-detects Link-header & cursor pagination, iterated lazily |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ✨ Features
|
|
43
|
+
|
|
44
|
+
| Feature | Details |
|
|
45
|
+
|---|---|
|
|
46
|
+
| 🔄 **Self-Healing OAuth 2.0** | Interactive local server interceptor, CSRF-protected browser flow, and background token refreshing. |
|
|
47
|
+
| 🗄️ **Atomic Token Vault** | `FileTokenStore` guarantees zero state corruption during concurrent writes using secure temp-file swapping. |
|
|
48
|
+
| 🔐 **Multiple Auth Strategies** | `BearerTokenAuth`, `HeaderApiKeyAuth`, `QueryApiKeyAuth`, `HmacAuth`. |
|
|
49
|
+
| 🔁 **Automatic Retries** | Exponential backoff on `429/500/502/503/504`, mounted transparently on every session. |
|
|
50
|
+
| 📄 **Smart Pagination** | Auto-detects Link-header (GitHub-style) and cursor/token (`meta.next_token`) pagination. |
|
|
51
|
+
| ⚠️ **Typed Exceptions** | `RateLimitError`, `AuthenticationError`, `ClientError`, `ServerError`, `RequestTimeoutError`. |
|
|
52
|
+
| 📦 **Ready-to-use Clients** | GitHub, Gmail, GoogleCalendar out of the box. |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install hakiapi
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires **Python 3.10+**.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
### 1. The Full-Cycle OAuth 2.0 Engine (Google Calendar)
|
|
70
|
+
|
|
71
|
+
Stop forcing users to copy and paste authorization codes. HakiAPI handles the complete browser flow, securely saves the state to an atomic file vault, and **automatically refreshes expired tokens in the background.**
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import os
|
|
75
|
+
from hakiapi.clients.google_calendar import GoogleCalendarClient
|
|
76
|
+
from hakiapi.core.oauth import GoogleOAuthFlow, FileTokenStore
|
|
77
|
+
|
|
78
|
+
# 1. Set up the secure vault and flow logic
|
|
79
|
+
oauth_flow = GoogleOAuthFlow(
|
|
80
|
+
client_id=os.environ.get("GOOGLE_CLIENT_ID"),
|
|
81
|
+
client_secret=os.environ.get("GOOGLE_CLIENT_SECRET"),
|
|
82
|
+
scopes=["https://www.googleapis.com/auth/calendar.readonly"],
|
|
83
|
+
store=FileTokenStore("my_secure_token.json"),
|
|
84
|
+
redirect_port=8765,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# 2. .get_token() does the heavy lifting:
|
|
88
|
+
# It loads the token if valid, silently refreshes if expired,
|
|
89
|
+
# or opens the browser and spins up a local interceptor if missing!
|
|
90
|
+
token = oauth_flow.get_token()
|
|
91
|
+
|
|
92
|
+
# 3. Initialize your client
|
|
93
|
+
with GoogleCalendarClient(token=token.access_token) as calendar:
|
|
94
|
+
for event in calendar.events.upcoming(max_results=3):
|
|
95
|
+
print(event.get("summary"))
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2. Automatic Pagination (GitHub)
|
|
100
|
+
|
|
101
|
+
Forget page numbers, `while` loops, and manually checking for a `next` page. HakiAPI follows Link headers and cursor pagination automatically, lazily yielding results:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from hakiapi.clients.github import GitHubClient
|
|
105
|
+
|
|
106
|
+
with GitHubClient() as github:
|
|
107
|
+
# Lazily iterates through every repository, handling all HTTP requests behind the scenes
|
|
108
|
+
for repo in github.get_all_user_repos("torvalds"):
|
|
109
|
+
print(repo["name"])
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Exception Handling
|
|
116
|
+
|
|
117
|
+
Never check HTTP status codes manually again. Every exception inherits from `HakiAPIError`, carrying the `status_code` and original `response` object.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from hakiapi.core.exceptions import AuthenticationError, RateLimitError, ServerError
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
github.get_user("torvalds")
|
|
124
|
+
except RateLimitError as e:
|
|
125
|
+
print(f"Rate limited — retry after {e.retry_after}s")
|
|
126
|
+
except AuthenticationError:
|
|
127
|
+
print("Invalid credentials.")
|
|
128
|
+
except ServerError:
|
|
129
|
+
print("GitHub is currently unavailable.")
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Architecture & Project Structure
|
|
136
|
+
|
|
137
|
+
HakiAPI is designed with strict separation of concerns. Every service client inherits production-ready infrastructure automatically.
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
hakiapi/
|
|
141
|
+
├── core/
|
|
142
|
+
│ ├── oauth/ # The Self-Healing OAuth 2.0 Engine
|
|
143
|
+
│ │ ├── google.py # Local server interceptor & CSRF protection
|
|
144
|
+
│ │ ├── token_store.py # Atomic file vault for token persistence
|
|
145
|
+
│ │ └── refresh.py # Background token refresh logic
|
|
146
|
+
│ ├── auth.py # Static auth strategies (Bearer, HMAC, API Keys)
|
|
147
|
+
│ ├── retry.py # Exponential-backoff HTTPAdapter factory
|
|
148
|
+
│ ├── paginator.py # Link-header + cursor/token pagination
|
|
149
|
+
│ ├── base_client.py # Request lifecycle & session management
|
|
150
|
+
│ └── exceptions.py # Typed exception hierarchy
|
|
151
|
+
│
|
|
152
|
+
└── clients/
|
|
153
|
+
├── github.py # GitHub API implementation
|
|
154
|
+
├── gmail.py # Gmail API implementation
|
|
155
|
+
└── google_calendar.py # Google Calendar implementation
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Create Your Own Client
|
|
162
|
+
|
|
163
|
+
Creating a new SDK is intentionally simple: subclass `BaseAPIClient`, point it at a base URL, and define your endpoints as plain methods.
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from hakiapi import BaseAPIClient
|
|
167
|
+
|
|
168
|
+
class WeatherClient(BaseAPIClient):
|
|
169
|
+
def __init__(self, **kwargs):
|
|
170
|
+
super().__init__(base_url="https://api.open-meteo.com/v1", **kwargs)
|
|
171
|
+
|
|
172
|
+
def get_weather(self, latitude: float, longitude: float):
|
|
173
|
+
return self.get(
|
|
174
|
+
"forecast",
|
|
175
|
+
params={
|
|
176
|
+
"latitude": latitude,
|
|
177
|
+
"longitude": longitude,
|
|
178
|
+
"current_weather": True,
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
# Hyderabad, Telangana, India
|
|
184
|
+
with WeatherClient() as client:
|
|
185
|
+
weather = client.get_weather(latitude=17.385, longitude=78.4867)
|
|
186
|
+
print(weather["current_weather"])
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Output
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"time": "2026-07-24T22:00",
|
|
195
|
+
"interval": 900,
|
|
196
|
+
"temperature": 30.6,
|
|
197
|
+
"windspeed": 13.9,
|
|
198
|
+
"winddirection": 271,
|
|
199
|
+
"is_day": 0,
|
|
200
|
+
"weathercode": 51
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
*Authentication, retries, pagination, sessions, and exceptions are entirely inherited — you only write the endpoint logic.*
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Design Principles
|
|
210
|
+
|
|
211
|
+
* Infrastructure should be written once.
|
|
212
|
+
* API clients should remain lightweight.
|
|
213
|
+
* Explicit is better than magical.
|
|
214
|
+
* Strong typing improves maintainability.
|
|
215
|
+
* Production readiness should be the default, not an afterthought.
|
|
216
|
+
* Developer experience matters as much as correctness.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Testing
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
pip install hakiapi[dev]
|
|
224
|
+
pytest
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
* ✅ **280+ tests passing**
|
|
229
|
+
* ✅ Core framework covered (auth, retry, paginator, base client, exceptions)
|
|
230
|
+
* ✅ Full OAuth 2.0 engine mocked and covered
|
|
231
|
+
* ✅ Atomic file vault concurrency tested
|
|
232
|
+
* ✅ Client implementations covered
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Roadmap
|
|
237
|
+
|
|
238
|
+
**Completed**
|
|
239
|
+
|
|
240
|
+
* [x] Base API framework (`BaseAPIClient`)
|
|
241
|
+
* [x] Authentication system (Bearer, Header, Query, HMAC)
|
|
242
|
+
* [x] Retry engine with exponential backoff
|
|
243
|
+
* [x] Automatic pagination (Link header + cursor/token)
|
|
244
|
+
* [x] Typed exception hierarchy
|
|
245
|
+
* [x] Full-cycle OAuth 2.0 Engine (Local server, Vault, Auto-Refresh)
|
|
246
|
+
* [x] GitHub, Gmail, & Google Calendar clients
|
|
247
|
+
|
|
248
|
+
**Planned**
|
|
249
|
+
|
|
250
|
+
* [ ] Stripe client
|
|
251
|
+
* [ ] Twitter/X client
|
|
252
|
+
* [ ] Async client (`httpx`-based)
|
|
253
|
+
* [ ] Plugin system
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Contributing
|
|
258
|
+
|
|
259
|
+
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.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT License — see [LICENSE](https://www.google.com/search?q=LICENSE) for details.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### ⭐ If HakiAPI saved you from rewriting the same API client for the tenth time, consider giving it a star.
|
|
270
|
+
|
|
271
|
+
It helps more developers discover the project and motivates future development.
|
|
272
|
+
|
|
273
|
+
Built with ❤️ by **Gugilla Aakash**
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
|
-
from hakiapi.core.base_client import BaseAPIClient
|
|
4
4
|
from hakiapi.core.auth import BearerTokenAuth
|
|
5
|
+
from hakiapi.core.base_client import BaseAPIClient
|
|
6
|
+
from hakiapi.core.exceptions import HakiAPIError
|
|
5
7
|
from hakiapi.core.paginator import paginate
|
|
6
8
|
|
|
7
9
|
|
|
@@ -27,6 +29,32 @@ class GitHubClient(BaseAPIClient):
|
|
|
27
29
|
"""Fetch a GitHub user's profile."""
|
|
28
30
|
return self.get(f"users/{username}", **kwargs)
|
|
29
31
|
|
|
32
|
+
def search_users(self, query: str, **kwargs: Any) -> dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
Search GitHub users matching a query string.
|
|
35
|
+
|
|
36
|
+
Returns the raw GitHub search response, e.g.:
|
|
37
|
+
{"total_count": N, "incomplete_results": bool, "items": [...]}
|
|
38
|
+
"""
|
|
39
|
+
# Safely cast to a dictionary, protecting against list-of-tuples
|
|
40
|
+
params: dict[str, Any] = dict(kwargs.pop("params", None) or {})
|
|
41
|
+
params["q"] = query
|
|
42
|
+
|
|
43
|
+
return self.get("search/users", params=params, **kwargs)
|
|
44
|
+
|
|
45
|
+
def get_all_search_users(
|
|
46
|
+
self, query: str, **kwargs: Any
|
|
47
|
+
) -> Iterator[dict[str, Any]]:
|
|
48
|
+
"""
|
|
49
|
+
Search GitHub users matching a query string, auto-paginating
|
|
50
|
+
through every page of results (GitHub's Link-header pagination).
|
|
51
|
+
"""
|
|
52
|
+
params: dict[str, Any] = dict(kwargs.pop("params", None) or {})
|
|
53
|
+
params["q"] = query
|
|
54
|
+
|
|
55
|
+
# Delegated directly to the paginator using yield from
|
|
56
|
+
yield from paginate(self, "search/users", params=params, **kwargs)
|
|
57
|
+
|
|
30
58
|
def get_user_repos(self, username: str, **kwargs: Any) -> list[dict[str, Any]]:
|
|
31
59
|
"""Fetch a single page of public repositories of a GitHub user."""
|
|
32
60
|
return self.get(f"users/{username}/repos", **kwargs)
|
|
@@ -35,7 +63,8 @@ class GitHubClient(BaseAPIClient):
|
|
|
35
63
|
self, username: str, **kwargs: Any
|
|
36
64
|
) -> Iterator[dict[str, Any]]:
|
|
37
65
|
"""Fetch ALL public repositories using automatic pagination."""
|
|
38
|
-
|
|
66
|
+
# Delegated directly to the paginator using yield from
|
|
67
|
+
yield from paginate(self, f"users/{username}/repos", **kwargs)
|
|
39
68
|
|
|
40
69
|
def get_repo_languages(
|
|
41
70
|
self, owner: str, repo: str, **kwargs: Any
|
|
@@ -67,7 +96,7 @@ class GitHubClient(BaseAPIClient):
|
|
|
67
96
|
aggregate_languages[lang] = (
|
|
68
97
|
aggregate_languages.get(lang, 0) + bytes_count
|
|
69
98
|
)
|
|
70
|
-
except
|
|
99
|
+
except HakiAPIError:
|
|
71
100
|
# Shield the iteration sequence if a single repo is deleted or inaccessible
|
|
72
101
|
continue
|
|
73
102
|
|
|
@@ -51,11 +51,15 @@ def paginate(
|
|
|
51
51
|
items = data["messages"]
|
|
52
52
|
elif isinstance(data.get("items"), list):
|
|
53
53
|
items = data["items"]
|
|
54
|
+
|
|
55
|
+
# Fixed google email pagination bug when it returns 0 results
|
|
56
|
+
elif data.get("resultSizeEstimate") == 0:
|
|
57
|
+
break
|
|
54
58
|
else:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
+
print("Unexpected response:")
|
|
60
|
+
print(data)
|
|
61
|
+
|
|
62
|
+
raise ValueError(f"Unexpected pagination response: {data}")
|
|
59
63
|
else:
|
|
60
64
|
raise ValueError("Unexpected response format.")
|
|
61
65
|
|