academic_benchmarks 0.0.10 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. checksums.yaml +5 -5
  2. data/lib/academic_benchmarks/api/auth.rb +6 -4
  3. data/lib/academic_benchmarks/api/constants.rb +2 -20
  4. data/lib/academic_benchmarks/api/handle.rb +0 -20
  5. data/lib/academic_benchmarks/api/standards.rb +127 -83
  6. data/lib/academic_benchmarks/lib/attr_to_vals.rb +7 -0
  7. data/lib/academic_benchmarks/lib/inst_vars_to_hash.rb +2 -0
  8. data/lib/academic_benchmarks/standards/authority.rb +35 -9
  9. data/lib/academic_benchmarks/standards/disciplines.rb +21 -0
  10. data/lib/academic_benchmarks/standards/document.rb +14 -6
  11. data/lib/academic_benchmarks/standards/education_levels.rb +21 -0
  12. data/lib/academic_benchmarks/standards/grade.rb +4 -5
  13. data/lib/academic_benchmarks/standards/number.rb +21 -0
  14. data/lib/academic_benchmarks/standards/publication.rb +59 -0
  15. data/lib/academic_benchmarks/standards/section.rb +23 -0
  16. data/lib/academic_benchmarks/standards/standard.rb +37 -70
  17. data/lib/academic_benchmarks/standards/standards_forest.rb +4 -38
  18. data/lib/academic_benchmarks/standards/standards_tree.rb +2 -63
  19. data/lib/academic_benchmarks/standards/statement.rb +21 -0
  20. data/lib/academic_benchmarks/standards/subject.rb +3 -4
  21. data/lib/academic_benchmarks/standards/utilizations.rb +19 -0
  22. metadata +37 -18
  23. data/lib/academic_benchmarks/lib/remove_obsolete_children.rb +0 -10
  24. data/lib/academic_benchmarks/standards/course.rb +0 -22
  25. data/lib/academic_benchmarks/standards/has_relations.rb +0 -21
  26. data/lib/academic_benchmarks/standards/parent.rb +0 -41
  27. data/lib/academic_benchmarks/standards/subject_doc.rb +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0bb4c826517fe34f15128864c5edb5d3164eafc5
4
- data.tar.gz: 5ca10259838803fcaac089aba1f1e0c40436b105
2
+ SHA256:
3
+ metadata.gz: e6ff603d8c11ac0b1800046ba7c13be13f1415482e2fa00b894a306eb44088c5
4
+ data.tar.gz: cc2fc48f7ed2ef3ee617abf3ce54d022bd706f49ca9b6ec92ee5da5492a51312
5
5
  SHA512:
6
- metadata.gz: 9fa8deeb2a15318401c7a9d49a4be8bc6848486c42e4ac906bb1e5e00c313bd01c992e09dee8b8f705c46b9fdb1966b21c360ae029dc63984368b3c2992b2466
7
- data.tar.gz: d42c89387bc9e5b5af64e7dd372f957d5e87a10ee5ecd167c11fff111cc704887dbf9a2e52085391f98e9bdb35b6c5e532a533d07e9c90ddddf80bbdda806240
6
+ metadata.gz: '0090cdf724490a82939a5f7e780c58f1de7f0118f0dc14bcbe32b901f0826312fb9dd62262a2df03c170131d80161e20ab7937e92516eb85beb7f557dfe77c7e'
7
+ data.tar.gz: bb84babbd1e4a10e5f94ec56958aa7da18515071bf66b56c7dab0012d41c4b254e382c18a4382a395f81d32aba492de5311062a02905cb724e05fe403c17f8ef
@@ -8,10 +8,12 @@ module AcademicBenchmarks
8
8
  "partner.id" => partner_id,
9
9
  "auth.signature" => signature_for(
10
10
  partner_key: partner_key,
11
- message: self.message(expires: expires, user_id: user_id)),
12
- "auth.expires" => expires,
13
- "user.id" => user_id
14
- }
11
+ message: self.message(expires: expires, user_id: user_id)
12
+ ),
13
+ "auth.expires" => expires
14
+ }.tap do |params|
15
+ params["user.id"] = user_id unless user_id.empty?
16
+ end
15
17
  end
16
18
 
17
19
  def self.signature_for(partner_key:, message:)
