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
|
@@ -1,62 +1,513 @@
|
|
|
1
1
|
require "net/http"
|
|
2
2
|
require "json"
|
|
3
3
|
require "uri"
|
|
4
|
+
require "mechanize"
|
|
4
5
|
require_relative "../itu"
|
|
5
|
-
require_relative "
|
|
6
|
+
require_relative "data_parser_t"
|
|
7
|
+
require_relative "family_cache"
|
|
6
8
|
|
|
7
9
|
module Relaton
|
|
8
10
|
module Itu
|
|
9
11
|
class DataFetcher < Core::DataFetcher
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
# ITU-T recommendation index (issue relaton-itu#80). main_edition_flag=0
|
|
13
|
+
# returns one row per edition, including supplements; a single request
|
|
14
|
+
# enumerates the whole ITU-T corpus.
|
|
15
|
+
SEARCH_RECS_URL = "https://www.itu.int/mws/api/recommendations/searchRecs".freeze
|
|
16
|
+
# A browser User-Agent — www.itu.int sits behind an F5 WAF that rejects
|
|
17
|
+
# non-browser clients (it is what killed the old RunSearch endpoint), so
|
|
18
|
+
# Net::HTTP's default "Ruby" UA must not be sent.
|
|
19
|
+
USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " \
|
|
20
|
+
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15".freeze
|
|
21
|
+
DEFAULT_CONCURRENCY = 8
|
|
22
|
+
|
|
23
|
+
# ITU-R crawl worker threads. Duplicated from DataCrawlerR::DEFAULT_CONCURRENCY
|
|
24
|
+
# rather than read from it, so this knob does not drag the whole ITU-R
|
|
25
|
+
# harvester into an ITU-T-only crawl — #fetch_publications requires that
|
|
26
|
+
# file lazily and deliberately.
|
|
27
|
+
DEFAULT_R_CONCURRENCY = 4
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Number of ITU-T enrichment worker threads. Each record costs ~4
|
|
31
|
+
# www.itu.int round-trips (~3.7 s wall clock), so a ~16k-record corpus is
|
|
32
|
+
# ~17 h single-threaded — past the 6 h GitHub Actions job cap. The work is
|
|
33
|
+
# pure I/O wait, so a small pool is close to linear. Tunable via env var so
|
|
34
|
+
# a run can dial it down when the F5 WAF in front of www.itu.int starts
|
|
35
|
+
# throttling (or up to 1 to reproduce the serial order). Never below 1.
|
|
36
|
+
def self.concurrency
|
|
37
|
+
[(ENV["RELATON_ITU_CONCURRENCY"] || DEFAULT_CONCURRENCY).to_i, 1].max
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Number of ITU-R crawl worker threads. Its own knob, not ITU-T's, because
|
|
41
|
+
# the two halves have different bottlenecks: ITU-T is latency-bound, ITU-R
|
|
42
|
+
# is *pacer*-bound. Throughput there is min(1/delay, concurrency/latency),
|
|
43
|
+
# so at the 1 s default and ITU's ~1.1 s latency two workers already
|
|
44
|
+
# saturate the politeness contract; the default of 4 is headroom for a slow
|
|
45
|
+
# page, not extra load. `RELATON_ITU_R_CONCURRENCY=1` restores a strictly
|
|
46
|
+
# serial crawl.
|
|
47
|
+
def self.r_concurrency
|
|
48
|
+
[(ENV["RELATON_ITU_R_CONCURRENCY"] || DEFAULT_R_CONCURRENCY).to_i, 1].max
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# How RELATON_ITU_DELAY is spent.
|
|
52
|
+
#
|
|
53
|
+
# slot (default) it is the minimum gap between request *starts*, shared
|
|
54
|
+
# by the pool, so ITU's own latency counts toward it instead of
|
|
55
|
+
# being added on top. www.itu.int sees at most 1/delay req/s no
|
|
56
|
+
# matter how many workers there are.
|
|
57
|
+
# fixed sleep it before every request, whatever else is happening. With
|
|
58
|
+
# RELATON_ITU_R_CONCURRENCY=1 this reproduces the pre-pool crawler
|
|
59
|
+
# exactly — one request per (delay + latency) — and is the rollback
|
|
60
|
+
# if ITU ever objects to the new profile.
|
|
61
|
+
def self.pace_mode
|
|
62
|
+
ENV["RELATON_ITU_PACE"].to_s == "fixed" ? :fixed : :slot
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The ITU-T cross-row cache for a crawl. `RELATON_ITU_CACHE_ENTRIES=0`
|
|
66
|
+
# turns it off entirely, restoring today's exact request pattern.
|
|
67
|
+
def self.family_cache
|
|
68
|
+
entries = ENV["RELATON_ITU_CACHE_ENTRIES"]
|
|
69
|
+
return NullCache.instance if entries.to_s.strip == "0"
|
|
70
|
+
|
|
71
|
+
size = entries.to_i
|
|
72
|
+
FamilyCache.new(max_entries: size.positive? ? size : FamilyCache::DEFAULT_MAX_ENTRIES)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def initialize(output, format)
|
|
76
|
+
super
|
|
77
|
+
# Guards the shared bookkeeping (@files, @seen, @errors, @unparseable_ids,
|
|
78
|
+
# @enrichment_failures) and the index mutation while workers fetch detail
|
|
79
|
+
# pages in parallel. The slow HTTP runs outside this lock; only the cheap
|
|
80
|
+
# build+write is held.
|
|
81
|
+
@mutex = Mutex.new
|
|
82
|
+
# output file => searchRecs position of the row that claimed it, so a
|
|
83
|
+
# duplicate filename resolves to the same last-by-position winner the
|
|
84
|
+
# serial crawl picked, regardless of worker completion order.
|
|
85
|
+
@seen = {}
|
|
86
|
+
@done = 0
|
|
87
|
+
@enrichment_failures = 0
|
|
88
|
+
end
|
|
13
89
|
|
|
14
90
|
def index
|
|
15
|
-
@index ||= Relaton::Index.find_or_create
|
|
91
|
+
@index ||= Relaton::Index.find_or_create(
|
|
92
|
+
:itu, file: "#{INDEXFILE}.yaml", pubid_class: ::Pubid::Itu::Identifier
|
|
93
|
+
)
|
|
16
94
|
end
|
|
17
95
|
|
|
18
96
|
def log_error(msg)
|
|
19
97
|
Util.error msg
|
|
20
98
|
end
|
|
21
99
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
100
|
+
# @param source [String, nil] "itu-t" harvests ITU-T recommendations via
|
|
101
|
+
# the searchRecs index (issue #80); "itu-r" (and nil, the legacy default)
|
|
102
|
+
# harvests ITU-R Recommendations and Reports by crawling ITU's
|
|
103
|
+
# server-rendered pages (issue #75, see #fetch_publications).
|
|
104
|
+
def fetch(source = nil)
|
|
105
|
+
source == "itu-t" ? fetch_recommendations : fetch_publications
|
|
106
|
+
index.save
|
|
107
|
+
report_errors
|
|
108
|
+
end
|
|
30
109
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
110
|
+
# ITU-R harvester. ITU decommissioned the RunSearch bulk-enumeration
|
|
111
|
+
# endpoint this used to page through, so enumeration walks the `/pub` +
|
|
112
|
+
# `/rec` pages ITU still renders server-side (`DataCrawlerR`).
|
|
113
|
+
#
|
|
114
|
+
# Two modes, because a full crawl is ~7k requests and ~4 h:
|
|
115
|
+
#
|
|
116
|
+
# * **`:full`** (default) — every edition of every document, deep. Pair it
|
|
117
|
+
# with wiping `data/itu-r-*` first: ITU is then the sole author of the
|
|
118
|
+
# result, which is the point of crawling it at all.
|
|
119
|
+
# * **`:top_up`** — enumerate the same documents, but deep-fetch only the
|
|
120
|
+
# editions the dataset does not already hold. Costs the enumeration
|
|
121
|
+
# (~1 request per document) plus one per genuinely new edition, so a day
|
|
122
|
+
# with no new publications is a small fraction of a full run.
|
|
123
|
+
#
|
|
124
|
+
# Set with `RELATON_ITU_MODE=top_up`, so the two scheduled jobs differ by
|
|
125
|
+
# environment rather than by code.
|
|
126
|
+
#
|
|
127
|
+
# A top-up assumes the dataset it tops up was built by a previous `:full`
|
|
128
|
+
# run. Against the pre-#110 dataset it would add every Report a second time
|
|
129
|
+
# under its `Report …` name, so the data repo runs the rebuild first and
|
|
130
|
+
# enables the daily job after — an ordering that belongs to the schedule, not
|
|
131
|
+
# to this class. That one wipe retires the old names for good, which is why
|
|
132
|
+
# the harvester carries no migration step.
|
|
133
|
+
#
|
|
134
|
+
# Cost, measured: the BO series of both families is ~230 requests in 484 s
|
|
135
|
+
# at the 1 s default — ~2.1 s per request once ITU's own latency is added.
|
|
136
|
+
# Lower RELATON_ITU_DELAY at your peril: 0.4 s tripped the WAF mid-run
|
|
137
|
+
# (503 on /rec, a 302 to notfound.aspx on /pub), 1 s did not.
|
|
138
|
+
#
|
|
139
|
+
# @param mode [Symbol] :full or :top_up
|
|
140
|
+
# @return [Hash] the merge tally, `collisions` being a count
|
|
141
|
+
def fetch_publications(mode: self.class.mode)
|
|
142
|
+
require_relative "data_crawler_r"
|
|
143
|
+
require_relative "data_merge_r"
|
|
144
|
+
crawler = DataCrawlerR.new delay: self.class.delay,
|
|
145
|
+
concurrency: self.class.r_concurrency,
|
|
146
|
+
pace_mode: self.class.pace_mode
|
|
147
|
+
stats = Hash.new 0
|
|
148
|
+
DataCrawlerR::FAMILIES.each_key { |family| harvest_family crawler, family, stats, mode }
|
|
149
|
+
Util.info "ITU-R (#{mode}): #{stats.reject { |k, _| k == :collisions }.map { |k, v| "#{k} #{v}" }.join ', '}"
|
|
150
|
+
# Say it out loud: a run that was throttled but recovered otherwise looks
|
|
151
|
+
# identical to a clean one, and a run the governor abandoned would
|
|
152
|
+
# otherwise be visible only as a thin corpus.
|
|
153
|
+
if crawler.throttle_count.positive?
|
|
154
|
+
Util.warn "ITU-R: #{crawler.throttle_count} rate-limit responses from www.itu.int" \
|
|
155
|
+
"#{'; crawl ABANDONED as rate-limited' if crawler.abandoned?}"
|
|
156
|
+
end
|
|
157
|
+
stats
|
|
158
|
+
ensure
|
|
159
|
+
crawler&.shutdown
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# One family, series by series, so a series that fails — ITU throttles by
|
|
163
|
+
# path family and a blocked series raises — costs that series rather than
|
|
164
|
+
# the whole run.
|
|
165
|
+
#
|
|
166
|
+
# @return [void]
|
|
167
|
+
def harvest_family(crawler, family, stats, mode = :full)
|
|
168
|
+
crawler.series(family).each do |series|
|
|
169
|
+
items = crawler.harvest series, family: family, errors: @errors,
|
|
170
|
+
skip: (method(:held?) if mode == :top_up)
|
|
171
|
+
# `collisions` comes back as the offending pairs; the run-level tally
|
|
172
|
+
# keeps their count and leaves the detail to DataMergeR's own errors.
|
|
173
|
+
DataMergeR.write_all(items, self).each { |k, v| stats[k] += v.is_a?(Array) ? v.size : v }
|
|
174
|
+
Util.info "ITU-R #{family}-#{series}: #{items.size} harvested"
|
|
175
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
|
176
|
+
log_error "ITU-R #{family}-#{series} skipped: #{e.message}"
|
|
177
|
+
end
|
|
178
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
|
179
|
+
log_error "ITU-R #{family} series index unavailable: #{e.message}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Does the dataset already hold this edition? Answered from the level-2 row
|
|
183
|
+
# alone — the id and displayed code are enough to derive the docid, and so
|
|
184
|
+
# the filename — which is what lets a top-up decide *before* paying for the
|
|
185
|
+
# edition page.
|
|
186
|
+
#
|
|
187
|
+
# @param row [Hash] a level-2 edition row, with :family
|
|
188
|
+
# @return [Boolean]
|
|
189
|
+
def held?(row)
|
|
190
|
+
docid = DataParserR.family_docid row
|
|
191
|
+
docid && File.exist?(output_file(docid))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Minimum seconds between ITU-R request *starts*, enforced by one
|
|
195
|
+
# Core::Pacer shared by the whole pool — not a sleep before each request,
|
|
196
|
+
# which would add to ITU's own ~1.1 s latency instead of being absorbed by
|
|
197
|
+
# it. www.itu.int therefore sees at most 1/delay req/s however many workers
|
|
198
|
+
# run. Its WAF answers 503 on `/rec` and a 302 to notfound.aspx on `/pub`
|
|
199
|
+
# when pushed; 0.4 s tripped it mid-run, 1 s did not.
|
|
200
|
+
def self.delay
|
|
201
|
+
(ENV["RELATON_ITU_DELAY"] || 1.0).to_f
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @return [Symbol] :top_up or :full
|
|
205
|
+
def self.mode
|
|
206
|
+
ENV["RELATON_ITU_MODE"].to_s == "top_up" ? :top_up : :full
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# ITU-T harvester: one searchRecs request enumerates every edition and
|
|
210
|
+
# supplement; each row is then enriched with getRecHdrDetail-sourced fields
|
|
211
|
+
# (abstract, ISO co-id, editorial-group contributors, status), so harvested
|
|
212
|
+
# records match the live runtime output. Enrichment is up to 4 requests per
|
|
213
|
+
# record — the bulk of the crawl's cost — so the rows are spread over a
|
|
214
|
+
# worker pool (see .concurrency) and progress is logged.
|
|
215
|
+
#
|
|
216
|
+
# Two things keep that cost down. The rows are enqueued **grouped by
|
|
217
|
+
# recommendation**, and the pool shares one FamilyCache, so the two
|
|
218
|
+
# endpoints that answer per *recommendation* rather than per *edition*
|
|
219
|
+
# (`getRecEditions` and the ~90 KB `rec.aspx` page) are fetched once per
|
|
220
|
+
# family instead of once per edition — H.264 alone was asking ITU the same
|
|
221
|
+
# question 21 times. And `mode: :top_up` skips a family the dataset already
|
|
222
|
+
# holds entirely, decided with no HTTP at all (see #held_t?).
|
|
223
|
+
#
|
|
224
|
+
# Each worker owns its own Mechanize agent: Mechanize is not thread-safe,
|
|
225
|
+
# and per-worker agents also keep one worker's cookie/history state out of
|
|
226
|
+
# another's. Rows carry their searchRecs position so #write_file can pick a
|
|
227
|
+
# deterministic winner for duplicate filenames.
|
|
228
|
+
def fetch_recommendations(mode: self.class.mode)
|
|
229
|
+
# `pos` is fixed HERE, against the unreordered searchRecs result, and
|
|
230
|
+
# keeps exactly its current meaning. Only the enqueue ORDER changes
|
|
231
|
+
# below, which #write_file's max-by-position rule is already immune to —
|
|
232
|
+
# it has to be, since worker completion order was never deterministic.
|
|
233
|
+
work = search_recs.each_with_index.to_a
|
|
234
|
+
work = top_up_rows(work) if mode == :top_up
|
|
235
|
+
work = self.class.group_by_family(work)
|
|
236
|
+
cache = self.class.family_cache
|
|
237
|
+
|
|
238
|
+
n = self.class.concurrency
|
|
239
|
+
agents = Array.new(n) { rec_agent }
|
|
240
|
+
queue = SizedQueue.new(n * 2)
|
|
241
|
+
workers = agents.map { |agent| spawn_rec_worker(queue, agent, work.size, cache) }
|
|
242
|
+
work.each { |pair| queue << pair }
|
|
243
|
+
n.times { queue << nil } # poison pills
|
|
244
|
+
workers.each(&:join)
|
|
245
|
+
Util.info "ITU-T: detail cache #{cache.stats[:hits]} hits / #{cache.stats[:misses]} misses"
|
|
246
|
+
report_enrichment_failures work.size
|
|
247
|
+
ensure
|
|
248
|
+
agents&.each(&:shutdown)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Does the dataset already hold this ITU-T edition? Answered from the
|
|
252
|
+
# searchRecs row alone, which is what lets a top-up decide *before* paying
|
|
253
|
+
# the four detail requests — the ITU-T counterpart of #held?.
|
|
254
|
+
#
|
|
255
|
+
# HTTP-free, and provably so: DataParserT.fetch_docid reads only
|
|
256
|
+
# `row["rec_name"]`, normalizes it with one #sub, and builds a
|
|
257
|
+
# Docidentifier. It takes no agent and touches none of the enrichment path.
|
|
258
|
+
#
|
|
259
|
+
# The filename it derives is the one #write_file would produce: that method
|
|
260
|
+
# uses `docidentifier.find(&:primary).content`, and DataParserT.parse puts
|
|
261
|
+
# the ITU docid first (`docid + enr[:iso]`) — which matters, because an
|
|
262
|
+
# enriched record's ISO co-identifier is *also* `primary: true`.
|
|
263
|
+
#
|
|
264
|
+
# @param row [Hash] a searchRecs row
|
|
265
|
+
# @return [Boolean]
|
|
266
|
+
def held_t?(row)
|
|
267
|
+
did = DataParserT.fetch_docid(row).first
|
|
268
|
+
did && File.exist?(output_file(did.content))
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Narrow a top-up to the rows worth re-harvesting.
|
|
272
|
+
#
|
|
273
|
+
# The unit is the **family, not the row**. A new edition changes its
|
|
274
|
+
# siblings' `hasEdition` relations, so topping up only the new row would
|
|
275
|
+
# leave the rest of the family on disk pointing at an incomplete edition
|
|
276
|
+
# list — a staleness the full run's wipe-and-rebuild never had. If any row
|
|
277
|
+
# of a family is new, every row of that family is re-harvested; the extra
|
|
278
|
+
# cost is one getRecHdrDetail per sibling, since the family-invariant
|
|
279
|
+
# fetches are shared by the cache.
|
|
280
|
+
#
|
|
281
|
+
# @param work [Array<Array(Hash, Integer)>] [row, searchRecs position]
|
|
282
|
+
# @return [Array<Array(Hash, Integer)>]
|
|
283
|
+
def top_up_rows(work)
|
|
284
|
+
kept = work.group_by { |row, _pos| self.class.family_key(row) }
|
|
285
|
+
.select { |_key, pairs| pairs.any? { |row, _pos| !held_t?(row) } }
|
|
286
|
+
.values.flatten(1)
|
|
287
|
+
Util.info "ITU-T (top_up): #{kept.size}/#{work.size} rows in families with new editions"
|
|
288
|
+
kept
|
|
289
|
+
end
|
|
34
290
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
291
|
+
# The recommendation a row is an edition of.
|
|
292
|
+
#
|
|
293
|
+
# Used **only** to decide enqueue order and top-up grouping, never as a
|
|
294
|
+
# cache key: cache correctness is keyed on idrecs ITU's own getRecEditions
|
|
295
|
+
# response supplied (see RecommendationFields#family_id), because two
|
|
296
|
+
# recommendations whose names normalize alike would otherwise silently
|
|
297
|
+
# share metadata. A wrong guess here costs a cache miss, nothing more.
|
|
298
|
+
#
|
|
299
|
+
# "H.264 (V16) (06/2026)" -> "H.264"; "H Suppl. 1 (05/1999)" -> "H Suppl. 1"
|
|
300
|
+
#
|
|
301
|
+
# @param row [Hash]
|
|
302
|
+
# @return [String]
|
|
303
|
+
def self.family_key(row)
|
|
304
|
+
# Cut at a bare "(" with no leading \s* — an unanchored \s* here is
|
|
305
|
+
# polynomial-time backtracking (CodeQL rb/polynomial-redos).
|
|
306
|
+
DataParserT.normalize_rec_name(row["rec_name"]).sub(/\(.*\z/m, "").strip
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Make each recommendation's rows contiguous, so its cache entry is still
|
|
310
|
+
# resident when its siblings are enriched. Every [row, pos] pair is carried
|
|
311
|
+
# through untouched. searchRecs already returns name-sorted rows, so on
|
|
312
|
+
# today's data this is close to a no-op — it is a guarantee, not a gain.
|
|
313
|
+
#
|
|
314
|
+
# @param work [Array<Array(Hash, Integer)>]
|
|
315
|
+
# @return [Array<Array(Hash, Integer)>]
|
|
316
|
+
def self.group_by_family(work)
|
|
317
|
+
work.group_by { |row, _pos| family_key(row) }.values.flatten(1)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# One pool worker: drains the queue with its own agent until the poison
|
|
321
|
+
# pill (nil). Per-row errors are logged and skipped, so one bad row never
|
|
322
|
+
# kills a worker and leaves the queue undrained.
|
|
323
|
+
def spawn_rec_worker(queue, agent, total, cache = NullCache.instance)
|
|
324
|
+
Thread.new do
|
|
325
|
+
while (item = queue.pop)
|
|
326
|
+
row, pos = item
|
|
327
|
+
begin
|
|
328
|
+
errors = Hash.new(true)
|
|
329
|
+
bib = DataParserT.parse(row, agent, errors, cache: cache)
|
|
330
|
+
if bib
|
|
331
|
+
# DataParserT#enrichment adds the ITU publisher unconditionally
|
|
332
|
+
# when it succeeds, so an empty contributor list is exactly the
|
|
333
|
+
# set of records whose detail fetch failed and degraded to the
|
|
334
|
+
# thin searchRecs shape.
|
|
335
|
+
count_enrichment_failure if bib.contributor.empty?
|
|
336
|
+
write_file bib, pos
|
|
337
|
+
end
|
|
338
|
+
merge_errors errors
|
|
339
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
|
340
|
+
Util.error "#{e.message}\n#{e.backtrace}"
|
|
341
|
+
end
|
|
342
|
+
progress total
|
|
41
343
|
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Each parse gets its own errors hash (DataParserT is per-call state, but
|
|
348
|
+
# the flags are AND-folded across all rows), merged back under the lock.
|
|
349
|
+
def merge_errors(errors)
|
|
350
|
+
@mutex.synchronize { errors.each { |k, v| @errors[k] &&= v } }
|
|
351
|
+
end
|
|
42
352
|
|
|
43
|
-
|
|
353
|
+
def progress(total)
|
|
354
|
+
done = @mutex.synchronize { @done += 1 }
|
|
355
|
+
Util.info "ITU-T: enriched #{done}/#{total}" if (done % 500).zero?
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def count_enrichment_failure
|
|
359
|
+
@mutex.synchronize { @enrichment_failures += 1 }
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Enrichment is best-effort: a failed detail fetch degrades that record to
|
|
363
|
+
# the thin searchRecs shape instead of losing it, and the crawl runs to
|
|
364
|
+
# completion either way. That is right for one flaky record and dangerous
|
|
365
|
+
# in bulk — a WAF block would quietly republish a metadata-thin corpus — so
|
|
366
|
+
# say how many records lost their enrichment.
|
|
367
|
+
def report_enrichment_failures(total)
|
|
368
|
+
return if @enrichment_failures.zero?
|
|
369
|
+
|
|
370
|
+
Util.warn "ITU-T: enrichment failed for #{@enrichment_failures}/#{total} records"
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Mechanize agent for per-record enrichment. A browser User-Agent is
|
|
374
|
+
# required — www.itu.int sits behind the F5 WAF that rejects non-browser
|
|
375
|
+
# clients (mirrors HitCollection#agent).
|
|
376
|
+
#
|
|
377
|
+
# max_history: Mechanize retains every response it fetches, unbounded by
|
|
378
|
+
# default. Enrichment issues ~4 requests per record and one of them is
|
|
379
|
+
# the ~90 KB rec.aspx page that #fetch_workgroup parses into a DOM; over
|
|
380
|
+
# a 16k-record corpus (times one agent per worker) that history grows
|
|
381
|
+
# into the gigabytes.
|
|
382
|
+
# timeouts: Mechanize sets none, so a single stalled www.itu.int socket
|
|
383
|
+
# would park a worker indefinitely with no error and no progress. A
|
|
384
|
+
# timeout raises Net::{Open,Read}Timeout < Timeout::Error, which
|
|
385
|
+
# RecommendationFields#request_document already turns into a
|
|
386
|
+
# Relaton::RequestError and DataParserT#enrichment rescues — so it costs
|
|
387
|
+
# one thin record rather than the run.
|
|
388
|
+
def rec_agent
|
|
389
|
+
Mechanize.new.tap do |a|
|
|
390
|
+
a.user_agent_alias = "Mac Safari"
|
|
391
|
+
a.max_history = 1
|
|
392
|
+
a.open_timeout = 15
|
|
393
|
+
a.read_timeout = 60
|
|
44
394
|
end
|
|
45
|
-
index.save
|
|
46
|
-
report_errors
|
|
47
395
|
end
|
|
48
396
|
|
|
49
397
|
# @param bib [Relaton::Itu::ItemData]
|
|
50
|
-
|
|
398
|
+
# @param pos [Integer, nil] source position of the row this came from, used
|
|
399
|
+
# only to break filename collisions deterministically (nil for the
|
|
400
|
+
# single-threaded ITU-R path)
|
|
401
|
+
def write_file(bib, pos = nil) # rubocop:disable Metrics/AbcSize
|
|
51
402
|
id = bib.docidentifier.find(&:primary).content
|
|
52
403
|
file = output_file(id)
|
|
53
|
-
|
|
54
|
-
|
|
404
|
+
content = serialize(bib) # outside the lock: it is the expensive part
|
|
405
|
+
@mutex.synchronize do
|
|
406
|
+
if @files.include? file
|
|
407
|
+
Util.warn "File #{file} exists."
|
|
408
|
+
# Distinct docids can sanitize to one filename, in which case the
|
|
409
|
+
# serial crawl left the last row's version on disk. Keep that
|
|
410
|
+
# outcome whatever order the workers finish in.
|
|
411
|
+
return if pos && @seen[file] && @seen[file] > pos
|
|
412
|
+
else
|
|
413
|
+
@files << file
|
|
414
|
+
end
|
|
415
|
+
@seen[file] = pos
|
|
416
|
+
index_primary(id, file)
|
|
417
|
+
File.write file, content, encoding: "UTF-8"
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Index records that are already on disk instead of harvesting them.
|
|
422
|
+
#
|
|
423
|
+
# ITU-R cannot be re-harvested — ITU decommissioned the bulk RunSearch
|
|
424
|
+
# enumeration, which is why #fetch now refuses the "itu-r" source
|
|
425
|
+
# (ITU_R_DISABLED, issue #75) — so the
|
|
426
|
+
# published ITU-R records are preserved and only re-indexed on each run of
|
|
427
|
+
# relaton-data-itu's crawler. Doing that here rather than in the data repo
|
|
428
|
+
# gives the pass the same pubid guard and the same unparseable-id reporting
|
|
429
|
+
# as the ITU-T harvest, instead of a copy of both living downstream. Drive
|
|
430
|
+
# it and #fetch off one instance so there is a single index, a single
|
|
431
|
+
# unparseable-id list and a single error report:
|
|
432
|
+
#
|
|
433
|
+
# fetcher = DataFetcher.new("data", "yaml")
|
|
434
|
+
# fetcher.index_files "data/itu-r-*.yaml"
|
|
435
|
+
# fetcher.fetch "itu-t"
|
|
436
|
+
#
|
|
437
|
+
# @param glob [String] e.g. "data/itu-r-*.yaml"
|
|
438
|
+
# @return [Integer] number of files indexed
|
|
439
|
+
def index_files(glob)
|
|
440
|
+
indexed = 0
|
|
441
|
+
files = Dir[glob].sort
|
|
442
|
+
files.each do |file|
|
|
443
|
+
item = Item.from_yaml(File.read(file, encoding: "UTF-8"))
|
|
444
|
+
id = (item.docidentifier.find(&:primary) || item.docidentifier.first)&.content
|
|
445
|
+
if id
|
|
446
|
+
indexed += 1 if index_primary(id, file)
|
|
447
|
+
else
|
|
448
|
+
unparseable_ids << ["(no docidentifier)", file]
|
|
449
|
+
end
|
|
450
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
|
451
|
+
Util.error "Failed to index #{file}: #{e.message}"
|
|
452
|
+
end
|
|
453
|
+
Util.info "ITU: indexed #{indexed}/#{files.size} existing records from #{glob}"
|
|
454
|
+
indexed
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Index the id's parsed pubid. If it can't be parsed/round-tripped, record
|
|
458
|
+
# it so #report_errors raises a tracked GitHub issue; the data file is
|
|
459
|
+
# still written, so the document is not lost — only unindexed until its id
|
|
460
|
+
# parses (mirrors Relaton::Iso::DataFetcher#index_primary).
|
|
461
|
+
#
|
|
462
|
+
# @param id [String] primary docidentifier content, e.g. "ITU-R BO.600-1"
|
|
463
|
+
# @param file [String] file name of the document
|
|
464
|
+
# @return [Boolean] whether the id was indexed
|
|
465
|
+
def index_primary(id, file)
|
|
466
|
+
if (pid = pubid(id))
|
|
467
|
+
index.add_or_update pid, file
|
|
468
|
+
true
|
|
55
469
|
else
|
|
56
|
-
|
|
470
|
+
unparseable_ids << [id, file]
|
|
471
|
+
false
|
|
57
472
|
end
|
|
58
|
-
|
|
59
|
-
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def unparseable_ids
|
|
476
|
+
@unparseable_ids ||= []
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Surface unparseable ids through the shared error machinery (the
|
|
480
|
+
# "Error fetching documents" GitHub issue in CI). The gh_issue channel is
|
|
481
|
+
# registered inside #gh_issue, so log at :error after it is set up and
|
|
482
|
+
# before super creates the issue (mirrors
|
|
483
|
+
# Relaton::Iso::DataFetcher#report_errors).
|
|
484
|
+
def report_errors
|
|
485
|
+
gh_issue
|
|
486
|
+
unparseable_ids.each do |content, file|
|
|
487
|
+
log_error "Unparseable primary id `#{content}` was not indexed (#{file})"
|
|
488
|
+
end
|
|
489
|
+
super
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Parse an ITU docid into a Pubid::Itu identifier, or nil when it can't be
|
|
493
|
+
# parsed or does not round-trip losslessly. Storing the pubid object (not
|
|
494
|
+
# its hash) lets Relaton::Index sort the index and serialize each id to its
|
|
495
|
+
# `_type: pubid:itu:*` hash on save. The round-trip check mirrors the index
|
|
496
|
+
# loader's own Index::FileIO#id_supported? acceptance test, so an id that
|
|
497
|
+
# would make Relaton::Index reject the whole index is dropped at write time.
|
|
498
|
+
# The pinned pubid models recommendations, handbooks and questions, so the
|
|
499
|
+
# guard only skips the few residual forms it can't parse (e.g. "ITU-R RR").
|
|
500
|
+
#
|
|
501
|
+
# @param id [String]
|
|
502
|
+
# @return [::Pubid::Itu::Identifier, nil]
|
|
503
|
+
def pubid(id)
|
|
504
|
+
pid = ::Pubid::Itu.parse id
|
|
505
|
+
hash = pid.to_hash
|
|
506
|
+
return nil unless ::Pubid::Itu::Identifier.from_hash(hash).to_hash == hash
|
|
507
|
+
|
|
508
|
+
pid
|
|
509
|
+
rescue StandardError
|
|
510
|
+
nil
|
|
60
511
|
end
|
|
61
512
|
|
|
62
513
|
def to_yaml(bib)
|
|
@@ -73,31 +524,26 @@ module Relaton
|
|
|
73
524
|
|
|
74
525
|
private
|
|
75
526
|
|
|
76
|
-
#
|
|
77
|
-
# @return [Array<Hash>]
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"Language" => "en", "SearchType" => "All",
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
uri = URI(SEARCH_URL)
|
|
527
|
+
# Fetch the full ITU-T recommendation index in one request.
|
|
528
|
+
# @return [Array<Hash>] rows from the searchRecs "Data" array
|
|
529
|
+
def search_recs
|
|
530
|
+
uri = URI(SEARCH_RECS_URL)
|
|
531
|
+
uri.query = URI.encode_www_form(
|
|
532
|
+
series: -1, type_of_text: -1, sg: -1, main_edition_flag: 0,
|
|
533
|
+
rows: 100_000, page: 1, status: "Z", sort_order: "asc"
|
|
534
|
+
)
|
|
535
|
+
|
|
89
536
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
90
537
|
http.use_ssl = true
|
|
91
538
|
|
|
92
|
-
request = Net::HTTP::
|
|
93
|
-
request["
|
|
94
|
-
request["
|
|
95
|
-
request["
|
|
96
|
-
request.body = "json=#{URI.encode_www_form_component(payload.to_json)}"
|
|
539
|
+
request = Net::HTTP::Get.new(uri)
|
|
540
|
+
request["Accept"] = "application/json"
|
|
541
|
+
request["Referer"] = "https://www.itu.int/myworkspace/"
|
|
542
|
+
request["User-Agent"] = USER_AGENT
|
|
97
543
|
|
|
98
544
|
response = http.request(request)
|
|
99
545
|
json = JSON.parse(response.body)
|
|
100
|
-
json["
|
|
546
|
+
json["Data"] || []
|
|
101
547
|
end
|
|
102
548
|
end
|
|
103
549
|
end
|