bas-http 1.0.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.
- bas/README.md +244 -0
- bas/__init__.py +97 -0
- bas/_curl.py +393 -0
- bas/async_session.py +352 -0
- bas/cookies.py +643 -0
- bas/curl_parser.py +267 -0
- bas/impersonate.py +424 -0
- bas/models.py +339 -0
- bas/session.py +648 -0
- bas/simple_session.py +299 -0
- bas/tests/__init__.py +0 -0
- bas/tests/test_cookies.py +778 -0
- bas/tests/test_curl_parser.py +172 -0
- bas/tests/test_real_request.py +142 -0
- bas/tests/test_spaceshooter.py +213 -0
- bas_http-1.0.0.dist-info/METADATA +25 -0
- bas_http-1.0.0.dist-info/RECORD +19 -0
- bas_http-1.0.0.dist-info/WHEEL +5 -0
- bas_http-1.0.0.dist-info/top_level.txt +1 -0
bas/simple_session.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple HTTP Session — no curl, no impersonation, no nonsense.
|
|
3
|
+
|
|
4
|
+
Just give it your browser's headers + cookies and it works.
|
|
5
|
+
Uses Python's built-in urllib — zero external dependencies.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from bas.simple_session import Session
|
|
9
|
+
|
|
10
|
+
s = Session()
|
|
11
|
+
s.headers = {
|
|
12
|
+
"User-Agent": "Mozilla/5.0 ...",
|
|
13
|
+
"Accept": "text/html,...",
|
|
14
|
+
}
|
|
15
|
+
s.set_cookie("cf_clearance", "abc123", domain="example.com")
|
|
16
|
+
|
|
17
|
+
r = s.get("https://example.com/page")
|
|
18
|
+
print(r.status_code, r.text)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import ssl
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from http.client import HTTPConnection, HTTPSConnection
|
|
26
|
+
from typing import Any, Optional, Union
|
|
27
|
+
from urllib.parse import urlparse, urlencode, urljoin, parse_qs
|
|
28
|
+
|
|
29
|
+
from .cookies import Cookie, CookieJar
|
|
30
|
+
from .models import Headers, PreparedRequest, Response
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Session:
|
|
34
|
+
"""
|
|
35
|
+
Simple HTTP session with automatic cookie management.
|
|
36
|
+
|
|
37
|
+
Uses Python's http.client — no external dependencies needed.
|
|
38
|
+
Just paste your browser's headers and cookies and go.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
headers: Optional[dict] = None,
|
|
44
|
+
cookies: Optional[dict] = None,
|
|
45
|
+
verify: bool = True,
|
|
46
|
+
timeout: float = 30,
|
|
47
|
+
allow_redirects: bool = True,
|
|
48
|
+
max_redirects: int = 20,
|
|
49
|
+
proxies: Optional[dict] = None,
|
|
50
|
+
):
|
|
51
|
+
self._headers = Headers()
|
|
52
|
+
if headers:
|
|
53
|
+
self._headers.update(headers)
|
|
54
|
+
|
|
55
|
+
self._cookie_jar = CookieJar()
|
|
56
|
+
if cookies:
|
|
57
|
+
for name, value in cookies.items():
|
|
58
|
+
self.set_cookie(name, value)
|
|
59
|
+
|
|
60
|
+
self.verify = verify
|
|
61
|
+
self.timeout = timeout
|
|
62
|
+
self.allow_redirects = allow_redirects
|
|
63
|
+
self.max_redirects = max_redirects
|
|
64
|
+
self.proxies = proxies or {}
|
|
65
|
+
self._request_count = 0
|
|
66
|
+
self._lock = threading.Lock()
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def headers(self) -> Headers:
|
|
70
|
+
return self._headers
|
|
71
|
+
|
|
72
|
+
@headers.setter
|
|
73
|
+
def headers(self, value):
|
|
74
|
+
self._headers = Headers(value)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def cookies(self) -> CookieJar:
|
|
78
|
+
return self._cookie_jar
|
|
79
|
+
|
|
80
|
+
def set_cookie(self, name: str, value: str, domain: str = "", path: str = "/"):
|
|
81
|
+
"""Set a cookie. If domain is empty, it's set for all domains."""
|
|
82
|
+
self._cookie_jar.add(Cookie(
|
|
83
|
+
name=name, value=value, domain=domain, path=path,
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
def clear_cookies(self):
|
|
87
|
+
self._cookie_jar.clear()
|
|
88
|
+
|
|
89
|
+
def _build_ssl_context(self):
|
|
90
|
+
"""Build SSL context."""
|
|
91
|
+
ctx = ssl.create_default_context()
|
|
92
|
+
if not self.verify:
|
|
93
|
+
ctx.check_hostname = False
|
|
94
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
95
|
+
return ctx
|
|
96
|
+
|
|
97
|
+
def _make_request(
|
|
98
|
+
self,
|
|
99
|
+
method: str,
|
|
100
|
+
url: str,
|
|
101
|
+
headers: Optional[dict] = None,
|
|
102
|
+
body: Optional[bytes] = None,
|
|
103
|
+
) -> tuple[int, dict, bytes, str]:
|
|
104
|
+
"""
|
|
105
|
+
Make a raw HTTP request.
|
|
106
|
+
|
|
107
|
+
Returns: (status_code, response_headers, body, effective_url)
|
|
108
|
+
"""
|
|
109
|
+
parsed = urlparse(url)
|
|
110
|
+
host = parsed.hostname
|
|
111
|
+
port = parsed.port
|
|
112
|
+
path = parsed.path or "/"
|
|
113
|
+
if parsed.query:
|
|
114
|
+
path += "?" + parsed.query
|
|
115
|
+
is_https = parsed.scheme == "https"
|
|
116
|
+
|
|
117
|
+
# Build request headers
|
|
118
|
+
req_headers = Headers(dict(self._headers))
|
|
119
|
+
if headers:
|
|
120
|
+
req_headers.update(headers)
|
|
121
|
+
|
|
122
|
+
# Auto-inject cookies
|
|
123
|
+
cookie_header = self._cookie_jar.to_header(url)
|
|
124
|
+
if cookie_header:
|
|
125
|
+
req_headers["Cookie"] = cookie_header
|
|
126
|
+
|
|
127
|
+
# Remove headers that shouldn't be sent
|
|
128
|
+
for h in ["Host", "Connection"]:
|
|
129
|
+
req_headers.pop(h, None)
|
|
130
|
+
|
|
131
|
+
# Set Host header
|
|
132
|
+
if port and port != (443 if is_https else 80):
|
|
133
|
+
req_headers["Host"] = f"{host}:{port}"
|
|
134
|
+
else:
|
|
135
|
+
req_headers["Host"] = host
|
|
136
|
+
|
|
137
|
+
# Connection
|
|
138
|
+
if is_https:
|
|
139
|
+
conn = HTTPSConnection(
|
|
140
|
+
host, port, timeout=self.timeout,
|
|
141
|
+
context=self._build_ssl_context(),
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
conn = HTTPConnection(host, port, timeout=self.timeout)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
conn.request(
|
|
148
|
+
method=method,
|
|
149
|
+
url=path,
|
|
150
|
+
body=body,
|
|
151
|
+
headers=dict(req_headers),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
resp = conn.getresponse()
|
|
155
|
+
|
|
156
|
+
# Read response
|
|
157
|
+
resp_body = resp.read()
|
|
158
|
+
|
|
159
|
+
# Build response headers
|
|
160
|
+
resp_headers = Headers(dict(resp.getheaders()))
|
|
161
|
+
|
|
162
|
+
# Get effective URL
|
|
163
|
+
effective_url = url
|
|
164
|
+
|
|
165
|
+
return resp.status, dict(resp_headers), resp_body, effective_url
|
|
166
|
+
|
|
167
|
+
finally:
|
|
168
|
+
conn.close()
|
|
169
|
+
|
|
170
|
+
def _send(
|
|
171
|
+
self,
|
|
172
|
+
method: str,
|
|
173
|
+
url: str,
|
|
174
|
+
headers: Optional[dict] = None,
|
|
175
|
+
data: Optional[Union[str, bytes, dict]] = None,
|
|
176
|
+
json_data: Optional[Any] = None,
|
|
177
|
+
allow_redirects: Optional[bool] = None,
|
|
178
|
+
) -> Response:
|
|
179
|
+
"""Execute a request with redirect handling."""
|
|
180
|
+
if allow_redirects is None:
|
|
181
|
+
allow_redirects = self.allow_redirects
|
|
182
|
+
|
|
183
|
+
# Prepare body
|
|
184
|
+
body = None
|
|
185
|
+
if json_data is not None:
|
|
186
|
+
body = json.dumps(json_data).encode("utf-8")
|
|
187
|
+
if headers is None:
|
|
188
|
+
headers = {}
|
|
189
|
+
headers["Content-Type"] = "application/json"
|
|
190
|
+
elif data is not None:
|
|
191
|
+
if isinstance(data, dict):
|
|
192
|
+
body = urlencode(data).encode("utf-8")
|
|
193
|
+
if headers is None:
|
|
194
|
+
headers = {}
|
|
195
|
+
if "Content-Type" not in {k.lower(): v for k, v in (headers or {}).items()}:
|
|
196
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
197
|
+
elif isinstance(data, str):
|
|
198
|
+
body = data.encode("utf-8")
|
|
199
|
+
elif isinstance(data, bytes):
|
|
200
|
+
body = data
|
|
201
|
+
|
|
202
|
+
start_time = time.time()
|
|
203
|
+
history = []
|
|
204
|
+
|
|
205
|
+
while True:
|
|
206
|
+
# Make the request
|
|
207
|
+
status, resp_headers, resp_body, effective_url = self._make_request(
|
|
208
|
+
method, url, headers, body,
|
|
209
|
+
)
|
|
210
|
+
elapsed = time.time() - start_time
|
|
211
|
+
|
|
212
|
+
# Build response
|
|
213
|
+
response = Response(
|
|
214
|
+
status_code=status,
|
|
215
|
+
headers=Headers(resp_headers),
|
|
216
|
+
body=resp_body,
|
|
217
|
+
url=effective_url,
|
|
218
|
+
elapsed=elapsed,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Parse Set-Cookie from response
|
|
222
|
+
self._cookie_jar.parse_set_cookie_headers(resp_headers, effective_url)
|
|
223
|
+
response.cookies = self._cookie_jar
|
|
224
|
+
|
|
225
|
+
# Handle redirects
|
|
226
|
+
if not allow_redirects or not response.is_redirect:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
location = response.location
|
|
230
|
+
if not location:
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
history.append(response)
|
|
234
|
+
url = urljoin(effective_url, location)
|
|
235
|
+
|
|
236
|
+
# Change method for 301/302/303
|
|
237
|
+
if status in (301, 302, 303):
|
|
238
|
+
method = "GET"
|
|
239
|
+
body = None
|
|
240
|
+
headers = None
|
|
241
|
+
|
|
242
|
+
if len(history) >= self.max_redirects:
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
response.history = history
|
|
246
|
+
self._request_count += 1
|
|
247
|
+
return response
|
|
248
|
+
|
|
249
|
+
# ── Public API ───────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
def get(self, url: str, **kwargs) -> Response:
|
|
252
|
+
return self._send("GET", url, **kwargs)
|
|
253
|
+
|
|
254
|
+
def post(self, url: str, **kwargs) -> Response:
|
|
255
|
+
return self._send("POST", url, **kwargs)
|
|
256
|
+
|
|
257
|
+
def put(self, url: str, **kwargs) -> Response:
|
|
258
|
+
return self._send("PUT", url, **kwargs)
|
|
259
|
+
|
|
260
|
+
def delete(self, url: str, **kwargs) -> Response:
|
|
261
|
+
return self._send("DELETE", url, **kwargs)
|
|
262
|
+
|
|
263
|
+
def patch(self, url: str, **kwargs) -> Response:
|
|
264
|
+
return self._send("PATCH", url, **kwargs)
|
|
265
|
+
|
|
266
|
+
def head(self, url: str, **kwargs) -> Response:
|
|
267
|
+
return self._send("HEAD", url, **kwargs)
|
|
268
|
+
|
|
269
|
+
def options(self, url: str, **kwargs) -> Response:
|
|
270
|
+
return self._send("OPTIONS", url, **kwargs)
|
|
271
|
+
|
|
272
|
+
# ── Cookie convenience ───────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
def get_cookies(self, url: str) -> dict:
|
|
275
|
+
return self._cookie_jar.get_dict(url)
|
|
276
|
+
|
|
277
|
+
def save_cookies(self, filepath: str, format: str = "json"):
|
|
278
|
+
if format == "netscape":
|
|
279
|
+
self._cookie_jar.save_netscape(filepath)
|
|
280
|
+
else:
|
|
281
|
+
self._cookie_jar.save_json(filepath)
|
|
282
|
+
|
|
283
|
+
def load_cookies(self, filepath: str, format: str = "json"):
|
|
284
|
+
if format == "netscape":
|
|
285
|
+
self._cookie_jar.load_netscape(filepath)
|
|
286
|
+
else:
|
|
287
|
+
self._cookie_jar.load_json(filepath)
|
|
288
|
+
|
|
289
|
+
def close(self):
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
def __enter__(self):
|
|
293
|
+
return self
|
|
294
|
+
|
|
295
|
+
def __exit__(self, *args):
|
|
296
|
+
self.close()
|
|
297
|
+
|
|
298
|
+
def __repr__(self):
|
|
299
|
+
return f"<Session(requests={self._request_count})>"
|
bas/tests/__init__.py
ADDED
|
File without changes
|