@@ -2,11 +2,11 @@ module AcademicBenchmarks
2
2
  module Api
3
3
  module Constants
4
4
  def self.base_url
5
- 'https://api.academicbenchmarks.com/rest/v3'
5
+ 'https://api.abconnect.certicaconnect.com/rest/v4.1'
6
6
  end
7
7
 
8
8
  def self.api_version
9
- '3'
9
+ '4.1'
10
10
  end
11
11
 
12
12
  def self.partner_id_env_var
@@ -20,24 +20,6 @@ module AcademicBenchmarks
20
20
  def self.user_id_env_var
21
21
  'ACADEMIC_BENCHMARKS_USER_ID'
22
22
  end
23
-
24
- def self.standards_search_params
25
- %w[
26
- query
27
- authority
28
- subject
29
- grade
30
- subject_doc
31
- course
32
- document
33
- parent
34
- deepest
35
- limit
36
- offset
37
- list
38
- fields
39
- ]
40
- end
41
23
  end
42
24
  end
43
25
  end
@@ -44,30 +44,10 @@ module AcademicBenchmarks
44
44
  @user_id = user_id.to_s
45
45
  end
46
46
 
47
- def related(guid:, fields: [])
48
- raise StandardError.new("Sorry, not implemented yet!")
49
- end
50
-
51
47
  def standards
52
48
  Standards.new(self)
53
49
  end
54
50
 
55
- def assets
56
- raise StandardError.new("Sorry, not implemented yet!")
57
- end
58
-
59
- def alignments
60
- raise StandardError.new("Sorry, not implemented yet!")
61
- end
62
-
63
- def topics
64
- raise StandardError.new("Sorry, not implemented yet!")
65
- end
66
-
67
- def special
68
- raise StandardError.new("Sorry, not implemented yet!")
69
- end
70
-
71
51
  private
72
52
 
73
53
  def api_resp_to_array_of_standards(api_resp)
@@ -8,92 +8,73 @@ module AcademicBenchmarks
8
8
  class Standards
9
9
  DEFAULT_PER_PAGE = 100
10
10
 
11
+ STANDARDS_FIELDS = %w[
12
+ guid
13
+ education_levels.grades.code
14
+ label
15
+ level
16
+ section.guid
17
+ section.descr
18
+ number.prefix_enhanced
19
+ status
20
+ disciplines.subjects.code
21
+ document.guid
22
+ document.descr
23
+ document.adopt_year
24
+ document.publication.descr
25
+ document.publication.guid
26
+ document.publication.authorities
27
+ statement.descr
28
+ utilizations.type
29
+ parent
30
+ ]
31
+
11
32
  def initialize(handle)
12
33
  @handle = handle
13
34
  end
14
35
 
15
- def search(opts = {})
16
- # query: "", authority: "", subject: "", grade: "", subject_doc: "", course: "",
17
- # document: "", parent: "", deepest: "", limit: -1, offset: -1, list: "", fields: []
18
- invalid_params = invalid_search_params(opts)
19
- if invalid_params.empty?
20
- raw_search(opts).map do |standard|
21
- AcademicBenchmarks::Standards::Standard.new(standard)
22
- end
23
- else
24
- raise ArgumentError.new(
25
- "Invalid search params: #{invalid_params.join(', ')}"
26
- )
27
- end
28
- end
29
-
30
- alias_method :where, :search
31
-
32
- def guid(guid, fields: [])
33
- query_params = if fields.empty?
34
- auth_query_params
35
- else
36
- auth_query_params.merge({
37
- fields: fields.join(",")
38
- })
39
- end
40
- @handle.class.get(
41
- "/standards/#{guid}",
42
- query: query_params
43
- ).parsed_response["resources"].map do |r|
44
- AcademicBenchmarks::Standards::Standard.new(r["data"])
36
+ # TODO: in the future, support OData filtering for flexible querying
37
+ def search(authority_guid: nil, publication_guid: nil)
38
+ raw_search(authority: authority_guid, publication: publication_guid).map do |standard|
39
+ AcademicBenchmarks::Standards::Standard.new(standard)
45
40
  end
46
41
  end
47
42
 
