academic_benchmarks 0.0.8 → 1.0.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 +7 -5
  3. data/lib/academic_benchmarks/api/constants.rb +2 -20
  4. data/lib/academic_benchmarks/api/handle.rb +2 -22
  5. data/lib/academic_benchmarks/api/standards.rb +130 -85
  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 +36 -10
  9. data/lib/academic_benchmarks/standards/disciplines.rb +21 -0
  10. data/lib/academic_benchmarks/standards/document.rb +15 -7
  11. data/lib/academic_benchmarks/standards/education_levels.rb +21 -0
  12. data/lib/academic_benchmarks/standards/grade.rb +5 -6
  13. data/lib/academic_benchmarks/standards/number.rb +23 -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 +38 -71
  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 +4 -5
  21. data/lib/academic_benchmarks/standards/utilizations.rb +19 -0
  22. metadata +52 -20
  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: 22555b94fb0f906a0e7dcfa8b0d1ef53c5d0e2ba
4
- data.tar.gz: 35b214fa68304abfee8dad1a398080de77ef2775
2
+ SHA256:
3
+ metadata.gz: 50df986c030289fb9f248460b600eebc30b059c7386fa6c5cdeba4d39bbe714a
4
+ data.tar.gz: 4421c98acc7fbe617d2a8b1b3a8c9a4eb736d7ea65016c4ba5933a52b6f28d35
5
5
  SHA512:
6
- metadata.gz: aacbcd26798eb35a8b6bad99bcf3ceb946d34a7690cf99adc3735617fa75845c9bc0084d9b71ea55dc42e0916400ed5644c20a3fc3bbf62d377f52ef73d935c7
7
- data.tar.gz: 71662373e163044b7afa10977d7e71b9956c3e6be76d60e96c7d493e2e4dc70dd2f611bb33b2c68af88c0818faaf7189e2f260f3f91584cee660add9866728f7
6
+ metadata.gz: 6f777d12e41775ac0cac11b9a76922b5b10c2957e480f16c0a5ff3c61328b92abfa409389cc6e116d4d41503c8821fce2cb99e0d401bdee09ce2daea2561ab8a
7
+ data.tar.gz: cf26bcd052ff8b71be7a360ba095580f3c6993dfd235d39e57a378a846bffbfc50790654c285a829e943c638a6f069492211c4c6fe1ec3f91a2e5cc93e040970
@@ -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:)
@@ -39,7 +41,7 @@ module AcademicBenchmarks
39
41
  end
40
42
 
41
43
  def self.expire_time_in(offset)
42
- Time.now.to_i + offset
44
+ Time.now.to_i + offset.to_i
43
45
  end
44
46
  end
45
47
  end
@@ -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
@@ -1,5 +1,5 @@
1
- require_relative 'constants'
2
- require_relative 'standards'
1
+ require 'academic_benchmarks/api/constants'
2
+ require 'academic_benchmarks/api/standards'
3
3
 
4
4
  module AcademicBenchmarks
5
5
  module Api
@@ -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)
@@ -1,99 +1,81 @@
1
1
  require 'active_support/hash_with_indifferent_access'
2
2
 
3
- require_relative 'auth'
4
- require_relative 'constants'
3
+ require 'academic_benchmarks/api/auth'
4
+ require 'academic_benchmarks/api/constants'
5
5
 
6
6
  module AcademicBenchmarks
7
7
  module Api
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.raw
19
+ number.enhanced
20
+ status
21
+ disciplines.subjects.code
22
+ document.guid
23
+ document.descr
24
+ document.adopt_year
25
+ document.publication.descr
26
+ document.publication.guid
27
+ document.publication.authorities
28
+ statement.descr
29
+ utilizations.type
30
+ parent
31
+ ]
32
+
11
33
  def initialize(handle)
12
34
  @handle = handle
13
35
  end
14
36
 
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"])
37
+ # TODO: in the future, support OData filtering for flexible querying
38
+ def search(authority_guid: nil, publication_guid: nil)
39
+ raw_search(authority: authority_guid, publication: publication_guid).map do |standard|
40
+ AcademicBenchmarks::Standards::Standard.new(standard)
45
41
  end
46
42
  end
47
43
 
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"])
44
+ def authorities
45
+ raw_facet("document.publication.authorities").map do |a|
46
+ AcademicBenchmarks::Standards::Authority.from_hash(a["data"])
55
47
  end
