w3c_api 0.3.1 → 0.3.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/CLAUDE.md +66 -9
- data/README.adoc +84 -7
- data/lib/w3c_api/commands/affiliation.rb +2 -0
- data/lib/w3c_api/commands/ecosystem.rb +2 -0
- data/lib/w3c_api/commands/group.rb +2 -0
- data/lib/w3c_api/commands/participation.rb +2 -0
- data/lib/w3c_api/commands/series.rb +2 -0
- data/lib/w3c_api/commands/specification.rb +2 -0
- data/lib/w3c_api/commands/specification_version.rb +2 -0
- data/lib/w3c_api/commands/translation.rb +2 -0
- data/lib/w3c_api/commands/user.rb +2 -0
- data/lib/w3c_api/commands/user_agent_option.rb +28 -0
- data/lib/w3c_api/hal.rb +135 -19
- data/lib/w3c_api/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1fa4a04e2e21f619281c54f4722ab649a430667aec4ffa5417d94ffdd5594b9
|
|
4
|
+
data.tar.gz: e59116fe79cd6dd32a6fba9bdc01f54f1f6256d406ca079af63bf0ecd443de4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 460db146f8185a32c6763bd57a11017042ce34b8d1784c8fdec2f4c4d3f156699ca2a0af3125c505c8989bb94eb284bcb0fd3ddfd70b4e764f380b792e7fc2fc
|
|
7
|
+
data.tar.gz: 64b7e45b394e78479a29c7a940fb61cb816c9cae806ce7042fb4647975509229872f7d680c600d67ca1e65502aa4b3809c4eba3f844395e40c2ab3f17b71db7b
|
data/CLAUDE.md
CHANGED
|
@@ -74,13 +74,58 @@ that embedded data instead of issuing a new HTTP request (see
|
|
|
74
74
|
Two cooperating layers, both tuned to grow 1→2→4→8→16s:
|
|
75
75
|
- lutaml-hal's `RateLimiter` (via `rate_limiting_options`) retries **429 and
|
|
76
76
|
5xx**.
|
|
77
|
-
- A Faraday `:retry` middleware in `connection` covers
|
|
78
|
-
the W3C API signals rate-limiting with **HTTP 403**,
|
|
79
|
-
errors.
|
|
77
|
+
- A Faraday `:retry` middleware in `connection` (`DEFAULT_RETRY_OPTIONS`) covers
|
|
78
|
+
what lutaml-hal does not: the W3C API signals rate-limiting with **HTTP 403**,
|
|
79
|
+
plus connection/timeout errors.
|
|
80
|
+
|
|
81
|
+
Two footguns in the Faraday layer, both encoded in constants in `hal.rb` — do
|
|
82
|
+
not "tidy" them away (see issue #23):
|
|
83
|
+
- `exceptions:` **replaces** faraday-retry's `DEFAULT_EXCEPTIONS`, it does not
|
|
84
|
+
merge. `retry_statuses: [403]` is implemented by raising
|
|
85
|
+
`Faraday::RetriableResponse` internally and rescuing it via `exceptions:`, so
|
|
86
|
+
dropping that class disables 403 retries *and* leaks the raise to the caller
|
|
87
|
+
on the first 403. Hence `RETRY_EXCEPTIONS` is built on top of the defaults.
|
|
88
|
+
- `max_interval` caps `Retry-After` as well as the computed backoff, and a
|
|
89
|
+
`Retry-After` above it makes faraday-retry stop retrying entirely rather than
|
|
90
|
+
wait longer. Hence 60s, well above the largest computed backoff (16s).
|
|
80
91
|
|
|
81
92
|
Owning retries in the client means consumers get resilience without wrapping.
|
|
82
|
-
Tune via `Hal.instance.configure_rate_limiting(...)
|
|
83
|
-
the
|
|
93
|
+
Tune via `Hal.instance.configure_rate_limiting(...)` and
|
|
94
|
+
`Hal.instance.configure_retry(...)` — both go through the memoization cascade
|
|
95
|
+
below. There is no `disable_retry`; `configure_retry(max: 0)` is the off switch.
|
|
96
|
+
|
|
97
|
+
Once retries are exhausted a 403 surfaces as `Lutaml::Hal::Error` (its
|
|
98
|
+
`handle_response` has no 403 branch, so 403 falls through to the generic
|
|
99
|
+
`raise Error`), not as a `Faraday::RetriableResponse`.
|
|
100
|
+
|
|
101
|
+
### User agent (`hal.rb`)
|
|
102
|
+
|
|
103
|
+
Requests carry `w3c_api/<VERSION> (+<repo url>)` (`DEFAULT_USER_AGENT`) — a bare
|
|
104
|
+
Faraday user agent is a prime trigger for the Cloudflare bot heuristics in front
|
|
105
|
+
of `api.w3.org`, and W3C asks consumers to identify themselves. Override with
|
|
106
|
+
`Hal.instance.configure_user_agent(...)` (wins), the `W3C_API_USER_AGENT`
|
|
107
|
+
environment variable, or the CLI's `--user-agent` flag. The flag comes from
|
|
108
|
+
`Commands::UserAgentOption`, mixed into every command class — Thor cannot parse
|
|
109
|
+
options placed before a subcommand name, so it cannot live on the root `Cli`.
|
|
110
|
+
|
|
111
|
+
Note that per-request `headers:` passed to `Client` methods never reach the wire:
|
|
112
|
+
lutaml-hal only forwards headers declared as endpoint parameters with
|
|
113
|
+
`location: :header`, and `SimpleParameter` only produces `:path`/`:query`. The
|
|
114
|
+
connection-level user agent is the working mechanism.
|
|
115
|
+
|
|
116
|
+
### Memoization cascade (`hal.rb`)
|
|
117
|
+
|
|
118
|
+
Memoization runs `@register → @client → @connection`, and `ModelRegister` keeps
|
|
119
|
+
the client it was built with — so any connection- or client-level change must
|
|
120
|
+
rebuild all three. Every `configure_*` setter funnels through
|
|
121
|
+
`reset_connection → reset_client → rebuild_register` accordingly.
|
|
122
|
+
|
|
123
|
+
`rebuild_register` rebuilds **eagerly**, and that is load-bearing: `Link#realize`
|
|
124
|
+
resolves the register via `GlobalRegister.instance.get(:w3c_api)`, which *raises*
|
|
125
|
+
when the name is absent. A lazy `reset_register` would leave every
|
|
126
|
+
already-fetched model unable to realize its links until something re-entered
|
|
127
|
+
`Hal#register`. The trade-off is that each `configure_*` call starts a fresh
|
|
128
|
+
object cache — they are start-up knobs, not mid-crawl ones.
|
|
84
129
|
|
|
85
130
|
### Caching (`hal.rb`)
|
|
86
131
|
|
|
@@ -98,10 +143,22 @@ unregisters from lutaml-hal's `GlobalRegister`, otherwise the rebuild raises
|
|
|
98
143
|
Specs use **VCR** (`hook_into :faraday`) with cassettes in
|
|
99
144
|
`spec/fixtures/vcr_cassettes/`. Default record mode is `:new_episodes` and
|
|
100
145
|
requests match on `method, uri, body` — so a new test that hits an unrecorded
|
|
101
|
-
request will perform a real HTTP call and record it
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
146
|
+
request will perform a real HTTP call and record it (pass `record: :none` to a
|
|
147
|
+
cassette to make a mismatch raise instead). `spec_helper.rb` calls
|
|
148
|
+
`configure_user_agent(nil)` before every example, which cascades through the
|
|
149
|
+
whole memoization chain — connection, client, register — and unregisters from
|
|
150
|
+
the lutaml-hal `GlobalRegister` to prevent cross-test endpoint-registration
|
|
151
|
+
bleed. That also gives each example a fresh object cache, so caching doesn't
|
|
152
|
+
mask expected requests.
|
|
153
|
+
|
|
154
|
+
`spec/w3c_api/hal_spec.rb` is the one spec that opts out of VCR: it drives a
|
|
155
|
+
`Faraday::Adapter::Test` connection and wraps every example in
|
|
156
|
+
`VCR.turned_off`, because the `:faraday` hook injects VCR into *that*
|
|
157
|
+
connection too and would otherwise reject the request as unhandled. It also
|
|
158
|
+
restores `@retry_options` in an `around` hook: the per-example
|
|
159
|
+
`configure_user_agent(nil)` rebuilds connection, client and register, but
|
|
160
|
+
nothing resets the retry options, so a `configure_retry` call would otherwise
|
|
161
|
+
persist for the whole suite process.
|
|
105
162
|
|
|
106
163
|
## Conventions
|
|
107
164
|
|
data/README.adoc
CHANGED
|
@@ -17,7 +17,8 @@ https://github.com/lutaml/lutaml-model[lutaml-model]
|
|
|
17
17
|
for all W3C API resources
|
|
18
18
|
* HAL (Hypertext Application Language) implementation for recursive resource
|
|
19
19
|
traversal
|
|
20
|
-
* Built-in rate limiting with exponential backoff
|
|
20
|
+
* Built-in rate limiting with exponential backoff, plus automatic retries of
|
|
21
|
+
HTTP 403 and transient network errors, for reliable API access
|
|
21
22
|
* A command-line interface using Thor following GitHub CLI patterns
|
|
22
23
|
|
|
23
24
|
The endpoint supported is at https://api.w3.org.
|
|
@@ -90,7 +91,7 @@ client = W3cApi::Client.new
|
|
|
90
91
|
W3cApi::Hal.instance.configure_rate_limiting(
|
|
91
92
|
max_retries: 5, # Maximum retry attempts
|
|
92
93
|
base_delay: 1.0, # Initial delay in seconds
|
|
93
|
-
max_delay:
|
|
94
|
+
max_delay: 30.0, # Maximum delay cap
|
|
94
95
|
backoff_factor: 2.0 # Exponential backoff multiplier
|
|
95
96
|
)
|
|
96
97
|
|
|
@@ -98,6 +99,46 @@ W3cApi::Hal.instance.configure_rate_limiting(
|
|
|
98
99
|
specifications = client.specifications
|
|
99
100
|
----
|
|
100
101
|
|
|
102
|
+
==== Retrying HTTP 403
|
|
103
|
+
|
|
104
|
+
The W3C API signals rate limiting with `HTTP 403` rather than `429`, which the
|
|
105
|
+
rate limiter above does not cover. A Faraday retry middleware in the shared
|
|
106
|
+
connection handles it: a `403` response is retried up to 5 times with
|
|
107
|
+
exponential backoff (1, 2, 4, 8, 16 seconds), as are connection and timeout
|
|
108
|
+
errors. If the response carries a `Retry-After` (or `RateLimit-Reset`) header,
|
|
109
|
+
that value is used in place of the computed backoff when it is the longer of the
|
|
110
|
+
two -- unless it exceeds `max_interval`, in which case the middleware gives up
|
|
111
|
+
rather than waiting. See the note below.
|
|
112
|
+
|
|
113
|
+
[source,ruby]
|
|
114
|
+
----
|
|
115
|
+
W3cApi::Hal.instance.configure_retry(
|
|
116
|
+
max: 5, # Maximum retry attempts
|
|
117
|
+
interval: 1.0, # Initial delay in seconds
|
|
118
|
+
backoff_factor: 2, # Exponential backoff multiplier
|
|
119
|
+
max_interval: 60.0, # Delay cap -- also caps `Retry-After`, see below
|
|
120
|
+
retry_statuses: [403]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Effectively turn retrying off
|
|
124
|
+
W3cApi::Hal.instance.configure_retry(max: 0)
|
|
125
|
+
----
|
|
126
|
+
|
|
127
|
+
Options are merged into the current ones, and calling `configure_retry` rebuilds
|
|
128
|
+
the connection, the client and the endpoint register -- so, like the other
|
|
129
|
+
`configure_*` setters, call it once at start-up rather than mid-crawl (see the
|
|
130
|
+
note under <<User agent>>).
|
|
131
|
+
|
|
132
|
+
[NOTE]
|
|
133
|
+
====
|
|
134
|
+
`max_interval` bounds the `Retry-After` header as well as the computed backoff,
|
|
135
|
+
but it does not clamp it. If a server asks for longer than `max_interval`, the
|
|
136
|
+
middleware stops retrying rather than waiting longer, so keep `max_interval`
|
|
137
|
+
comfortably above the largest `Retry-After` you expect.
|
|
138
|
+
====
|
|
139
|
+
|
|
140
|
+
If every retry is exhausted, the final `403` surfaces as a `Lutaml::Hal::Error`.
|
|
141
|
+
|
|
101
142
|
==== Demo and examples
|
|
102
143
|
|
|
103
144
|
To see rate limiting in action, run the included demonstration scripts:
|
|
@@ -123,6 +164,43 @@ The stress test example demonstrates:
|
|
|
123
164
|
* Dynamic configuration changes
|
|
124
165
|
* Bulk operation patterns
|
|
125
166
|
|
|
167
|
+
=== User agent
|
|
168
|
+
|
|
169
|
+
Every request identifies the gem, its version and a contact URL, e.g. `w3c_api/0.3.2 (+https://github.com/relaton/w3c_api)`.
|
|
170
|
+
|
|
171
|
+
`api.w3.org` is fronted by Cloudflare, and a generic HTTP library user agent (the Faraday default) is a common trigger for its bot and rate limiting heuristics. Applications that embed this gem should add their own identity so that W3C can tell traffic sources apart and reach the operator instead of throttling silently.
|
|
172
|
+
|
|
173
|
+
==== Quick start
|
|
174
|
+
|
|
175
|
+
[source,ruby]
|
|
176
|
+
----
|
|
177
|
+
require 'w3c_api'
|
|
178
|
+
|
|
179
|
+
# Inspect the current value
|
|
180
|
+
W3cApi::Hal.instance.user_agent
|
|
181
|
+
# => "w3c_api/0.3.2 (+https://github.com/relaton/w3c_api)"
|
|
182
|
+
|
|
183
|
+
# Identify your application, keeping the gem's own token
|
|
184
|
+
W3cApi::Hal.instance.configure_user_agent(
|
|
185
|
+
"my-crawler/1.4 (+https://example.com/contact) #{W3cApi::Hal::DEFAULT_USER_AGENT}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Restore the default (or the W3C_API_USER_AGENT value)
|
|
189
|
+
W3cApi::Hal.instance.configure_user_agent(nil)
|
|
190
|
+
----
|
|
191
|
+
|
|
192
|
+
Alternatively, set the `W3C_API_USER_AGENT` environment variable, or pass `--user-agent` to any CLI command:
|
|
193
|
+
|
|
194
|
+
[source,shell]
|
|
195
|
+
----
|
|
196
|
+
$ export W3C_API_USER_AGENT="my-crawler/1.4 (+https://example.com/contact)"
|
|
197
|
+
$ w3c_api specification fetch --user-agent "my-crawler/1.4 (+https://example.com/contact)"
|
|
198
|
+
----
|
|
199
|
+
|
|
200
|
+
An explicit `configure_user_agent` call (which is what `--user-agent` performs) takes precedence over the environment variable, which in turn takes precedence over the gem default.
|
|
201
|
+
|
|
202
|
+
NOTE: Changing the user agent rebuilds the HTTP connection, the client and the endpoint register — as do `configure_rate_limiting` and the cache setters. The rebuilt register starts with an empty in-memory object cache, so configure these once at start-up, before issuing requests, rather than mid-crawl. Models fetched earlier keep working: the register is rebuilt eagerly, so their links still realize.
|
|
203
|
+
|
|
126
204
|
=== Caching
|
|
127
205
|
|
|
128
206
|
Realized objects are cached keyed by their (canonical) URL, so a resource linked from many places — editors, working groups, versions — is fetched only once per process. Caching is enabled by default (in-memory) and works transparently with `fetch` and link `.realize`.
|
|
@@ -311,9 +389,10 @@ end
|
|
|
311
389
|
specifications = client.specifications(page: 2, per_page: 50)
|
|
312
390
|
groups = client.groups(page: 1, per_page: 10, limit: 25, offset: 100)
|
|
313
391
|
|
|
314
|
-
# HTTP
|
|
315
|
-
|
|
316
|
-
|
|
392
|
+
# NOTE: HTTP-level settings are not per-request. Options such as `headers:`,
|
|
393
|
+
# `timeout:`, `read_timeout:` and `open_timeout:` are not endpoint parameters
|
|
394
|
+
# and are silently ignored. Configure the user agent connection-wide with
|
|
395
|
+
# W3cApi::Hal.instance.configure_user_agent (see "User agent" above).
|
|
317
396
|
|
|
318
397
|
# Query parameters for filtering and sorting
|
|
319
398
|
rec_specs = client.specifications_by_status('REC', sort: 'date', order: 'desc')
|
|
@@ -323,8 +402,6 @@ active_groups = client.groups(type: 'working-group', status: 'active')
|
|
|
323
402
|
options = {
|
|
324
403
|
page: 1,
|
|
325
404
|
per_page: 25,
|
|
326
|
-
headers: { 'Accept-Language' => 'en-US' },
|
|
327
|
-
timeout: 60,
|
|
328
405
|
sort: 'name'
|
|
329
406
|
}
|
|
330
407
|
specs = client.specifications(options)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "output_formatter"
|
|
5
|
+
require_relative "user_agent_option"
|
|
5
6
|
require_relative "../client"
|
|
6
7
|
|
|
7
8
|
module W3cApi
|
|
@@ -9,6 +10,7 @@ module W3cApi
|
|
|
9
10
|
# Thor CLI command for affiliation operations
|
|
10
11
|
class Affiliation < Thor
|
|
11
12
|
include OutputFormatter
|
|
13
|
+
include UserAgentOption
|
|
12
14
|
|
|
13
15
|
desc "fetch [OPTIONS]", "Fetch affiliations"
|
|
14
16
|
option :id, type: :numeric, desc: "Affiliation ID"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "output_formatter"
|
|
5
|
+
require_relative "user_agent_option"
|
|
5
6
|
require_relative "../client"
|
|
6
7
|
|
|
7
8
|
module W3cApi
|
|
@@ -9,6 +10,7 @@ module W3cApi
|
|
|
9
10
|
# Thor CLI command for ecosystem operations
|
|
10
11
|
class Ecosystem < Thor
|
|
11
12
|
include OutputFormatter
|
|
13
|
+
include UserAgentOption
|
|
12
14
|
|
|
13
15
|
desc "fetch [OPTIONS]", "Fetch ecosystems"
|
|
14
16
|
option :shortname, type: :string, desc: "Ecosystem shortname"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "output_formatter"
|
|
5
|
+
require_relative "user_agent_option"
|
|
5
6
|
require_relative "../client"
|
|
6
7
|
|
|
7
8
|
module W3cApi
|
|
@@ -9,6 +10,7 @@ module W3cApi
|
|
|
9
10
|
# Thor CLI command for group operations
|
|
10
11
|
class Group < Thor
|
|
11
12
|
include OutputFormatter
|
|
13
|
+
include UserAgentOption
|
|
12
14
|
|
|
13
15
|
desc "fetch [OPTIONS]", "Fetch groups"
|
|
14
16
|
option :id, type: :string, desc: "Group ID"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "output_formatter"
|
|
5
|
+
require_relative "user_agent_option"
|
|
5
6
|
require_relative "../client"
|
|
6
7
|
|
|
7
8
|
module W3cApi
|
|
@@ -9,6 +10,7 @@ module W3cApi
|
|
|
9
10
|
# Thor CLI command for participation operations
|
|
10
11
|
class Participation < Thor
|
|
11
12
|
include OutputFormatter
|
|
13
|
+
include UserAgentOption
|
|
12
14
|
|
|
13
15
|
desc "fetch", "Fetch a participation by ID"
|
|
14
16
|
option :id, type: :numeric, required: true, desc: "Participation ID"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "output_formatter"
|
|
5
|
+
require_relative "user_agent_option"
|
|
5
6
|
require_relative "../client"
|
|
6
7
|
|
|
7
8
|
module W3cApi
|
|
@@ -9,6 +10,7 @@ module W3cApi
|
|
|
9
10
|
# Thor CLI command for series operations
|
|
10
11
|
class Series < Thor
|
|
11
12
|
include OutputFormatter
|
|
13
|
+
include UserAgentOption
|
|
12
14
|
|
|
13
15
|
desc "fetch [OPTIONS]", "Fetch specification series"
|
|
14
16
|
option :shortname, type: :string, desc: "Series shortname"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "output_formatter"
|
|
5
|
+
require_relative "user_agent_option"
|
|
5
6
|
require_relative "../client"
|
|
6
7
|
|
|
7
8
|
module W3cApi
|
|
@@ -9,6 +10,7 @@ module W3cApi
|
|
|
9
10
|
# Thor CLI command for specification operations
|
|
10
11
|
class Specification < Thor
|
|
11
12
|
include OutputFormatter
|
|
13
|
+
include UserAgentOption
|
|
12
14
|
|
|
13
15
|
desc "fetch [OPTIONS]", "Fetch specifications"
|
|
14
16
|
option :shortname, type: :string, desc: "Filter by shortname"
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "output_formatter"
|
|
5
|
+
require_relative "user_agent_option"
|
|
5
6
|
|
|
6
7
|
module W3cApi
|
|
7
8
|
module Commands
|
|
8
9
|
class SpecificationVersion < Thor
|
|
9
10
|
include OutputFormatter
|
|
11
|
+
include UserAgentOption
|
|
10
12
|
|
|
11
13
|
desc "editors", "Fetch editors of a specification version"
|
|
12
14
|
option :shortname, type: :string, required: true,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "output_formatter"
|
|
5
|
+
require_relative "user_agent_option"
|
|
5
6
|
require_relative "../client"
|
|
6
7
|
|
|
7
8
|
module W3cApi
|
|
@@ -9,6 +10,7 @@ module W3cApi
|
|
|
9
10
|
# Thor CLI command for translation operations
|
|
10
11
|
class Translation < Thor
|
|
11
12
|
include OutputFormatter
|
|
13
|
+
include UserAgentOption
|
|
12
14
|
|
|
13
15
|
desc "fetch [OPTIONS]", "Fetch a translation by ID"
|
|
14
16
|
option :id, type: :string, desc: "Translation ID"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "output_formatter"
|
|
5
|
+
require_relative "user_agent_option"
|
|
5
6
|
require_relative "../client"
|
|
6
7
|
|
|
7
8
|
module W3cApi
|
|
@@ -9,6 +10,7 @@ module W3cApi
|
|
|
9
10
|
# Thor CLI command for user operations
|
|
10
11
|
class User < Thor
|
|
11
12
|
include OutputFormatter
|
|
13
|
+
include UserAgentOption
|
|
12
14
|
|
|
13
15
|
desc "fetch [OPTIONS]", "Fetch a user by ID"
|
|
14
16
|
option :id, type: :string, required: true, desc: "User ID (required)"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../hal"
|
|
4
|
+
|
|
5
|
+
module W3cApi
|
|
6
|
+
module Commands
|
|
7
|
+
# Adds a --user-agent flag to a command class and applies it before the
|
|
8
|
+
# command runs. Thor cannot parse options that precede a subcommand name,
|
|
9
|
+
# so the flag lives on each command class rather than on the root Cli:
|
|
10
|
+
#
|
|
11
|
+
# w3c_api specification fetch --user-agent "my-crawler/1.0 (+url)"
|
|
12
|
+
module UserAgentOption
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.class_option :user_agent,
|
|
15
|
+
type: :string,
|
|
16
|
+
desc: "User-Agent sent to api.w3.org " \
|
|
17
|
+
"(default: #{Hal::DEFAULT_USER_AGENT})"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(*args)
|
|
21
|
+
super
|
|
22
|
+
# Configure before the command body builds a Client: changing the user
|
|
23
|
+
# agent rebuilds the connection, client and register.
|
|
24
|
+
Hal.instance.configure_user_agent(options[:user_agent]) if options[:user_agent]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/w3c_api/hal.rb
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "singleton"
|
|
4
4
|
require "faraday/retry"
|
|
5
|
+
require "faraday/follow_redirects"
|
|
5
6
|
require "lutaml/hal"
|
|
7
|
+
require_relative "version"
|
|
6
8
|
require_relative "models"
|
|
7
9
|
|
|
8
10
|
module W3cApi
|
|
@@ -44,6 +46,51 @@ module W3cApi
|
|
|
44
46
|
|
|
45
47
|
API_URL = "https://api.w3.org/"
|
|
46
48
|
|
|
49
|
+
# Exceptions the Faraday retry middleware treats as retriable.
|
|
50
|
+
#
|
|
51
|
+
# faraday-retry implements `retry_statuses` by raising
|
|
52
|
+
# Faraday::RetriableResponse internally and rescuing it with a matcher built
|
|
53
|
+
# from `exceptions:`. Passing `exceptions:` REPLACES faraday-retry's
|
|
54
|
+
# DEFAULT_EXCEPTIONS, it does not merge with them -- so omitting
|
|
55
|
+
# Faraday::RetriableResponse silently disables `retry_statuses` *and* lets
|
|
56
|
+
# the internal raise escape to the caller on the very first matching
|
|
57
|
+
# response. Always build this list on top of the defaults.
|
|
58
|
+
RETRY_EXCEPTIONS = (
|
|
59
|
+
Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [Faraday::ConnectionFailed]
|
|
60
|
+
).freeze
|
|
61
|
+
|
|
62
|
+
# Retry policy for the W3C-specific transient failures: HTTP 403 (how the
|
|
63
|
+
# W3C API signals rate-limiting) plus connection/timeout errors. The
|
|
64
|
+
# computed backoff grows 1, 2, 4, 8, 16s, matching rate_limiting_options.
|
|
65
|
+
#
|
|
66
|
+
# max_interval sits far above the largest computed backoff (16s) because it
|
|
67
|
+
# also caps `Retry-After`: faraday-retry's `calculate_sleep_amount` returns
|
|
68
|
+
# nil -- aborting the retry loop entirely, with zero retries -- when a
|
|
69
|
+
# response's `Retry-After` exceeds max_interval. Too small a cap turns a
|
|
70
|
+
# polite server hint into an immediate give-up.
|
|
71
|
+
DEFAULT_RETRY_OPTIONS = {
|
|
72
|
+
max: 5,
|
|
73
|
+
interval: 1.0,
|
|
74
|
+
backoff_factor: 2,
|
|
75
|
+
max_interval: 60.0,
|
|
76
|
+
# Both nested values are frozen too: Hash#freeze is shallow, and
|
|
77
|
+
# retry_options hands out a shallow dup, so an unfrozen array here would
|
|
78
|
+
# let a caller corrupt this baseline for the whole process.
|
|
79
|
+
retry_statuses: [403].freeze,
|
|
80
|
+
exceptions: RETRY_EXCEPTIONS,
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
# api.w3.org is fronted by Cloudflare, and a bare HTTP-library User-Agent
|
|
84
|
+
# such as "Faraday v2.12.2" is a well-known trigger for its bot heuristics —
|
|
85
|
+
# which is how a polite crawl turns into an all-or-nothing 429 storm. W3C
|
|
86
|
+
# also asks API consumers to identify themselves, so name the gem, its
|
|
87
|
+
# version, and a contact URL.
|
|
88
|
+
DEFAULT_USER_AGENT = "w3c_api/#{VERSION} (+https://github.com/relaton/w3c_api)".freeze
|
|
89
|
+
|
|
90
|
+
# Lets an operator put their own contact details on the wire without a code
|
|
91
|
+
# change. An explicit configure_user_agent call still wins.
|
|
92
|
+
USER_AGENT_ENV_VAR = "W3C_API_USER_AGENT"
|
|
93
|
+
|
|
47
94
|
def initialize
|
|
48
95
|
# Don't call setup here - it will be called when register is first accessed
|
|
49
96
|
end
|
|
@@ -56,13 +103,38 @@ module W3cApi
|
|
|
56
103
|
)
|
|
57
104
|
end
|
|
58
105
|
|
|
106
|
+
# User-Agent sent with every request. Precedence: an explicit
|
|
107
|
+
# configure_user_agent call, then W3C_API_USER_AGENT, then the gem default.
|
|
108
|
+
def user_agent
|
|
109
|
+
@user_agent ||=
|
|
110
|
+
normalize_user_agent(ENV.fetch(USER_AGENT_ENV_VAR, nil)) ||
|
|
111
|
+
DEFAULT_USER_AGENT
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Set the User-Agent sent with every request.
|
|
115
|
+
#
|
|
116
|
+
# Applications embedding this gem should identify themselves and keep the
|
|
117
|
+
# gem's own token, e.g.
|
|
118
|
+
# configure_user_agent("my-crawler/1.4 (+https://example.com) #{DEFAULT_USER_AGENT}")
|
|
119
|
+
#
|
|
120
|
+
# Pass nil or a blank string to fall back to W3C_API_USER_AGENT / the gem
|
|
121
|
+
# default. Rebuilds the connection, so call it before issuing requests.
|
|
122
|
+
def configure_user_agent(user_agent)
|
|
123
|
+
@user_agent = normalize_user_agent(user_agent)
|
|
124
|
+
reset_connection
|
|
125
|
+
self.user_agent
|
|
126
|
+
end
|
|
127
|
+
|
|
59
128
|
# Faraday connection mirroring lutaml-hal's default middleware stack, with a
|
|
60
129
|
# retry layer for the failures lutaml-hal's RateLimiter does not cover: the
|
|
61
130
|
# W3C API signals rate-limiting with HTTP 403, plus transient connection and
|
|
62
131
|
# timeout errors. (lutaml-hal still retries 429 and 5xx.) Owning retries here
|
|
63
132
|
# means every consumer of the client is resilient without its own wrapper.
|
|
64
133
|
def connection
|
|
65
|
-
@connection ||= Faraday.new(
|
|
134
|
+
@connection ||= Faraday.new(
|
|
135
|
+
url: API_URL.delete_suffix("/"),
|
|
136
|
+
headers: { "User-Agent" => user_agent },
|
|
137
|
+
) do |conn|
|
|
66
138
|
conn.request :retry, retry_options
|
|
67
139
|
conn.use Faraday::FollowRedirects::Middleware
|
|
68
140
|
conn.request :json
|
|
@@ -71,20 +143,27 @@ module W3cApi
|
|
|
71
143
|
end
|
|
72
144
|
end
|
|
73
145
|
|
|
74
|
-
# Retry policy for the W3C-specific transient failures
|
|
75
|
-
#
|
|
146
|
+
# Retry policy for the W3C-specific transient failures. See
|
|
147
|
+
# DEFAULT_RETRY_OPTIONS for why `exceptions` and `max_interval` are what
|
|
148
|
+
# they are -- both have non-obvious failure modes.
|
|
76
149
|
def retry_options
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
150
|
+
# dup: DEFAULT_RETRY_OPTIONS is frozen and must stay the pristine baseline.
|
|
151
|
+
@retry_options ||= DEFAULT_RETRY_OPTIONS.dup
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Set retry options (merged into the current ones)
|
|
155
|
+
#
|
|
156
|
+
# The retry middleware is baked into the Faraday connection when the
|
|
157
|
+
# connection is built, so this goes through the usual
|
|
158
|
+
# reset_connection -> reset_client -> rebuild_register cascade. Like the
|
|
159
|
+
# other configure_* setters it starts a fresh object cache, so it is a
|
|
160
|
+
# start-up knob rather than a mid-crawl one.
|
|
161
|
+
#
|
|
162
|
+
# There is no separate disable switch -- `configure_retry(max: 0)` turns
|
|
163
|
+
# retrying off, returning the response as-is on the first attempt.
|
|
164
|
+
def configure_retry(options = {})
|
|
165
|
+
@retry_options = retry_options.merge(options)
|
|
166
|
+
reset_connection
|
|
88
167
|
end
|
|
89
168
|
|
|
90
169
|
# Configure rate limiting options
|
|
@@ -106,8 +185,10 @@ module W3cApi
|
|
|
106
185
|
# Set rate limiting options
|
|
107
186
|
def configure_rate_limiting(options = {})
|
|
108
187
|
@rate_limiting_options = rate_limiting_options.merge(options)
|
|
109
|
-
# Reset client to pick up new options
|
|
110
|
-
@client
|
|
188
|
+
# Reset the client *and* the register to pick up the new options: the
|
|
189
|
+
# register keeps the client it was built with, so dropping @client alone
|
|
190
|
+
# left an already-built register on the old rate-limiting settings.
|
|
191
|
+
reset_client
|
|
111
192
|
end
|
|
112
193
|
|
|
113
194
|
# Disable rate limiting
|
|
@@ -136,19 +217,19 @@ module W3cApi
|
|
|
136
217
|
# Set cache options (merged into the current ones)
|
|
137
218
|
def configure_cache(options = {})
|
|
138
219
|
@cache_options = (cache_options || {}).merge(options)
|
|
139
|
-
|
|
220
|
+
rebuild_register
|
|
140
221
|
end
|
|
141
222
|
|
|
142
223
|
# Disable caching of realized objects
|
|
143
224
|
def disable_cache
|
|
144
225
|
@cache_options = nil
|
|
145
|
-
|
|
226
|
+
rebuild_register
|
|
146
227
|
end
|
|
147
228
|
|
|
148
229
|
# Enable caching of realized objects
|
|
149
230
|
def enable_cache(options = nil)
|
|
150
231
|
@cache_options = options || { adapter: :memory }
|
|
151
|
-
|
|
232
|
+
rebuild_register
|
|
152
233
|
end
|
|
153
234
|
|
|
154
235
|
def register
|
|
@@ -167,6 +248,30 @@ module W3cApi
|
|
|
167
248
|
@register
|
|
168
249
|
end
|
|
169
250
|
|
|
251
|
+
# Drop the memoized Faraday connection. Memoization runs
|
|
252
|
+
# register -> client -> connection, so a connection-level change (headers,
|
|
253
|
+
# middleware) only takes effect once all three are rebuilt.
|
|
254
|
+
def reset_connection
|
|
255
|
+
@connection = nil
|
|
256
|
+
reset_client
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Drop the memoized lutaml-hal client. The register holds the client it was
|
|
260
|
+
# constructed with (and hands it to its cache manager), so it must go too.
|
|
261
|
+
def reset_client
|
|
262
|
+
@client = nil
|
|
263
|
+
rebuild_register
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Rebuild the register immediately rather than lazily. Link#realize looks
|
|
267
|
+
# the register up in lutaml-hal's GlobalRegister, which *raises* when the
|
|
268
|
+
# name is absent — so leaving it torn down would break `.realize` on every
|
|
269
|
+
# model fetched before a configure_* call.
|
|
270
|
+
def rebuild_register
|
|
271
|
+
reset_register
|
|
272
|
+
register
|
|
273
|
+
end
|
|
274
|
+
|
|
170
275
|
def reset_register
|
|
171
276
|
# Drop the global registration too, otherwise rebuilding the register
|
|
172
277
|
# raises "replacing another one" when it re-registers the same name.
|
|
@@ -176,6 +281,17 @@ module W3cApi
|
|
|
176
281
|
|
|
177
282
|
private
|
|
178
283
|
|
|
284
|
+
# Header values cannot contain CR/LF, and the value can come from an
|
|
285
|
+
# environment variable or a CLI flag — fold them to spaces rather than let
|
|
286
|
+
# the adapter raise deep inside a request. Blank becomes nil so the caller
|
|
287
|
+
# falls back to the environment or the default.
|
|
288
|
+
def normalize_user_agent(value)
|
|
289
|
+
return nil if value.nil?
|
|
290
|
+
|
|
291
|
+
normalized = value.to_s.gsub(/[\r\n]+/, " ").strip
|
|
292
|
+
normalized.empty? ? nil : normalized
|
|
293
|
+
end
|
|
294
|
+
|
|
179
295
|
# Common pagination parameters (simplified without EndpointParameter)
|
|
180
296
|
def pagination_parameters
|
|
181
297
|
[
|
data/lib/w3c_api/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: w3c_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.2.
|
|
61
|
+
version: 0.2.1
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.2.
|
|
68
|
+
version: 0.2.1
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: lutaml-model
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -154,6 +154,7 @@ files:
|
|
|
154
154
|
- lib/w3c_api/commands/specification_version.rb
|
|
155
155
|
- lib/w3c_api/commands/translation.rb
|
|
156
156
|
- lib/w3c_api/commands/user.rb
|
|
157
|
+
- lib/w3c_api/commands/user_agent_option.rb
|
|
157
158
|
- lib/w3c_api/embed.rb
|
|
158
159
|
- lib/w3c_api/hal.rb
|
|
159
160
|
- lib/w3c_api/models.rb
|