48
- def all
49
- request_search_pages_and_concat_resources(auth_query_params)
50
- end
51
-
52
- def authorities(query_params = {})
53
- raw_search({list: "authority"}.merge(query_params)).map do |a|
54
- AcademicBenchmarks::Standards::Authority.from_hash(a["data"]["authority"])
43
+ def authorities
44
+ raw_facet("document.publication.authorities").map do |a|
45
+ AcademicBenchmarks::Standards::Authority.from_hash(a["data"])
55
46
  end
56
47
  end
57
48
 
58
- def documents(query_params = {})
59
- raw_search({list: "document"}.merge(query_params)).map do |a|
60
- AcademicBenchmarks::Standards::Document.from_hash(a["data"]["document"])
49
+ def publications(authority_guid: nil)
50
+ raw_facet("document.publication", authority: authority_guid).map do |a|
51
+ AcademicBenchmarks::Standards::Publication.from_hash(a["data"])
61
52
  end
62
53
  end
63
54
 
64
- def authority_documents(authority_or_auth_code_guid_or_desc)
55
+ def authority_publications(authority_or_auth_code_guid_or_desc)
65
56
  authority = auth_from_code_guid_or_desc(authority_or_auth_code_guid_or_desc)
66
- documents(authority: authority.code)
57
+ publications(authority_guid: authority.guid)
67
58
  end
68
59
 
69
- def authority_tree(authority_or_auth_code_guid_or_desc, include_obsolete_standards: true)
60
+ def authority_tree(authority_or_auth_code_guid_or_desc, include_obsolete_standards: true, exclude_examples: false)
70
61
  authority = auth_from_code_guid_or_desc(authority_or_auth_code_guid_or_desc)
71
- auth_children = search(authority: authority.code)
62
+ auth_children = raw_search(authority: authority.guid, include_obsoletes: include_obsolete_standards, exclude_examples: exclude_examples)
72
63
  AcademicBenchmarks::Standards::StandardsForest.new(
73
- auth_children,
74
- include_obsoletes: include_obsolete_standards
64
+ auth_children
75
65
  ).consolidate_under_root(authority)
76
66
  end
77
67
 
78
- def document_tree(document_or_guid, include_obsolete_standards: true)
79
- document = doc_from_guid(document_or_guid)
80
- doc_children = search(document: document.guid)
68
+ def publication_tree(publication_or_pub_code_guid_or_desc, include_obsolete_standards: true, exclude_examples: false)
69
+ publication = pub_from_guid(publication_or_pub_code_guid_or_desc)
70
+ pub_children = raw_search(publication: publication.guid, include_obsoletes: include_obsolete_standards, exclude_examples: exclude_examples)
81
71
  AcademicBenchmarks::Standards::StandardsForest.new(
82
- doc_children,
83
- include_obsoletes: include_obsolete_standards
84
- ).consolidate_under_root(document)
72
+ pub_children
73
+ ).consolidate_under_root(publication)
85
74
  end
86
75
 
87
76
  private
88
77
 
89
- def doc_from_guid(document_or_guid)
90
- if document_or_guid.is_a?(AcademicBenchmarks::Standards::Document)
91
- document_or_guid
92
- else
93
- find_type(type: "document", data: document_or_guid)
94
- end
95
- end
96
-
97
78
  def auth_from_code_guid_or_desc(authority_or_auth_code_guid_or_desc)
98
79
  if authority_or_auth_code_guid_or_desc.is_a?(AcademicBenchmarks::Standards::Authority)
99
80
  authority_or_auth_code_guid_or_desc
@@ -102,6 +83,14 @@ module AcademicBenchmarks
102
83
  end
103
84
  end
104
85
 
86
+ def pub_from_guid(publication_or_pub_code_guid_or_desc)
87
+ if publication_or_pub_code_guid_or_desc.is_a?(AcademicBenchmarks::Standards::Publication)
88
+ publication_or_pub_code_guid_or_desc
89
+ else
90
+ find_type(type: "publication", data: publication_or_pub_code_guid_or_desc)
91
+ end
92
+ end
93
+
105
94
  def find_type(type:, data:)
106
95
  matches = send("match_#{type}", data)
107
96
  if matches.empty?
@@ -110,7 +99,7 @@ module AcademicBenchmarks
110
99
  )
111
100
  elsif matches.count > 1
112
101
  raise StandardError.new(
113
- "Authority code, guid, or description matched more than one authority. " \
102
+ "#{type.upcase} code, guid, or description matched more than one #{type}. " \
114
103
  "matched '#{matches.map(&:to_json).join('; ')}'"
115
104
  )
