academic_benchmarks 0.0.10 → 1.1.1

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 (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