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
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mechanize"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "date"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
module Bipm
|
|
10
|
+
module Data
|
|
11
|
+
module Importer
|
|
12
|
+
module Strategies
|
|
13
|
+
# Scrapes a body whose data is reachable through a static HTML
|
|
14
|
+
# listing page (one URL per language) plus per-resolution pages
|
|
15
|
+
# that the listing links to. The BIPM SPA rewrite preserved this
|
|
16
|
+
# shape for CGPM; other bodies moved to SPA-only rendering and
|
|
17
|
+
# are handled by SpaMeetings instead.
|
|
18
|
+
class StaticIndex < Base
|
|
19
|
+
def self.recording_mode
|
|
20
|
+
:live
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(agent:)
|
|
24
|
+
listings = fetch_listings(agent)
|
|
25
|
+
urls_by_lang = build_urls_by_language(listings)
|
|
26
|
+
log_url_summary(urls_by_lang)
|
|
27
|
+
|
|
28
|
+
meetings = {}
|
|
29
|
+
scrape_resolutions(agent, urls_by_lang, meetings)
|
|
30
|
+
enrich_meetings(agent, meetings)
|
|
31
|
+
write_files(meetings)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ---------- URL handling ----------
|
|
35
|
+
|
|
36
|
+
def normalize_res_url(url)
|
|
37
|
+
url.to_s.sub(%r{/(en|fr)/committees/}, "/committees/")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def extract_resolution_urls(listing_html, language)
|
|
41
|
+
doc = Nokogiri::HTML(listing_html)
|
|
42
|
+
doc.css("a").map do |link|
|
|
43
|
+
href = link["href"].to_s
|
|
44
|
+
next nil unless href =~ %r{/committees/cg/cgpm/\d+-\d+/resolution-\d+\z}
|
|
45
|
+
path = href.sub(%r{^https?://[^/]+}, "").sub(%r{^/(?:en|fr)?/?(?=committees)}, "")
|
|
46
|
+
"#{Body::BIPM_ORIGIN}/#{language}/#{path}"
|
|
47
|
+
end.compact.uniq
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ---------- Date parsing ----------
|
|
51
|
+
|
|
52
|
+
def parse_date_text(text)
|
|
53
|
+
Text::French.parse_date(text)&.iso8601
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# ---------- Clause handling ----------
|
|
57
|
+
|
|
58
|
+
def flush_clause(verb, message_parts, list_items, considerations, actions, date_str)
|
|
59
|
+
full_message = message_parts.concat(list_items).reject(&:empty?).join("\n").gsub(/\s+/, " ").strip
|
|
60
|
+
return if full_message.empty?
|
|
61
|
+
|
|
62
|
+
type, category = Clauses.lookup_verb(verb)
|
|
63
|
+
clause = { "type" => type, "date_effective" => date_str, "message" => full_message }
|
|
64
|
+
|
|
65
|
+
if category == Clauses::CATEGORY_CONSIDERATION
|
|
66
|
+
considerations << clause
|
|
67
|
+
else
|
|
68
|
+
actions << clause
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# ---------- Page parsing ----------
|
|
73
|
+
|
|
74
|
+
def parse_resolution(page, _language)
|
|
75
|
+
return nil unless page
|
|
76
|
+
doc = Nokogiri::HTML(page.body)
|
|
77
|
+
article = doc.at_css("div.journal-content-article") ||
|
|
78
|
+
doc.at_css("div.asset-entry") ||
|
|
79
|
+
doc.css("div").find { |d| d.inner_html.include?("Resolution") && d.inner_html.size > 1000 }
|
|
80
|
+
return nil unless article
|
|
81
|
+
|
|
82
|
+
h1 = article.at_css("h1")
|
|
83
|
+
h1_text = h1&.text&.strip&.gsub(/\s+/, " ")
|
|
84
|
+
h2s = article.css("h2").map { |h| h.text.strip }
|
|
85
|
+
code_h2 = h2s.find { |t| t =~ /^Resolution-CGPM-\d+-\d+\z/i }
|
|
86
|
+
subject_h2 = h2s.find { |t| t != code_h2 && t !~ /^(CGPM logo|Menu Display)$/i }
|
|
87
|
+
|
|
88
|
+
res_id = nil
|
|
89
|
+
meeting_id = nil
|
|
90
|
+
year = nil
|
|
91
|
+
if h1_text
|
|
92
|
+
en_m = h1_text.match(/(?:Resolution|Declaration|Recommendation)\s+(\d+)\s+of\s+the\s+(\d+)(?:st|nd|rd|th)?\s+CGPM(?:\s*\((\d{4})\))?/i)
|
|
93
|
+
fr_m = h1_text.match(/(?:R[ée]solution|D[ée]claration|Recommandation)\s+(\d+)\s+de la\s+(\d+)[eè]?(?:\s+CGPM)(?:\s*\((\d{4})\))?/i)
|
|
94
|
+
if (m = en_m || fr_m)
|
|
95
|
+
res_id = m[1].to_i
|
|
96
|
+
meeting_id = m[2].to_i
|
|
97
|
+
year = m[3]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
date_str = nil
|
|
102
|
+
if (date_node = doc.at_css("p.session__date"))
|
|
103
|
+
date_str = parse_date_text(date_node.text)
|
|
104
|
+
end
|
|
105
|
+
date_str ||= year ? "#{year}-01-01" : nil
|
|
106
|
+
|
|
107
|
+
reference_url = nil
|
|
108
|
+
reference_name = nil
|
|
109
|
+
reference_page = nil
|
|
110
|
+
article.css("a").each do |link|
|
|
111
|
+
text = link.text.strip
|
|
112
|
+
href = link["href"].to_s
|
|
113
|
+
next unless href.include?("/documents/")
|
|
114
|
+
next unless text =~ /Proceedings of the\s+(\d+)(?:st|nd|rd|th)?\s+CGPM/i
|
|
115
|
+
reference_name = text
|
|
116
|
+
reference_url = href
|
|
117
|
+
reference_page = Regexp.last_match(1).to_i if text =~ /p(\d+)\s*\z/
|
|
118
|
+
break
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
considerations = []
|
|
122
|
+
actions = []
|
|
123
|
+
approvals = []
|
|
124
|
+
|
|
125
|
+
preamble = article.css("p").map(&:text).find { |t| t =~ /General Conference|CGPM.*meeting/i }
|
|
126
|
+
|
|
127
|
+
current_verb = nil
|
|
128
|
+
current_message_parts = []
|
|
129
|
+
current_list_items = []
|
|
130
|
+
|
|
131
|
+
article.children.each do |node|
|
|
132
|
+
next unless node.element? || node.text?
|
|
133
|
+
next if node.name == "script" || node.name == "style"
|
|
134
|
+
|
|
135
|
+
if node.name == "p"
|
|
136
|
+
b = node.at_css("b")
|
|
137
|
+
if b
|
|
138
|
+
verb_text = Clauses.normalize_verb(b.text)
|
|
139
|
+
rest = node.dup
|
|
140
|
+
rest.at_css("b").remove
|
|
141
|
+
rest_text = rest.text.strip.gsub(/\s+/, " ")
|
|
142
|
+
if current_verb
|
|
143
|
+
flush_clause(current_verb, current_message_parts, current_list_items, considerations, actions, date_str)
|
|
144
|
+
current_message_parts = []
|
|
145
|
+
current_list_items = []
|
|
146
|
+
end
|
|
147
|
+
current_verb = verb_text
|
|
148
|
+
current_message_parts << rest_text if rest_text && !rest_text.empty?
|
|
149
|
+
elsif current_verb && (text = node.text.strip) && !text.empty?
|
|
150
|
+
current_message_parts << text
|
|
151
|
+
end
|
|
152
|
+
elsif node.name == "ul" && current_verb
|
|
153
|
+
node.css("li").each do |li|
|
|
154
|
+
current_list_items << li.text.strip.gsub(/\s+/, " ")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
flush_clause(current_verb, current_message_parts, current_list_items, considerations, actions, date_str) if current_verb
|
|
159
|
+
|
|
160
|
+
if preamble
|
|
161
|
+
approvals << { "message" => preamble.strip, "date_effective" => date_str }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
"metadata" => {
|
|
166
|
+
"title" => h1_text || "CGPM Resolution",
|
|
167
|
+
"identifier" => res_id&.to_s,
|
|
168
|
+
"date" => date_str,
|
|
169
|
+
"source" => "BIPM - Pavillon de Breteuil",
|
|
170
|
+
"url" => page.uri.to_s,
|
|
171
|
+
},
|
|
172
|
+
"resolutions" => [{
|
|
173
|
+
"dates" => [date_str].compact,
|
|
174
|
+
"subject" => "CGPM",
|
|
175
|
+
"type" => "resolution",
|
|
176
|
+
"title" => subject_h2 || h1_text,
|
|
177
|
+
"identifier" => res_id&.to_s,
|
|
178
|
+
"url" => page.uri.to_s,
|
|
179
|
+
"reference" => reference_url,
|
|
180
|
+
"reference_name" => reference_name,
|
|
181
|
+
"reference_page" => reference_page,
|
|
182
|
+
"approvals" => approvals,
|
|
183
|
+
"considerations" => considerations,
|
|
184
|
+
"actions" => actions,
|
|
185
|
+
}],
|
|
186
|
+
"_meeting_id" => meeting_id&.to_s,
|
|
187
|
+
"_year" => year,
|
|
188
|
+
}
|
|
189
|
+
rescue => e
|
|
190
|
+
warn "[static_index] parse_resolution failed for #{page&.uri}: #{e.message}"
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# ---------- Orchestration ----------
|
|
195
|
+
|
|
196
|
+
def fetch_listings(agent)
|
|
197
|
+
listings = {}
|
|
198
|
+
languages.each do |lang|
|
|
199
|
+
url = body.url(lang)
|
|
200
|
+
listings[lang] = agent.get(url)
|
|
201
|
+
puts "[static_index] #{lang.to_s.upcase} listing: #{listings[lang].body.size} bytes"
|
|
202
|
+
rescue => e
|
|
203
|
+
warn "[static_index] FAILED to fetch #{lang.to_s.upcase} listing: #{e.message}"
|
|
204
|
+
listings[lang] = nil
|
|
205
|
+
end
|
|
206
|
+
listings
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def build_urls_by_language(listings)
|
|
210
|
+
listings.each_with_object({}) do |(lang, page), h|
|
|
211
|
+
h[lang] = page ? extract_resolution_urls(page.body, lang) : []
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def log_url_summary(urls_by_lang)
|
|
216
|
+
en_urls = urls_by_lang[Language::EN] || []
|
|
217
|
+
fr_urls = urls_by_lang[Language::FR] || []
|
|
218
|
+
en_by_norm = en_urls.each_with_object({}) { |u, h| h[normalize_res_url(u)] = u }
|
|
219
|
+
fr_by_norm = fr_urls.each_with_object({}) { |u, h| h[normalize_res_url(u)] = u }
|
|
220
|
+
all_norm = (en_by_norm.keys + fr_by_norm.keys).uniq.sort
|
|
221
|
+
puts "[static_index] EN: #{en_urls.size}, FR: #{fr_urls.size}, paired: #{all_norm.size}"
|
|
222
|
+
{ en_by_norm: en_by_norm, fr_by_norm: fr_by_norm, all_norm: all_norm }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def scrape_resolutions(agent, urls_by_lang, meetings)
|
|
226
|
+
en_urls = urls_by_lang[Language::EN] || []
|
|
227
|
+
fr_urls = urls_by_lang[Language::FR] || []
|
|
228
|
+
en_by_norm = en_urls.each_with_object({}) { |u, h| h[normalize_res_url(u)] = u }
|
|
229
|
+
fr_by_norm = fr_urls.each_with_object({}) { |u, h| h[normalize_res_url(u)] = u }
|
|
230
|
+
all_norm = (en_by_norm.keys + fr_by_norm.keys).uniq.sort
|
|
231
|
+
|
|
232
|
+
all_norm.each_with_index do |norm_url, idx|
|
|
233
|
+
parsed = {}
|
|
234
|
+
[en_by_norm[norm_url], fr_by_norm[norm_url]].compact.each do |url|
|
|
235
|
+
lang = url[%r{/(fr)/committees/}, 1] || "en"
|
|
236
|
+
page = agent.get(url)
|
|
237
|
+
sleep 0.5
|
|
238
|
+
parsed[lang] = parse_resolution(page, lang)
|
|
239
|
+
next unless parsed[lang]
|
|
240
|
+
mid = parsed[lang].delete("_meeting_id")
|
|
241
|
+
year = parsed[lang].delete("_year")
|
|
242
|
+
next unless mid
|
|
243
|
+
meetings[mid] ||= { "year" => year, "resolutions" => {}, "meta" => {} }
|
|
244
|
+
meetings[mid]["year"] ||= year
|
|
245
|
+
meetings[mid]["meta"][lang] = parsed[lang]["metadata"]
|
|
246
|
+
parsed[lang]["resolutions"]&.each do |res|
|
|
247
|
+
meetings[mid]["resolutions"][lang] ||= {}
|
|
248
|
+
meetings[mid]["resolutions"][lang][res["identifier"].to_s] = res
|
|
249
|
+
end
|
|
250
|
+
rescue => e
|
|
251
|
+
warn "[static_index] fetch #{lang} #{url} failed: #{e.class}: #{e.message}"
|
|
252
|
+
end
|
|
253
|
+
puts "[static_index] (#{idx + 1}/#{all_norm.size}) #{norm_url} -> EN=#{!!parsed["en"]} FR=#{!!parsed["fr"]}"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def enrich_meetings(agent, meetings)
|
|
258
|
+
puts "[static_index] Fetching #{meetings.size} meeting session pages..."
|
|
259
|
+
meetings.each_value do |data|
|
|
260
|
+
data["resolutions"].each do |lang, res_map|
|
|
261
|
+
next if res_map.empty?
|
|
262
|
+
sample_res = res_map.values.first
|
|
263
|
+
next unless sample_res && sample_res["url"]
|
|
264
|
+
meeting_url = sample_res["url"].sub(%r{/resolution-\d+\z}, "")
|
|
265
|
+
page = agent.get(meeting_url)
|
|
266
|
+
sleep 0.3
|
|
267
|
+
doc = Nokogiri::HTML(page.body)
|
|
268
|
+
if (title_node = doc.at_css("h1.session__title, .journal-content-article h1"))
|
|
269
|
+
data["meta"][lang] ||= {}
|
|
270
|
+
data["meta"][lang]["title"] = title_node.text.strip.gsub(/\s+/, " ")
|
|
271
|
+
end
|
|
272
|
+
if (date_node = doc.at_css("p.session__date")) && (parsed_date = parse_date_text(date_node.text))
|
|
273
|
+
data["meta"][lang] ||= {}
|
|
274
|
+
data["meta"][lang]["date"] = parsed_date
|
|
275
|
+
res_map.each_value do |res|
|
|
276
|
+
res["dates"] = [parsed_date]
|
|
277
|
+
res["considerations"]&.each { |c| c["date_effective"] = parsed_date }
|
|
278
|
+
res["actions"]&.each { |x| x["date_effective"] = parsed_date }
|
|
279
|
+
res["approvals"]&.each { |x| x["date_effective"] = parsed_date }
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
rescue => e
|
|
283
|
+
warn "[static_index] meeting page fetch #{lang} #{meeting_url} failed: #{e.message}"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def write_files(meetings)
|
|
289
|
+
dir = "#{base_dir}/#{body.id}"
|
|
290
|
+
FileUtils.mkdir_p("#{dir}/meetings-en")
|
|
291
|
+
FileUtils.mkdir_p("#{dir}/meetings-fr")
|
|
292
|
+
|
|
293
|
+
written = { Language::EN => 0, Language::FR => 0 }
|
|
294
|
+
meetings.sort.each do |mid, data|
|
|
295
|
+
data["resolutions"].each do |lang, res_map|
|
|
296
|
+
lang_obj = Language.find(lang)
|
|
297
|
+
res_map = res_map.reject { |_, r| r.nil? }
|
|
298
|
+
next if res_map.empty?
|
|
299
|
+
res_list = res_map.sort_by { |rid, _| rid.to_i }.map { |_, r| r }
|
|
300
|
+
first = res_list.first
|
|
301
|
+
next unless first
|
|
302
|
+
|
|
303
|
+
meeting_meta = (data["meta"][lang] || {}).dup
|
|
304
|
+
meeting_meta["title"] ||= "CGPM meeting #{mid}"
|
|
305
|
+
meeting_meta["date"] ||= data["year"] ? "#{data["year"]}-01-01" : nil
|
|
306
|
+
meeting_meta["identifier"] = mid.to_s
|
|
307
|
+
meeting_meta["url"] = (first["url"] || meeting_meta["url"])&.sub(%r{/resolution-\d+\z}, "")
|
|
308
|
+
meeting_meta["source"] ||= "BIPM - Pavillon de Breteuil"
|
|
309
|
+
|
|
310
|
+
payload = { "metadata" => meeting_meta, "resolutions" => res_list }
|
|
311
|
+
path = "#{dir}/meetings-#{lang_obj}/meeting-#{format('%02d', mid.to_i)}.yml"
|
|
312
|
+
File.write(path, YAML.dump(payload))
|
|
313
|
+
written[lang_obj] += 1
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
total_en = meetings.values.sum { |m| (m["resolutions"]["en"] || {}).size }
|
|
318
|
+
puts "[static_index] Wrote #{written[Language::EN]} EN + #{written[Language::FR]} FR meeting files across #{meetings.size} CGPM sessions (#{total_en} EN resolutions)."
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bipm
|
|
4
|
+
module Data
|
|
5
|
+
module Importer
|
|
6
|
+
module Strategies
|
|
7
|
+
autoload :Base, "bipm/data/importer/strategies/base"
|
|
8
|
+
autoload :StaticIndex, "bipm/data/importer/strategies/static_index"
|
|
9
|
+
autoload :SpaMeetings, "bipm/data/importer/strategies/spa_meetings"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Bipm
|
|
6
|
+
module Data
|
|
7
|
+
module Importer
|
|
8
|
+
module Text
|
|
9
|
+
# French-language text utilities shared by every scraping strategy.
|
|
10
|
+
# Single source of truth for month-name translation and date parsing
|
|
11
|
+
# (previously duplicated between Common.extract_date and CGPM.parse_date_text).
|
|
12
|
+
module French
|
|
13
|
+
MONTH_TRANSLATIONS = {
|
|
14
|
+
/janvier/i => "january",
|
|
15
|
+
/f[eé]vrier/i => "february",
|
|
16
|
+
/mars/i => "march",
|
|
17
|
+
/avril/i => "april",
|
|
18
|
+
/mai/i => "may",
|
|
19
|
+
/juin/i => "june",
|
|
20
|
+
/juillet/i => "july",
|
|
21
|
+
/ao[uû]t/i => "august",
|
|
22
|
+
/septembre/i => "september",
|
|
23
|
+
/octobre/i => "october",
|
|
24
|
+
/novembre/i => "november",
|
|
25
|
+
/d[ée]cembre/i => "december",
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
RANGE_SEPARATORS = /, | to | au | – | — /.freeze
|
|
29
|
+
|
|
30
|
+
def self.translate_months(str)
|
|
31
|
+
MONTH_TRANSLATIONS.reduce(str) { |s, (fr, en)| s.gsub(fr, en) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.parse_date(str)
|
|
35
|
+
return nil if str.nil? || str.to_s.strip.empty?
|
|
36
|
+
|
|
37
|
+
translated = translate_months(str.strip.gsub(/\s+/, " "))
|
|
38
|
+
last = translated.split(RANGE_SEPARATORS).map(&:strip).reject(&:empty?).last
|
|
39
|
+
return nil unless last
|
|
40
|
+
|
|
41
|
+
Date.parse(last)
|
|
42
|
+
rescue Date::Error
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/bipm/data/importer.rb
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
# NOTE: This is the one internal require that does not use autoload.
|
|
4
|
+
# `autoload` only works for module/class constants — it cannot lazily
|
|
5
|
+
# resolve `Bipm::Data::Importer::VERSION` (a String) without eagerly
|
|
6
|
+
# defining it. version.rb is a minimal, dependency-free file that the
|
|
7
|
+
# gemspec also loads at gem-build time, before Bundler has installed
|
|
8
|
+
# mechanize/nokogiri/etc.
|
|
9
|
+
require "bipm/data/importer/version"
|
|
7
10
|
|
|
8
11
|
module Bipm
|
|
9
12
|
module Data
|
|
10
13
|
module Importer
|
|
11
14
|
class Error < StandardError; end
|
|
12
15
|
|
|
16
|
+
autoload :AsciiMath, "bipm/data/importer/asciimath"
|
|
17
|
+
autoload :Body, "bipm/data/importer/body"
|
|
18
|
+
autoload :Bodies, "bipm/data/importer/bodies"
|
|
19
|
+
autoload :CGPM, "bipm/data/importer/cgpm"
|
|
20
|
+
autoload :Clauses, "bipm/data/importer/clauses"
|
|
21
|
+
autoload :CLI, "bipm/data/importer/cli"
|
|
22
|
+
autoload :Common, "bipm/data/importer/common"
|
|
23
|
+
autoload :Fetcher, "bipm/data/importer/fetcher"
|
|
24
|
+
autoload :Language, "bipm/data/importer/language"
|
|
25
|
+
autoload :Quirks, "bipm/data/importer/quirks"
|
|
26
|
+
autoload :Strategies, "bipm/data/importer/strategies"
|
|
27
|
+
autoload :Text, "bipm/data/importer/text"
|
|
28
|
+
|
|
29
|
+
# Build a Fetcher for one body. Does not start any work — call .call on
|
|
30
|
+
# the returned object, optionally passing an agent (defaults to a fresh
|
|
31
|
+
# Mechanize instance).
|
|
32
|
+
def self.fetch(body_id, languages: Language.all, base_dir: "data")
|
|
33
|
+
Fetcher.new(body: body_id, languages: languages, base_dir: base_dir)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build a Fetcher for every known body.
|
|
37
|
+
def self.fetch_all(languages: Language.all, base_dir: "data")
|
|
38
|
+
Bodies.all.map { |b| Fetcher.new(body: b, languages: languages, base_dir: base_dir) }
|
|
39
|
+
end
|
|
13
40
|
end
|
|
14
41
|
end
|
|
15
42
|
end
|
|
@@ -19,7 +19,7 @@ module Bipm
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def document
|
|
22
|
-
YAML.load(File.open(file_path))
|
|
22
|
+
@document ||= YAML.load(File.open(file_path))
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
|
|
@@ -36,7 +36,7 @@ module Bipm
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def url
|
|
39
|
-
document["metadata"]["
|
|
39
|
+
document["metadata"]["url"]
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def pdf
|
data/lib/bipm/data/outcomes.rb
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
require_relative "outcomes/localized_body"
|
|
3
|
-
require_relative "outcomes/meeting"
|
|
1
|
+
# frozen_string_literal: true
|
|
4
2
|
|
|
5
3
|
module Bipm
|
|
6
4
|
module Data
|
|
7
5
|
module Outcomes
|
|
6
|
+
autoload :Body, "bipm/data/outcomes/body"
|
|
7
|
+
autoload :LocalizedBody, "bipm/data/outcomes/localized_body"
|
|
8
|
+
autoload :Meeting, "bipm/data/outcomes/meeting"
|
|
9
|
+
autoload :Resolution, "bipm/data/outcomes/resolution"
|
|
10
|
+
autoload :Approval, "bipm/data/outcomes/approval"
|
|
11
|
+
autoload :Action, "bipm/data/outcomes/action"
|
|
12
|
+
autoload :Consideration, "bipm/data/outcomes/consideration"
|
|
13
|
+
|
|
8
14
|
def self.file_path
|
|
9
15
|
@file_path ||= "#{__dir__}/../../../data/"
|
|
10
16
|
end
|
|
@@ -30,16 +36,6 @@ module Bipm
|
|
|
30
36
|
end
|
|
31
37
|
singleton_class.include Enumerable
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
autoload :Body, "bipm/data/outcomes/body"
|
|
35
|
-
autoload :LocalizedBody, "bipm/data/outcomes/localized_body"
|
|
36
|
-
autoload :Meeting, "bipm/data/outcomes/meeting"
|
|
37
|
-
autoload :Resolution, "bipm/data/outcomes/resolution"
|
|
38
|
-
autoload :Approval, "bipm/data/outcomes/approval"
|
|
39
|
-
autoload :Consideration, "bipm/data/outcomes/consideration"
|
|
40
|
-
autoload :Action, "bipm/data/outcomes/action"
|
|
41
|
-
|
|
42
|
-
# It may be possible, that we don't have the BIPM data loaded.
|
|
43
39
|
def self.ensure_downloaded
|
|
44
40
|
if File.exist?(file_path)
|
|
45
41
|
return
|
data/lib/bipm-data-importer.rb
CHANGED