116
105
  end
@@ -119,22 +108,26 @@ module AcademicBenchmarks
119
108
 
120
109
  def match_authority(data)
121
110
  authorities.select do |auth|
122
- auth.code == data ||
111
+ auth.acronym == data ||
123
112
  auth.guid == data ||
124
113
  auth.descr == data
125
114
  end
126
115
  end
127
116
 
128
- def match_document(data)
129
- documents.select { |doc| doc.guid == data }
117
+ def match_publication(data)
118
+ publications.select do |pub|
119
+ pub.acronym == data ||
120
+ pub.guid == data ||
121
+ pub.descr == data
122
+ end
130
123
  end
131
124
 
132
- def raw_search(opts = {})
133
- request_search_pages_and_concat_resources(opts.merge(auth_query_params))
125
+ def raw_facet(facet, query_params = {})
126
+ request_facet({facet: facet}.merge(query_params).merge(auth_query_params))
134
127
  end
135
128
 
136
- def invalid_search_params(opts)
137
- opts.keys.map(&:to_s) - AcademicBenchmarks::Api::Constants.standards_search_params
129
+ def raw_search(opts = {})
130
+ request_search_pages_and_concat_resources(opts.merge(auth_query_params))
138
131
  end
139
132
 
140
133
  def auth_query_params
@@ -146,7 +139,49 @@ module AcademicBenchmarks
146
139
  )
147
140
  end
148
141
 
142
+ def odata_filters(query_params)
143
+ if query_params.key? :authority
144
+ value = query_params.delete :authority
145
+ query_params['filter[standards]'] = "document.publication.authorities.guid eq '#{value}'" if value
146
+ end
147
+ if query_params.key? :publication
148
+ value = query_params.delete :publication
149
+ query_params['filter[standards]'] = "document.publication.guid eq '#{value}'" if value
150
+ end
151
+
152
+ if query_params.key? :include_obsoletes
153
+ unless query_params.delete :include_obsoletes
154
+ if query_params.key? 'filter[standards]'
155
+ query_params['filter[standards]'] += " and status eq 'active'"
156
+ else
157
+ query_params['filter[standards]'] = "status eq 'active'"
158
+ end
159
+ end
160
+ end
161
+
162
+ if query_params.delete :exclude_examples
163
+ if query_params.key? 'filter[standards]'
164
+ query_params['filter[standards]'] += " and utilizations.type not eq 'example'"
165
+ else
166
+ query_params['filter[standards]'] = "utilizations.type not eq 'example'"
167
+ end
168
+ end
169
+ end
170
+
171
+ def request_facet(query_params)
172
+ odata_filters query_params
173
+ page = request_page(
174
+ query_params: query_params,
175
+ limit: 0, # return no standards since facets are separate
176
+ offset: 0
177
+ ).parsed_response
178
+
179
+ page.dig("meta", "facets", 0, "details")
180
+ end
181
+
149
182
  def request_search_pages_and_concat_resources(query_params)
183
+ query_params['fields[standards]'] = STANDARDS_FIELDS.join(',')
184
+ odata_filters query_params
150
185
  query_params.reverse_merge!({limit: DEFAULT_PER_PAGE})
151
186
 
152
187
  if !query_params[:limit] || query_params[:limit] <= 0
@@ -161,10 +196,9 @@ module AcademicBenchmarks
161
196
  offset: 0
162
197
  ).parsed_response
163
198
 
164
- resources = first_page["resources"]
165
- count = first_page["count"]
199
+ resources = first_page["data"]
200
+ count = first_page["meta"]["count"]
166
201
  offset = query_params[:limit]
167
-
168
202
  while offset < count
169
203
  page = request_page(
170
204
  query_params: query_params,
@@ -172,7 +206,7 @@ module AcademicBenchmarks
172
206
  offset: offset
173
207
  )
174
208
  offset += query_params[:limit]
175
- resources.push(page.parsed_response["resources"])
209
+ resources.push(page.parsed_response["data"])
176
210
  end
177
211
 
178
212
  resources.flatten
@@ -183,19 +217,29 @@ module AcademicBenchmarks
183
217
  limit: limit,
184
218
  offset: offset,
185
219
  })
