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/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