relaton 3.0.0.pre.alpha.1 → 3.0.0.pre.alpha.2
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/README.adoc +57 -1
- data/bin/console +0 -1
- data/lib/relaton/3gpp/bibliography.rb +82 -7
- data/lib/relaton/3gpp/data_fetcher.rb +51 -3
- data/lib/relaton/3gpp/docidentifier.rb +114 -0
- data/lib/relaton/3gpp/item.rb +6 -0
- data/lib/relaton/3gpp/parser.rb +1 -1
- data/lib/relaton/3gpp/processor.rb +4 -1
- data/lib/relaton/3gpp.rb +5 -1
- data/lib/relaton/adobe/bibdata.rb +8 -0
- data/lib/relaton/adobe/bibitem.rb +8 -0
- data/lib/relaton/adobe/bibliography.rb +92 -0
- data/lib/relaton/adobe/docidentifier.rb +49 -0
- data/lib/relaton/adobe/doctype.rb +14 -0
- data/lib/relaton/adobe/ext.rb +32 -0
- data/lib/relaton/adobe/item.rb +15 -0
- data/lib/relaton/adobe/item_base.rb +18 -0
- data/lib/relaton/adobe/item_data.rb +6 -0
- data/lib/relaton/adobe/processor.rb +45 -0
- data/lib/relaton/adobe/util.rb +8 -0
- data/lib/relaton/adobe.rb +37 -0
- data/lib/relaton/bib/model/address.rb +2 -2
- data/lib/relaton/bib/model/docidentifier.rb +24 -9
- data/lib/relaton/bib/model/localized_string.rb +1 -1
- data/lib/relaton/bib/model/structured_identifier.rb +10 -9
- data/lib/relaton/bib/sanitizer.rb +202 -6
- data/lib/relaton/bib.rb +0 -2
- data/lib/relaton/bipm/bibliography.rb +159 -10
- data/lib/relaton/bipm/data_fetcher.rb +26 -2
- data/lib/relaton/bipm/data_outcomes_parser.rb +3 -4
- data/lib/relaton/bipm/id_parser.rb +5 -4
- data/lib/relaton/bipm/model/structured_identifier.rb +21 -0
- data/lib/relaton/bipm/processor.rb +2 -2
- data/lib/relaton/bipm/rawdata_bipm_metrologia/fetcher.rb +2 -4
- data/lib/relaton/bipm/si_brochure_parser.rb +5 -3
- data/lib/relaton/bipm.rb +7 -1
- data/lib/relaton/bsi/bibliography.rb +115 -43
- data/lib/relaton/bsi/hit.rb +14 -0
- data/lib/relaton/bsi/hit_collection.rb +15 -16
- data/lib/relaton/bsi/model/docidentifier.rb +99 -1
- data/lib/relaton/bsi/processor.rb +1 -0
- data/lib/relaton/calconnect/bibliography.rb +12 -14
- data/lib/relaton/calconnect/data_fetcher.rb +77 -9
- data/lib/relaton/calconnect/docidentifier.rb +80 -0
- data/lib/relaton/calconnect/hit_collection.rb +65 -57
- data/lib/relaton/calconnect/model/item.rb +7 -0
- data/lib/relaton/calconnect/processor.rb +7 -1
- data/lib/relaton/calconnect.rb +11 -1
- data/lib/relaton/ccsds/data/fetcher.rb +17 -12
- data/lib/relaton/ccsds/data/parser.rb +1 -1
- data/lib/relaton/ccsds/hit_collection.rb +6 -1
- data/lib/relaton/ccsds/model/docidentifier.rb +121 -0
- data/lib/relaton/ccsds/model/item.rb +2 -0
- data/lib/relaton/cen/bibliography.rb +75 -47
- data/lib/relaton/cen/hit.rb +16 -1
- data/lib/relaton/cen/hit_collection.rb +59 -11
- data/lib/relaton/cen/model/docidentifier.rb +92 -1
- data/lib/relaton/cen/processor.rb +13 -8
- data/lib/relaton/cen/scraper.rb +13 -5
- data/lib/relaton/cen.rb +1 -0
- data/lib/relaton/cie/data_fetcher.rb +215 -30
- data/lib/relaton/cie/processor.rb +3 -1
- data/lib/relaton/cie/scrapper.rb +15 -2
- data/lib/relaton/cie.rb +2 -1
- data/lib/relaton/core/data_fetcher.rb +150 -3
- data/lib/relaton/core/governor.rb +320 -0
- data/lib/relaton/core/pacer.rb +134 -0
- data/lib/relaton/core/processor.rb +19 -0
- data/lib/relaton/core/request_error.rb +14 -0
- data/lib/relaton/core.rb +3 -0
- data/lib/relaton/db/registry.rb +41 -1
- data/lib/relaton/doi/crossref.rb +19 -2
- data/lib/relaton/doi/parser.rb +109 -15
- data/lib/relaton/easc/bibdata.rb +8 -0
- data/lib/relaton/easc/bibitem.rb +8 -0
- data/lib/relaton/easc/bibliography.rb +95 -0
- data/lib/relaton/easc/docidentifier.rb +100 -0
- data/lib/relaton/easc/doctype.rb +14 -0
- data/lib/relaton/easc/ext.rb +44 -0
- data/lib/relaton/easc/item.rb +13 -0
- data/lib/relaton/easc/item_base.rb +18 -0
- data/lib/relaton/easc/item_data.rb +6 -0
- data/lib/relaton/easc/processor.rb +46 -0
- data/lib/relaton/easc/util.rb +8 -0
- data/lib/relaton/easc.rb +35 -0
- data/lib/relaton/ecma/bibliography.rb +93 -25
- data/lib/relaton/ecma/data_fetcher.rb +71 -12
- data/lib/relaton/ecma/docidentifier.rb +124 -0
- data/lib/relaton/ecma/item.rb +2 -0
- data/lib/relaton/ecma/memento_parser.rb +1 -1
- data/lib/relaton/ecma/page_fetcher.rb +15 -3
- data/lib/relaton/ecma/parser_common.rb +2 -2
- data/lib/relaton/ecma/processor.rb +4 -1
- data/lib/relaton/ecma/standard_parser.rb +2 -2
- data/lib/relaton/ecma.rb +10 -1
- data/lib/relaton/etsi/bibliography.rb +67 -2
- data/lib/relaton/etsi/data_fetcher.rb +43 -4
- data/lib/relaton/etsi/processor.rb +3 -1
- data/lib/relaton/etsi.rb +2 -1
- data/lib/relaton/gb/bibliography.rb +55 -29
- data/lib/relaton/gb/docidentifier.rb +58 -9
- data/lib/relaton/gb/processor.rb +3 -0
- data/lib/relaton/gb/scraper.rb +27 -10
- data/lib/relaton/gost/bibdata.rb +8 -0
- data/lib/relaton/gost/bibitem.rb +8 -0
- data/lib/relaton/gost/bibliography.rb +107 -0
- data/lib/relaton/gost/docidentifier.rb +80 -0
- data/lib/relaton/gost/doctype.rb +16 -0
- data/lib/relaton/gost/ext.rb +46 -0
- data/lib/relaton/gost/item.rb +15 -0
- data/lib/relaton/gost/item_base.rb +18 -0
- data/lib/relaton/gost/item_data.rb +6 -0
- data/lib/relaton/gost/processor.rb +49 -0
- data/lib/relaton/gost/util.rb +8 -0
- data/lib/relaton/gost.rb +36 -0
- data/lib/relaton/iala/bibdata.rb +8 -0
- data/lib/relaton/iala/bibitem.rb +8 -0
- data/lib/relaton/iala/bibliography.rb +146 -0
- data/lib/relaton/iala/docidentifier.rb +89 -0
- data/lib/relaton/iala/doctype.rb +18 -0
- data/lib/relaton/iala/ext.rb +32 -0
- data/lib/relaton/iala/item.rb +21 -0
- data/lib/relaton/iala/item_base.rb +18 -0
- data/lib/relaton/iala/item_data.rb +6 -0
- data/lib/relaton/iala/processor.rb +43 -0
- data/lib/relaton/iala/relation.rb +7 -0
- data/lib/relaton/iala/util.rb +8 -0
- data/lib/relaton/iala.rb +35 -0
- data/lib/relaton/iana/bibliography.rb +67 -14
- data/lib/relaton/iana/data_fetcher.rb +35 -5
- data/lib/relaton/iana/processor.rb +3 -1
- data/lib/relaton/iana.rb +12 -1
- data/lib/relaton/iec/data_fetcher.rb +7 -1
- data/lib/relaton/iec/hit_collection.rb +1 -1
- data/lib/relaton/iec/model/docidentifier.rb +9 -5
- data/lib/relaton/iec/model/ext.rb +2 -2
- data/lib/relaton/iec/processor.rb +1 -0
- data/lib/relaton/ieee/bibliography.rb +25 -3
- data/lib/relaton/ieee/data_fetcher.rb +158 -17
- data/lib/relaton/ieee/idams_parser.rb +18 -11
- data/lib/relaton/ieee/processor.rb +4 -1
- data/lib/relaton/ieee/rawbib_id_parser.rb +291 -86
- data/lib/relaton/ieee.rb +2 -1
- data/lib/relaton/ietf/data_fetcher.rb +295 -12
- data/lib/relaton/ietf/processor.rb +7 -3
- data/lib/relaton/ietf/rfc/entry.rb +39 -3
- data/lib/relaton/ietf/scraper.rb +69 -36
- data/lib/relaton/ietf.rb +4 -1
- data/lib/relaton/iho/bibliography.rb +1 -1
- data/lib/relaton/iho/docidentifier.rb +1 -1
- data/lib/relaton/index/file_io.rb +11 -11
- data/lib/relaton/index/file_storage.rb +6 -1
- data/lib/relaton/index/pool.rb +6 -1
- data/lib/relaton/index/shard_source.rb +201 -0
- data/lib/relaton/index/type.rb +63 -12
- data/lib/relaton/index.rb +2 -1
- data/lib/relaton/iso/bibliography.rb +20 -15
- data/lib/relaton/iso/data_fetcher.rb +3 -3
- data/lib/relaton/iso/data_parser.rb +17 -3
- data/lib/relaton/iso/hit_collection.rb +27 -15
- data/lib/relaton/iso/item_data.rb +22 -0
- data/lib/relaton/iso/model/docidentifier.rb +24 -12
- data/lib/relaton/iso/processor.rb +1 -0
- data/lib/relaton/iso/scraper.rb +19 -3
- data/lib/relaton/itu/bibliography.rb +9 -4
- data/lib/relaton/itu/data_crawler_r.rb +664 -0
- data/lib/relaton/itu/data_fetcher.rb +496 -50
- data/lib/relaton/itu/data_merge_r.rb +149 -0
- data/lib/relaton/itu/data_parser_r.rb +163 -89
- data/lib/relaton/itu/data_parser_t.rb +228 -0
- data/lib/relaton/itu/family_cache.rb +177 -0
- data/lib/relaton/itu/governor.rb +56 -0
- data/lib/relaton/itu/hit.rb +9 -3
- data/lib/relaton/itu/hit_collection.rb +258 -86
- data/lib/relaton/itu/model/docidentifier.rb +67 -1
- data/lib/relaton/itu/model/structured_identifier.rb +19 -0
- data/lib/relaton/itu/processor.rb +10 -4
- data/lib/relaton/itu/pubid.rb +27 -5
- data/lib/relaton/itu/recommendation_fields.rb +334 -0
- data/lib/relaton/itu/recommendation_parser.rb +18 -149
- data/lib/relaton/itu/scraper.rb +13 -3
- data/lib/relaton/itu.rb +2 -1
- data/lib/relaton/jcgm/bibdata.rb +8 -0
- data/lib/relaton/jcgm/bibitem.rb +8 -0
- data/lib/relaton/jcgm/bibliography.rb +97 -0
- data/lib/relaton/jcgm/data_fetcher.rb +81 -0
- data/lib/relaton/jcgm/docidentifier.rb +102 -0
- data/lib/relaton/jcgm/doctype.rb +12 -0
- data/lib/relaton/jcgm/ext.rb +23 -0
- data/lib/relaton/jcgm/item.rb +20 -0
- data/lib/relaton/jcgm/item_base.rb +18 -0
- data/lib/relaton/jcgm/item_data.rb +6 -0
- data/lib/relaton/jcgm/meetings_parser.rb +175 -0
- data/lib/relaton/jcgm/processor.rb +71 -0
- data/lib/relaton/jcgm/relation.rb +9 -0
- data/lib/relaton/jcgm/structured_identifier.rb +40 -0
- data/lib/relaton/jcgm/util.rb +8 -0
- data/lib/relaton/jcgm.rb +24 -0
- data/lib/relaton/jis/bibliography.rb +8 -10
- data/lib/relaton/jis/data_fetcher.rb +21 -19
- data/lib/relaton/jis/docidentifier.rb +104 -5
- data/lib/relaton/jis/hit.rb +18 -23
- data/lib/relaton/jis/hit_collection.rb +19 -18
- data/lib/relaton/jis/processor.rb +1 -1
- data/lib/relaton/jis.rb +2 -3
- data/lib/relaton/logger/channels/gh_issue.rb +78 -13
- data/lib/relaton/nist/data_fetcher.rb +63 -13
- data/lib/relaton/nist/docidentifier.rb +165 -0
- data/lib/relaton/nist/item.rb +2 -0
- data/lib/relaton/nist/item_base.rb +16 -0
- data/lib/relaton/nist/mods_parser.rb +38 -12
- data/lib/relaton/nist/processor.rb +2 -1
- data/lib/relaton/nist/relation.rb +3 -0
- data/lib/relaton/nist/scraper.rb +6 -3
- data/lib/relaton/oasis/bibliography.rb +147 -6
- data/lib/relaton/oasis/data_fetcher.rb +41 -5
- data/lib/relaton/oasis/data_parser_utils.rb +37 -3
- data/lib/relaton/oasis/docidentifier.rb +54 -0
- data/lib/relaton/oasis/item.rb +3 -0
- data/lib/relaton/oasis/processor.rb +7 -1
- data/lib/relaton/oasis.rb +14 -1
- data/lib/relaton/ogc/data_fetcher.rb +23 -2
- data/lib/relaton/ogc/docidentifier.rb +105 -0
- data/lib/relaton/ogc/hit_collection.rb +78 -3
- data/lib/relaton/ogc/processor.rb +2 -1
- data/lib/relaton/ogc.rb +5 -1
- data/lib/relaton/oiml/bibliography.rb +90 -15
- data/lib/relaton/oiml/docidentifier.rb +18 -3
- data/lib/relaton/omg/docidentifier.rb +67 -0
- data/lib/relaton/omg/item.rb +1 -0
- data/lib/relaton/omg/processor.rb +1 -0
- data/lib/relaton/omg/scraper.rb +61 -16
- data/lib/relaton/omg.rb +1 -0
- data/lib/relaton/plateau/bibliography.rb +10 -3
- data/lib/relaton/plateau/data_fetcher.rb +25 -2
- data/lib/relaton/plateau/handbook_parser.rb +8 -1
- data/lib/relaton/plateau/hit.rb +10 -2
- data/lib/relaton/plateau/hit_collection.rb +31 -11
- data/lib/relaton/plateau/processor.rb +3 -1
- data/lib/relaton/plateau/technical_report_parser.rb +8 -1
- data/lib/relaton/plateau.rb +2 -1
- data/lib/relaton/sdo/config.rb +34 -0
- data/lib/relaton/sdo/fetcher.rb +52 -0
- data/lib/relaton/sdo/logo.rb +95 -0
- data/lib/relaton/sdo/name.rb +26 -0
- data/lib/relaton/sdo/organization.rb +71 -0
- data/lib/relaton/sdo/store.rb +49 -0
- data/lib/relaton/sdo.rb +29 -0
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/bibliography.rb +132 -12
- data/lib/relaton/w3c/data_fetcher.rb +194 -16
- data/lib/relaton/w3c/data_parser.rb +3 -3
- data/lib/relaton/w3c/docidentifier.rb +48 -0
- data/lib/relaton/w3c/governor.rb +32 -0
- data/lib/relaton/w3c/item.rb +3 -0
- data/lib/relaton/w3c/pubid.rb +12 -0
- data/lib/relaton/w3c/safe_realize.rb +110 -21
- data/lib/relaton/w3c.rb +12 -1
- data/lib/relaton/xsf/bibliography.rb +61 -1
- data/lib/relaton/xsf/data_fetcher.rb +55 -5
- data/lib/relaton/xsf/docidentifier.rb +46 -0
- data/lib/relaton/xsf/hit_collection.rb +31 -3
- data/lib/relaton/xsf/item.rb +6 -0
- data/lib/relaton/xsf/processor.rb +1 -0
- data/lib/relaton/xsf.rb +5 -1
- data/lib/relaton.rb +42 -0
- metadata +135 -24
- data/lib/relaton/ieee/pub_id.rb +0 -161
- data/lib/relaton/index/id_number.rb +0 -30
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Relaton
|
|
4
|
+
module Itu
|
|
5
|
+
#
|
|
6
|
+
# Cross-row cache for the ITU-T detail endpoints that answer the same thing
|
|
7
|
+
# for every edition of one recommendation.
|
|
8
|
+
#
|
|
9
|
+
# `searchRecs` runs with `main_edition_flag=0`, so it returns one row **per
|
|
10
|
+
# edition** — ~16k rows over ~7.7k recommendations. Enrichment then costs
|
|
11
|
+
# four www.itu.int requests per row, and three of them are keyed by `idrec`
|
|
12
|
+
# but answer for the whole recommendation. Verified from
|
|
13
|
+
# `spec/itu/vcr_cassettes/itu_t_h_264.yml`: `getRecEditions?idrec=16818` and
|
|
14
|
+
# `?idrec=14659` return **byte-identical** 21-row payloads. H.264's 21
|
|
15
|
+
# editions therefore issue 21 identical requests, and the corpus average is
|
|
16
|
+
# ~2.1 editions per recommendation.
|
|
17
|
+
#
|
|
18
|
+
# The cache is created per crawl by `DataFetcher#fetch_recommendations` and
|
|
19
|
+
# threaded down as a parameter — never global state, and never reachable
|
|
20
|
+
# from the live `Bibliography.get` path, which gets {NullCache} instead.
|
|
21
|
+
#
|
|
22
|
+
# Two locks, and the split matters. `@mutex` guards only the entry *table*;
|
|
23
|
+
# each entry carries its own lock, which the block (an HTTP request) is run
|
|
24
|
+
# under. Holding `@mutex` across the request would serialize all eight
|
|
25
|
+
# workers onto one connection's latency and collapse the pool to one thread;
|
|
26
|
+
# the per-entry lock is also what makes two workers racing on two editions
|
|
27
|
+
# of one recommendation issue **one** request rather than two.
|
|
28
|
+
#
|
|
29
|
+
class FamilyCache
|
|
30
|
+
# Entries resident at once. Bounded because an unbounded cache over ~7.7k
|
|
31
|
+
# recommendations, each holding an editions list and a supplements list,
|
|
32
|
+
# runs to hundreds of MB on a CI runner alongside eight Mechanize agents.
|
|
33
|
+
# With rows enqueued family-by-family only a handful are ever live, so the
|
|
34
|
+
# cap costs no hit rate.
|
|
35
|
+
DEFAULT_MAX_ENTRIES = 512
|
|
36
|
+
|
|
37
|
+
Entry = Struct.new(:mutex, :value, :computed)
|
|
38
|
+
|
|
39
|
+
def initialize(max_entries: DEFAULT_MAX_ENTRIES)
|
|
40
|
+
@max = [max_entries.to_i, 1].max
|
|
41
|
+
@entries = {}
|
|
42
|
+
@mutex = Mutex.new
|
|
43
|
+
@hits = 0
|
|
44
|
+
@misses = 0
|
|
45
|
+
@evictions = 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Value for `key`, computing it under that key's own lock when absent.
|
|
49
|
+
#
|
|
50
|
+
# A block that raises leaves the entry uncached, so a transient failure is
|
|
51
|
+
# retried by the next caller instead of being served to the whole family
|
|
52
|
+
# as a permanent nil.
|
|
53
|
+
#
|
|
54
|
+
# @param key [Object]
|
|
55
|
+
# @return [Object]
|
|
56
|
+
def fetch(key)
|
|
57
|
+
entry = @mutex.synchronize { touch key }
|
|
58
|
+
entry.mutex.synchronize do
|
|
59
|
+
if entry.computed
|
|
60
|
+
@mutex.synchronize { @hits += 1 }
|
|
61
|
+
next entry.value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
value = yield
|
|
65
|
+
entry.value = value
|
|
66
|
+
entry.computed = true
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
@misses += 1
|
|
69
|
+
# Re-publish. The table lock is released between #touch and the
|
|
70
|
+
# entry lock above, and in that window this entry is unlocked and so
|
|
71
|
+
# a valid eviction candidate for another thread's #touch. Nothing
|
|
72
|
+
# wrong is ever served — this thread holds the Entry directly — but
|
|
73
|
+
# without this the write is lost and the next caller pays for the
|
|
74
|
+
# request again. `unless key?` so a replacement entry another thread
|
|
75
|
+
# has since created is not discarded.
|
|
76
|
+
@entries[key] = entry unless @entries.key? key
|
|
77
|
+
end
|
|
78
|
+
value
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Publish an already-computed value under every other key it also answers.
|
|
83
|
+
#
|
|
84
|
+
# This is what makes one request warm a whole family: a `getRecEditions`
|
|
85
|
+
# response names every sibling `idrec`, and by the endpoint's own testimony
|
|
86
|
+
# the payload for each of them is the same one. An entry whose lock is held
|
|
87
|
+
# is skipped — it is mid-fetch and will land on this value anyway.
|
|
88
|
+
#
|
|
89
|
+
# @param keys [Array<Object>]
|
|
90
|
+
# @param value [Object]
|
|
91
|
+
# @return [Object] value
|
|
92
|
+
def warm(keys, value)
|
|
93
|
+
keys.each do |key|
|
|
94
|
+
entry = @mutex.synchronize { touch key }
|
|
95
|
+
next unless entry.mutex.try_lock
|
|
96
|
+
|
|
97
|
+
begin
|
|
98
|
+
next if entry.computed
|
|
99
|
+
|
|
100
|
+
entry.value = value
|
|
101
|
+
entry.computed = true
|
|
102
|
+
ensure
|
|
103
|
+
entry.mutex.unlock
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @return [Boolean] whether this cache actually caches. Callers use it to
|
|
110
|
+
# skip building a key whose computation would itself cost a request.
|
|
111
|
+
def caching?
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Hash] for the run summary
|
|
116
|
+
def stats
|
|
117
|
+
@mutex.synchronize do
|
|
118
|
+
{ hits: @hits, misses: @misses, entries: @entries.size, evictions: @evictions }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
# Fetch-or-create `key` and mark it most recently used. Callers hold
|
|
125
|
+
# @mutex. Eviction skips an entry currently being computed, so a worker
|
|
126
|
+
# can never lose the entry it is holding.
|
|
127
|
+
def touch(key)
|
|
128
|
+
if @entries.key? key
|
|
129
|
+
entry = @entries.delete key
|
|
130
|
+
return @entries[key] = entry
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
evict while @entries.size >= @max
|
|
134
|
+
@entries[key] = Entry.new(Mutex.new, nil, false)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def evict
|
|
138
|
+
victim = @entries.each_key.find { |k| !@entries[k].mutex.locked? }
|
|
139
|
+
# Everything resident is in flight: grow rather than block a worker.
|
|
140
|
+
return @max = @entries.size + 1 unless victim
|
|
141
|
+
|
|
142
|
+
@entries.delete victim
|
|
143
|
+
@evictions += 1
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
#
|
|
148
|
+
# The no-op cache: `#fetch` is a bare yield.
|
|
149
|
+
#
|
|
150
|
+
# What the live `Bibliography.get` path gets, and the default everywhere, so
|
|
151
|
+
# runtime behaviour and runtime request count are byte-identical to what
|
|
152
|
+
# they were before the cache existed and no state survives a lookup. Only
|
|
153
|
+
# `DataParserT`, walking one corpus of 16k rows, injects a real one.
|
|
154
|
+
#
|
|
155
|
+
class NullCache
|
|
156
|
+
def self.instance
|
|
157
|
+
@instance ||= new
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def fetch(_key)
|
|
161
|
+
yield
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def warm(_keys, value)
|
|
165
|
+
value
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def caching?
|
|
169
|
+
false
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def stats
|
|
173
|
+
{ hits: 0, misses: 0, entries: 0, evictions: 0 }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mechanize"
|
|
4
|
+
require_relative "../core/governor"
|
|
5
|
+
|
|
6
|
+
module Relaton
|
|
7
|
+
module Itu
|
|
8
|
+
#
|
|
9
|
+
# ITU binding of {Relaton::Core::Governor}, for the ITU-R crawl.
|
|
10
|
+
#
|
|
11
|
+
# ITU does not signal throttling with a distinct exception class the way
|
|
12
|
+
# api.w3.org does, so membership is decided by status *code* and by one
|
|
13
|
+
# marker module rather than by an error hierarchy. Measured over a
|
|
14
|
+
# 49-minute full run, the F5 WAF in front of www.itu.int answers `/rec`
|
|
15
|
+
# with **HTTP 503** and `/pub` with a **302 to notfound.aspx** — a soft
|
|
16
|
+
# block Mechanize follows to a perfectly good 200, indistinguishable from
|
|
17
|
+
# "no such document" unless the crawler marks it (see
|
|
18
|
+
# `DataCrawlerR::SoftBlock`, which includes {SoftBlock} for exactly this).
|
|
19
|
+
#
|
|
20
|
+
# Why a governor at all, when `DataCrawlerR#get` already retries: a rate
|
|
21
|
+
# limit is a *pool* event. One worker's 503 must pause every other worker,
|
|
22
|
+
# for a window that outlasts the ban, instead of four workers each running
|
|
23
|
+
# their own 5/10/15 s ladder and keeping the limiter engaged. That is the
|
|
24
|
+
# failure the W3C crawl spent 4 h 56 m in before this class existed.
|
|
25
|
+
#
|
|
26
|
+
class Governor < Relaton::Core::Governor
|
|
27
|
+
# Mixed into the crawler's soft-block error. A module rather than a class
|
|
28
|
+
# so the exception can stay a `Mechanize::ResponseCodeError` subclass —
|
|
29
|
+
# which is what keeps `#get`'s existing rescue list unchanged — while
|
|
30
|
+
# still being distinguishable from the 404 it otherwise looks like.
|
|
31
|
+
module SoftBlock; end
|
|
32
|
+
|
|
33
|
+
# Response codes that mean "the WAF is pushing back", as opposed to "that
|
|
34
|
+
# document is not there" or "that record is broken".
|
|
35
|
+
#
|
|
36
|
+
# **500 is deliberately absent.** Same call the W3C governor makes: a
|
|
37
|
+
# persistent 5xx on one resource is a broken record, and routing it here
|
|
38
|
+
# would open a pool-wide cooldown for every bad page in the corpus. Only
|
|
39
|
+
# 503 (measured, on /rec) and the gateway codes, which are infrastructure
|
|
40
|
+
# rather than content, get the pool treatment.
|
|
41
|
+
THROTTLE_CODES = %w[429 502 503 504].freeze
|
|
42
|
+
|
|
43
|
+
# RELATON_ITU_THROTTLE_BASE / _MAX / _GIVEUP.
|
|
44
|
+
ENV_PREFIX = "RELATON_ITU"
|
|
45
|
+
|
|
46
|
+
# @param error [StandardError]
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def self.throttle?(error)
|
|
49
|
+
return true if error.is_a? SoftBlock
|
|
50
|
+
return false unless error.is_a? ::Mechanize::ResponseCodeError
|
|
51
|
+
|
|
52
|
+
THROTTLE_CODES.include? error.response_code.to_s
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/relaton/itu/hit.rb
CHANGED
|
@@ -6,10 +6,16 @@ module Relaton
|
|
|
6
6
|
class Hit < Relaton::Core::Hit
|
|
7
7
|
attr_writer :item
|
|
8
8
|
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# The document itself. An index-derived hit (`:file`) is loaded from the
|
|
10
|
+
# data repository on demand — a reference matching many editions builds a
|
|
11
|
+
# hit per edition, and only the one Bibliography selects is downloaded.
|
|
12
|
+
# Everything else is scraped from www.itu.int.
|
|
13
|
+
#
|
|
14
|
+
# @return [Relaton::Itu::ItemData, nil]
|
|
11
15
|
def item
|
|
12
|
-
@item ||=
|
|
16
|
+
@item ||= if hit[:file] then hit_collection.fetch_item(hit[:url])
|
|
17
|
+
else Scraper.parse_page self, imp: gi_imp
|
|
18
|
+
end
|
|
13
19
|
end
|
|
14
20
|
|
|
15
21
|
private
|
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "json"
|
|
3
5
|
require_relative "hit"
|
|
4
6
|
|
|
5
7
|
module Relaton
|
|
6
8
|
module Itu
|
|
7
9
|
# Page of hit collection.
|
|
10
|
+
#
|
|
11
|
+
# Reference discovery has three sources, because ITU decommissioned the
|
|
12
|
+
# `net4/.../GlobalSearch/RunSearch` endpoint that used to serve all of them:
|
|
13
|
+
#
|
|
14
|
+
# - Recommendations (`ITU-T …`) and ITU-R publications come from the combined
|
|
15
|
+
# `relaton-data-itu` `index-v2`; an ITU-T reference the index can't serve
|
|
16
|
+
# falls back to the live `rec.aspx` + `getRecEditions` pair.
|
|
17
|
+
# - Radio Regulations (`ITU-R RR …`) and Operational Bulletins (`… OB.N …`)
|
|
18
|
+
# are not in the dataset; their landing page URL is derivable from the
|
|
19
|
+
# reference, so they are resolved directly against `www.itu.int/pub/…`.
|
|
8
20
|
class HitCollection < Relaton::Core::HitCollection
|
|
9
21
|
DOMAIN = "https://www.itu.int"
|
|
10
|
-
|
|
22
|
+
GH_ITU = "https://raw.githubusercontent.com/relaton/relaton-data-itu/refs/heads/main/"
|
|
23
|
+
REC_URL = "#{DOMAIN}/ITU-T/recommendations/rec.aspx?rec=%<rec>s&lang=en".freeze
|
|
24
|
+
RECEDITIONS_URL = "#{DOMAIN}/mws/api/recommendations/getRecEditions?idrec=%<idrec>s&lang=en".freeze
|
|
25
|
+
HANDLE_URL = "http://handle.itu.int/11.1002/1000/%<idrec>s-en".freeze
|
|
11
26
|
|
|
12
27
|
def search
|
|
13
28
|
case ref.to_ref
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
# `ref` is a parsed Pubid, so only the abbreviated `OB.N` form survives
|
|
30
|
+
# here — the local grammar reduces "Operational Bulletin 1096" to the
|
|
31
|
+
# code "Operational" and drops the year, which no route can resolve.
|
|
32
|
+
when %r{^ITU-R\sRR}, %r{\bOB\.} then request_publication
|
|
33
|
+
when /^ITU-T/ then request_recommendation
|
|
34
|
+
# A Report reference renders "Report ITU-R …" (the discriminator leads,
|
|
35
|
+
# as ITU cites it), so it no longer starts with the sector — but it is
|
|
36
|
+
# still served from the index like any other ITU-R document.
|
|
37
|
+
when /^ITU-R\s/, /^Report\sITU-R\s/ then request_document
|
|
18
38
|
end
|
|
19
39
|
rescue Mechanize::ResponseCodeError, SocketError, Timeout::Error, Errno::ECONNRESET,
|
|
20
40
|
EOFError, Net::ProtocolError, OpenSSL::SSL::SSLError => e
|
|
@@ -25,102 +45,254 @@ module Relaton
|
|
|
25
45
|
@agent ||= Mechanize.new.tap { |agent| agent.user_agent_alias = "Mac Safari" }
|
|
26
46
|
end
|
|
27
47
|
|
|
48
|
+
# Fetch and parse one document from the data repository. Public because
|
|
49
|
+
# `Hit#item` calls it to load an index-derived hit lazily — a reference
|
|
50
|
+
# that matches many editions must not download them all.
|
|
51
|
+
#
|
|
52
|
+
# @param url [String] raw URL of the document's YAML
|
|
53
|
+
# @return [Relaton::Itu::ItemData, nil] nil when the file is missing
|
|
54
|
+
def fetch_item(url)
|
|
55
|
+
resp = agent.get url
|
|
56
|
+
return if resp.code == "404"
|
|
57
|
+
|
|
58
|
+
Item.from_yaml(resp.body).tap { |i| i.fetched = Date.today.to_s }
|
|
59
|
+
rescue Mechanize::ResponseCodeError, SocketError, Timeout::Error, Errno::ECONNRESET,
|
|
60
|
+
EOFError, Net::ProtocolError, OpenSSL::SSL::SSLError => e
|
|
61
|
+
# Mechanize *raises* on 404 rather than returning the response, so an
|
|
62
|
+
# index row whose data file has gone missing is caught here —
|
|
63
|
+
# Bibliography then moves on to the next edition instead of failing.
|
|
64
|
+
return if e.is_a?(Mechanize::ResponseCodeError) && e.response_code == "404"
|
|
65
|
+
|
|
66
|
+
# `#search` wraps its own failures, but a lazy hit is loaded later, from
|
|
67
|
+
# `Hit#item` — outside that rescue.
|
|
68
|
+
raise Relaton::RequestError, "Could not access #{url}: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
|
|
28
71
|
private
|
|
29
72
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
73
|
+
# ITU-T Recommendations and supplements. The index is preferred (offline,
|
|
74
|
+
# every edition, no scraping); the live path covers what the dataset does
|
|
75
|
+
# not have — Implementers' Guides, the `(V##)`/`Annex` forms the data
|
|
76
|
+
# repo's crawler leaves unindexed, and editions approved since its last
|
|
77
|
+
# crawl. Indexed editions are kept when the live path finds nothing, so a
|
|
78
|
+
# wrong-year reference still reports the years that do exist.
|
|
79
|
+
def request_recommendation
|
|
80
|
+
hits = index_hits
|
|
81
|
+
return @array = hits if year_satisfied? hits
|
|
82
|
+
|
|
83
|
+
live = live_hits
|
|
84
|
+
@array = live.empty? ? hits : live
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Array<Relaton::Itu::Hit>] newest edition first
|
|
88
|
+
def index_hits
|
|
89
|
+
pubid = index_pubid_ref
|
|
90
|
+
return [] unless pubid
|
|
91
|
+
|
|
92
|
+
Util.info "Fetching from Relaton repository ...", key: ref.to_s
|
|
93
|
+
index.search(pubid) { |i| index_match?(pubid, i[:id]) }
|
|
94
|
+
.map { |r| index_hit r }.sort_by { |h| edition_key h.hit[:code] }.reverse
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @param row [Hash] an index row
|
|
98
|
+
# @return [Relaton::Itu::Hit]
|
|
99
|
+
def index_hit(row)
|
|
100
|
+
# `row[:id].to_s` renders the dated docidentifier the data record itself
|
|
101
|
+
# carries ("ITU-T L.163 (11/2018)"), which is what Bibliography's year
|
|
102
|
+
# filtering parses — so no separate edition bookkeeping is needed.
|
|
103
|
+
Hit.new({ code: row[:id].to_s, url: "#{GH_ITU}#{row[:file]}", file: row[:file] }, self)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Can the index answer the reference? A year-less reference is satisfied by
|
|
107
|
+
# any hit; a dated one needs the year to be present, otherwise a newer (or
|
|
108
|
+
# unindexed) edition has to come from www.itu.int.
|
|
109
|
+
#
|
|
110
|
+
# @param hits [Array<Relaton::Itu::Hit>]
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def year_satisfied?(hits)
|
|
113
|
+
return false if hits.empty?
|
|
114
|
+
|
|
115
|
+
ref.year.nil? || hits.any? { |h| edition_key(h.hit[:code]).first == ref.year.to_i }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The edition date of a rendered docidentifier, as `[year, month]`. Uses
|
|
119
|
+
# the **first** date in the string, mirroring Bibliography's year filter:
|
|
120
|
+
# an amendment ("ITU-T G.989.2 (2014) Amd 1 (04/2016)") is identified by
|
|
121
|
+
# the year of the recommendation it amends.
|
|
122
|
+
#
|
|
123
|
+
# @param code [String, nil]
|
|
124
|
+
# @return [Array<Integer>]
|
|
125
|
+
def edition_key(code)
|
|
126
|
+
month, year = code.to_s.match(%r{\((?:(\d{2})/)?(\d{4})\)})&.captures
|
|
127
|
+
[year.to_i, month.to_i]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def index
|
|
131
|
+
Relaton::Index.find_or_create :itu, url: "#{GH_ITU}#{INDEXFILE}.zip",
|
|
132
|
+
file: "#{INDEXFILE}.yaml",
|
|
133
|
+
pubid_class: ::Pubid::Itu::Identifier
|
|
36
134
|
end
|
|
37
135
|
|
|
38
136
|
def request_document # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
|
|
39
137
|
Util.info "Fetching from Relaton repository ...", key: ref.to_s
|
|
40
|
-
|
|
41
|
-
|
|
138
|
+
# Pass the reference's parsed pubid (not a String) so Relaton::Index can
|
|
139
|
+
# narrow candidates by document number before the block; `index_match?`
|
|
140
|
+
# then matches every edition when the reference omits the part and the
|
|
141
|
+
# exact edition when it names one. Rows are Pubid::Itu objects (not
|
|
142
|
+
# Comparable), so rank by the numeric edition in `code.parts` (`["3"]` → 3)
|
|
143
|
+
# to return the latest — index order isn't by edition.
|
|
144
|
+
pubid = pubid_ref
|
|
145
|
+
row = index.search(pubid) { |i| index_match?(pubid, i[:id]) }
|
|
146
|
+
.max_by { |i| i[:id].code&.parts&.last.to_i }
|
|
42
147
|
return unless row
|
|
43
148
|
|
|
44
|
-
url = "#{
|
|
45
|
-
|
|
46
|
-
return
|
|
149
|
+
url = "#{GH_ITU}#{row[:file]}"
|
|
150
|
+
item = fetch_item url
|
|
151
|
+
return unless item
|
|
47
152
|
|
|
48
|
-
item = Item.from_yaml(resp.body).tap { |i| i.fetched = Date.today.to_s }
|
|
49
153
|
hit = Hit.new({ url: url, ref: ref }, self)
|
|
50
154
|
hit.item = item
|
|
51
155
|
@array = [hit]
|
|
52
156
|
end
|
|
53
157
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"Topics" => "",
|
|
108
|
-
"ClientData" => {},
|
|
109
|
-
"Language" => "en",
|
|
110
|
-
"SearchType" => "All",
|
|
111
|
-
}
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# @param data [Hash]
|
|
158
|
+
# Parse the reference into a Pubid::Itu identifier for the index lookup. A
|
|
159
|
+
# ref pubid can't parse raises (a `Pubid`/`Parslet` error) and propagates to
|
|
160
|
+
# the caller (relaton-cli / API callers rescue it), mirroring the ETSI
|
|
161
|
+
# flavor — the consumer no longer degrades to a raw-string substring search.
|
|
162
|
+
#
|
|
163
|
+
# @return [::Pubid::Itu::Identifier]
|
|
164
|
+
def pubid_ref
|
|
165
|
+
::Pubid::Itu.parse ref.to_ref
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The reference as a pubid for an ITU-T index lookup, stripped of its
|
|
169
|
+
# edition date (every edition is a candidate; Bibliography selects the
|
|
170
|
+
# year). Unlike `#pubid_ref` this **rescues** an unparseable reference: the
|
|
171
|
+
# ITU-T path has a live fallback to degrade to, and a malformed reference
|
|
172
|
+
# (e.g. `ITU-T G.Suppl.47`) should warn and report "Not found", not raise.
|
|
173
|
+
#
|
|
174
|
+
# @return [::Pubid::Itu::Identifier, nil]
|
|
175
|
+
def index_pubid_ref
|
|
176
|
+
undated = ref.dup.tap { |r| r.year = r.month = r.day = nil }
|
|
177
|
+
::Pubid::Itu.parse undated.to_ref
|
|
178
|
+
rescue StandardError
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Does an index row's id match the reference? When the reference omits the
|
|
183
|
+
# part/edition (a bare `ITU-R P.838`), every edition of that exact document
|
|
184
|
+
# matches — "search all parts"; when it names a part (`ITU-R P.838-2`), only
|
|
185
|
+
# that edition matches. Delegates to pubid's structured `matches?`, ignoring
|
|
186
|
+
# `:parts` only when the reference omits the part — so a bare `ITU-R M.1`
|
|
187
|
+
# does not match `ITU-R M.10` (a different document number) and `-1` differs
|
|
188
|
+
# from `-10`, without the local `-`-separator anchor. Mirrors the ETSI
|
|
189
|
+
# flavor's `Bibliography#best_match`. Both ids are `Pubid::Itu` identifiers.
|
|
190
|
+
#
|
|
191
|
+
# `:year`/`:month`/`:version` are always ignored because ITU-T rows are one
|
|
192
|
+
# per **edition** (`ITU-T Z.100 (06/2021)`, `ITU-T H.264 (V14) (08/2021)`)
|
|
193
|
+
# while the reference is normalised to the undated form. They are separable
|
|
194
|
+
# trailing components, the same contract ETSI has (`omits: %i[version date]`);
|
|
195
|
+
# Bibliography then picks the requested year and version.
|
|
196
|
+
#
|
|
197
|
+
# @param pubid [::Pubid::Itu::Identifier] the reference
|
|
198
|
+
# @param id [::Pubid::Itu::Identifier] an index row's id
|
|
199
|
+
# @return [Boolean]
|
|
200
|
+
def index_match?(pubid, id)
|
|
201
|
+
ignore = %i[year month version]
|
|
202
|
+
ignore << :parts if pubid.code&.parts.to_a.empty?
|
|
203
|
+
pubid.matches?(id, ignore: ignore)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Resolve an ITU-T Recommendation to its editions via the public rec.aspx
|
|
207
|
+
# page (which exposes the record's handle/idrec) and the getRecEditions
|
|
208
|
+
# API. One hit is built per edition, mirroring the multi-result shape the
|
|
209
|
+
# removed RunSearch endpoint returned, so year filtering keeps working.
|
|
210
|
+
#
|
|
115
211
|
# @return [Array<Relaton::Itu::Hit>]
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
212
|
+
def live_hits
|
|
213
|
+
Util.info "Fetching from www.itu.int ...", key: ref.to_s
|
|
214
|
+
idrec = fetch_idrec
|
|
215
|
+
return [] unless idrec
|
|
216
|
+
|
|
217
|
+
editions(idrec).map { |ed| recommendation_hit ed }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @return [String, nil] the record's idrec, or nil when the code is unknown
|
|
221
|
+
def fetch_idrec
|
|
222
|
+
url = format(REC_URL, rec: CGI.escape(rec_query))
|
|
223
|
+
agent.get(url).body[%r{11\.1002/1000/(\d+)}, 1]
|
|
224
|
+
rescue Mechanize::ResponseCodeError => e
|
|
225
|
+
raise unless e.response_code == "404" # unknown code => treat as not found
|
|
226
|
+
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# @return [String] the `rec=` value for the rec.aspx lookup
|
|
231
|
+
def rec_query
|
|
232
|
+
ref.suppl ? "#{ref.code} Suppl. #{ref.suppl}" : ref.code
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# @param idrec [String]
|
|
236
|
+
# @return [Array<Hash>] editions of the recommendation
|
|
237
|
+
def editions(idrec)
|
|
238
|
+
# a `null` body parses to nil, so `|| []` is not redundant
|
|
239
|
+
JSON.parse(agent.get(format(RECEDITIONS_URL, idrec: idrec)).body) || []
|
|
240
|
+
rescue JSON::ParserError
|
|
241
|
+
[]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# @param edition [Hash] a getRecEditions entry
|
|
245
|
+
# @return [Relaton::Itu::Hit]
|
|
246
|
+
def recommendation_hit(edition)
|
|
247
|
+
# No `:ref` key: it drives Hit#gi_imp, which would route an Implementers'
|
|
248
|
+
# Guide reference onto the getImplGuides endpoints.
|
|
249
|
+
Hit.new({
|
|
250
|
+
code: "ITU-T #{edition['rec_name']}",
|
|
251
|
+
title: edition["title"],
|
|
252
|
+
url: format(HANDLE_URL, idrec: edition["idrec"]),
|
|
253
|
+
type: "recommendation",
|
|
254
|
+
}, self)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Resolve an ITU-R Radio Regulation or Operational Bulletin to its stable
|
|
258
|
+
# /pub landing page, whose id is derivable from the reference. They are not
|
|
259
|
+
# part of the relaton-data-itu dataset.
|
|
260
|
+
def request_publication
|
|
261
|
+
Util.info "Fetching from www.itu.int ...", key: ref.to_s
|
|
262
|
+
id = ref.year && publication_id
|
|
263
|
+
return @array = [] unless id
|
|
264
|
+
|
|
265
|
+
url = "#{DOMAIN}/pub/#{id}"
|
|
266
|
+
page = fetch_publication_page url
|
|
267
|
+
return @array = [] if page.nil? || page.uri.to_s.match?(/notfound/i)
|
|
268
|
+
|
|
269
|
+
@array = [Hit.new({ code: publication_code, url: url, type: "publication" }, self)]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# @return [Mechanize::Page, nil] nil when the publication does not exist
|
|
273
|
+
def fetch_publication_page(url)
|
|
274
|
+
agent.get url
|
|
275
|
+
rescue Mechanize::ResponseCodeError => e
|
|
276
|
+
raise unless e.response_code == "404" # unknown publication => not found
|
|
277
|
+
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# The /pub identifier for a Radio Regulation or an Operational Bulletin
|
|
282
|
+
# (`OB.1096` → `T-SP-OB.1096-2016`). nil — no hit — rather than a bogus URL
|
|
283
|
+
# when the bulletin reference carries no number.
|
|
284
|
+
#
|
|
285
|
+
# @return [String, nil]
|
|
286
|
+
def publication_id
|
|
287
|
+
return "R-REG-RR-#{ref.year}" if ref.code == "RR"
|
|
288
|
+
|
|
289
|
+
num = ref.code[/\d+/]
|
|
290
|
+
"T-SP-OB.#{num}-#{ref.year}" if num
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# @return [String] the docidentifier-friendly code (year only, no month)
|
|
294
|
+
def publication_code
|
|
295
|
+
"#{ref.prefix}-#{ref.sector} #{ref.code} (#{ref.year})"
|
|
124
296
|
end
|
|
125
297
|
end
|
|
126
298
|
end
|