bipm-data-importer 0.2.1 → 0.4.0

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +68 -9
  3. data/TODO.refactor/01-remove-debug-code.md +18 -0
  4. data/TODO.refactor/02-fix-gemspec.md +21 -0
  5. data/TODO.refactor/03-update-readme.md +28 -0
  6. data/TODO.refactor/04-replace-require-relative-with-autoload.md +34 -0
  7. data/TODO.refactor/05-fix-outcomes-bugs.md +22 -0
  8. data/TODO.refactor/06-unify-clause-taxonomy.md +31 -0
  9. data/TODO.refactor/07-extract-french-parsing.md +21 -0
  10. data/TODO.refactor/08-extract-data-quirks.md +25 -0
  11. data/TODO.refactor/09-introduce-body-registry.md +23 -0
  12. data/TODO.refactor/10-introduce-strategy-pattern.md +34 -0
  13. data/TODO.refactor/11-extract-cli-class.md +20 -0
  14. data/TODO.refactor/12-specs-common.md +23 -0
  15. data/TODO.refactor/13-specs-cgpm.md +22 -0
  16. data/TODO.refactor/14-specs-asciimath.md +23 -0
  17. data/TODO.refactor/15-specs-outcomes.md +24 -0
  18. data/TODO.refactor/16-quarantine-failing-specs.md +23 -0
  19. data/TODO.refactor/17-typed-domain-models.md +32 -0
  20. data/TODO.refactor/README.md +62 -0
  21. data/bipm-data-importer.gemspec +11 -7
  22. data/exe/bipm-fetch +2 -376
  23. data/lib/bipm/data/importer/asciimath.rb +3 -3
  24. data/lib/bipm/data/importer/bodies.rb +45 -0
  25. data/lib/bipm/data/importer/body.rb +43 -0
  26. data/lib/bipm/data/importer/cgpm.rb +21 -0
  27. data/lib/bipm/data/importer/clauses.rb +239 -0
  28. data/lib/bipm/data/importer/cli.rb +83 -0
  29. data/lib/bipm/data/importer/common.rb +32 -121
  30. data/lib/bipm/data/importer/fetcher.rb +62 -0
  31. data/lib/bipm/data/importer/language.rb +66 -0
  32. data/lib/bipm/data/importer/quirks.rb +73 -0
  33. data/lib/bipm/data/importer/strategies/base.rb +40 -0
  34. data/lib/bipm/data/importer/strategies/spa_meetings.rb +340 -0
  35. data/lib/bipm/data/importer/strategies/static_index.rb +324 -0
  36. data/lib/bipm/data/importer/strategies.rb +13 -0
  37. data/lib/bipm/data/importer/text/french.rb +49 -0
  38. data/lib/bipm/data/importer/text.rb +11 -0
  39. data/lib/bipm/data/importer/version.rb +1 -1
  40. data/lib/bipm/data/importer.rb +31 -4
  41. data/lib/bipm/data/outcomes/action.rb +1 -1
  42. data/lib/bipm/data/outcomes/approval.rb +1 -1
  43. data/lib/bipm/data/outcomes/body.rb +1 -1
  44. data/lib/bipm/data/outcomes/consideration.rb +1 -1
  45. data/lib/bipm/data/outcomes/localized_body.rb +1 -1
  46. data/lib/bipm/data/outcomes/meeting.rb +2 -2
  47. data/lib/bipm/data/outcomes/resolution.rb +1 -1
  48. data/lib/bipm/data/outcomes.rb +9 -13
  49. data/lib/bipm-data-importer.rb +1 -1
  50. metadata +46 -19
  51. data/.hound.yml +0 -5
  52. data/.rspec +0 -3
  53. data/.rubocop.yml +0 -10
  54. data/exe/bipm-fetch-cgpm +0 -3
@@ -11,17 +11,17 @@ Gem::Specification.new do |spec|
11
11
  spec.summary = "Importer for BIPM CGPM and CIPM content"
12
12
  spec.description = "Importer for BIPM CGPM and CIPM content"
13
13
  spec.homepage = "https://github.com/metanorma/bipm-data-importer"