186
- resp = @handle.class.get(
187
- '/standards',
188
- query: query_params.merge({
189
- limit: limit,
190
- offset: offset,
191
- })
192
- )
193
- if resp.code != 200
194
- raise RuntimeError.new(
195
- "Received response '#{resp.code}: #{resp.message}' requesting standards from Academic Benchmarks:"
220
+ 1.times do
221
+ resp = @handle.class.get(
222
+ '/standards',
223
+ query: query_params.merge({
224
+ limit: limit,
225
+ offset: offset,
226
+ })
196
227
  )
228
+ if resp.code == 429
229
+ sleep retry_after(resp)
230
+ redo
231
+ end
232
+ if resp.code != 200
233
+ raise RuntimeError.new(
234
+ "Received response '#{resp.code}: #{resp.message}' requesting standards from Academic Benchmarks:"
235
+ )
236
+ end
237
+ return resp
197
238
  end
198
- resp
239
+ end
240
+
241
+ def retry_after(response)
242
+ ENV['ACADEMIC_BENCHMARKS_TOO_MANY_REQUESTS_RETRY']&.to_f || 5
199
243
  end
200
244
  end
201
245
  end
@@ -0,0 +1,7 @@
1
+ module AttrToVals
2
+ def attr_to_vals(klass, arr)
3
+ return [] if arr.nil?
4
+
5
+ arr.map {|v| klass.from_hash(v)}
6
+ end
7
+ end
@@ -19,6 +19,8 @@ module InstVarsToHash
19
19
  def to_h(omit_parent: true, omit_empty_children: true)
20
20
  retval = {}
21
21
  instance_variables.each do |iv|
22
+ # Don't hashify these attributes, otherwise it can lead to infinite recursion
23
+ next if %w[@authority @document @publication @section].include? iv.to_s
22
24
  if !(skip_parent?(omit_parent, iv) || skip_children?(omit_empty_children, iv))
23
25
  retval[iv.to_s.delete('@').to_sym] = elem_to_h(instance_variable_get(iv))
24
26
  end
@@ -1,30 +1,56 @@
1
1
  require 'academic_benchmarks/lib/inst_vars_to_hash'
2
- require 'academic_benchmarks/lib/remove_obsolete_children'
3
2
 
4
3
  module AcademicBenchmarks
5
4
  module Standards
6
5
  class Authority
7
6
  include InstVarsToHash
8
- include RemoveObsoleteChildren
9
7
 
10
- attr_accessor :code, :guid, :description, :children
8
+ attr_accessor :acronym, :descr, :guid, :children
11
9
 
12
- alias_method :descr, :description
10
+ alias_method :code, :acronym
11
+ alias_method :description, :descr
13
12
 
14
13
  def self.from_hash(hash)
15
14
  self.new(
16
- code: hash["code"],
15
+ acronym: hash["acronym"],
17
16
  guid: hash["guid"],
18
- description: (hash["descr"] || hash["description"])
17
+ descr: hash["descr"]
19
18
  )
20
19
  end
21
20
 
22
- def initialize(code:, guid:, description:, children: [])
23
- @code = code
21
+ def initialize(acronym:, guid:, descr:, children: [])
22
+ @acronym = acronym
24
23
  @guid = guid
25
- @description = description
24
+ @descr = descr
26
25
  @children = children
27
26
  end
27
+
28
+ # Children are standards, so rebranch them so we have
29
+ # the following structure:
30
+ #
31
+ # Authority -> Publication -> Document -> Section -> Standard
32
+ def rebranch_children
33
+ @seen = Set.new()
34
+ @guid_to_obj = {}
35
+ new_children = []
36
+ @children.each do |child|
37
+ pub = reparent(child.document.publication, new_children)
38
+ doc = reparent(child.document, pub.children)
39
+ sec = reparent(child.section, doc.children)
40
+ sec.children.push(child)
41
+ end
42
+ @children.replace(new_children)
43
+ remove_instance_variable('@seen')
44
+ remove_instance_variable('@guid_to_obj')
45
+ end
46
+
47
+ private
48
+
49
+ def reparent(object, children)
50
+ cached_object = (@guid_to_obj[object.guid] ||= object)
51
+ children.push(cached_object) if @seen.add? cached_object.guid
52
+ cached_object
53
+ end
28
54
  end
29
55
  end
30
56
  end