tha-req-runner 0.1.0__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.
tha_req_runner/errors.py
ADDED
tha_req_runner/py.typed
ADDED
|
File without changes
|
tha_req_runner/runner.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Collection
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from requests.adapters import HTTPAdapter
|
|
9
|
+
from urllib3.util.retry import Retry
|
|
10
|
+
|
|
11
|
+
_DEFAULT_RETRIES = 3
|
|
12
|
+
_DEFAULT_BACKOFF = 0.5
|
|
13
|
+
_DEFAULT_STATUS_FORCELIST = (500, 502, 503, 504)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ThaReq:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._local = threading.local()
|
|
19
|
+
|
|
20
|
+
def get_session(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
status_forcelist: tuple[int, ...] = _DEFAULT_STATUS_FORCELIST,
|
|
24
|
+
allowed_methods: Collection[str] | None = None, # None → urllib3 safe-method default; POST excluded
|
|
25
|
+
) -> requests.Session:
|
|
26
|
+
# config applies only on first call per thread; subsequent calls return the cached session
|
|
27
|
+
if not hasattr(self._local, "session"):
|
|
28
|
+
session = requests.Session()
|
|
29
|
+
retry = Retry(
|
|
30
|
+
total=_DEFAULT_RETRIES,
|
|
31
|
+
backoff_factor=_DEFAULT_BACKOFF,
|
|
32
|
+
status_forcelist=status_forcelist,
|
|
33
|
+
allowed_methods=allowed_methods,
|
|
34
|
+
)
|
|
35
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
36
|
+
session.mount("https://", adapter)
|
|
37
|
+
session.mount("http://", adapter)
|
|
38
|
+
self._local.session = session
|
|
39
|
+
return self._local.session # type: ignore[no-any-return]
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def parse_response(result: requests.Response | Exception) -> dict[str, Any]:
|
|
43
|
+
if isinstance(result, Exception):
|
|
44
|
+
raw = getattr(result, "response", None)
|
|
45
|
+
if not isinstance(raw, requests.Response):
|
|
46
|
+
raw = None
|
|
47
|
+
return {
|
|
48
|
+
"status": raw.status_code if raw is not None else None,
|
|
49
|
+
"data": None,
|
|
50
|
+
"message": str(result),
|
|
51
|
+
"raw_response": raw,
|
|
52
|
+
}
|
|
53
|
+
try:
|
|
54
|
+
data: Any = result.json()
|
|
55
|
+
except Exception:
|
|
56
|
+
data = None
|
|
57
|
+
return {
|
|
58
|
+
"status": result.status_code,
|
|
59
|
+
"data": data,
|
|
60
|
+
"message": None,
|
|
61
|
+
"raw_response": result,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def safe_call(
|
|
65
|
+
self,
|
|
66
|
+
fn: Callable[..., requests.Response],
|
|
67
|
+
*args: Any,
|
|
68
|
+
**kwargs: Any,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
try:
|
|
71
|
+
return self.parse_response(fn(*args, **kwargs))
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
return self.parse_response(exc)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tha-req-runner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Tabular Helper API library that wraps requests with thread-safe session reuse, automatic retries, and a normalized response dict.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: api,http,requests,retry,session
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Typing :: Typed
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: requests
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# tha-req-runner
|
|
20
|
+
|
|
21
|
+
[](https://github.com/tha-guy-nate/tha-req-runner/actions/workflows/ci.yml)
|
|
22
|
+
|
|
23
|
+
A small Python library that provides a thread-safe `requests.Session` with automatic retries and a normalized response parser. Intended as the HTTP transport layer for other `tha-*` runners.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install tha-req-runner
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from tha_req_runner import ThaReq
|
|
35
|
+
|
|
36
|
+
req = ThaReq()
|
|
37
|
+
session = req.get_session()
|
|
38
|
+
|
|
39
|
+
# safe_call wraps the try/except for you
|
|
40
|
+
result = req.safe_call(session.get, "https://api.example.com/students", params={"limit": 100})
|
|
41
|
+
# {"status": 200, "data": [...], "message": None, "raw_response": <Response>}
|
|
42
|
+
|
|
43
|
+
# network errors return the same shape — no try/except needed
|
|
44
|
+
result = req.safe_call(session.get, "https://unreachable.example.com")
|
|
45
|
+
# {"status": None, "data": None, "message": "Connection refused", "raw_response": None}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Response dict
|
|
49
|
+
|
|
50
|
+
Every method returns the same shape whether the call succeeded or raised:
|
|
51
|
+
|
|
52
|
+
| Key | Type | Description |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `status` | `int \| None` | HTTP status code, or `None` on network error |
|
|
55
|
+
| `data` | `object` | Parsed JSON body, or `None` if not JSON |
|
|
56
|
+
| `message` | `str \| None` | Exception message on error, otherwise `None` |
|
|
57
|
+
| `raw_response` | `Response \| None` | The raw `requests.Response` object |
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
### `ThaReq`
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
ThaReq()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `req.get_session()`
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
req.get_session(
|
|
71
|
+
*,
|
|
72
|
+
status_forcelist: tuple[int, ...] = (500, 502, 503, 504),
|
|
73
|
+
allowed_methods: Collection[str] | None = None,
|
|
74
|
+
) -> requests.Session
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Returns a `requests.Session` configured with automatic retries (`total=3`, `backoff_factor=0.5`). Config is applied only on the **first call per thread** — subsequent calls on the same thread return the cached session regardless of args. Two `ThaReq` instances never share a session.
|
|
78
|
+
|
|
79
|
+
`allowed_methods=None` uses urllib3's safe-method default, which **excludes POST**. To retry POST (e.g. token endpoints):
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
session = req.get_session(
|
|
83
|
+
status_forcelist=(429, 500, 502, 503, 504),
|
|
84
|
+
allowed_methods=frozenset(["GET", "POST"]),
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `ThaReq.parse_response()`
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
ThaReq.parse_response(result: requests.Response | Exception) -> dict[str, Any]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Normalizes a `requests.Response` or a caught `Exception` into a consistent dict. Also callable as `req.parse_response(result)` without instantiation.
|
|
95
|
+
|
|
96
|
+
### `req.safe_call()`
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
req.safe_call(fn, *args, **kwargs) -> dict[str, Any]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Calls `fn(*args, **kwargs)`, catches any exception, and returns a normalized response dict. Equivalent to:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
try:
|
|
106
|
+
result = req.parse_response(fn(*args, **kwargs))
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
result = req.parse_response(exc)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
result = req.safe_call(session.get, url, params={"limit": 100})
|
|
113
|
+
result = req.safe_call(session.post, token_url, data={"grant_type": "client_credentials"})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Session and retries
|
|
117
|
+
|
|
118
|
+
- **Thread-safe**: each thread gets its own session via `threading.local` on the instance
|
|
119
|
+
- **Retry defaults**: `total=3`, `backoff_factor=0.5` (delays: 0.5s → 1s → 2s)
|
|
120
|
+
- **Retry statuses**: `500`, `502`, `503`, `504` by default
|
|
121
|
+
- **POST not retried by default** — pass `allowed_methods` explicitly to enable it
|
|
122
|
+
- Sessions are reused across calls on the same thread
|
|
123
|
+
|
|
124
|
+
## Used by
|
|
125
|
+
|
|
126
|
+
- `tha-edfi-runner` — uses `ThaReq` as its HTTP transport layer
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
tha_req_runner/__init__.py,sha256=cV4-D2JlauAll-dqFLTgiogKoT1zIW4eDHYbbLaMw5Q,210
|
|
2
|
+
tha_req_runner/errors.py,sha256=Acqpx-WxNGcmpHKW8c9RhNst9fJMeIXA6q1qMGt0tMY,36
|
|
3
|
+
tha_req_runner/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
tha_req_runner/runner.py,sha256=TD7rFEELcdRJl9zy2bjAl8vDddkZncPot2xRRY7ZnyE,2380
|
|
5
|
+
tha_req_runner-0.1.0.dist-info/METADATA,sha256=ygXswxhkRfRqnvKhvUlqCtgmC8GgHtCi_IXTVIX4P1E,4121
|
|
6
|
+
tha_req_runner-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
tha_req_runner-0.1.0.dist-info/RECORD,,
|