relaton-itu 2.1.1 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d26cbbb715a19961467253a86e1a90d1a238469d0f9011da6a4cd9d42ae67ea3
4
- data.tar.gz: 3dd15dc14484d326f1b2ce57b817f9e56a902669934494da3f9df17cf3ac6837
3
+ metadata.gz: 6934a71bcfd52917864d6cf3af90fd07333ce87cc9c5d8cd045217d86a423b28
4
+ data.tar.gz: 0a1daa40935cb00409c1d40186aa63d7c25bd70866356a635fa52dc4ef512d87
5
5
  SHA512:
6
- metadata.gz: cedaef1be6243a4c25eddac7b4b3418a11390485153cafb4792e0cec593a937c90e2f3895a6473dc46cdf67a7f5b34d6b47b301af7ccbb281bc11e060447071d
7
- data.tar.gz: 7c86eee320f302f6d957fae54115bc5e06658ca30e44bdac89a9a44b5660a390272bbc817347656c1a1ebd33db5b9568cf28575b0113fe002cbe54bfaa41ae81
6
+ metadata.gz: 11d05f246d70e236f0995ec2a80abfef38c8085b602aa6bff5aa4c5033fdc5d4984cec8e9125348f9f31739903cf82e89d9d16819a4233e4855ece1488455366
7
+ data.tar.gz: 42cfd40edebcd128a3cc402a3c6e3c6fd4b2d7e1bad5a4a9d2403dd59b020cf729034eeaa864d915e69e1bf192b0bb7d2e90ce31465fa7db942dbaa608d3eb7f
data/CLAUDE.md CHANGED
@@ -45,6 +45,28 @@ All model classes use `Lutaml::Model::Serializable` for XML/YAML serialization:
45
45
  - **`DataParserR`** — module that parses ITU-R JSON search API results into `ItemData` instances (sets `flavor: "itu"` on all parsed documents)
46
46
  - Sources: recommendations (JSON index), questions, reports, handbooks, resolutions (HTML indices)
47
47
 
