bipm-data-importer 0.2.2 → 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 (53) 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 -440
  23. data/lib/bipm/data/importer/bodies.rb +45 -0
  24. data/lib/bipm/data/importer/body.rb +43 -0
  25. data/lib/bipm/data/importer/cgpm.rb +21 -0
  26. data/lib/bipm/data/importer/clauses.rb +239 -0
  27. data/lib/bipm/data/importer/cli.rb +83 -0
  28. data/lib/bipm/data/importer/common.rb +31 -120
  29. data/lib/bipm/data/importer/fetcher.rb +62 -0
  30. data/lib/bipm/data/importer/language.rb +66 -0
  31. data/lib/bipm/data/importer/quirks.rb +73 -0
  32. data/lib/bipm/data/importer/strategies/base.rb +40 -0
  33. data/lib/bipm/data/importer/strategies/spa_meetings.rb +340 -0
  34. data/lib/bipm/data/importer/strategies/static_index.rb +324 -0
  35. data/lib/bipm/data/importer/strategies.rb +13 -0
  36. data/lib/bipm/data/importer/text/french.rb +49 -0
  37. data/lib/bipm/data/importer/text.rb +11 -0
  38. data/lib/bipm/data/importer/version.rb +1 -1
  39. data/lib/bipm/data/importer.rb +31 -4
  40. data/lib/bipm/data/outcomes/action.rb +1 -1
  41. data/lib/bipm/data/outcomes/approval.rb +1 -1
  42. data/lib/bipm/data/outcomes/body.rb +1 -1
  43. data/lib/bipm/data/outcomes/consideration.rb +1 -1
  44. data/lib/bipm/data/outcomes/localized_body.rb +1 -1
  45. data/lib/bipm/data/outcomes/meeting.rb +2 -2
  46. data/lib/bipm/data/outcomes/resolution.rb +1 -1
  47. data/lib/bipm/data/outcomes.rb +9 -13
  48. data/lib/bipm-data-importer.rb +1 -1
  49. metadata +46 -19
  50. data/.hound.yml +0 -5
  51. data/.rspec +0 -3
  52. data/.rubocop.yml +0 -10
  53. 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,442 +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
