requests_ruby 1.0.2 → 1.0.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/README.md +107 -5
- data/lib/requests/adapters.rb +102 -22
- data/lib/requests/api.rb +1 -3
- data/lib/requests/cookies.rb +92 -19
- data/lib/requests/sessions.rb +45 -13
- data/lib/requests/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 308045ed13c88c9a9eb1f89a483692c177b605e6aaac49e4eafa7aa0de2a3a74
|
|
4
|
+
data.tar.gz: 7f62813cf1d17285f51b10a79bd4b5f08ea1f91e71172b7b50a9ab416330bcfd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a26999d7ae3f856351a416736f08ff8d1ebd5fb21addd94bcee508d30f3d49de7af9e09c8ea52dc4d4c61bd81126ad884bb3203c5349552863930f2e33cc7a82
|
|
7
|
+
data.tar.gz: f199826c39a900bab90eba7445218b0c9c0f8830a7e33f588591667551c58f53f2c206c26424fcaae51cb207652611baba2ec70648c1138487b9c8ab66f89dc9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<<<<<<< HEAD
|
|
4
|
+
=======
|
|
5
|
+
## 1.0.3
|
|
6
|
+
|
|
7
|
+
Focused on the two things people actually hit walls on with a stdlib-only
|
|
8
|
+
client: downloading big files without blowing up memory, and cookies that
|
|
9
|
+
behave like cookies instead of a flat global hash.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Real connection pooling.** `HTTPAdapter` now keeps the underlying
|
|
13
|
+
`Net::HTTP` connection open and reuses it for subsequent requests to the
|
|
14
|
+
same host/port/proxy instead of doing a fresh TCP+TLS handshake every
|
|
15
|
+
single call. Connections are dropped and reopened automatically if the
|
|
16
|
+
server closes them or an error occurs. `Session#close` now actually
|
|
17
|
+
closes every pooled connection instead of being a no-op.
|
|
18
|
+
- **`Session#download(url, to:, ...)` / `Requests.download(url, to:, ...)`
|
|
19
|
+
now streams the response straight to disk in chunks** instead of
|
|
20
|
+
buffering the whole file in memory first. Options: `chunk_size:`,
|
|
21
|
+
`resume: true` (sends a `Range` header and appends to a partially
|
|
22
|
+
downloaded file), `progress: ->(downloaded, total) { ... }`.
|
|
23
|
+
Handles gzip/deflate on the fly, one chunk at a time.
|
|
24
|
+
- **`Requests::Jar` is now a real, domain-and-path-aware cookie jar**
|
|
25
|
+
instead of a flat name => value hash. Cookies set by one host are no
|
|
26
|
+
longer sent to every other host through the same session. Supports
|
|
27
|
+
`secure`, `http_only`, `expires`/`Max-Age`, explicit `set(name, value,
|
|
28
|
+
domain:, path:, secure:, http_only:, expires:)`, `for_domain(host)`,
|
|
29
|
+
and `for_url(url)`.
|
|
30
|
+
- `Jar#save(path)` / `Requests::Jar.load(path)` - persist a cookie jar to
|
|
31
|
+
a JSON file and load it back later (survives process restarts, useful
|
|
32
|
+
for scripts that log in once and reuse the session cookies).
|
|
33
|
+
- `HTTPAdapter#status_forcelist` - retry on specific response status codes
|
|
34
|
+
(e.g. `[502, 503, 504]`), on top of the existing connection-error retries.
|
|
35
|
+
- `Session#put!` / `#patch!` / `#delete!` to match the existing `get!` /
|
|
36
|
+
`post!` raise-on-error helpers.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- A custom adapter passed to `Session#mount` with its own `max_retries`,
|
|
40
|
+
`backoff_factor`, or `status_forcelist` no longer gets silently
|
|
41
|
+
overwritten back to the session's defaults on the next request.
|
|
42
|
+
|
|
43
|
+
>>>>>>> cf89f78 (v1.0.3)
|
|
44
|
+
## 1.0.2
|
|
4
45
|
|
|
5
46
|
Big cleanup pass. Split the single `lib/requests.rb` file into a proper
|
|
6
47
|
`lib/requests/*` structure, fixed several real bugs, and filled in a bunch
|
data/README.md
CHANGED
|
@@ -27,6 +27,8 @@ A zero-dependency, stdlib-only HTTP client for Ruby, modeled closely on Python's
|
|
|
27
27
|
- [Cookies](#cookies)
|
|
28
28
|
- [Hooks](#hooks)
|
|
29
29
|
- [Streaming & downloads](#streaming--downloads)
|
|
30
|
+
- [Downloading big files in chunks](#downloading-big-files-in-chunks)
|
|
31
|
+
- [Connection pooling](#connection-pooling)
|
|
30
32
|
- [Custom transport adapters](#custom-transport-adapters)
|
|
31
33
|
- [Status codes](#status-codes)
|
|
32
34
|
- [API reference](#-api-reference)
|
|
@@ -247,6 +249,24 @@ s.cookies.set('theme', 'dark')
|
|
|
247
249
|
s.get(url) # sent automatically on every request through this session
|
|
248
250
|
```
|
|
249
251
|
|
|
252
|
+
`Requests::Jar` is a real cookie jar as of 1.0.3 — it tracks domain, path,
|
|
253
|
+
`secure`, `http_only` and expiry per cookie, same as a browser would, so a
|
|
254
|
+
cookie a session picked up from `api.example.com` won't leak into a request
|
|
255
|
+
to some unrelated host through the same session:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
s = Requests::Session.new
|
|
259
|
+
s.get('https://api.example.com/login') # server sets a cookie scoped to api.example.com
|
|
260
|
+
s.get('https://other-service.example') # that cookie is NOT sent here
|
|
261
|
+
|
|
262
|
+
# set one explicitly with full control
|
|
263
|
+
s.cookies.set('session_id', 'abc123', domain: 'api.example.com', path: '/', secure: true, http_only: true)
|
|
264
|
+
|
|
265
|
+
# save/restore a session's cookies across process runs
|
|
266
|
+
s.cookies.save('cookies.json')
|
|
267
|
+
s.cookies = Requests::Jar.load('cookies.json')
|
|
268
|
+
```
|
|
269
|
+
|
|
250
270
|
### Hooks
|
|
251
271
|
|
|
252
272
|
```ruby
|
|
@@ -266,7 +286,51 @@ r.iter_lines { |line| ... }
|
|
|
266
286
|
Requests.download('https://example.com/file.zip', to: 'file.zip')
|
|
267
287
|
```
|
|
268
288
|
|
|
269
|
-
> ℹ️ Note:
|
|
289
|
+
> ℹ️ Note: `iter_content`/`iter_lines` on a regular `Response` still read the whole body first — this library stays stdlib-only, and true lazy streaming for arbitrary responses would need extra machinery to keep a `net/http` connection open across method calls. See [Known limitations](#-known-limitations). For actual large files, use `download` below — that one *is* real chunk-by-chunk streaming straight to disk.
|
|
290
|
+
|
|
291
|
+
### Downloading big files in chunks
|
|
292
|
+
|
|
293
|
+
As of 1.0.3, `download` doesn't buffer the file in memory at all — it reads
|
|
294
|
+
from the socket in chunks and writes each one straight to disk, decoding
|
|
295
|
+
gzip/deflate on the fly. This is the one to reach for instead of
|
|
296
|
+
`Requests.get(url).save_to(path)` once files get past a few MB.
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
Requests.download('https://example.com/big-file.zip', to: 'big-file.zip')
|
|
300
|
+
|
|
301
|
+
# custom chunk size + a progress callback
|
|
302
|
+
Requests.download('https://example.com/big-file.zip', to: 'big-file.zip',
|
|
303
|
+
chunk_size: 65536,
|
|
304
|
+
progress: ->(downloaded, total) { print "\r#{downloaded}/#{total}" })
|
|
305
|
+
|
|
306
|
+
# resume an interrupted download (sends a Range header, appends to the file)
|
|
307
|
+
Requests.download('https://example.com/big-file.zip', to: 'big-file.zip', resume: true)
|
|
308
|
+
|
|
309
|
+
# same thing on a Session, so it reuses the pooled connection and session auth/headers
|
|
310
|
+
s = Requests::Session.new
|
|
311
|
+
s.headers['Authorization'] = 'Bearer xyz'
|
|
312
|
+
s.download('https://example.com/private-file.zip', to: 'private-file.zip')
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Connection pooling
|
|
316
|
+
|
|
317
|
+
As of 1.0.3, `HTTPAdapter` keeps the underlying `Net::HTTP` connection open
|
|
318
|
+
and reuses it for further requests to the same host/port/proxy instead of
|
|
319
|
+
paying for a new TCP+TLS handshake on every single call — this is the same
|
|
320
|
+
idea as `urllib3`'s connection pool under python's `requests`. It's
|
|
321
|
+
automatic and requires nothing from you:
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
s = Requests::Session.new
|
|
325
|
+
s.get('https://api.example.com/a') # opens + keeps the connection
|
|
326
|
+
s.get('https://api.example.com/b') # reuses it, no new handshake
|
|
327
|
+
s.close # closes every pooled connection when you're done
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
If a pooled connection goes stale (the server closed it, a timeout, whatever)
|
|
331
|
+
it's transparently dropped and reopened on the next request; combined with
|
|
332
|
+
`retries`/`backoff_factor` this makes long-lived sessions much more resilient
|
|
333
|
+
than opening a fresh connection per call.
|
|
270
334
|
|
|
271
335
|
### Custom transport adapters
|
|
272
336
|
|
|
@@ -298,13 +362,51 @@ Requests.codes.im_a_teapot # 418, yes really
|
|
|
298
362
|
|---|---|---|
|
|
299
363
|
| `Requests.get/post/put/patch/delete/head/options` | `session.get/post/put/patch/delete/head/options` | `status_code`, `status` |
|
|
300
364
|
| `Requests.request(method, url, **opts)` | `session.request(method, url, **opts)` | `ok?`, `redirect?` |
|
|
301
|
-
| `Requests.session` → new `Session` | `session.get!/post!` (raise on error) | `text`, `content`, `json` |
|
|
302
|
-
| `Requests.download(url, to:)` | `session.
|
|
303
|
-
| `Requests.codes` | `session.
|
|
304
|
-
| | `session.
|
|
365
|
+
| `Requests.session` → new `Session` | `session.get!/post!/put!/patch!/delete!` (raise on error) | `text`, `content`, `json` |
|
|
366
|
+
| `Requests.download(url, to:, chunk_size:, resume:, progress:)` | `session.download(url, to:, ...)` (same, but reuses the session's pool/auth/headers) | `headers`, `cookies` |
|
|
367
|
+
| `Requests.codes` | `session.mount(prefix, adapter)`, `session.close` | `elapsed`, `history`, `url` |
|
|
368
|
+
| | `session.hooks[:response]` | `raise_for_status`, `save_to`, `links` |
|
|
369
|
+
| | `session.headers`, `.cookies`, `.auth`, `.proxies`, `.retries`, `.backoff_factor`, `.status_forcelist` | |
|
|
370
|
+
| | `Requests::Jar#save(path)` / `Requests::Jar.load(path)` | |
|
|
371
|
+
| | `HTTPAdapter.new(max_retries:, backoff_factor:, status_forcelist:)` | |
|
|
305
372
|
|
|
306
373
|
Every request-level method accepts: `params`, `data`, `json`, `headers`, `cookies`, `files`, `auth`, `timeout`, `allow_redirects`, `proxies`, `verify`, `cert`, `hooks`, `retries`.
|
|
307
374
|
|
|
375
|
+
## Python parity cheat sheet
|
|
376
|
+
|
|
377
|
+
| Python `requests` | `requests_ruby` |
|
|
378
|
+
|---|---|
|
|
379
|
+
| `requests.get(url, params={...})` | `Requests.get(url, params: {...})` |
|
|
380
|
+
| `requests.post(url, json={...})` | `Requests.post(url, json: {...})` |
|
|
381
|
+
| `r.status_code` | `r.status_code` |
|
|
382
|
+
| `r.ok` | `r.ok?` |
|
|
383
|
+
| `r.text` / `r.content` | `r.text` / `r.content` |
|
|
384
|
+
| `r.json()` | `r.json` |
|
|
385
|
+
| `r.raise_for_status()` | `r.raise_for_status` |
|
|
386
|
+
| `r.headers['x']` | `r.headers['x']` (case-insensitive both ways) |
|
|
387
|
+
| `s = requests.Session()` | `s = Requests::Session.new` |
|
|
388
|
+
| `s.mount('https://', adapter)` | `s.mount('https://', adapter)` |
|
|
389
|
+
| `requests.auth.HTTPBasicAuth(u, p)` | `Requests::BasicAuth.new(u, p)` |
|
|
390
|
+
| `requests.exceptions.Timeout` | `Requests::Timeoutable` (module, catches both) |
|
|
391
|
+
| `r.iter_content(chunk_size=...)` | `r.iter_content(chunk_size: ...)` |
|
|
392
|
+
| `s.cookies.get_dict()` | `s.cookies.to_h` |
|
|
393
|
+
| n/a | `Requests.download(url, to:, resume:, progress:)` — no python equivalent, this one's ours |
|
|
394
|
+
|
|
395
|
+
## Known limitations
|
|
396
|
+
|
|
397
|
+
- `iter_content`/`iter_lines` on a regular response read the whole body up
|
|
398
|
+
front rather than lazily streaming it off the socket — `download` (added
|
|
399
|
+
in 1.0.3) is the one that streams for real, use that for big files.
|
|
400
|
+
- No HTTP/2, no automatic NTLM/Kerberos/OAuth — bring your own `auth:`
|
|
401
|
+
object (anything with `#call(headers)` works) if you need those.
|
|
402
|
+
- `verify: '/path/to/ca.pem'` accepts a single bundle file, not a
|
|
403
|
+
directory of certs (`Net::HTTP` itself only takes `ca_file` or
|
|
404
|
+
`ca_path`, and this gem only wires up the former today).
|
|
405
|
+
- The connection pool added in 1.0.3 is per-`Session`/per-`HTTPAdapter`
|
|
406
|
+
instance and isn't thread-safe for concurrent requests on the *same*
|
|
407
|
+
`Session` object — use one `Session` per thread, or one connection per
|
|
408
|
+
thread, same advice as most connection-pooling HTTP clients.
|
|
409
|
+
|
|
308
410
|
|
|
309
411
|
## 🔧 Development
|
|
310
412
|
|
data/lib/requests/adapters.rb
CHANGED
|
@@ -16,24 +16,72 @@ module Requests
|
|
|
16
16
|
'DELETE' => Net::HTTP::Delete,
|
|
17
17
|
'HEAD' => Net::HTTP::Head,
|
|
18
18
|
'OPTIONS' => Net::HTTP::Options}.freeze
|
|
19
|
-
attr_accessor :max_retries, :backoff_factor
|
|
20
|
-
def initialize(max_retries: 0, backoff_factor: 0)
|
|
19
|
+
attr_accessor :max_retries, :backoff_factor, :status_forcelist
|
|
20
|
+
def initialize(max_retries: 0, backoff_factor: 0, status_forcelist: [])
|
|
21
21
|
@max_retries = max_retries
|
|
22
22
|
@backoff_factor = backoff_factor
|
|
23
|
+
@status_forcelist = status_forcelist
|
|
24
|
+
@pool = {}
|
|
23
25
|
end
|
|
24
26
|
def send_once(meth, url, hdrs, body, timeout, proxies, verify, cert, auth)
|
|
25
27
|
attempt = 0
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
loop do
|
|
29
|
+
begin
|
|
30
|
+
r = do_send(meth, url, hdrs, body, timeout, proxies, verify, cert, auth)
|
|
31
|
+
if @status_forcelist.include?(r.code.to_i) && attempt < @max_retries
|
|
32
|
+
attempt += 1
|
|
33
|
+
sleep(@backoff_factor * attempt) if @backoff_factor.to_f > 0
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
return r
|
|
37
|
+
rescue Requests::ConnectionError, Requests::ConnectTimeout => e
|
|
38
|
+
attempt += 1
|
|
39
|
+
raise e unless attempt <= @max_retries && idempotent?(meth)
|
|
31
40
|
sleep(@backoff_factor * attempt) if @backoff_factor.to_f > 0
|
|
32
|
-
retry
|
|
33
41
|
end
|
|
34
|
-
raise e
|
|
35
42
|
end
|
|
36
43
|
end
|
|
44
|
+
def close_pool
|
|
45
|
+
@pool.each_value { |h| (h.finish if h.started?) rescue nil }
|
|
46
|
+
@pool.clear
|
|
47
|
+
end
|
|
48
|
+
def stream_to(meth, url, hdrs, body, timeout, proxies, verify, cert, auth, io, chunk_size: 65536, progress: nil)
|
|
49
|
+
u = URI.parse(url)
|
|
50
|
+
auth.call(hdrs) if auth.respond_to?(:call) && !auth.is_a?(Requests::DigestAuth)
|
|
51
|
+
open_t, read_t = split_timeout(timeout)
|
|
52
|
+
http = conn_for(u, proxies, verify, cert, open_t, read_t)
|
|
53
|
+
klass = METHS[meth] || Net::HTTP::Get
|
|
54
|
+
req = klass.new(u.request_uri)
|
|
55
|
+
hdrs.each { |k, v| req[k] = v }
|
|
56
|
+
req.body = body if body
|
|
57
|
+
net_resp = nil
|
|
58
|
+
total = 0
|
|
59
|
+
begin
|
|
60
|
+
http.request(req) do |nr|
|
|
61
|
+
net_resp = nr
|
|
62
|
+
dec = stream_decoder(nr)
|
|
63
|
+
nr.read_body do |chunk|
|
|
64
|
+
piece = dec == :raw ? chunk : dec.inflate(chunk)
|
|
65
|
+
io.write(piece)
|
|
66
|
+
total += piece.bytesize
|
|
67
|
+
progress.call(total, nr.content_length) if progress
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
rescue Net::OpenTimeout
|
|
71
|
+
drop_conn(u, proxies)
|
|
72
|
+
raise Requests::ConnectTimeout, "connect timeout: #{url}"
|
|
73
|
+
rescue Net::ReadTimeout
|
|
74
|
+
drop_conn(u, proxies)
|
|
75
|
+
raise Requests::ReadTimeout, "read timeout: #{url}"
|
|
76
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
77
|
+
drop_conn(u, proxies)
|
|
78
|
+
raise Requests::SSLError, e.message
|
|
79
|
+
rescue EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT => e
|
|
80
|
+
drop_conn(u, proxies)
|
|
81
|
+
raise Requests::ConnectionError, e.message
|
|
82
|
+
end
|
|
83
|
+
net_resp
|
|
84
|
+
end
|
|
37
85
|
def decode_body(net_resp)
|
|
38
86
|
raw = net_resp.body
|
|
39
87
|
return '' if raw.nil?
|
|
@@ -59,30 +107,62 @@ module Requests
|
|
|
59
107
|
def do_send(meth, url, hdrs, body, timeout, proxies, verify, cert, auth)
|
|
60
108
|
u = URI.parse(url)
|
|
61
109
|
auth.call(hdrs) if auth.respond_to?(:call) && !auth.is_a?(Requests::DigestAuth)
|
|
62
|
-
popt = proxy_opts(u, proxies)
|
|
63
110
|
open_t, read_t = split_timeout(timeout)
|
|
64
|
-
http =
|
|
65
|
-
configure_ssl(http, u, verify, cert)
|
|
66
|
-
http.open_timeout = open_t if open_t
|
|
67
|
-
http.read_timeout = read_t if read_t
|
|
111
|
+
http = conn_for(u, proxies, verify, cert, open_t, read_t)
|
|
68
112
|
begin
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
h.request(req)
|
|
75
|
-
end
|
|
113
|
+
klass = METHS[meth] || Net::HTTP::Get
|
|
114
|
+
req = klass.new(u.request_uri)
|
|
115
|
+
hdrs.each { |k, v| req[k] = v }
|
|
116
|
+
req.body = body if body
|
|
117
|
+
http.request(req)
|
|
76
118
|
rescue Net::OpenTimeout
|
|
119
|
+
drop_conn(u, proxies)
|
|
77
120
|
raise Requests::ConnectTimeout, "connect timeout: #{url}"
|
|
78
121
|
rescue Net::ReadTimeout
|
|
122
|
+
drop_conn(u, proxies)
|
|
79
123
|
raise Requests::ReadTimeout, "read timeout: #{url}"
|
|
80
124
|
rescue OpenSSL::SSL::SSLError => e
|
|
125
|
+
drop_conn(u, proxies)
|
|
81
126
|
raise Requests::SSLError, e.message
|
|
82
|
-
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT
|
|
127
|
+
rescue EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT => e
|
|
128
|
+
drop_conn(u, proxies)
|
|
83
129
|
raise Requests::ConnectionError, e.message
|
|
84
130
|
end
|
|
85
131
|
end
|
|
132
|
+
def conn_for(u, proxies, verify, cert, ot, rt)
|
|
133
|
+
key = pool_key(u, proxies)
|
|
134
|
+
h = @pool[key]
|
|
135
|
+
if h && h.started?
|
|
136
|
+
h.open_timeout = ot if ot
|
|
137
|
+
h.read_timeout = rt if rt
|
|
138
|
+
return h
|
|
139
|
+
end
|
|
140
|
+
popt = proxy_opts(u, proxies)
|
|
141
|
+
h = Net::HTTP.new(u.host, u.port, *popt)
|
|
142
|
+
configure_ssl(h, u, verify, cert)
|
|
143
|
+
h.open_timeout = ot if ot
|
|
144
|
+
h.read_timeout = rt if rt
|
|
145
|
+
h.keep_alive_timeout = 30
|
|
146
|
+
h.start
|
|
147
|
+
@pool[key] = h
|
|
148
|
+
h
|
|
149
|
+
end
|
|
150
|
+
def drop_conn(u, proxies)
|
|
151
|
+
key = pool_key(u, proxies)
|
|
152
|
+
h = @pool.delete(key)
|
|
153
|
+
(h.finish if h && h.started?) rescue nil
|
|
154
|
+
end
|
|
155
|
+
def pool_key(u, proxies)
|
|
156
|
+
p = proxy_opts(u, proxies)
|
|
157
|
+
"#{u.scheme}|#{u.host}|#{u.port}|#{p.join(',')}"
|
|
158
|
+
end
|
|
159
|
+
def stream_decoder(nr)
|
|
160
|
+
case nr['content-encoding'].to_s.downcase
|
|
161
|
+
when 'gzip', 'x-gzip' then Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
|
|
162
|
+
when 'deflate' then Zlib::Inflate.new
|
|
163
|
+
else :raw
|
|
164
|
+
end
|
|
165
|
+
end
|
|
86
166
|
def configure_ssl(http, uri, verify, cert)
|
|
87
167
|
return unless uri.scheme == 'https'
|
|
88
168
|
http.use_ssl = true
|
data/lib/requests/api.rb
CHANGED
data/lib/requests/cookies.rb
CHANGED
|
@@ -1,55 +1,128 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
3
6
|
module Requests
|
|
4
7
|
class Jar
|
|
5
8
|
include Enumerable
|
|
9
|
+
Ck = Struct.new(:name, :value, :domain, :path, :secure, :http_only, :expires)
|
|
10
|
+
|
|
6
11
|
def initialize
|
|
7
|
-
@
|
|
12
|
+
@s = {}
|
|
8
13
|
end
|
|
9
14
|
def [](n)
|
|
10
|
-
@c
|
|
15
|
+
e = @s.values.reverse.find { |c| c.name == n }
|
|
16
|
+
e && e.value
|
|
11
17
|
end
|
|
12
18
|
def []=(n, v)
|
|
13
|
-
|
|
19
|
+
set(n, v)
|
|
14
20
|
end
|
|
15
21
|
def get(name, default = nil)
|
|
16
|
-
|
|
22
|
+
v = self[name]
|
|
23
|
+
v.nil? ? default : v
|
|
17
24
|
end
|
|
18
|
-
def set(name, value)
|
|
19
|
-
@
|
|
25
|
+
def set(name, value, domain: '', path: '/', secure: false, http_only: false, expires: nil)
|
|
26
|
+
@s[ck(domain, path, name)] = Ck.new(name, value, domain, path, secure, http_only, expires)
|
|
20
27
|
self
|
|
21
28
|
end
|
|
29
|
+
alias_method :set_cookie, :set
|
|
22
30
|
def delete(name)
|
|
23
|
-
@c.
|
|
31
|
+
@s.delete_if { |_, c| c.name == name }
|
|
24
32
|
end
|
|
25
33
|
def clear
|
|
26
|
-
@
|
|
34
|
+
@s.clear
|
|
27
35
|
end
|
|
28
36
|
def to_h
|
|
29
|
-
|
|
37
|
+
h = {}
|
|
38
|
+
@s.each_value { |c| h[c.name] = c.value }
|
|
39
|
+
h
|
|
30
40
|
end
|
|
31
41
|
def to_a
|
|
32
|
-
|
|
42
|
+
to_h.to_a
|
|
33
43
|
end
|
|
34
44
|
def each(&b)
|
|
35
|
-
|
|
45
|
+
return enum_for(:each) unless block_given?
|
|
46
|
+
@s.each_value { |c| yield c.name, c.value }
|
|
47
|
+
end
|
|
48
|
+
def all
|
|
49
|
+
@s.values
|
|
36
50
|
end
|
|
37
51
|
def empty?
|
|
38
|
-
@
|
|
52
|
+
@s.empty?
|
|
53
|
+
end
|
|
54
|
+
def for_url(url)
|
|
55
|
+
u = safe_uri(url)
|
|
56
|
+
now = Time.now
|
|
57
|
+
@s.values.select do |c|
|
|
58
|
+
next false if c.expires && c.expires < now
|
|
59
|
+
next false if c.secure && u.scheme != 'https'
|
|
60
|
+
h = u.host.to_s
|
|
61
|
+
dm = c.domain.to_s.empty? || h == c.domain || h.end_with?(".#{c.domain}")
|
|
62
|
+
pp = u.path.to_s
|
|
63
|
+
pm = pp.empty? ? c.path == '/' : pp.start_with?(c.path)
|
|
64
|
+
dm && pm
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
def for_domain(host)
|
|
68
|
+
@s.values.select { |c| c.domain.to_s.empty? || host.to_s == c.domain || host.to_s.end_with?(".#{c.domain}") }
|
|
39
69
|
end
|
|
40
|
-
def to_header
|
|
41
|
-
@
|
|
70
|
+
def to_header(url = nil)
|
|
71
|
+
list = url ? for_url(url) : @s.values.select { |c| !(c.expires && c.expires < Time.now) }
|
|
72
|
+
list.map { |c| "#{c.name}=#{c.value}" }.join('; ')
|
|
42
73
|
end
|
|
43
|
-
def update(net_resp)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
74
|
+
def update(net_resp, url = nil)
|
|
75
|
+
u = url ? safe_uri(url) : nil
|
|
76
|
+
(net_resp.get_fields('set-cookie') || []).each do |line|
|
|
77
|
+
parts = line.split(';').map(&:strip)
|
|
78
|
+
kv = parts.shift
|
|
47
79
|
next unless kv
|
|
48
80
|
k, v = kv.split('=', 2)
|
|
49
81
|
next unless k
|
|
50
|
-
|
|
82
|
+
at = {}
|
|
83
|
+
parts.each do |p|
|
|
84
|
+
pk, pv = p.split('=', 2)
|
|
85
|
+
at[pk.to_s.downcase] = pv
|
|
86
|
+
end
|
|
87
|
+
dom = at['domain'].to_s.sub(/\A\./, '')
|
|
88
|
+
dom = u.host.to_s if dom.empty? && u
|
|
89
|
+
exp = nil
|
|
90
|
+
if at['max-age']
|
|
91
|
+
exp = (Time.now + at['max-age'].to_i rescue nil)
|
|
92
|
+
elsif at['expires']
|
|
93
|
+
exp = (Time.parse(at['expires']) rescue nil)
|
|
94
|
+
end
|
|
95
|
+
set(k, v.to_s, domain: dom, path: at['path'] || '/', secure: at.key?('secure'),
|
|
96
|
+
http_only: at.key?('httponly'), expires: exp)
|
|
51
97
|
end
|
|
52
98
|
end
|
|
99
|
+
def save(path)
|
|
100
|
+
data = @s.values.map do |c|
|
|
101
|
+
{ name: c.name, value: c.value, domain: c.domain, path: c.path, secure: c.secure,
|
|
102
|
+
http_only: c.http_only, expires: c.expires && c.expires.iso8601 }
|
|
103
|
+
end
|
|
104
|
+
File.write(path, JSON.generate(data))
|
|
105
|
+
path
|
|
106
|
+
end
|
|
107
|
+
def self.load(path)
|
|
108
|
+
j = new
|
|
109
|
+
arr = JSON.parse(File.read(path))
|
|
110
|
+
arr.each do |c|
|
|
111
|
+
j.set(c['name'], c['value'], domain: c['domain'].to_s, path: c['path'] || '/',
|
|
112
|
+
secure: !!c['secure'], http_only: !!c['http_only'],
|
|
113
|
+
expires: (c['expires'] ? Time.parse(c['expires']) : nil))
|
|
114
|
+
end
|
|
115
|
+
j
|
|
116
|
+
end
|
|
117
|
+
private
|
|
118
|
+
def ck(domain, path, name)
|
|
119
|
+
"#{domain}|#{path}|#{name}"
|
|
120
|
+
end
|
|
121
|
+
def safe_uri(url)
|
|
122
|
+
URI.parse(url)
|
|
123
|
+
rescue
|
|
124
|
+
URI.parse('')
|
|
125
|
+
end
|
|
53
126
|
end
|
|
54
127
|
CookieJar = Jar
|
|
55
128
|
end
|
data/lib/requests/sessions.rb
CHANGED
|
@@ -6,7 +6,7 @@ require 'time'
|
|
|
6
6
|
module Requests
|
|
7
7
|
class Session
|
|
8
8
|
attr_accessor :headers, :cookies, :auth, :params, :proxies, :verify,
|
|
9
|
-
:cert, :max_redirects, :timeout, :retries, :backoff_factor
|
|
9
|
+
:cert, :max_redirects, :timeout, :retries, :backoff_factor, :status_forcelist
|
|
10
10
|
def initialize
|
|
11
11
|
@headers = Requests::Utils.default_headers
|
|
12
12
|
@cookies = Jar.new
|
|
@@ -19,8 +19,10 @@ module Requests
|
|
|
19
19
|
@timeout = nil
|
|
20
20
|
@retries = 0
|
|
21
21
|
@backoff_factor = 0
|
|
22
|
+
@status_forcelist = []
|
|
22
23
|
@hooks = { response: [] }
|
|
23
24
|
@adapters = { 'https://' => HTTPAdapter.new, 'http://' => HTTPAdapter.new }
|
|
25
|
+
@default_adapters = @adapters.values.dup
|
|
24
26
|
end
|
|
25
27
|
def hooks
|
|
26
28
|
@hooks
|
|
@@ -41,9 +43,39 @@ module Requests
|
|
|
41
43
|
end
|
|
42
44
|
def get!(url, **kw); get(url, **kw).raise_for_status; end
|
|
43
45
|
def post!(url, **kw); post(url, **kw).raise_for_status; end
|
|
46
|
+
def put!(url, **kw); put(url, **kw).raise_for_status; end
|
|
47
|
+
def patch!(url, **kw); patch(url, **kw).raise_for_status; end
|
|
48
|
+
def delete!(url, **kw); delete(url, **kw).raise_for_status; end
|
|
44
49
|
def close
|
|
50
|
+
@adapters.values.uniq.each { |a| a.close_pool if a.respond_to?(:close_pool) }
|
|
45
51
|
true
|
|
46
52
|
end
|
|
53
|
+
def download(url, to:, chunk_size: 65536, resume: false, progress: nil, headers: nil, params: nil,
|
|
54
|
+
timeout: nil, proxies: nil, verify: nil, cert: nil, auth: nil)
|
|
55
|
+
validate_url!(url)
|
|
56
|
+
full_url = build_url(url, merge_hash(@params, params))
|
|
57
|
+
hdrs = CIHash.new(@headers.to_h)
|
|
58
|
+
hdrs.merge!(headers)
|
|
59
|
+
mode = 'wb'
|
|
60
|
+
if resume && File.exist?(to) && File.size(to) > 0
|
|
61
|
+
hdrs['Range'] = "bytes=#{File.size(to)}-"
|
|
62
|
+
mode = 'ab'
|
|
63
|
+
end
|
|
64
|
+
adapter = adapter_for(full_url)
|
|
65
|
+
adapter.max_retries = @retries
|
|
66
|
+
adapter.backoff_factor = @backoff_factor
|
|
67
|
+
use_auth = auth || @auth
|
|
68
|
+
use_auth.call(hdrs) if use_auth.respond_to?(:call) && !use_auth.is_a?(DigestAuth)
|
|
69
|
+
File.open(to, mode) do |f|
|
|
70
|
+
net_resp = adapter.stream_to('GET', full_url, hdrs, nil, timeout || @timeout, proxies || @proxies,
|
|
71
|
+
verify.nil? ? @verify : verify, cert || @cert, nil, f,
|
|
72
|
+
chunk_size: chunk_size, progress: progress)
|
|
73
|
+
unless [200, 206].include?(net_resp.code.to_i)
|
|
74
|
+
raise Requests::HTTPError, "download failed: #{net_resp.code} for #{full_url}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
to
|
|
78
|
+
end
|
|
47
79
|
def request(method, url, params: nil, data: nil, json: nil, headers: nil, cookies: nil, files: nil,
|
|
48
80
|
auth: nil, timeout: nil, allow_redirects: true, proxies: nil, verify: nil, stream: false,
|
|
49
81
|
cert: nil, hooks: nil, retries: nil)
|
|
@@ -62,23 +94,26 @@ module Requests
|
|
|
62
94
|
hdrs.merge!(headers)
|
|
63
95
|
hdrs['Content-Type'] = content_type if content_type && !hdrs.key?('content-type')
|
|
64
96
|
hdrs.delete('Authorization') if redirs > 0 && URI.parse(cur_url).host != original_host
|
|
65
|
-
|
|
66
|
-
|
|
97
|
+
ck = @cookies.to_header(cur_url)
|
|
98
|
+
ck = [ck, cookies.map { |k, v| "#{k}=#{v}" }.join('; ')].reject { |x| x.nil? || x.empty? }.join('; ') if cookies
|
|
99
|
+
hdrs['Cookie'] = ck unless ck.empty?
|
|
67
100
|
adapter = adapter_for(cur_url)
|
|
68
|
-
adapter.max_retries = retries.nil?
|
|
69
|
-
adapter.
|
|
101
|
+
adapter.max_retries = retries unless retries.nil?
|
|
102
|
+
adapter.max_retries = @retries if @retries.to_i > 0 && retries.nil? && default_adapter?(adapter)
|
|
103
|
+
adapter.backoff_factor = @backoff_factor if @backoff_factor.to_f > 0 && default_adapter?(adapter)
|
|
104
|
+
adapter.status_forcelist = @status_forcelist if !@status_forcelist.empty? && adapter.respond_to?(:status_forcelist=) && default_adapter?(adapter)
|
|
70
105
|
t0 = Time.now
|
|
71
106
|
net_resp = adapter.send_once(cur_method, cur_url, hdrs, body, timeout || @timeout,proxies || @proxies, verify.nil? ? @verify : verify,cert || @cert, use_auth)
|
|
72
107
|
elapsed = Time.now - t0
|
|
73
108
|
resp = to_response(adapter, net_resp, cur_url, elapsed, cur_method, hdrs, body)
|
|
74
|
-
@cookies.update(net_resp)
|
|
109
|
+
@cookies.update(net_resp, cur_url)
|
|
75
110
|
resp.cookies = @cookies
|
|
76
111
|
if use_auth.is_a?(DigestAuth) && resp.status_code == 401 && redirs.zero? && hist.empty?
|
|
77
112
|
use_auth.call(hdrs, meth: cur_method, url: cur_url, prev_resp: resp)
|
|
78
113
|
t1 = Time.now
|
|
79
114
|
net_resp = adapter.send_once(cur_method, cur_url, hdrs, body, timeout || @timeout,proxies || @proxies, verify.nil? ? @verify : verify,cert || @cert, nil)
|
|
80
115
|
resp = to_response(adapter, net_resp, cur_url, Time.now - t1, cur_method, hdrs, body)
|
|
81
|
-
@cookies.update(net_resp)
|
|
116
|
+
@cookies.update(net_resp, cur_url)
|
|
82
117
|
resp.cookies = @cookies
|
|
83
118
|
end
|
|
84
119
|
hist << resp
|
|
@@ -107,6 +142,9 @@ module Requests
|
|
|
107
142
|
raise Requests::MissingSchema, "invalid url, no scheme: #{url}" unless s.include?('://')
|
|
108
143
|
raise Requests::InvalidSchema, "unsupported scheme: #{url}" unless s =~ %r{\Ahttps?://}
|
|
109
144
|
end
|
|
145
|
+
def default_adapter?(a)
|
|
146
|
+
@default_adapters.include?(a)
|
|
147
|
+
end
|
|
110
148
|
def adapter_for(url)
|
|
111
149
|
match = @adapters.keys.select { |prefix| url.start_with?(prefix) }.max_by(&:length)
|
|
112
150
|
match ? @adapters[match] : @adapters['https://']
|
|
@@ -116,12 +154,6 @@ module Requests
|
|
|
116
154
|
return a unless b
|
|
117
155
|
a.merge(b)
|
|
118
156
|
end
|
|
119
|
-
def merge_jar(session_jar, extra)
|
|
120
|
-
j = Jar.new
|
|
121
|
-
session_jar.each { |k, v| j[k] = v }
|
|
122
|
-
extra&.each { |k, v| j[k] = v }
|
|
123
|
-
j
|
|
124
|
-
end
|
|
125
157
|
def build_url(base, params)
|
|
126
158
|
return base if params.nil? || (params.respond_to?(:empty?) && params.empty?)
|
|
127
159
|
qs = build_qs(params)
|
data/lib/requests/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: requests_ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- monji024
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-07-
|
|
10
|
+
date: 2026-07-30 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: minitest
|