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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39c4cbcba9b1ee122bcd4ee5168716a9c7aed48b3ed463e1db32a3b2382789ad
4
- data.tar.gz: b7fbe77ea7b887ba479fbf51aa186d350fa30f7b27135bb51a80d790ca0d335b
3
+ metadata.gz: 308045ed13c88c9a9eb1f89a483692c177b605e6aaac49e4eafa7aa0de2a3a74
4
+ data.tar.gz: 7f62813cf1d17285f51b10a79bd4b5f08ea1f91e71172b7b50a9ab416330bcfd
5
5
  SHA512:
6
- metadata.gz: 51632dd4038d0d55285a1b08b4ad2909855a29635237f087c10982a06d769dfecafefa899930e3a2c5fd329d498db41a2eb7e8a1ece0fe707f41f41c8d58b39b
7
- data.tar.gz: d5bec1101e2e23e63d5a093b9fee6b2b044014970346cb0b70e7b52440d7e5f4dd20b2159e3082490648297cfd155e95c0e70de723e64284d0992439e2173969
6
+ metadata.gz: a26999d7ae3f856351a416736f08ff8d1ebd5fb21addd94bcee508d30f3d49de7af9e09c8ea52dc4d4c61bd81126ad884bb3203c5349552863930f2e33cc7a82
7
+ data.tar.gz: f199826c39a900bab90eba7445218b0c9c0f8830a7e33f588591667551c58f53f2c206c26424fcaae51cb207652611baba2ec70648c1138487b9c8ab66f89dc9
data/CHANGELOG.md CHANGED
@@ -1,6 +1,47 @@
1
1
  # Changelog
2
2
 
3
- ## 2.1.0
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: unlike Python's `requests`, the body is fully read before `iter_content`/`iter_lines` yield anything — this library deliberately stays stdlib-only, and true lazy streaming would need extra machinery to keep a `net/http` connection open across method calls. See [Known limitations](#-known-limitations).
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.mount(prefix, adapter)` | `headers`, `cookies` |
303
- | `Requests.codes` | `session.hooks[:response]` | `elapsed`, `history`, `url` |
304
- | | `session.headers`, `.cookies`, `.auth`, `.proxies`, `.retries` | `raise_for_status`, `save_to`, `links` |
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
 
@@ -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
- begin
27
- do_send(meth, url, hdrs, body, timeout, proxies, verify, cert, auth)
28
- rescue Requests::ConnectionError, Requests::ConnectTimeout => e
29
- attempt += 1
30
- if attempt <= @max_retries && idempotent?(meth)
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 = Net::HTTP.new(u.host, u.port, *popt)
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
- http.start do |h|
70
- klass = METHS[meth] || Net::HTTP::Get
71
- req = klass.new(u.request_uri)
72
- hdrs.each { |k, v| req[k] = v }
73
- req.body = body if body
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, Errno::ECONNRESET => e
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
@@ -18,8 +18,6 @@ module Requests
18
18
  Session.new
19
19
  end
20
20
  def self.download(url, to:, **kw)
21
- resp = get(url, **kw)
22
- resp.raise_for_status
23
- resp.save_to(to)
21
+ Session.new.download(url, to: to, **kw)
24
22
  end
25
23
  end
@@ -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
- @c = {}
12
+ @s = {}
8
13
  end
9
14
  def [](n)
10
- @c[n]
15
+ e = @s.values.reverse.find { |c| c.name == n }
16
+ e && e.value
11
17
  end
12
18
  def []=(n, v)
13
- @c[n] = v
19
+ set(n, v)
14
20
  end
15
21
  def get(name, default = nil)
16
- @c.fetch(name, default)
22
+ v = self[name]
23
+ v.nil? ? default : v
17
24
  end
18
- def set(name, value)
19
- @c[name] = value
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.delete(name)
31
+ @s.delete_if { |_, c| c.name == name }
24
32
  end
25
33
  def clear
26
- @c.clear
34
+ @s.clear
27
35
  end
28
36
  def to_h
29
- @c.dup
37
+ h = {}
38
+ @s.each_value { |c| h[c.name] = c.value }
39
+ h
30
40
  end
31
41
  def to_a
32
- @c.to_a
42
+ to_h.to_a
33
43
  end
34
44
  def each(&b)
35
- @c.each(&b)
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
- @c.empty?
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
- @c.map { |k, v| "#{k}=#{v}" }.join('; ')
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
- lines = net_resp.get_fields('set-cookie') || []
45
- lines.each do |l|
46
- kv = l.split(';').first
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
- @c[k.strip] = v.to_s.strip
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
@@ -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
- jar = merge_jar(@cookies, cookies)
66
- hdrs['Cookie'] = jar.to_header unless jar.empty?
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? ? @retries : retries
69
- adapter.backoff_factor = @backoff_factor
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Requests
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.3'
5
5
  end
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.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
- - requests_ruby contributors
7
+ - monji024
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-07-29 00:00:00.000000000 Z
10
+ date: 2026-07-30 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: minitest