- resolutions = {}
33
- %w[en fr].each do |meeting_lang|
34
- next if ARGV[0] == '--fork' && fork
35
-
36
- meeting_lang_sfx = (meeting_lang == 'fr') ? "-fr" : ""
37
- meeting_lang_sfx_dir = (meeting_lang == 'fr') ? "-fr" : "-en"
38
-
39
- bodyurl_local = meeting_lang == "en" ? bodyurl : bodyurl.gsub("/en/", "/fr/")
40
-
41
- cassfx = meeting_lang == "en" ? "" : "-fr"
42
-
43
- pages = {}
44
-
45
- pages[:index] = VCR.use_cassette "#{body}/#{body}-index#{meeting_lang_sfx}" do
46
- a.get "#{bodyurl_local}"
47
- end
48
-
49
- pages[:meetings] = VCR.use_cassette "#{body}/#{body}-meetings#{meeting_lang_sfx}" do
50
- a.get "#{bodyurl_local}/meetings"
51
- end
52
-
53
- pages[:publications] = VCR.use_cassette "#{body}/#{body}-publications#{meeting_lang_sfx}" do
54
- a.get "#{bodyurl_local}/publications"
55
- end
56
-
57
- # CIPM
58
- pages[:recommendations] = VCR.use_cassette "#{body}/#{body}-recommendations#{meeting_lang_sfx}" do
59
- a.get "#{bodyurl_local}/recommendations"
60
- rescue Mechanize::ResponseCodeError
61
- nil
62
- end
63
-
64
- # CIPM has outcomes, JCRB has meeting-outcomes
65
- # As of 2024-12, no other body has this special case.
66
- outcomes_path = bodyid == :CIPM ? "outcomes" : "meeting-outcomes"
67
-
68
- pages[:outcomes] = VCR.use_cassette "#{body}/#{body}-outcomes#{meeting_lang_sfx}" do
69
- a.get "#{bodyurl_local}/#{outcomes_path}"
70
- rescue Mechanize::ResponseCodeError
71
- nil
72
- end
73
-
74
- meetings = pages[:meetings]
75
- publications = pages[:publications]
76
- recommendations = pages[:recommendations]
77
- outcomes = pages[:outcomes]
78
-
79
- index = {
80
- "meetings" => {"fr" => [], "en" => []},
81
- "decisions" => {"fr" => [], "en" => []},
82
- }
83
-
84
- meetings.css('.meetings-list__item').each do |meeting_div|
85
- date = Bipm::Data::Importer::Common.extract_date(meeting_div.at_css('.meetings-list__informations-date').text)
86
-
87
- title = meeting_div.at_css('.meetings-list__informations-title').text.strip
88
- href = meeting_div.at_css('.meetings-list__informations-title').attr('href')
89
- href = "/#{meeting_lang}" + href unless href.start_with? "/#{meeting_lang}/"
90
-
91
- ident = href.split("/#{body}/").last.gsub('/', '.')
92
- yr = href.include?("/wg/") ? nil : href.split('-').last
93
- meeting_id = if href.include?("/wg/")
94
- # workgroup logic
95
- wg = href.split("/wg/").last.split("/").first
96
- href.split("/").last
97
- else
98
- parts = href.split("/").last.split("-")
99
- if parts.length == 2
100
- # Only the number
101
- parts[0]
102
- else
103
- parts[0] + parts[1].sub("_", "-")
104
- end
105
- end
106
-
107
- meeting = VCR.use_cassette "#{body}/#{body}-meeting-#{ident}#{meeting_lang_sfx}" do
108
- a.get href
109
- end
110
-
111
- # Duplicate data upstream:
112
- # - https://www.bipm.org/en/committees/ci/cipm/104-_1-2015
113
- # - https://www.bipm.org/en/committees/ci/cipm/104-_2-2015
114
- yr = nil if [bodyid, meeting_id] == [:CIPM, '104-2']
115
-
116
- # meeting logic, as in for the `meetings-xx` structure
117
- # this structure contains recommendations
118
-
119
- if yr
120
- has_recommendations = false
121
-
122
- pdf = Bipm::Data::Importer::Common.extract_pdf(meeting, meeting_lang)
123
-
124
- h = {
125
- "metadata" => {
126
- "title" => title,
127
- "identifier" => meeting_id,
128
- "date" => date.to_s,
129
- "source" => "BIPM - Pavillon de Breteuil",
130
- "url" => meeting.uri.to_s,
131
- }
132
- }
133
-
134
- h["pdf"] = pdf if pdf
135
-
136
- resolutions = meeting.css(".bipm-resolutions .publications__content").map do |res_div|
137
- res_div.at_css('a').attr('href')
138
- end
139
-
140
- resolutions_additional = recommendations&.css(".bipm-resolutions .publications__content")&.map do |res_div|
141
- href = res_div.at_css('a').attr('href')
142
-
143
- # bad case of french data...
144
- href = href.gsub('/106-2017/', '/104-_1-2015/') if href =~ %r"/ci/cipm/106-2017/resolution-[12]\z"
145
-
146
- href
147
- end&.select do |href|
148
- href.include? "/#{ident}/"
149
- end || []
150
-
151
- # A mistake on a website, resolution 2 listed twice...
152
- # https://www.bipm.org/fr/committees/ci/cipm/94-2005/
153
- if [bodyid, meeting_lang, meeting_id] == [:CIPM, 'fr', '94'] && resolutions.sort.uniq != resolutions.sort
154
- resolutions = (1..3).map do |i|
155
- "https://www.bipm.org/fr/committees/ci/cipm/94-2005/resolution-#{i}"
156
- end
157
- end
158
-
159
- resolutions = (resolutions + resolutions_additional).uniq
160
-
161
- h["resolutions"] = resolutions.map do |href|
162
- href = href.gsub('/web/guest/', "/#{meeting_lang}/")
163
- href = href.sub("www.bipm.org/", "www.bipm.org/#{meeting_lang}/") unless href.include? "/#{meeting_lang}/"
164
-
165
- # error: https://www.bipm.org/fr/committees/ci/cipm/104-_1-2015 has wrong references to Recommandations
166
- href = href.gsub('/104-2015/', '/104-_1-2015/')
167
-
168
- res_id = href.split("-").last.to_i
169
- res = VCR.use_cassette("#{body}/#{body}-recommendation-#{yr}-#{res_id}#{meeting_lang_sfx}") do
170
- a.get href
171
- end
172
-
173
- has_recommendations = true
174
-
175
- Bipm::Data::Importer::Common.parse_resolution(res, res_id, date, body, meeting_lang, "recommendation?")
176
- end
177
-
178
- index["meetings"][meeting_lang] << h
179
- end
180
-
181
- # decisions logic, as in for the `decisions-xx` structure
182
- # this structure contains decisions
183
-
184
- if meeting_id
185
- h = {
186
- "metadata" => {
187
- "title" => title,
188
- "identifier" => meeting_id,
189
- "date" => date.to_s,
190
- "source" => "BIPM - Pavillon de Breteuil",
191
- "url" => meeting.uri.to_s
192
- }
193
- }
194
-
195
- h["metadata"]["workgroup"] = wg if wg
196
-
197
- decisions = meeting.css('.bipm-decisions .decisions')
198
-
199
- # For some bodies, decisions/outcomes are on a different page altogether.
200
- # But then we must select only decisions pertaining to our meeting.
201
- if outcomes
202
- decisions_additional = outcomes.css('.bipm-decisions .decisions')
203
-
204
- decisions_additional = decisions_additional.select do |i|
205
- pass = true if i["data-meeting_key"] == meeting_id
206
- pass = true if i["data-meeting_key"] == "#{meeting_id}-0" # Some are with a 0, some without
207
- pass = true if i["data-meeting"] == meeting_id # Some don't have meeting_key set, but have meeting set
208
-
209
- pass
210
- end
211
-
212
- decisions = decisions.to_a + decisions_additional.to_a
213
-
214
- # duplicates check...
215
- duplicates = decisions.map{|i|i.at_css('.title-third').text}
216
- if duplicates != duplicates.uniq
217
- pp [:duplicates_found, decisions]
218
- end
219
- end
220
-
221
- h["resolutions"] = decisions.map do |titletr|
222
- title = titletr.at_css('.title-third').text.strip
223
-
224
- type = case title
225
- when /\AD[eé]cision/
226
- "decision"
227
- when /\AR[eé]solution/
228
- "resolution"
229
- when /\AAction/
230
- "action"
231
- when /\ARecomm[ae]ndation/
232
- "recommendation"
233
- else
234
- "decision"
235
- end
236
-
237
- categories = titletr.attr('data-decisioncategories')
238
- categories ||= "[]"
239
-
240
- r = {
241
- "dates" => [date.to_s],
242
- "subject" => bodyid.to_s,
243
- "type" => type,
244
- "title" => title,
245
- "identifier" => "#{titletr.attr('data-meeting')}-#{titletr.attr('data-number')}",
246
- "url" => meeting.uri.to_s,
247
- #TODO: "reference" => meeting.uri.merge(titletr.attr('data-link')).to_s,
248
-
249
- "categories" => JSON.parse(categories).map(&:strip).uniq,
250
-
251
- "considerations" => [],
252
- "actions" => [],
253
- }
254
-
255
- contenttr = titletr.attr('data-text')
256
- if contenttr.empty?
257
- # This document has empty data-text attribute https://www.bipm.org/fr/committees/jc/jcrb/4-2000
258
- contenttr = titletr.css('p').map(&:inner_html).join("\n")
259
- end
260
- contenttr = Nokogiri::HTML(contenttr)
261
-
262
-
263
- Bipm::Data::Importer::Common.replace_links contenttr, meeting, meeting_lang
264
-
265
- part = Bipm::Data::Importer::Common.ng_to_string(contenttr)
266
- part = part.gsub(%r"<a name=\"haut\">(.*?)</a>"m, '\1')
267
- parse = Nokogiri::HTML(part).text.strip
268
-
269
- 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
270
- subject = $1
271
- if subject
272
- parse = parse[subject.length..-1]
273
- part = part.gsub(/\A(<html><body>\n*<p>)#{Regexp.escape subject}/, '\1')
274
- # Note: we set "CIPM" as the subject for all CIPM decisions
275
- # r['subject'] = subject.strip
276
- end
277
-
278
- xparse = parse
279
-
280
- (1..1024).each do |pass|
281
- Bipm::Data::Importer::CONSIDERATIONS.any? do |k,v|
282
- if xparse =~ /\A#{Bipm::Data::Importer::PREFIX}#{k}\b/i
283
- r["considerations"] << {
284
- "type" => v,
285
- "date_effective" => date.to_s,
286
- "message" => Bipm::Data::Importer::Common.format_message(part),
287
- }
288
- end
289
- end && break
290
-
291
- Bipm::Data::Importer::ACTIONS.any? do |k,v|
292
- if xparse =~ /\A#{Bipm::Data::Importer::PREFIX}#{k}\b/i
293
- r["actions"] << {
294
- "type" => v,
295
- "date_effective" => date.to_s,
296
- "message" => Bipm::Data::Importer::Common.format_message(part),
297
- }
298
- end
299
- end && break
300
-
301
- case pass
302
- when 1, 2, 3
303
- xparse = xparse.gsub(/\A.*?(CIPM( President| in its meeting)?:?\n*|\((2018|CIPM)\)|de la 103e session) /m, '')
304
- when 4
305
- xparse = parse.gsub(/\A.*?, /m, '')
306
- when 5
307
- xparse = parse.gsub(/\A.*? (and|et) /m, '')
308
- else
309
- r["x-unparsed"] ||= []
310
- r["x-unparsed"] << parse
311
-
312
- break
313
- end
314
- end
315
-
316
- r
317
- end.sort_by { |i| [i["type"], i["identifier"].scan(/([0-9]+|[^0-9]+)/).map(&:first).map { |i| i =~ /[0-9]/ ? i.to_i : i }] }
318
-
319
- # Mistake on a website: numbering is wrong
320
- # https://www.bipm.org/fr/committees/jc/jcrb/43-2021
321
- if [bodyid, meeting_lang, meeting_id] == [:JCRB, "fr", "43"]
322
- ids = h["resolutions"].map { |i| [i["type"], i["identifier"]] }.sort
323
-
324
- # Unfixed yet still, let's "massage" it
325
- if ids != ids.uniq
326
- idx = 1
327
- h["resolutions"].each do |i|
328
- next unless i["identifier"] == "43-1" && i["type"] == "action"
329
- i["identifier"] += "-xxx-#{idx}"
330
- idx += 1
331
- end
332
- end
333
- end
334
-
335
- h["working_documents"] = meeting.css('.portlet-boundary_CommitteePublications_ .publications__content').map do |d|
336
- # Website depends on an Accept-Language header but caches responses. This creates non-determinism. We must unify those.
337
- # $ curl -H "Accept-Language: " "https://www.bipm.org/en/committees/cc/ccqm/22-2016?$RANDOM$RANDOM" 2>/dev/null | grep -A2 publications__date
338
- # <p class="publications__date">
339
- # Mon Jul 09 00:00:00 GMT 2018
340
- # </p>
341
- # $ 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
342
- # <p class="publications__date">
343
- # 09/07/2018
344
- # </p>
345
- date_str = d.at_css('.publications__date')&.text&.strip
346
- date_str = Date.parse(date_str).strftime("%d/%m/%Y") if date_str
347
-
348
- {
349
- "title" => d.at_css('.title-third').text.strip,
350
- "pdf" => d.at_css('.title-third').attr('href'),
351
- "description" => d.css('.publications__body').first.text.strip,
352
- "author" => d.css('.publications__body').last.text.strip,
353
- "date" => date_str,
354
- }.compact
355
- end
356
-
357
- index["decisions"][meeting_lang] << h
358
- end
359
-
360
- # index logic: creates an index of all meetings mentioned on the website and
361
- # their correct references to `decisions-xx` and `meetings-xx` if present.
362
- # meeting = {
363
- # "title" => title,
364
- # "url" => href,
365
- # "ident" => ident,
366
- # "meeting_ref" => yr,
367
- # "decisions_ref" => meeting_id,
368
- # }.compact
369
-
370
- # index["meetings"] << meeting
371
- end
372
-
373
- # File.write("#{BASE_DIR}/cipm/index#{meeting_lang_sfx_dir}.yml", YAML.dump(index))
374
-
375
- parts = index["meetings"][meeting_lang] + index["decisions"][meeting_lang]
376
- parts = parts.group_by { |i| [i["metadata"]["workgroup"].to_s, i["metadata"]["identifier"].to_s] }
377
- parts = parts.sort_by { |(wg,i),| [wg, i.to_i, i] }.to_h # Numeric sort by workgroup, then key
378
-
379
- parts.each do |(wg, mid), hs|
380
- h = {
381
- "metadata" => hs.first["metadata"],
382
- "pdf" => hs.first["pdf"],
383
- "resolutions" => hs.map { |i| i["resolutions"] }.sum([]),
384
- "working_documents" => hs.map { |i| i["working_documents"] || [] }.sum([])
385
- }
386
-
387
- h.delete("pdf") unless h["pdf"]
388
- h.delete("working_documents") if h["working_documents"].empty?
389
-
390
- wg = hs.first["metadata"]["workgroup"]
391
- if wg
392
- fn = "#{BASE_DIR}/#{body}/workgroups/#{wg}/meetings#{meeting_lang_sfx_dir}/meeting-#{mid}.yml"
393
- elsif body == :cgpm
394
- # CGPM old script used numbering like 00, 01, ..., 11, ...
395
- fn = "#{BASE_DIR}/#{body}/meetings#{meeting_lang_sfx_dir}/meeting-#{"%02d" % mid}.yml"
396
- else
397
- fn = "#{BASE_DIR}/#{body}/meetings#{meeting_lang_sfx_dir}/meeting-#{mid}.yml"
398
- end
399
-
400
- FileUtils.mkdir_p(File.dirname(fn))
401
- File.write(fn, YAML.dump(h))
402
- end
403
-
404
- # Publications handling
405
- categories = publications.css(".publications").map do |i|
406
- title = i.previous_element&.text&.strip || "Misc"
407
-
408
- {
409
- "title" => title,
410
- "documents" => i.css(".publications__content").map do |d|
411
- {
412
- "title" => d.at_css(".title-third").text.strip.gsub(/\s+/, ' '),
413
- "pdf" => d.at_css(".title-third")&.attr("href")&.split('?')&.first,
414
- # "description" => d.css('.publications__body')[0]&.text&.strip,
415
- # "author" => d.css('.publications__body')[1]&.text&.strip,
416
- }.compact
417
- end
418
- }
419
- end
420
-
421
- unless categories.empty?
422
- doc = {
423
- "metadata" => {
424
- "url" => bodyurl.gsub("/en/", "/#{meeting_lang}/") + "/publications/"
425
- },
426
- "categories" => categories
427
- }
428
-
429
- fn = "#{BASE_DIR}/#{body}/publications-#{meeting_lang}.yml"
430
-
431
- File.write(fn, YAML.dump(doc))
432
- end
433
-
434
- puts "* #{bodyid}/#{meeting_lang} parsing done"
435
- exit! if ARGV[0] == '--fork'
436
- end
437
-
438
- 2.times { Process.wait } if ARGV[0] == '--fork'
439
- exit! if ARGV[0] == '--fork'
440
- end
441
-
442
- bodies.length.times { Process.wait } if ARGV[0] == '--fork'
3
+ require "bipm-data-importer"
4
+ exit Bipm::Data::Importer::CLI.new(ARGV).run
@@ -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