14
- spec.required_ruby_version = ">= 2.5.0"
14
+ spec.required_ruby_version = ">= 3.3.0"
15
15
 
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
17
  spec.metadata["source_code_uri"] = "https://github.com/metanorma/bipm-data-importer"
18
18
  spec.metadata["changelog_uri"] = "https://github.com/metanorma/bipm-data-importer"
19
19
 
20
- # Specify which files should be added to the gem when it is released.
21
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
20
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
21
  `git ls-files -z`.split("\x0").reject do |f|
24
- (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
22
+ f == __FILE__ ||
23
+ f.match(%r{\A(?:spec|features|\.git|\.circleci|\.github)/}) ||
24
+ f.start_with?(".gitignore", ".rspec", ".rubocop")
25
25
  end
26
26
  end
27
27
  spec.bindir = "exe"
@@ -30,10 +30,14 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  spec.add_dependency "nokogiri"
32
32
  spec.add_dependency "mechanize"
33
- spec.add_dependency "coradoc"
34
- spec.add_dependency "pry"
35
-
33
+ # coradoc 2.0 removed `coradoc/input/html` (called in
34
+ # lib/bipm/data/importer/common.rb) as part of a major refactor.
35
+ # Pinned to 1.x until a follow-up ports the HTML input calls to the
36
+ # coradoc 2.x API (or wherever that functionality has been extracted).
37
+ spec.add_dependency "coradoc", "~> 1.1"
36
38
  spec.add_dependency "vcr"
39
+
40
+ spec.add_development_dependency "pry"
37
41
  spec.add_development_dependency "webmock"
38
42
  spec.add_development_dependency "rake", "~> 13.0"
39
43
  spec.add_development_dependency "rspec", "~> 3.0"
data/exe/bipm-fetch CHANGED
@@ -1,378 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative '../lib/bipm-data-importer'
4
-
5
- bodies = {
6
- "JCRB": 'https://www.bipm.org/en/committees/jc/jcrb',
7
- "JCGM": 'https://www.bipm.org/en/committees/jc/jcgm',
8
- "CCU": 'https://www.bipm.org/en/committees/cc/ccu',
9
- "CCTF": 'https://www.bipm.org/en/committees/cc/cctf',
10
- "CCT": 'https://www.bipm.org/en/committees/cc/cct',
11
- "CCRI": 'https://www.bipm.org/en/committees/cc/ccri',
12
- "CCPR": 'https://www.bipm.org/en/committees/cc/ccpr',
13
- "CCQM": 'https://www.bipm.org/en/committees/cc/ccqm',
14
- "CCM": 'https://www.bipm.org/en/committees/cc/ccm',
15
- "CCL": 'https://www.bipm.org/en/committees/cc/ccl',
16
- "CCEM": 'https://www.bipm.org/en/committees/cc/ccem',
17
- "CCAUV": 'https://www.bipm.org/en/committees/cc/ccauv',
18
- "CIPM": 'https://www.bipm.org/en/committees/ci/cipm',
19
- "CGPM": 'https://www.bipm.org/en/committees/cg/cgpm',
20
- }
21
-
22
- BASE_DIR = "data"
23
- a = Mechanize.new
24
-
25
- bodies.each do |bodyid, bodyurl|
26
- next if ARGV[0] == '--fork' && fork
27
-
28
- next if ARGV[0] && ARGV[0].start_with?("--body=") && ARGV[0].downcase != "--body=#{bodyid}".downcase
29
-
30
- body = bodyid.to_s.downcase.gsub(" ", "-").to_sym
31
-
32
- meetings_en = VCR.use_cassette "#{body}/#{body}-meetings" do
33
- a.get "#{bodyurl}/meetings"
34
- end
35
-
36
- meetings_fr = VCR.use_cassette "#{body}/#{body}-meetings-fr" do
37
- a.get "#{bodyurl.gsub("/en/", "/fr/")}/meetings"
38
- end
39
-
40
- publications_en = VCR.use_cassette "#{body}/#{body}-publications" do
41
- a.get "#{bodyurl}/publications"
42
- end
43
-
44
- publications_fr = VCR.use_cassette "#{body}/#{body}-publications-fr" do
45
- a.get "#{bodyurl.gsub("/en/", "/fr/")}/publications"
46
- end
47
-
48
- resolutions = {}
49
- %w[en fr].each do |meeting_lang|
50
- next if ARGV[0] == '--fork' && fork
51
-
52
- meeting_lang_sfx = (meeting_lang == 'fr') ? "-fr" : ""
53
- meeting_lang_sfx_dir = (meeting_lang == 'fr') ? "-fr" : "-en"
54
-
55
- meetings = (meeting_lang == 'en') ? meetings_en : meetings_fr
56
- publications = (meeting_lang == 'en') ? publications_en : publications_fr
57
-
58
- index = {
59
- "meetings" => {"fr" => [], "en" => []},
60
- "decisions" => {"fr" => [], "en" => []},
61
- }
62
-
63
- meetings.css('.meetings-list__item').each do |meeting_div|
64
- date = Bipm::Data::Importer::Common.extract_date(meeting_div.at_css('.meetings-list__informations-date').text)
65
-
66
- title = meeting_div.at_css('.meetings-list__informations-title').text.strip
67
- href = meeting_div.at_css('.meetings-list__informations-title').attr('href')
68
- href = "/#{meeting_lang}" + href unless href.start_with? "/#{meeting_lang}/"
69
-
70
- ident = href.split("/#{body}/").last.gsub('/', '.')
71
- yr = href.include?("/wg/") ? nil : href.split('-').last
72
- meeting_id = if href.include?("/wg/")
73
- # workgroup logic
74
- wg = href.split("/wg/").last.split("/").first
75
- href.split("/").last
76
- else
77
- parts = href.split("/").last.split("-")
78
- if parts.length == 2
79
- # Only the number
80
- parts[0]
81
- else
82
- parts[0] + parts[1].sub("_", "-")
83
- end
84
- end
85
-
86
- meeting = VCR.use_cassette "#{body}/#{body}-meeting-#{ident}#{meeting_lang_sfx}" do
87
- a.get href
88
- end
89
-
90
- # Duplicate data upstream:
91
- # - https://www.bipm.org/en/committees/ci/cipm/104-_1-2015
92
- # - https://www.bipm.org/en/committees/ci/cipm/104-_2-2015
93
- yr = nil if [bodyid, meeting_id] == [:CIPM, '104-2']
94
-
95
- # meeting logic, as in for the `meetings-xx` structure
96
- # this structure contains recommendations
97
-
98
- if yr
99
- has_recommendations = false
100
-
101
- pdf = Bipm::Data::Importer::Common.extract_pdf(meeting, meeting_lang)
102
-
103
- h = {
104
- "metadata" => {
105
- "title" => title,
106
- "identifier" => meeting_id,
107
- "date" => date.to_s,
108
- "source" => "BIPM - Pavillon de Breteuil",
109
- "url" => meeting.uri.to_s,
110
- }
111
- }
112
-
113
- h["pdf"] = pdf if pdf
114
-
115
- resolutions = meeting.css(".bipm-resolutions .publications__content").map do |res_div|
116
- res_div.at_css('a').attr('href')
117
- end
118
-
119
- # A mistake on a website, resolution 2 listed twice...
120
- # https://www.bipm.org/fr/committees/ci/cipm/94-2005/
121
- if [bodyid, meeting_lang, meeting_id] == [:CIPM, 'fr', '94'] && resolutions.sort.uniq != resolutions.sort
122
- resolutions = (1..3).map do |i|
123
- "https://www.bipm.org/fr/committees/ci/cipm/94-2005/resolution-#{i}"
124
- end
125
- end
126
-
127
- h["resolutions"] = resolutions.map do |href|
128
- href = href.gsub('/web/guest/', "/#{meeting_lang}/")
129
- href = href.sub("www.bipm.org/", "www.bipm.org/#{meeting_lang}/") unless href.include? "/#{meeting_lang}/"
130
-
131
- # error: https://www.bipm.org/fr/committees/ci/cipm/104-_1-2015 has wrong references to Recommandations
132
- href = href.gsub('/104-2015/', '/104-_1-2015/')
133
-
134
- res_id = href.split("-").last.to_i
135
- res = VCR.use_cassette("#{body}/#{body}-recommendation-#{yr}-#{res_id}#{meeting_lang_sfx}") do
136
- a.get href
137
- end
138
-
139
- has_recommendations = true
140
-
141
- Bipm::Data::Importer::Common.parse_resolution(res, res_id, date, body, meeting_lang, "recommendation?")
142
- end
143
-
144
- index["meetings"][meeting_lang] << h
145
- end
146
-
147
- # decisions logic, as in for the `decisions-xx` structure
148
- # this structure contains decisions
149
-
150
- if meeting_id
151
- h = {
152
- "metadata" => {
153
- "title" => title,
154
- "identifier" => meeting_id,
155
- "date" => date.to_s,
156
- "source" => "BIPM - Pavillon de Breteuil",
157
- "url" => meeting.uri.to_s
158
- }
159
- }
160
-
161
- h["metadata"]["workgroup"] = wg if wg
162
-
163
- h["resolutions"] = meeting.css('.bipm-decisions .decisions').map do |titletr|
164
- title = titletr.at_css('.title-third').text.strip
165
-
166
- type = case title
167
- when /\AD[eé]cision/
168
- "decision"
169
- when /\AR[eé]solution/
170
- "resolution"
171
- when /\AAction/
172
- "action"
173
- when /\ARecomm[ae]ndation/
174
- "recommendation"
175
- else
176
- "decision"
177
- end
178
-
179
- r = {
180
- "dates" => [date.to_s],
181
- "subject" => bodyid.to_s,
182
- "type" => type,
183
- "title" => title,
184
- "identifier" => "#{titletr.attr('data-meeting')}-#{titletr.attr('data-number')}",
185
- "url" => meeting.uri.to_s,
186
- #TODO: "reference" => meeting.uri.merge(titletr.attr('data-link')).to_s,
187
-
188
- "categories" => JSON.parse(titletr.attr('data-decisioncategories')).map(&:strip).uniq,
189
-
190
- "considerations" => [],
191
- "actions" => [],
192
- }
193
-
194
- contenttr = titletr.attr('data-text')
195
- if contenttr.empty?
196
- # This document has empty data-text attribute https://www.bipm.org/fr/committees/jc/jcrb/4-2000
197
- contenttr = titletr.css('p').map(&:inner_html).join("\n")
198
- end
199
- contenttr = Nokogiri::HTML(contenttr)
200
-
201
-
202
- Bipm::Data::Importer::Common.replace_links contenttr, meeting, meeting_lang
203
-
204
- part = Bipm::Data::Importer::Common.ng_to_string(contenttr)
205
- part = part.gsub(%r"<a name=\"haut\">(.*?)</a>"m, '\1')
206
- parse = Nokogiri::HTML(part).text.strip
207
-
208
- parse =~ /\A(((the|le|la|Le secrétaire du|Le président du|Les membres du|Le directeur du) +[BC][IG]PM( Director| President| Secretary| members)?|Dr May|W\.E\. May)[, ]+)/i
209
- subject = $1
210
- if subject
211
- parse = parse[subject.length..-1]
212
- part = part.gsub(/\A(<html><body>\n*<p>)#{Regexp.escape subject}/, '\1')
213
- # Note: we set "CIPM" as the subject for all CIPM decisions
214
- # r['subject'] = subject.strip
215
- end
216
-
217
- xparse = parse
218
-
219
- (1..1024).each do |pass|
220
- Bipm::Data::Importer::CONSIDERATIONS.any? do |k,v|
221
- if xparse =~ /\A#{Bipm::Data::Importer::PREFIX}#{k}\b/i
222
- r["considerations"] << {
223
- "type" => v,
224
- "date_effective" => date.to_s,
225
- "message" => Bipm::Data::Importer::Common.format_message(part),
226
- }
227
- end
228
- end && break
229
-
230
- Bipm::Data::Importer::ACTIONS.any? do |k,v|
231
- if xparse =~ /\A#{Bipm::Data::Importer::PREFIX}#{k}\b/i
232
- r["actions"] << {
233
- "type" => v,
234
- "date_effective" => date.to_s,
235
- "message" => Bipm::Data::Importer::Common.format_message(part),
236
- }
237
- end
238
- end && break
239
-
240
- case pass
241
- when 1, 2, 3
242
- xparse = xparse.gsub(/\A.*?(CIPM( President| in its meeting)?:?\n*|\((2018|CIPM)\)|de la 103e session) /m, '')
243
- when 4
244
- xparse = parse.gsub(/\A.*?, /m, '')
245
- when 5
246
- xparse = parse.gsub(/\A.*? (and|et) /m, '')
247
- else
248
- r["x-unparsed"] ||= []
249
- r["x-unparsed"] << parse
250
-
251
- break
252
- end
253
- end
254
-
255
- r
256
- end.sort_by { |i| [i["type"], i["identifier"].scan(/([0-9]+|[^0-9]+)/).map(&:first).map { |i| i =~ /[0-9]/ ? i.to_i : i }] }
257
-
258
- # Mistake on a website: numbering is wrong
259
- # https://www.bipm.org/fr/committees/jc/jcrb/43-2021
260
- if [bodyid, meeting_lang, meeting_id] == [:JCRB, "fr", "43"]
261
- ids = h["resolutions"].map { |i| [i["type"], i["identifier"]] }.sort
262
-
263
- # Unfixed yet still, let's "massage" it
264
- if ids != ids.uniq
265
- idx = 1
266
- h["resolutions"].each do |i|
267
- next unless i["identifier"] == "43-1" && i["type"] == "action"
268
- i["identifier"] += "-xxx-#{idx}"
269
- idx += 1
270
- end
271
- end
272
- end
273
-
274
- h["working_documents"] = meeting.css('.portlet-boundary_CommitteePublications_ .publications__content').map do |d|
275
- # Website depends on an Accept-Language header but caches responses. This creates non-determinism. We must unify those.
276
- # $ curl -H "Accept-Language: " "https://www.bipm.org/en/committees/cc/ccqm/22-2016?$RANDOM$RANDOM" 2>/dev/null | grep -A2 publications__date
277
- # <p class="publications__date">
278
- # Mon Jul 09 00:00:00 GMT 2018
279
- # </p>
280
- # $ curl -H "Accept-Language: en-us, en" "https://www.bipm.org/en/committees/cc/ccqm/22-2016?$RANDOM$RANDOM" 2>/dev/null | grep -A2 publications__date
281
- # <p class="publications__date">
282
- # 09/07/2018
283
- # </p>
284
- date_str = d.at_css('.publications__date')&.text&.strip
285
- date_str = Date.parse(date_str).strftime("%d/%m/%Y") if date_str
286
-
287
- {
288
- "title" => d.at_css('.title-third').text.strip,
289
- "pdf" => d.at_css('.title-third').attr('href'),
290
- "description" => d.css('.publications__body').first.text.strip,
291
- "author" => d.css('.publications__body').last.text.strip,
292
- "date" => date_str,
293
- }.compact
294
- end
295
-
296
- index["decisions"][meeting_lang] << h
297
- end
298
-
299
- # index logic: creates an index of all meetings mentioned on the website and
300
- # their correct references to `decisions-xx` and `meetings-xx` if present.
301
- # meeting = {
302
- # "title" => title,
303
- # "url" => href,
304
- # "ident" => ident,
305
- # "meeting_ref" => yr,
306
- # "decisions_ref" => meeting_id,
307
- # }.compact
308
-
309
- # index["meetings"] << meeting
310
- end
311
-
312
- # File.write("#{BASE_DIR}/cipm/index#{meeting_lang_sfx_dir}.yml", YAML.dump(index))
313
-
314
- parts = index["meetings"][meeting_lang] + index["decisions"][meeting_lang]
315
- parts = parts.group_by { |i| [i["metadata"]["workgroup"].to_s, i["metadata"]["identifier"].to_s] }
316
- parts = parts.sort_by { |(wg,i),| [wg, i.to_i, i] }.to_h # Numeric sort by workgroup, then key
317
-
318
- parts.each do |(wg, mid), hs|
319
- h = {
320
- "metadata" => hs.first["metadata"],
321
- "pdf" => hs.first["pdf"],
322
- "resolutions" => hs.map { |i| i["resolutions"] }.sum([]),
323
- "working_documents" => hs.map { |i| i["working_documents"] || [] }.sum([])
324
- }
325
-
326
- h.delete("pdf") unless h["pdf"]
327
- h.delete("working_documents") if h["working_documents"].empty?
328
-
329
- wg = hs.first["metadata"]["workgroup"]
330
- if wg
331
- fn = "#{BASE_DIR}/#{body}/workgroups/#{wg}/meetings#{meeting_lang_sfx_dir}/meeting-#{mid}.yml"
332
- else
333
- fn = "#{BASE_DIR}/#{body}/meetings#{meeting_lang_sfx_dir}/meeting-#{mid}.yml"
334
- end
335
-
336
- FileUtils.mkdir_p(File.dirname(fn))
337
- File.write(fn, YAML.dump(h))
338
- end
339
-
340
- # Publications handling
341
- categories = publications.css(".publications").map do |i|
342
- title = i.previous_element&.text&.strip || "Misc"
343
-
344
- {
345
- "title" => title,
346
- "documents" => i.css(".publications__content").map do |d|
347
- {
348
- "title" => d.at_css(".title-third").text.strip.gsub(/\s+/, ' '),
349
- "pdf" => d.at_css(".title-third")&.attr("href")&.split('?')&.first,
350
- # "description" => d.css('.publications__body')[0]&.text&.strip,
351
- # "author" => d.css('.publications__body')[1]&.text&.strip,
352
- }.compact
353
- end
354
- }
355
- end
356
-
357
- unless categories.empty?
358
- doc = {
359
- "metadata" => {
360
- "url" => bodyurl.gsub("/en/", "/#{meeting_lang}/") + "/publications/"
361
- },
362
- "categories" => categories
363
- }
364
-
365
- fn = "#{BASE_DIR}/#{body}/publications-#{meeting_lang}.yml"
366
-
367
- File.write(fn, YAML.dump(doc))
368
- end
369
-
370
- puts "* #{bodyid}/#{meeting_lang} parsing done"
371
- exit! if ARGV[0] == '--fork'
372
- end
373
-
374
- 2.times { Process.wait } if ARGV[0] == '--fork'
375
- exit! if ARGV[0] == '--fork'
376
- end
377
-
378
- bodies.length.times { Process.wait } if ARGV[0] == '--fork'
3
+ require "bipm-data-importer"
4
+ exit Bipm::Data::Importer::CLI.new(ARGV).run
@@ -9,10 +9,10 @@ module Bipm
9
9
  SPACE_AFTER=/(?:\Z|(?=&nbsp;|\s|[,()\/.~"^]))/
10
10
 
11
11
  PREFIXES = /m|c|d|k|M|G|T|/
12
- UNITS = /t|m|mol|cal|µ|s|g|W|cd|Hz|J|K|N|V|H|A|C|F|T|Wb|sr|lx|lm|bar|sb|h|rad|°C|°F|°K/
12
+ UNITS = /t|m|mol|cal|µ|s|g|W|cd|Hz|J|K|N|V|H|A|C|F|T|Wb|sr|lx|lm|bar|sb|h|rad|fm|°C|°F|°K/
13
13
 
14
14
  def asciidoc_extract_math str
15
- str.gsub(/\b_(#{MATH}{1,3})_/, 'stem:[\1]')
15
+ str.gsub(/\b_?_(#{MATH}{1,3})_?_/, 'stem:[\1]')
16
16
  .gsub("_,_", ',') # Some mistake in formatting
17
17
  .gsub("^er^", 'ESCUPerESCUP') # French specialities
18
18
  .gsub(/(bar|A) (table|of|key|de|being|full|1)( |,)/, 'ESC\1 \2\3') # A is Ampere, but also a particle, bar is a bar but also a bar
@@ -69,4 +69,4 @@ module Bipm
69
69
  end
70
70
  end
71
71
  end
72
- end
72
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bipm
4
+ module Data
5
+ module Importer
6
+ # Open/closed registry of all known BIPM bodies.
7
+ # Adding a body = adding a row here, not editing CLI/scraper code.
8
+ module Bodies
9
+ ALL = {
10
+ jcrb: Body.new(id: :jcrb, display_name: "JCRB", path: "committees/jc/jcrb", strategy: :spa_meetings),
11
+ jcgm: Body.new(id: :jcgm, display_name: "JCGM", path: "committees/jc/jcgm", strategy: :spa_meetings),
12
+ ccu: Body.new(id: :ccu, display_name: "CCU", path: "committees/cc/ccu", strategy: :spa_meetings),
13
+ cctf: Body.new(id: :cctf, display_name: "CCTF", path: "committees/cc/cctf", strategy: :spa_meetings),
14
+ cct: Body.new(id: :cct, display_name: "CCT", path: "committees/cc/cct", strategy: :spa_meetings),
15
+ ccri: Body.new(id: :ccri, display_name: "CCRI", path: "committees/cc/ccri", strategy: :spa_meetings),
16
+ ccpr: Body.new(id: :ccpr, display_name: "CCPR", path: "committees/cc/ccpr", strategy: :spa_meetings),
17
+ ccqm: Body.new(id: :ccqm, display_name: "CCQM", path: "committees/cc/ccqm", strategy: :spa_meetings),
18
+ ccm: Body.new(id: :ccm, display_name: "CCM", path: "committees/cc/ccm", strategy: :spa_meetings),
19
+ ccl: Body.new(id: :ccl, display_name: "CCL", path: "committees/cc/ccl", strategy: :spa_meetings),
20
+ ccem: Body.new(id: :ccem, display_name: "CCEM", path: "committees/cc/ccem", strategy: :spa_meetings),
21
+ ccauv: Body.new(id: :ccauv, display_name: "CCAUV", path: "committees/cc/ccauv", strategy: :spa_meetings),
22
+ cipm: Body.new(id: :cipm, display_name: "CIPM", path: "committees/ci/cipm", strategy: :spa_meetings),
23
+ cgpm: Body.new(id: :cgpm, display_name: "CGPM", path: "worldwide-metrology/cgpm/resolutions.html", strategy: :static_index),
24
+ }.freeze
25
+
26
+ UnknownBodyError = Class.new(ArgumentError)
27
+
28
+ def self.all
29
+ ALL.values
30
+ end
31
+
32
+ def self.find(id)
33
+ key = id.to_sym
34
+ ALL[key] || raise(UnknownBodyError, "unknown body: #{id.inspect} (known: #{ALL.keys.inspect})")
35
+ end
36
+ singleton_class.alias_method :[], :find
37
+
38
+ def self.each(&block)
39
+ ALL.values.each(&block)
40
+ end
41
+ singleton_class.include Enumerable
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bipm
4
+ module Data
5
+ module Importer
6
+ # A BIPM committee body (CIPM, CGPM, CCAUV, etc.).
7
+ #
8
+ # `path` is locale-agnostic — `url(:en)` and `url(:fr)` are constructed
9
+ # symmetrically by inserting the language segment into the BIPM URL
10
+ # template. Neither language is canonical; both are first-class.
11
+ class Body
12
+ BIPM_ORIGIN = "https://www.bipm.org".freeze
13
+
14
+ attr_reader :id, :display_name, :path, :strategy
15
+
16
+ def initialize(id:, display_name:, path:, strategy:)
17
+ @id = id
18
+ @display_name = display_name
19
+ @path = path
20
+ @strategy = strategy
21
+ end
22
+
23
+ def url(language)
24
+ lang = language.is_a?(Language) ? language : Language.find(language)
25
+ "#{BIPM_ORIGIN}/#{lang}/#{path}"
26
+ end
27
+
28
+ def eql?(other)
29
+ other.is_a?(Body) && @id == other.id
30
+ end
31
+ alias == eql?
32
+
33
+ def hash
34
+ @id.hash
35
+ end
36
+
37
+ def to_s
38
+ "#{display_name} (#{id})"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bipm
4
+ module Data
5
+ module Importer
6
+ # Backward-compatibility facade. The implementation lives in
7
+ # Bipm::Data::Importer::Strategies::StaticIndex; this module exists
8
+ # so that older code calling CGPM.run(agent, base_dir) keeps working.
9
+ #
10
+ # New code should go through Bipm::Data::Importer.fetch(:cgpm).
11
+ module CGPM
12
+ def self.run(agent, base_dir)
13
+ Strategies::StaticIndex.new(
14
+ body: Bodies[:cgpm],
15
+ base_dir: base_dir,
16
+ ).call(agent: agent)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end