56
48
  end
57
49
 
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"])
50
+ def publications(authority_guid: nil)
51
+ raw_facet("document.publication", authority: authority_guid).map do |a|
52
+ AcademicBenchmarks::Standards::Publication.from_hash(a["data"])
61
53
  end
62
54
  end
63
55
 
64
- def authority_documents(authority_or_auth_code_guid_or_desc)
56
+ def authority_publications(authority_or_auth_code_guid_or_desc)
65
57
  authority = auth_from_code_guid_or_desc(authority_or_auth_code_guid_or_desc)
66
- documents(authority: authority.code)
58
+ publications(authority_guid: authority.guid)
67
59
  end
68
60
 
69
- def authority_tree(authority_or_auth_code_guid_or_desc, include_obsolete_standards: true)
61
+ def authority_tree(authority_or_auth_code_guid_or_desc, include_obsolete_standards: true, exclude_examples: false)
70
62
  authority = auth_from_code_guid_or_desc(authority_or_auth_code_guid_or_desc)
71
- auth_children = search(authority: authority.code)
63
+ auth_children = raw_search(authority: authority.guid, include_obsoletes: include_obsolete_standards, exclude_examples: exclude_examples)
72
64
  AcademicBenchmarks::Standards::StandardsForest.new(
73
- auth_children,
74
- include_obsoletes: include_obsolete_standards
65
+ auth_children
75
66
  ).consolidate_under_root(authority)
76
67
  end
77
68
 
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)
69
+ def publication_tree(publication_or_pub_code_guid_or_desc, include_obsolete_standards: true, exclude_examples: false)
70
+ publication = pub_from_guid(publication_or_pub_code_guid_or_desc)
71
+ pub_children = raw_search(publication: publication.guid, include_obsoletes: include_obsolete_standards, exclude_examples: exclude_examples)
81
72
  AcademicBenchmarks::Standards::StandardsForest.new(
82
- doc_children,
83
- include_obsoletes: include_obsolete_standards
84
- ).consolidate_under_root(document)
73
+ pub_children
74
+ ).consolidate_under_root(publication)
85
75
  end
86
76
 
87
77
  private
88
78
 
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
79
  def auth_from_code_guid_or_desc(authority_or_auth_code_guid_or_desc)
98
80
  if authority_or_auth_code_guid_or_desc.is_a?(AcademicBenchmarks::Standards::Authority)
99
81
  authority_or_auth_code_guid_or_desc
@@ -102,6 +84,14 @@ module AcademicBenchmarks
102
84
  end
103
85
  end
104
86
 
87
+ def pub_from_guid(publication_or_pub_code_guid_or_desc)
88
+ if publication_or_pub_code_guid_or_desc.is_a?(AcademicBenchmarks::Standards::Publication)
89
+ publication_or_pub_code_guid_or_desc
90
+ else
91
+ find_type(type: "publication", data: publication_or_pub_code_guid_or_desc)
92
+ end
93
+ end
94
+
105
95
  def find_type(type:, data:)
106
96
  matches = send("match_#{type}", data)
107
97
  if matches.empty?
@@ -110,7 +100,7 @@ module AcademicBenchmarks
110
100
  )
111
101
  elsif matches.count > 1
112
102
  raise StandardError.new(
113
- "Authority code, guid, or description matched more than one authority. " \
103
+ "#{type.upcase} code, guid, or description matched more than one #{type}. " \
114
104
  "matched '#{matches.map(&:to_json).join('; ')}'"
115
105
  )
116
106
  end
@@ -119,22 +109,26 @@ module AcademicBenchmarks
119
109
 
120
110
  def match_authority(data)
121
111
  authorities.select do |auth|
122
- auth.code == data ||
112
+ auth.acronym == data ||
123
113
  auth.guid == data ||
124
114
  auth.descr == data
125
115
  end
126
116
  end
127
117
 
128
- def match_document(data)
129
- documents.select { |doc| doc.guid == data }
118
+ def match_publication(data)
119
+ publications.select do |pub|
120
+ pub.acronym == data ||
121
+ pub.guid == data ||
122
+ pub.descr == data
123
+ end
130
124
  end
131
125
 
132
- def raw_search(opts = {})
133
- request_search_pages_and_concat_resources(opts.merge(auth_query_params))
126
+ def raw_facet(facet, query_params = {})
127
+ request_facet({facet: facet}.merge(query_params).merge(auth_query_params))
134
128
  end
