academic_benchmarks 0.0.8 → 1.0.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 +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