48
+ ### Runtime lookup (`HitCollection#search`)
49
+
50
+ `Bibliography.get`/`search` routes by reference in `HitCollection#search`:
51
+
52
+ - **ITU-T Recommendations** (`request_recommendation`) — discovery is a two-stage HTTP flow against
53
+ `www.itu.int`. The `RunSearch` Deep Search endpoint was removed/WAF-blocked (issue #88), so
54
+ discovery now GETs the public `ITU-T/recommendations/rec.aspx?rec={code}` page, extracts the
55
+ record handle (`11.1002/1000/{idrec}`), and calls the `mws/api/recommendations/getRecEditions`
56
+ API to build one `Hit` per edition (each `hit[:url]` is a `handle.itu.int/.../{idrec}-en` URL so
57
+ `Scraper#idrec` re-parses it). Year/edition filtering stays in `Bibliography#search_filter` /
58
+ `#isobib_results_filter`. Full metadata is fetched by `RecommendationParser` (the `mws/api/*`
59
+ endpoints), unchanged.
60
+ - **ITU-R RR & Operational Bulletins** (`request_publication`) — resolved to their predictable
61
+ `/pub/{pubid}` landing page (`R-REG-RR-{year}`, `T-SP-OB.{num}-{year}`) and scraped by
62
+ `RadioRegulationsParser`. A redirect to `notfound.aspx` (or a 404) yields no hit.
63
+ - **ITU-R (other)** (`request_document`) — reads the pre-built `relaton-data-itu-r` GitHub dataset
64
+ via `Relaton::Index`; does not hit `www.itu.int`.
65
+
66
+ Note: `DataFetcher` (the offline ITU-R harvester) still POSTs to the removed `RunSearch` endpoint,
67
+ so a dataset *refresh* is currently broken — a follow-up should migrate it (and, longer term, ITU-T)
68
+ to a relaton-data dataset.
69
+
48
70
  ### Processor
49
71
 
50
72
  `Relaton::Itu::Processor` extends `Relaton::Core::Processor` and is the entry point for the Relaton plugin system. Provides `get`, `fetch_data`, `from_xml`, `hash_to_bib`, `grammar_hash`, and `remove_index_file`.
@@ -1,5 +1,7 @@
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
@@ -8,11 +10,16 @@ module Relaton
8
10
  class HitCollection < Relaton::Core::HitCollection
9
11
  DOMAIN = "https://www.itu.int"
10
12
  GH_ITU_R = "https://raw.githubusercontent.com/relaton/relaton-data-itu-r/refs/heads/v2/"
13
+ REC_URL = "#{DOMAIN}/ITU-T/recommendations/rec.aspx?rec=%<rec>s&lang=en".freeze
14
+ RECEDITIONS_URL = "#{DOMAIN}/mws/api/recommendations/getRecEditions?idrec=%<idrec>s&lang=en".freeze
15
+ HANDLE_URL = "http://handle.itu.int/11.1002/1000/%<idrec>s-en".freeze
11
16
 
12
17
  def search
13
18
  case ref.to_ref
14
- when /^(ITU-T|ITU-R\sRR)/
15
- request_search
19
+ when %r{^ITU-R\sRR}, %r{\bOB\.|Operational Bulletin}
20
+ request_publication
21
+ when /^ITU-T/
22
+ request_recommendation
16
23
  when /^ITU-R\s/
17
24
  request_document
18
25
  end
@@ -27,12 +34,87 @@ module Relaton
27
34
 
28
35
  private
29
36
 
30
- def request_search
37
+ # Resolve an ITU-T Recommendation to its editions via the public rec.aspx
38
+ # page (which exposes the record's handle/idrec) and the getRecEditions API.
39
+ # One hit is built per edition, mirroring the multi-result shape the old
40
+ # RunSearch endpoint used to return, so year filtering keeps working.
41
+ def request_recommendation
31
42
  Util.info "Fetching from www.itu.int ...", key: ref.to_s
32
- url = "#{DOMAIN}/net4/ITU-T/search/GlobalSearch/RunSearch"
33
- data = { json: params.to_json }
34
- resp = agent.post url, data
35
- @array = hits JSON.parse(resp.body)
43
+ idrec = fetch_idrec
44
+ return @array = [] unless idrec
45
+
46
+ @array = editions(idrec).map { |ed| recommendation_hit(ed) }
47
+ end
48
+
49
+ # @return [String, nil] the record's idrec, or nil when the code is unknown
50
+ def fetch_idrec
51
+ url = format(REC_URL, rec: CGI.escape(rec_query))
52
+ agent.get(url).body[%r{11\.1002/1000/(\d+)}, 1]
53
+ rescue Mechanize::ResponseCodeError => e
54
+ raise unless e.response_code == "404" # unknown code => treat as not found
55
+
56
+ nil
57
+ end
58
+
59
+ # @return [String] the `rec=` value for the rec.aspx lookup
60
+ def rec_query
61
+ ref.suppl ? "#{ref.code} Suppl. #{ref.suppl}" : ref.code
62
+ end
63
+
64
+ # @param idrec [String]
65
+ # @return [Array<Hash>] editions of the recommendation
66
+ def editions(idrec)
67
+ JSON.parse agent.get(format(RECEDITIONS_URL, idrec: idrec)).body
68
+ rescue JSON::ParserError
69
+ []
70
+ end
71
+
72
+ # @param edition [Hash] a getRecEditions entry
73
+ # @return [Relaton::Itu::Hit]
74
+ def recommendation_hit(edition)
75
+ Hit.new({
76
+ code: "ITU-T #{edition['rec_name']}",
77
+ title: edition["title"],
78
+ url: format(HANDLE_URL, idrec: edition["idrec"]),
79
+ type: "recommendation",
80
+ }, self)
81
+ end
82
+
83
+ # Resolve an ITU-R Radio Regulation or Operational Bulletin to its stable
84
+ # /pub landing page, whose id is derivable from the reference.
85
+ def request_publication
86
+ Util.info "Fetching from www.itu.int ...", key: ref.to_s
87
+ return @array = [] unless ref.year
88
+
89
+ url = "#{DOMAIN}/pub/#{publication_id}"
90
+ page = fetch_publication_page url
91
+ return @array = [] if page.nil? || page.uri.to_s.match?(/notfound/i)
92
+
93
+ hit = Hit.new({ code: publication_code, title: nil, url: url, type: "publication" }, self)
94
+ @array = [hit]
95
+ end
96
+
97
+ # @return [Mechanize::Page, nil] nil when the publication does not exist
98
+ def fetch_publication_page(url)
99
+ agent.get url
100
+ rescue Mechanize::ResponseCodeError => e
101
+ raise unless e.response_code == "404" # unknown publication => not found
102
+
103
+ nil
104
+ end
105
+
106
+ # @return [String] the /pub identifier for RR or OB
107
+ def publication_id
108
+ if ref.code == "RR"
109
+ "R-REG-RR-#{ref.year}"
110
+ else # Operational Bulletin, e.g. OB.1096
111
+ "T-SP-OB.#{ref.code[/\d+/]}-#{ref.year}"
112
+ end
113
+ end
114
+
115
+ # @return [String] the docidentifier-friendly code (year only, no month)
116
+ def publication_code
117
+ "#{ref.prefix}-#{ref.sector} #{ref.code} (#{ref.year})"
36
118
  end
37
119
 
38
120
  def request_document # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
@@ -50,78 +132,6 @@ module Relaton
50
132
  hit.item = item
51
133
  @array = [hit]
52
134
  end
53
-
54
- # @return [String]
55
- def group
56
- @group ||= case ref.to_ref
57
- when %r{OB|Operational Bulletin}, %r{^ITU-R\sRR}
58
- "Publications"
59
- when %r{^ITU-T} then "Recommendations"
60
- end
61
- end
62
-
63
- # @return [Hash]
64
- def params # rubocop:disable Metrics/MethodLength
65
- input = ref.dup
66
- input.year = nil
67
- {
68
- "Input" => input.to_s,
69
- "Start" => 0,
70
- "Rows" => 20,
71
- "SortBy" => "RELEVANCE",
72
- "ExactPhrase" => false,
73
- "CollectionName" => "General",
74
- "CollectionGroup" => group,
75
- "Sector" => ref.to_ref.match(/(?<=^ITU-)\w/).to_s.downcase,
76
- "Criterias" => [{
77
- "Name" => "Search in",
78
- "Criterias" => [
79
- {
80
- "Selected" => false,
81
- "Value" => "",
82
- "Label" => "Name",
83
- "Target" => "/name_s",
84
- "TypeName" => "CHECKBOX",
85
- "GetCriteriaType" => 0,
86
- },
87
- {
88
- "Selected" => false,
89
- "Value" => "",
90
- "Label" => "Short description",
91
- "Target" => "/short_description_s",
92
- "TypeName" => "CHECKBOX",
93
- "GetCriteriaType" => 0,
94
- },
95
- {
96
- "Selected" => false,
97
- "Value" => "",
98
- "Label" => "File content",
99
- "Target" => "/file",
100
- "TypeName" => "CHECKBOX",
101
- "GetCriteriaType" => 0,
102
- },
103
- ],
104
- "ShowCheckbox" => true,
105
- "Selected" => false,
106
- }],
107
- "Topics" => "",
108
- "ClientData" => {},
109
- "Language" => "en",
110
- "SearchType" => "All",
111
- }
112
- end
113
-
114
- # @param data [Hash]
115
- # @return [Array<Relaton::Itu::Hit>]
116
- def hits(data)
117
- data["results"].map do |h|
118
- code = h["Media"]["Name"]
119
- title = h["Title"]
120
- url = "#{DOMAIN}#{h['Redirection']}"
121
- type = h["Collection"]["Group"].downcase[0...-1]
122
- Hit.new({ code: code, title: title, url: url, type: type }, self)
123
- end
124
- end
125
135
  end
126
136
  end
127
137
  end
@@ -19,7 +19,8 @@ module Relaton
19
19
  end
20
20
 
21
21
  def doc_url
22
- CGI.unescape(hit.hit[:url]).split("dest=").last
22
+ url = CGI.unescape(hit.hit[:url])
23
+ url.include?("dest=") ? url.split("dest=").last : url
23
24
  end
24
25
 
25
26
  def fetch_edition = nil
@@ -91,10 +91,24 @@ module Relaton
91
91
  @docid ||= begin
92
92
  docids = hit.hit[:code].to_s.split(" | ").map { |c| createdocid(c) }
93
93
  docids << createdocid(doc["rec_name"]) if docids.empty?
94
+ iso = iso_docid
95
+ docids << iso if iso
94
96
  docids
95
97
  end
96
98
  end
97
99
 
100
+ # The equivalent ISO/IEC identifier used to come from the search result's
101
+ # media name; it is now re-sourced from the recommendation header's
102
+ # `iso_number` field (e.g. "Equivalent standard: ISO/IEC 17788:2014 (Common)").
103
+ # @return [Relaton::Bib::Docidentifier, nil]
104
+ def iso_docid
105
+ return unless idrec # only recommendations carry an idrec / iso_number
106
+
107
+ num = parser.doc && parser.doc["iso_number"]
108
+ id = num && num[%r{ISO(?:/IEC)?(?:/IEEE)?\s+\d[\d-]*}]
109
+ createdocid(id) if id
110
+ end
111
+
98
112
  # @param text [String]
99
113
  # @return [Relaton::Bib::Docidentifier]
100
114
  def createdocid(text) # rubocop:disable Metrics/MethodLength
@@ -1,5 +1,5 @@
1
1
  module Relaton
2
2
  module Itu
3
- VERSION = "2.1.1".freeze
3
+ VERSION = "2.1.2".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaton-itu
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.1.2
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-05-12 00:00:00.000000000 Z
11
+ date: 2026-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mechanize
@@ -102,11 +102,6 @@ files:
102
102
  - bin/rspec
103
103
  - bin/setup
104
104
  - data.json
105
- - grammars/basicdoc.rng
106
- - grammars/biblio-standoc.rng
107
- - grammars/biblio.rng
108
- - grammars/relaton-itu-compile.rng
109
- - grammars/relaton-itu.rng
110
105
  - lib/relaton/itu.rb
111
106
  - lib/relaton/itu/bibliography.rb
112
107
  - lib/relaton/itu/data_fetcher.rb