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
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Single source of truth for the clause taxonomy used by every scraping
4
+ # strategy. Replaces the previously-duplicated CONSIDERATIONS/ACTIONS hashes
5
+ # in common.rb and the VERB_TO_TYPE/FR_VERB_TO_EN/CONSIDERATION_VERBS/
6
+ # ACTION_VERBS constants in cgpm.rb.
7
+ #
8
+ # Design principle: French and English are first-class citizens. The VERBS
9
+ # table is keyed by either language's verb form and maps directly to a
10
+ # (type, category) pair — there is no "translate FR → EN then look up"
11
+ # step. Adding a new language means adding new keys to the same table.
12
+ module Bipm
13
+ module Data
14
+ module Importer
15
+ module Clauses
16
+ CATEGORY_CONSIDERATION = :consideration
17
+ CATEGORY_ACTION = :action
18
+
19
+ # Phrase-level regex patterns. Order matters: tried top-to-bottom,
20
+ # first match wins. Two hashes preserved (rather than one merged
21
+ # table) so that consumers can ask "is this text a consideration?"
22
+ # without scanning action patterns. Patterns themselves freely mix
23
+ # English and French alternatives — no language is canonical.
24
+ CONSIDERATIONS = {
25
+ /(?:having(?: regard)?|ayant|concerne|vu la|agissant conformément|sachant|de porter)/i => "having / having regard",
26
+ /(?:noting|to note|took note|note[sd]?|taking note|takes note|constatant|constate|that|notant|notant que|note également|(?:prend|prenant) (?:acte|note))/i => "noting",
27
+ /(?:recognizing|recognizes|reconnaissant|reconnaît)/i => "recognizing",
28
+ /(?:acknowledging|admet|entendu|(?:and |)aware that|anticipa(?:ting|nt))/i => "acknowledging",
29
+ /(?:(?:further )?recall(?:ing|s|ed)|rappelant|rappelle|rappelantla)/i => "recalling / further recalling",
30
+ /(?:re-?affirm(?:ing|s)|réaffirme)/i => "reaffirming",
31
+ /(?:consid(?:er(?:ing|)|érant|ère|ers|ered|érantque|érantle)|après examen|estime|is of the opinion|examinera|en raison|by reason)/i => "considering",
32
+ /(?:The Consultative Committee for Time and Frequency \(CCTF\), at|Le Comité consultatif du temps et des fréquences \(CCTF\), à)/i => "considering",
33
+ /(?:taking into account|(prend|prenant) en considération|taking into consideration|tenant compte|envisager)/i => "taking into account",
34
+ "pursuant to" => "pursuant to",
35
+ /(?:bearing in mind)/i => "bearing in mind",
36
+ /(?:emphasizing|soulignant)/i => "emphasizing",
37
+ "concerned" => "concerned",
38
+ /(?:accept(?:s|ed|ing|e)|acceptant)/i => "accepts",
39
+ /(?:observing|observant que|r[ée]ali(?:zing|sant))/i => "observing",
40
+ /(?:referring|se référant)/i => "referring",
41
+ /(?:acting in accordance|agissant conformément|conformément)/i => "acting",
42
+ /(?:empowered by|habilité par)/i => "empowers",
43
+ }.freeze
44
+
45
+ ACTIONS = {
46
+ /(?:adopts|adopt[eé]d?|convient d'adopter)/ => "adopts",
47
+ /(?:thanks|thanked|expresse[sd](?:[ -]| its )appreciation|appréciant|pays tribute|rend hommage|remercie|support(?:ed|s))/i => "thanks / expresses-appreciation",
48
+ /(?:approu?v[eé][ds]?|approuv[ae]nt|approving|entérine|(?:It was )?agree[sd]?|soutient|exprime son accord|n'est pas d'accord|convient)/i => "approves",
49
+ /(?:d[eé]cid(?:e[ds]?|é)|ratifies?|r[eé]vised?)/i => "decides",
50
+ /(?:d[ée]clares?|d[ée]finition)/i => "declares",
51
+ /(?:The unit of length is|Supplementary units|Derived units|Principl?es|Les Délégués des États|Les v(?:œ|\u{9C})ux ou propositions)/i => "declares",
52
+ /(?:L'unité de longueur|Unités supplémentaires|Unités dérivées|(?:\*_)?New candle|(?:\*_)?New lumen|(?:A\) )?D[ée]finitions (?:of|des)|Cubic decimetre|Clarification of|Revision of)/i => "declares",
53
+ /(?:Unit of force|Définitions des|Décimètre cube|Étalons secondaires|Unité spéciale|Efficacités lumineuses|From three names|Entre les trois termes)/i => "declares",
54
+ /(?:Unité de force|(?:Joule|Watt|Volt|Ohm|Amp[eè]re|Coulomb|Farad|Henry|Weber) \(unité?|Bougie nouvelle|Lumen nouveau|announces that|annonce que)/i => "declares",
55
+ /(?:Les unités photométriques|\(A\) D[eé]finitions|The photometric units|will (?:provide|circulate|issue|identify|notify|contact|review))/i => "declares",
56
+ /(?:Appendix 1 of the|L'Annexe 1 de la|increased|a (?:examiné|préparé)|transmettra|fournira|increased|developed a document|prendra contact)/i => "declares",
57
+ /(?:Le Temps Atomique International |International Atomic Time \(TAI\) |will meet )/i => "declares",
58
+ /(?:ask[s ]|asked|souhaite|souhaiterait)/i => "asks",
59
+ /(?:(?:further |et )?invit(?:[ée][ds]?|era)|renouvelle en conséquence|convient d'inviter)/i => "invites / further invites",
60
+ /(?:resolve[sd]?)/i => "resolves",
61
+ /(?:confirms|confirmed?|confirme que|committed|s'engageant)/i => "confirms",
62
+ /(?:welcom(?:e[sd]?|ing)|accueille favorablement(?:les)?|salu(?:e|ant))/i => "welcomes",
63
+ /(?:recomm(?:ends?|ande(?:nt|)|ended)|endorsed|LISTE DES RADIATIONS|1 Radiations recommandées|LIST OF RECOMMENDED|1 Recommended radiations|aim(?:s|ing)|a pour objectif|should)/i => "recommends",
64
+ /(?:requests?|requested|demande(?:ra)?|requi[eè]r(?:en|)t|must)|l'intention d’examiner/ => "requests",
65
+ /(?:(?:is |are |)(?:to |)(?:re-?|)(?:amend|investigate|delete|help|present|develop|create|refer|add|formalise|update|collaborate|ensure|modify|prepare|look|report|consider|continue|make|bring|post|request|draw|raise|draft|circulate|arrange|provide|send|write|check|amend|forward|distribute|pursue|inform|coordinate|discuss|submit|ask|inquire|put)|will)/i => "requests",
66
+ /(?:congratulate[sd]?|félicite)/i => "congratulates",
67
+ /(?:instructs|instructed|inform[es]|intends to)/i => "instructs",
68
+ /(?:(?:strongly |)urges|prie instamment)/i => "urges",
69
+ /(?:appoints|(?:re)?appointed|granted|reconduit|commended|accorde)/i => "appoints",
70
+ /(?:donn(?:e|ées)|Pendant la période|voted|established a \w+ task group)/i => "appoints",
71
+ /(?:convient d'éablir|transfère|confie|établit|Étant donné que trois sièges|As there will be three vacancies)/i => "appoints",
72
+ /(?:La Recommandation 1 du Groupe|Recommendation 1 of the ad hoc)/i => "appoints",
73
+ /(?:élit|nomme|elected|nominated)/ => "elects",
74
+ /(?:gave the \w+ \w+ the authority|autorise|authorized)/ => "authorizes",
75
+ /(?:charged?)/ => "charges",
76
+ /(?:resolve[sd]? further)/i => "resolves further",
77
+ /(?:calls upon|draws the attention|attire l'attention|lance un appel|called upon)/i => "calls upon",
78
+ /(?:encourages?d?|espère|propose[ds]?)/i => "encourages",
79
+ /(?:affirms|reaffirming|réaffirmant|concurs)/i => "affirms / reaffirming",
80
+ /(?:states)/i => "states",
81
+ /(?:remarks|remarques)/i => "remarks",
82
+ /(?:judges)/i => "judges",
83
+ /(?:sanction(?:s|né?e))/i => "sanctions",
84
+ /(?:abrogates|abroge)/i => "abrogates",
85
+ /(?:empowers|habilite)/i => "empowers",
86
+ }.freeze
87
+
88
+ # Verb → (type, category). Symmetric across English and French —
89
+ # both languages are first-class keys, no canonical form.
90
+ VERBS = {
91
+ # consideration verbs
92
+ "having" => ["having / having regard", CATEGORY_CONSIDERATION],
93
+ "ayant" => ["having / having regard", CATEGORY_CONSIDERATION],
94
+ "vu" => ["having / having regard", CATEGORY_CONSIDERATION],
95
+ "considering" => ["considering", CATEGORY_CONSIDERATION],
96
+ "considérant" => ["considering", CATEGORY_CONSIDERATION],
97
+ "noting" => ["noting", CATEGORY_CONSIDERATION],
98
+ "notant" => ["noting", CATEGORY_CONSIDERATION],
99
+ "notes" => ["notes", CATEGORY_CONSIDERATION],
100
+ "note" => ["notes", CATEGORY_CONSIDERATION],
101
+ "recognizing" => ["recognizing", CATEGORY_CONSIDERATION],
102
+ "reconnaissant" => ["recognizing", CATEGORY_CONSIDERATION],
103
+ "reconnaît" => ["recognizing", CATEGORY_CONSIDERATION],
104
+ "reaffirming" => ["reaffirming", CATEGORY_CONSIDERATION],
105
+ "réaffirmant" => ["reaffirming", CATEGORY_CONSIDERATION],
106
+ "recalling" => ["recalling / further recalling", CATEGORY_CONSIDERATION],
107
+ "rappelant" => ["recalling / further recalling", CATEGORY_CONSIDERATION],
108
+ "rappelle" => ["recalling / further recalling", CATEGORY_CONSIDERATION],
109
+ "acknowledging" => ["acknowledging", CATEGORY_CONSIDERATION],
110
+ "taking" => ["taking into account", CATEGORY_CONSIDERATION],
111
+ "tenant" => ["taking into account", CATEGORY_CONSIDERATION],
112
+ "pursuant" => ["pursuant to", CATEGORY_CONSIDERATION],
113
+ "bearing" => ["bearing in mind", CATEGORY_CONSIDERATION],
114
+ "emphasizing" => ["emphasizing", CATEGORY_CONSIDERATION],
115
+ "soulignant" => ["emphasizing", CATEGORY_CONSIDERATION],
116
+ "concerned" => ["concerned", CATEGORY_CONSIDERATION],
117
+ "concerné" => ["concerned", CATEGORY_CONSIDERATION],
118
+ "accepts" => ["accepts", CATEGORY_CONSIDERATION],
119
+ "accepte" => ["accepts", CATEGORY_CONSIDERATION],
120
+ "observing" => ["observing", CATEGORY_CONSIDERATION],
121
+ "observant" => ["observing", CATEGORY_CONSIDERATION],
122
+ "réalise" => ["observing", CATEGORY_CONSIDERATION],
123
+ "referring" => ["referring", CATEGORY_CONSIDERATION],
124
+ "se référant" => ["referring", CATEGORY_CONSIDERATION],
125
+ "acting" => ["acting", CATEGORY_CONSIDERATION],
126
+ "agissant" => ["acting", CATEGORY_CONSIDERATION],
127
+ "empowers" => ["empowers", CATEGORY_CONSIDERATION],
128
+ "habilité" => ["empowers", CATEGORY_CONSIDERATION],
129
+ "habilite" => ["empowers", CATEGORY_CONSIDERATION],
130
+ # action verbs
131
+ "decides" => ["decides", CATEGORY_ACTION],
132
+ "décide" => ["decides", CATEGORY_ACTION],
133
+ "declares" => ["declares", CATEGORY_ACTION],
134
+ "déclare" => ["declares", CATEGORY_ACTION],
135
+ "invites" => ["invites / further invites", CATEGORY_ACTION],
136
+ "invite" => ["invites / further invites", CATEGORY_ACTION],
137
+ "resolves" => ["resolves", CATEGORY_ACTION],
138
+ "résout" => ["resolves", CATEGORY_ACTION],
139
+ "confirms" => ["confirms", CATEGORY_ACTION],
140
+ "confirme" => ["confirms", CATEGORY_ACTION],
141
+ "welcomes" => ["welcomes", CATEGORY_ACTION],
142
+ "accueille" => ["welcomes", CATEGORY_ACTION],
143
+ "recommends" => ["recommends", CATEGORY_ACTION],
144
+ "recommande" => ["recommends", CATEGORY_ACTION],
145
+ "requests" => ["requests", CATEGORY_ACTION],
146
+ "demande" => ["requests", CATEGORY_ACTION],
147
+ "appoints" => ["appoints", CATEGORY_ACTION],
148
+ "nomme" => ["appoints", CATEGORY_ACTION],
149
+ "encourages" => ["encourages", CATEGORY_ACTION],
150
+ "encourage" => ["encourages", CATEGORY_ACTION],
151
+ "affirms" => ["affirms / reaffirming", CATEGORY_ACTION],
152
+ "affirme" => ["affirms / reaffirming", CATEGORY_ACTION],
153
+ "calls" => ["calls upon", CATEGORY_ACTION],
154
+ "lance un appel" => ["calls upon", CATEGORY_ACTION],
155
+ "states" => ["states", CATEGORY_ACTION],
156
+ "indique" => ["states", CATEGORY_ACTION],
157
+ "remarks" => ["remarks", CATEGORY_ACTION],
158
+ "remarques" => ["remarks", CATEGORY_ACTION],
159
+ "urges" => ["urges", CATEGORY_ACTION],
160
+ "prie" => ["urges", CATEGORY_ACTION],
161
+ "instructs" => ["instructs", CATEGORY_ACTION],
162
+ "informe" => ["instructs", CATEGORY_ACTION],
163
+ "adopts" => ["adopts", CATEGORY_ACTION],
164
+ "adopte" => ["adopts", CATEGORY_ACTION],
165
+ "thanks" => ["thanks / expresses-appreciation", CATEGORY_ACTION],
166
+ "remercie" => ["thanks / expresses-appreciation", CATEGORY_ACTION],
167
+ "approves" => ["approves", CATEGORY_ACTION],
168
+ "approuve" => ["approves", CATEGORY_ACTION],
169
+ "asks" => ["asks", CATEGORY_ACTION],
170
+ "souhaite" => ["asks", CATEGORY_ACTION],
171
+ "congratulates" => ["congratulates", CATEGORY_ACTION],
172
+ "félicite" => ["congratulates", CATEGORY_ACTION],
173
+ "elects" => ["elects", CATEGORY_ACTION],
174
+ "élit" => ["elects", CATEGORY_ACTION],
175
+ "authorizes" => ["authorizes", CATEGORY_ACTION],
176
+ "autorise" => ["authorizes", CATEGORY_ACTION],
177
+ "charges" => ["charges", CATEGORY_ACTION],
178
+ "judges" => ["judges", CATEGORY_ACTION],
179
+ "juge" => ["judges", CATEGORY_ACTION],
180
+ "sanctions" => ["sanctions", CATEGORY_ACTION],
181
+ "sanctionne" => ["sanctions", CATEGORY_ACTION],
182
+ "abrogates" => ["abrogates", CATEGORY_ACTION],
183
+ "abroge" => ["abrogates", CATEGORY_ACTION],
184
+ }.freeze
185
+
186
+ CONSIDERATION_TYPES = CONSIDERATIONS.values.uniq.freeze
187
+ ACTION_TYPES = ACTIONS.values.uniq.freeze
188
+
189
+ PREFIX1 = /(?:The|Le) CIPM |La Conférence |M. Volterra |M. le Président |unanimously |would |a |sont |will |were |did not |strongly |(?:La|The) (?:\d+(?:e|th)|Quinzième) Conférence Générale des Poids et Mesures(?: a |,\s+)?/i
190
+ PREFIX2 = /The \d+th Conférence Générale des Poids et Mesures |The Conference |and |et (?:en |)|has |renouvelle sa |renews its |further |and further |En ce qui |après avoir |\.\.\.\n+\t*/i
191
+ PREFIX3 = /Sur la proposition de M. le Président, la convocation de cette Conférence de Thermométrie est |Le texte corrigé, finalement |(?:The|Le) Comité International(?: des Poids et Mesures)?(?: \(CIPM\))?(?: a |,)?\s*/i
192
+ PREFIX4 = /(?:The |Le |)(?:JCRB|JCGM|CCU|CCTF|CCT|CCRI|CCPR|CCQM|CCM|CCL|CCEM|CCAUV|KCDB),? (?:also |)|Each RMO |fully |The JCRB Rules of Procedure are |Bob Watters and Claudine Thomas /
193
+ PREFIX5 = /(?:The |Le |All |)(?:incoming |)(?:JCRB |KCDB |)(?:documents|(?:Consultative |)Committees?|Office|Chairman(?: and Secretary|)|Joint BIPM[\/-]ILAC Working Group(?: \(see Action 22\))|RMO(?:[- ]JCRB|) Representatives(?: to the JRCB|)|(?:BIPM |)Director(?: of BIPM|)|SIM|(?:Exec(?:utive|) |)Secretary(?:\(ies\)|)|RMOs, except SIM,|RMOs|APMP|\(?(?:[MD]r|Prof) [A-Z][a-zR-]+\)?|CMCs|EUR[AO]MET|COOMET|GULFMET) |It was /
194
+ PREFIX6 = /“|"|« à |All RMO documents related to review procedures |Mr Lam and Dr Kühne |The Prof. Kühne, Mr Jones and the Executive Secretary |Ajchara Charoensook, from APMP, /
195
+
196
+ PREFIX = /(?:#{PREFIX1}|#{PREFIX2}|#{PREFIX3}|#{PREFIX4}|#{PREFIX5}|#{PREFIX6})?/i
197
+
198
+ SUFFIX = / (?:that|que)\b|(?: (?:the |that |le |que les )?((?:[A-Z]|national|laboratoires).{0,80}?)(?: to)?\b|)/
199
+
200
+ LEADING_CONJUNCTIONS = /\A(?:et|and|further|de plus|puis|also)\s+/.freeze
201
+ TRAILING_PUNCTUATION = /[.:,;]\z/.freeze
202
+
203
+ # Strip leading conjunctions and trailing punctuation from a verb token.
204
+ # Returns the verb in whichever language it was supplied — does NOT
205
+ # translate between French and English. Use lookup_verb to find the
206
+ # canonical (type, category) for either language's verb.
207
+ def self.normalize_verb(raw)
208
+ raw.to_s.downcase.strip
209
+ .sub(TRAILING_PUNCTUATION, "")
210
+ .strip
211
+ .sub(LEADING_CONJUNCTIONS, "")
212
+ .strip
213
+ end
214
+
215
+ # Lookup a normalized verb's (type, category). Verb may be in either
216
+ # English or French — VERBS table has both as first-class keys.
217
+ def self.lookup_verb(verb)
218
+ VERBS.fetch(verb) { [fallback_type(verb), fallback_category(verb)] }
219
+ end
220
+
221
+ def self.category_for_type(type)
222
+ return CATEGORY_CONSIDERATION if CONSIDERATION_TYPES.include?(type)
223
+ return CATEGORY_ACTION if ACTION_TYPES.include?(type)
224
+ CATEGORY_ACTION
225
+ end
226
+
227
+ def self.fallback_type(verb)
228
+ return "considering" if verb == "having" || verb == "ayant" || verb == "vu"
229
+ "decides"
230
+ end
231
+
232
+ def self.fallback_category(verb)
233
+ return CATEGORY_CONSIDERATION if verb == "having" || verb == "ayant" || verb == "vu"
234
+ CATEGORY_ACTION
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Bipm
6
+ module Data
7
+ module Importer
8
+ # Argument parsing and dispatch. The exe/bipm-fetch shim delegates here
9
+ # so the executable stays a one-liner and the CLI is fully testable.
10
+ class CLI
11
+ Options = Struct.new(:body_id, :languages, :base_dir, :fork)
12
+
13
+ def initialize(argv)
14
+ @argv = argv
15
+ end
16
+
17
+ def run
18
+ opts = parse(@argv)
19
+ if opts.body_id
20
+ run_one(opts)
21
+ else
22
+ run_all(opts)
23
+ end
24
+ 0
25
+ rescue Bodies::UnknownBodyError => e
26
+ warn "error: #{e.message}"
27
+ 2
28
+ rescue => e
29
+ warn "fatal: #{e.class}: #{e.message}"
30
+ warn e.backtrace.first(5).join("\n")
31
+ 1
32
+ end
33
+
34
+ def parse(argv)
35
+ opts = Options.new(nil, nil, "data", false)
36
+ parser = OptionParser.new do |p|
37
+ p.banner = "Usage: bipm-fetch [options]"
38
+ p.on("--body=ID", "Fetch a single body (e.g. cgpm, cipm)") { |v| opts.body_id = v.to_sym }
39
+ p.on("--language=LANG", "Restrict to one language (en or fr)") do |v|
40
+ opts.languages = [Language.find(v)]
41
+ end
42
+ p.on("--base-dir=DIR", "Output directory (default: data)") { |v| opts.base_dir = v }
43
+ p.on("--fork", "Fork one process per body") { opts.fork = true }
44
+ end
45
+ parser.parse!(argv.dup)
46
+ opts
47
+ end
48
+
49
+ private
50
+
51
+ def run_one(opts)
52
+ fetcher = Fetcher.new(
53
+ body: opts.body_id,
54
+ languages: opts.languages || Language.all,
55
+ base_dir: opts.base_dir,
56
+ )
57
+ fetcher.call
58
+ end
59
+
60
+ def run_all(opts)
61
+ languages = opts.languages || Language.all
62
+ fetchers = Bodies.all.map do |body|
63
+ Fetcher.new(body: body, languages: languages, base_dir: opts.base_dir)
64
+ end
65
+ if opts.fork
66
+ run_forked(fetchers)
67
+ else
68
+ fetchers.each(&:call)
69
+ end
70
+ end
71
+
72
+ def run_forked(fetchers)
73
+ pids = []
74
+ fetchers.each do |f|
75
+ pid = fork { f.call; exit! }
76
+ pids << pid if pid
77
+ end
78
+ pids.each { |pid| Process.wait(pid) }
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,93 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "mechanize"
4
+ require "nokogiri"
2
5
  require "coradoc/input/html"
3
6
  require "vcr"
4
7
  require "date"
5
8
  require "fileutils"
6
- require "pry"
7
- require_relative "asciimath"
8
9
 
9
10
  VCR.configure do |c|
10
11
  c.cassette_library_dir = __dir__ + "/../../../../cassettes"
11
12
  c.hook_into :webmock
13
+ # Default: replay existing cassettes (deterministic, no network).
14
+ # Set BIPM_VCR_RECORD=all to overwrite cassettes with fresh responses
15
+ # from bipm.org; set BIPM_VCR_RECORD=new_episodes to record only URLs
16
+ # not already in a cassette. VCR's :once (default) raises if a request
17
+ # is missing from its cassette.
18
+ if ENV["BIPM_VCR_RECORD"]
19
+ c.default_cassette_options = { record: ENV["BIPM_VCR_RECORD"].to_sym }
20
+ end
12
21
  end
13
22
 
14
23
  module Bipm
15
24
  module Data
16
25
  module Importer
17
- CONSIDERATIONS = {
18
- /(?:having(?: regard)?|ayant|concerne|vu la|agissant conformément|sachant|de porter)/i => "having / having regard",
19
- /(?:noting|to note|took note|note[sd]?|taking note|takes note|constatant|constate|that|notant|notant que|note également|(?:prend|prenant) (?:acte|note))/i => "noting",
20
- /(?:recognizing|recognizes|reconnaissant|reconnaît)/i => "recognizing",
21
- /(?:acknowledging|admet|entendu|(?:and |)aware that|anticipa(?:ting|nt))/i => "acknowledging",
22
- /(?:(?:further )?recall(?:ing|s|ed)|rappelant|rappelle|rappelantla)/i => "recalling / further recalling",
23
- /(?:re-?affirm(?:ing|s)|réaffirme)/i => "reaffirming",
24
- /(?:consid(?:er(?:ing|)|érant|ère|ers|ered|érantque|érantle)|après examen|estime|is of the opinion|examinera|en raison|by reason)/i => "considering",
25
- /(?:The Consultative Committee for Time and Frequency \(CCTF\), at|Le Comité consultatif du temps et des fréquences \(CCTF\), à)/i => "considering",
26
- /(?:taking into account|(prend|prenant) en considération|taking into consideration|tenant compte|envisager)/i => "taking into account",
27
- "pursuant to" => "pursuant to",
28
- /(?:bearing in mind)/i => "bearing in mind",
29
- /(?:emphasizing|soulignant)/i => "emphasizing",
30
- "concerned" => "concerned",
31
- /(?:accept(?:s|ed|ing|e)|acceptant)/i => "accepts",
32
- /(?:observing|observant que|r[ée]ali(?:zing|sant))/i => "observing",
33
- /(?:referring|se référant)/i => "referring",
34
- /(?:acting in accordance|agissant conformément|conformément)/i => "acting",
35
- /(?:empowered by|habilité par)/i => "empowers",
36
- }
37
-
38
- ACTIONS = {
39
- /(?:adopts|adopt[eé]d?|convient d'adopter)/ => "adopts",
40
- /(?:thanks|thanked|expresse[sd](?:[ -]| its )appreciation|appréciant|pays tribute|rend hommage|remercie|support(?:ed|s))/i => "thanks / expresses-appreciation",
41
- /(?:approu?v[eé][ds]?|approuv[ae]nt|approving|entérine|(?:It was )?agree[sd]?|soutient|exprime son accord|n'est pas d'accord|convient)/i => "approves",
42
- /(?:d[eé]cid(?:e[ds]?|é)|ratifies?|r[eé]vised?)/i => "decides",
43
- /(?:d[ée]clares?|d[ée]finition)/i => "declares",
44
- /(?:The unit of length is|Supplementary units|Derived units|Principl?es|Les Délégués des États|Les v(?:œ|\u{9C})ux ou propositions)/i => "declares",
45
- /(?:L'unité de longueur|Unités supplémentaires|Unités dérivées|(?:\*_)?New candle|(?:\*_)?New lumen|(?:A\) )?D[ée]finitions (?:of|des)|Cubic decimetre|Clarification of|Revision of)/i => "declares",
46
- /(?:Unit of force|Définitions des|Décimètre cube|Étalons secondaires|Unité spéciale|Efficacités lumineuses|From three names|Entre les trois termes)/i => "declares",
47
- /(?:Unité de force|(?:Joule|Watt|Volt|Ohm|Amp[eè]re|Coulomb|Farad|Henry|Weber) \(unité?|Bougie nouvelle|Lumen nouveau|announces that|annonce que)/i => "declares",
48
- /(?:Les unités photométriques|\(A\) D[eé]finitions|The photometric units|will (?:provide|circulate|issue|identify|notify|contact|review))/i => "declares",
49
- /(?:Appendix 1 of the|L'Annexe 1 de la|increased|a (?:examiné|préparé)|transmettra|fournira|increased|developed a document|prendra contact)/i => "declares",
50
- /(?:Le Temps Atomique International |International Atomic Time \(TAI\) |will meet )/i => "declares",
51
- /(?:ask[s ]|asked|souhaite|souhaiterait)/i => "asks",
52
- /(?:(?:further |et )?invit(?:[ée][ds]?|era)|renouvelle en conséquence|convient d'inviter)/i => "invites / further invites",
53
- /(?:resolve[sd]?)/i => "resolves",
54
- /(?:confirms|confirmed?|confirme que|committed|s'engageant)/i => "confirms",
55
- /(?:welcom(?:e[sd]?|ing)|accueille favorablement(?:les)?|salu(?:e|ant))/i => "welcomes",
56
- /(?:recomm(?:ends?|ande(?:nt|)|ended)|endorsed|LISTE DES RADIATIONS|1 Radiations recommandées|LIST OF RECOMMENDED|1 Recommended radiations|aim(?:s|ing)|a pour objectif|should)/i => "recommends",
57
- /(?:requests?|requested|demande(?:ra)?|requi[eè]r(?:en|)t|must)|l'intention d’examiner/ => "requests",
58
- /(?:(?:is |are |)(?:to |)(?:re-?|)(?:amend|investigate|delete|help|present|develop|create|refer|add|formalise|update|collaborate|ensure|modify|prepare|look|report|consider|continue|make|bring|post|request|draw|raise|draft|circulate|arrange|provide|send|write|check|amend|forward|distribute|pursue|inform|coordinate|discuss|submit|ask|inquire|put)|will)/i => "requests",
59
- /(?:congratulate[sd]?|félicite)/i => "congratulates",
60
- /(?:instructs|instructed|inform[es]|intends to)/i => "instructs",
61
- /(?:(?:strongly |)urges|prie instamment)/i => "urges",
62
- /(?:appoints|(?:re)?appointed|granted|reconduit|commended|accorde)/i => "appoints",
63
- /(?:donn(?:e|ées)|Pendant la période|voted|established a \w+ task group)/i => "appoints",
64
- /(?:convient d'éablir|transfère|confie|établit|Étant donné que trois sièges|As there will be three vacancies)/i => "appoints",
65
- /(?:La Recommandation 1 du Groupe|Recommendation 1 of the ad hoc)/i => "appoints",
66
- /(?:élit|nomme|elected|nominated)/ => "elects",
67
- /(?:gave the \w+ \w+ the authority|autorise|authorized)/ => "authorizes",
68
- /(?:charged?)/ => "charges",
69
- /(?:resolve[sd]? further)/i => "resolves further",
70
- /(?:calls upon|draws the attention|attire l'attention|lance un appel|called upon)/i => "calls upon",
71
- /(?:encourages?d?|espère|propose[ds]?)/i => "encourages",
72
- /(?:affirms|reaffirming|réaffirmant|concurs)/i => "affirms / reaffirming",
73
- /(?:states)/i => "states",
74
- /(?:remarks|remarques)/i => "remarks",
75
- /(?:judges)/i => "judges",
76
- /(?:sanction(?:s|né?e))/i => "sanctions",
77
- /(?:abrogates|abroge)/i => "abrogates",
78
- /(?:empowers|habilite)/i => "empowers",
79
- }
80
-
81
- PREFIX1 = /(?:The|Le) CIPM |La Conférence |M. Volterra |M. le Président |unanimously |would |a |sont |will |were |did not |strongly |(?:La|The) (?:\d+(?:e|th)|Quinzième) Conférence Générale des Poids et Mesures(?: a |,\s+)?/i
82
- PREFIX2 = /The \d+th Conférence Générale des Poids et Mesures |The Conference |and |et (?:en |)|has |renouvelle sa |renews its |further |and further |En ce qui |après avoir |\.\.\.\n+\t*/i
83
- PREFIX3 = /Sur la proposition de M. le Président, la convocation de cette Conférence de Thermométrie est |Le texte corrigé, finalement |(?:The|Le) Comité International(?: des Poids et Mesures)?(?: \(CIPM\))?(?: a |,)?\s*/i
84
- PREFIX4 = /(?:The |Le |)(?:JCRB|JCGM|CCU|CCTF|CCT|CCRI|CCPR|CCQM|CCM|CCL|CCEM|CCAUV|KCDB),? (?:also |)|Each RMO |fully |The JCRB Rules of Procedure are |Bob Watters and Claudine Thomas /
85
- PREFIX5 = /(?:The |Le |All |)(?:incoming |)(?:JCRB |KCDB |)(?:documents|(?:Consultative |)Committees?|Office|Chairman(?: and Secretary|)|Joint BIPM[\/-]ILAC Working Group(?: \(see Action 22\))|RMO(?:[- ]JCRB|) Representatives(?: to the JRCB|)|(?:BIPM |)Director(?: of BIPM|)|SIM|(?:Exec(?:utive|) |)Secretary(?:\(ies\)|)|RMOs, except SIM,|RMOs|APMP|\(?(?:[MD]r|Prof) [A-Z][a-zR-]+\)?|CMCs|EUR[AO]MET|COOMET|GULFMET) |It was /
86
- PREFIX6 = /“|"|« à |All RMO documents related to review procedures |Mr Lam and Dr Kühne |The Prof. Kühne, Mr Jones and the Executive Secretary |Ajchara Charoensook, from APMP, /
87
-
88
- PREFIX = /(?:#{PREFIX1}|#{PREFIX2}|#{PREFIX3}|#{PREFIX4}|#{PREFIX5}|#{PREFIX6})?/i
89
-
90
- SUFFIX = / (?:that|que)\b|(?: (?:the |that |le |que les )?((?:[A-Z]|national|laboratoires).{0,80}?)(?: to)?\b|)/
26
+ # @deprecated use Clauses::CONSIDERATIONS / Clauses::ACTIONS directly.
27
+ CONSIDERATIONS = Clauses::CONSIDERATIONS
28
+ # @deprecated use Clauses::ACTIONS directly.
29
+ ACTIONS = Clauses::ACTIONS
30
+ # @deprecated use Clauses::PREFIX directly.
31
+ PREFIX = Clauses::PREFIX
32
+ # @deprecated use Clauses::SUFFIX directly.
33
+ SUFFIX = Clauses::SUFFIX
91
34
 
92
35
  DOIREGEX = %r'\s+<p>\s+<b>DOI :</b> (.*?)\s+</p>\n\n'
93
36
 
@@ -95,16 +38,7 @@ module Bipm
95
38
  def replace_links(ps, res, lang)
96
39
  ps.css("a[href]").each do |a|
97
40
  href = a.attr("href")
98
-
99
- href = href.gsub(%r'\Ahttps://www\.bipm\.org/', "")
100
-
101
- # Correct links
102
- href = href.gsub("/web/guest/", "/#{lang}/")
103
-
104
- # Account for some mistakes from an upstream document
105
- href = href.gsub(%r"\A/jen/", "/en/")
106
- href = href.gsub(%r"\A/en/CGPM/jsp/", "/en/CGPM/db/")
107
-
41
+ href = Quirks.fix_href(href, language: lang)
108
42
  href = case href
109
43
  when %r'\A/(\w{2})/CGPM/db/(\d+)/(\d+)/(#.*)?\z',
110
44
  %r'\A/jsp/(\w{2})/ViewCGPMResolution\.jsp\?CGPM=(\d+)&RES=(\d+)(#.*)?\z',
@@ -165,7 +99,7 @@ module Bipm
165
99
 
166
100
  def format_message(part)
167
101
  AsciiMath.asciidoc_extract_math(
168
- Coradoc::Input::HTML.convert(part).strip.gsub("&nbsp;", " ").gsub(" \n", "\n")
102
+ Coradoc::Input::Html.convert(part).strip.gsub("&nbsp;", " ").gsub(" \n", "\n")
169
103
  )
170
104
  rescue
171
105
  warn "Bug in Coradoc, couldn't parse the following document:"
@@ -195,7 +129,7 @@ module Bipm
195
129
 
196
130
  refs = ng.css(".publication-card_reference a")
197
131
 
198
- if rec_type.end_with? "?"
132
+ if rec_type&.end_with?("?")
199
133
  rec_type = case supertitle
200
134
  when /\AD[eé]claration/
201
135
  "statement"
@@ -242,8 +176,6 @@ module Bipm
242
176
 
243
177
  ps = ng.css("div.journal-content-article").first
244
178
 
245
- #binding.pry if ps.count != 1
246
-
247
179
  # Replace links
248
180
  Common.replace_links(ps, res, lang)
249
181
 
@@ -251,7 +183,6 @@ module Bipm
251
183
  Common.replace_centers(ps)
252
184
 
253
185
  doc = Common.ng_to_string(ps)
254
- # doc = AsciiMath.html_to_asciimath(doc)
255
186
 
256
187
  if doc.match? DOIREGEX
257
188
  doc = doc.sub(DOIREGEX, "")
@@ -284,8 +215,8 @@ module Bipm
284
215
  next
285
216
  end
286
217
 
287
- CONSIDERATIONS.any? do |k, v|
288
- if parse =~ /\A#{PREFIX}#{k}\b/i
218
+ Clauses::CONSIDERATIONS.any? do |k, v|
219
+ if parse =~ /\A#{Clauses::PREFIX}#{k}\b/i
289
220
  r["considerations"] << prev = {
290
221
  "type" => v,
291
222
  "date_effective" => date.to_s,
@@ -294,8 +225,8 @@ module Bipm
294
225
  end
295
226
  end && next
296
227
 
297
- ACTIONS.any? do |k, v|
298
- if parse =~ /\A#{PREFIX}#{k}\b/i
228
+ Clauses::ACTIONS.any? do |k, v|
229
+ if parse =~ /\A#{Clauses::PREFIX}#{k}\b/i
299
230
  r["actions"] << prev = {
300
231
  "type" => v,
301
232
  "date_effective" => date.to_s,
@@ -321,17 +252,17 @@ module Bipm
321
252
  next if parse =~ /\A(|\[Cliquer ici\]|Click here|\.\.\.)\z/
322
253
 
323
254
  r["x-unparsed"] ||= []
324
- r["x-unparsed"] << parse #ReverseAdoc.convert(part).strip
255
+ r["x-unparsed"] << parse
325
256
  end
326
257
 
327
258
  %w[considerations actions].each do |type|
328
- map = type == "actions" ? ACTIONS : CONSIDERATIONS
259
+ map = type == "actions" ? Clauses::ACTIONS : Clauses::CONSIDERATIONS
329
260
  r[type] = r[type].map do |i|
330
261
  islist = false
331
262
 
332
263
  kk = nil
333
264
 
334
- if map.any? { |k, v| (i["message"].split("\n").first =~ /\A\s*([*_]?)(#{PREFIX}#{k})\1?(#{SUFFIX})\1?\s*\z/i) && (kk = k) }
265
+ if map.any? { |k, v| (i["message"].split("\n").first =~ /\A\s*([*_]?)(#{Clauses::PREFIX}#{k})\1?(#{Clauses::SUFFIX})\1?\s*\z/i) && (kk = k) }
335
266
  prefix = $2
336
267
  suffix = $3
337
268
  subject = $4
@@ -340,7 +271,7 @@ module Bipm
340
271
  listitems = []
341
272
  if (i["message"].split(/(?<!\+)\n(?!\+)/).all? { |j|
342
273
  case j
343
- when /\A\s*[*_]?#{PREFIX}#{kk}/i
274
+ when /\A\s*[*_]?#{Clauses::PREFIX}#{kk}/i
344
275
  true
345
276
  when /\A\s*\z/
346
277
  true
@@ -357,7 +288,6 @@ module Bipm
357
288
  end
358
289
 
359
290
  if subject
360
- #p subject
361
291
  r["subject"] ||= []
362
292
  r["subject"] << subject
363
293
  end
@@ -400,26 +330,7 @@ module Bipm
400
330
  end
401
331
 
402
332
  def extract_date(date_str)
403
- return nil unless date_str
404
-
405
- date = date_str.strip
406
- .gsub(/\s+/, " ")
407
- .gsub("février", "february") # 3 first letters must match English
408
- .gsub("juin", "june")
409
- .gsub("avril", "april")
410
- .gsub("mai", "may")
411
- .gsub("juillet", "july")
412
- .gsub(/ao[uû]t/, "august")
413
- .gsub("décembre", "december")
414
- .split(/, | to | au /) # Get last date
415
- .last
416
- date = Date.parse(date)
417
-
418
- binding.pry if date <= Date.parse("0000-01-01")
419
-
420
- date
421
- rescue Date::Error
422
- binding.pry
333
+ Text::French.parse_date(date_str)
423
334
  end
424
335
 
425
336
  extend self
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mechanize"
4
+
5
+ module Bipm
6
+ module Data
7
+ module Importer
8
+ # Orchestrates a single body's scrape. Picks the strategy declared
9
+ # by the Body, configures VCR/WebMock for the strategy's recording
10
+ # mode, then delegates the actual work. The CLI never touches VCR.
11
+ class Fetcher
12
+ attr_reader :body, :languages, :base_dir
13
+
14
+ def initialize(body:, languages: Language.all, base_dir: "data")
15
+ @body = body.is_a?(Body) ? body : Bodies.find(body)
16
+ @languages = Array(languages).map { |l| l.is_a?(Language) ? l : Language.find(l) }
17
+ @base_dir = base_dir
18
+ end
19
+
20
+ def call(agent: Mechanize.new)
21
+ strategy = strategy_class.new(body: body, base_dir: base_dir, languages: languages)
22
+ with_recording_mode(strategy.recording_mode) do
23
+ strategy.call(agent: agent)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def strategy_class
30
+ case body.strategy
31
+ when :static_index then Strategies::StaticIndex
32
+ when :spa_meetings then Strategies::SpaMeetings
33
+ else raise ArgumentError, "unknown strategy #{body.strategy.inspect} for body #{body.id.inspect}"
34
+ end
35
+ end
36
+
37
+ def with_recording_mode(mode)
38
+ return yield if mode == :cassette
39
+
40
+ disable_vcr_and_webmock!
41
+ yield
42
+ end
43
+
44
+ def disable_vcr_and_webmock!
45
+ begin
46
+ require "vcr"
47
+ VCR.eject_cassette
48
+ VCR.turn_off!(ignore_cassettes: true)
49
+ rescue LoadError, VCR::Errors::Error
50
+ # VCR not loaded or already turned off.
51
+ end
52
+ begin
53
+ require "webmock"
54
+ WebMock.disable!
55
+ rescue LoadError
56
+ # webmock not loaded.
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end