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.
- checksums.yaml +4 -4
- data/README.adoc +68 -9
- data/TODO.refactor/01-remove-debug-code.md +18 -0
- data/TODO.refactor/02-fix-gemspec.md +21 -0
- data/TODO.refactor/03-update-readme.md +28 -0
- data/TODO.refactor/04-replace-require-relative-with-autoload.md +34 -0
- data/TODO.refactor/05-fix-outcomes-bugs.md +22 -0
- data/TODO.refactor/06-unify-clause-taxonomy.md +31 -0
- data/TODO.refactor/07-extract-french-parsing.md +21 -0
- data/TODO.refactor/08-extract-data-quirks.md +25 -0
- data/TODO.refactor/09-introduce-body-registry.md +23 -0
- data/TODO.refactor/10-introduce-strategy-pattern.md +34 -0
- data/TODO.refactor/11-extract-cli-class.md +20 -0
- data/TODO.refactor/12-specs-common.md +23 -0
- data/TODO.refactor/13-specs-cgpm.md +22 -0
- data/TODO.refactor/14-specs-asciimath.md +23 -0
- data/TODO.refactor/15-specs-outcomes.md +24 -0
- data/TODO.refactor/16-quarantine-failing-specs.md +23 -0
- data/TODO.refactor/17-typed-domain-models.md +32 -0
- data/TODO.refactor/README.md +62 -0
- data/bipm-data-importer.gemspec +11 -7
- data/exe/bipm-fetch +2 -440
- data/lib/bipm/data/importer/bodies.rb +45 -0
- data/lib/bipm/data/importer/body.rb +43 -0
- data/lib/bipm/data/importer/cgpm.rb +21 -0
- data/lib/bipm/data/importer/clauses.rb +239 -0
- data/lib/bipm/data/importer/cli.rb +83 -0
- data/lib/bipm/data/importer/common.rb +31 -120
- data/lib/bipm/data/importer/fetcher.rb +62 -0
- data/lib/bipm/data/importer/language.rb +66 -0
- data/lib/bipm/data/importer/quirks.rb +73 -0
- data/lib/bipm/data/importer/strategies/base.rb +40 -0
- data/lib/bipm/data/importer/strategies/spa_meetings.rb +340 -0
- data/lib/bipm/data/importer/strategies/static_index.rb +324 -0
- data/lib/bipm/data/importer/strategies.rb +13 -0
- data/lib/bipm/data/importer/text/french.rb +49 -0
- data/lib/bipm/data/importer/text.rb +11 -0
- data/lib/bipm/data/importer/version.rb +1 -1
- data/lib/bipm/data/importer.rb +31 -4
- data/lib/bipm/data/outcomes/action.rb +1 -1
- data/lib/bipm/data/outcomes/approval.rb +1 -1
- data/lib/bipm/data/outcomes/body.rb +1 -1
- data/lib/bipm/data/outcomes/consideration.rb +1 -1
- data/lib/bipm/data/outcomes/localized_body.rb +1 -1
- data/lib/bipm/data/outcomes/meeting.rb +2 -2
- data/lib/bipm/data/outcomes/resolution.rb +1 -1
- data/lib/bipm/data/outcomes.rb +9 -13
- data/lib/bipm-data-importer.rb +1 -1
- metadata +46 -19
- data/.hound.yml +0 -5
- data/.rspec +0 -3
- data/.rubocop.yml +0 -10
- data/exe/bipm-fetch-cgpm +0 -3
data/bipm-data-importer.gemspec
CHANGED
|
@@ -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 = ">=
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|