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.
@@ -0,0 +1,7 @@
1
+ """tha-req-runner: thread-safe HTTP requests with automatic retries and normalized responses."""
2
+
3
+ from .errors import ReqError
4
+ from .runner import ThaReq
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["ReqError", "ThaReq"]
@@ -0,0 +1,2 @@
1
+ class ReqError(Exception):
2
+ pass
File without changes
@@ -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
+ [![CI](https://github.com/tha-guy-nate/tha-req-runner/actions/workflows/ci.yml/badge.svg)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any