135
129
 
136
- def invalid_search_params(opts)
137
- opts.keys.map(&:to_s) - AcademicBenchmarks::Api::Constants.standards_search_params
130
+ def raw_search(opts = {})
131
+ request_search_pages_and_concat_resources(opts.merge(auth_query_params))
138
132
  end
139
133
 
140
134
  def auth_query_params
@@ -146,7 +140,49 @@ module AcademicBenchmarks
146
140
  )
147
141
  end
148
142
 
143
+ def odata_filters(query_params)
144
+ if query_params.key? :authority
145
+ value = query_params.delete :authority
146
+ query_params['filter[standards]'] = "document.publication.authorities.guid eq '#{value}'" if value
147
+ end
148
+ if query_params.key? :publication
149
+ value = query_params.delete :publication
150
+ query_params['filter[standards]'] = "document.publication.guid eq '#{value}'" if value
151
+ end
152
+
153
+ if query_params.key? :include_obsoletes
154
+ unless query_params.delete :include_obsoletes
155
+ if query_params.key? 'filter[standards]'
156
+ query_params['filter[standards]'] += " and status eq 'active'"
157
+ else
158
+ query_params['filter[standards]'] = "status eq 'active'"
159
+ end
160
+ end
161
+ end
162
+
163
+ if query_params.delete :exclude_examples
164
+ if query_params.key? 'filter[standards]'
165
+ query_params['filter[standards]'] += " and utilizations.type not eq 'example'"
166
+ else
167
+ query_params['filter[standards]'] = "utilizations.type not eq 'example'"
168
+ end
169
+ end
170
+ end
171
+
172
+ def request_facet(query_params)
173
+ odata_filters query_params
174
+ page = request_page(
175
+ query_params: query_params,
176
+ limit: 0, # return no standards since facets are separate
177
+ offset: 0
178
+ ).parsed_response
179
+
180
+ page.dig("meta", "facets", 0, "details")
181
+ end
182
+
149
183
  def request_search_pages_and_concat_resources(query_params)
184
+ query_params['fields[standards]'] = STANDARDS_FIELDS.join(',')
185
+ odata_filters query_params
150
186
  query_params.reverse_merge!({limit: DEFAULT_PER_PAGE})
151
187
 
152
188
  if !query_params[:limit] || query_params[:limit] <= 0
@@ -161,10 +197,9 @@ module AcademicBenchmarks
161
197
  offset: 0
162
198
  ).parsed_response
163
199
 
164
- resources = first_page["resources"]
165
- count = first_page["count"]
200
+ resources = first_page["data"]
201
+ count = first_page["meta"]["count"]
166
202
  offset = query_params[:limit]
167
-
168
203
  while offset < count
169
204
  page = request_page(
170
205
  query_params: query_params,
@@ -172,7 +207,7 @@ module AcademicBenchmarks
172
207
  offset: offset
173
208
  )
174
209
  offset += query_params[:limit]
175
- resources.push(page.parsed_response["resources"])
210
+ resources.push(page.parsed_response["data"])
176
211
  end
177
212
 
178
213
  resources.flatten
@@ -183,19 +218,29 @@ module AcademicBenchmarks
183
218
  limit: limit,
184
219
  offset: offset,
185
220
  })
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:"
221
+ 1.times do
222
+ resp = @handle.class.get(
223
+ '/standards',
224
+ query: query_params.merge({
225
+ limit: limit,
226
+ offset: offset,
227
+ })
196
228
  )
229
+ if resp.code == 429
230
+ sleep retry_after(resp)
231
+ redo
232
+ end
233
+ if resp.code != 200
234
+ raise RuntimeError.new(
235
+ "Received response '#{resp.code}: #{resp.message}' requesting standards from Academic Benchmarks:"
236
+ )
237
+ end
238
+ return resp
197
239
  end
198
- resp
240
+ end
241
+
242
+ def retry_after(response)
243
+ ENV['ACADEMIC_BENCHMARKS_TOO_MANY_REQUESTS_RETRY']&.to_f || 5
199
244
  end
200
245
  end
201
246
  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
- require_relative '../lib/inst_vars_to_hash'
2
- require_relative '../lib/remove_obsolete_children'
1
+ require 'academic_benchmarks/lib/inst_vars_to_hash'
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