academic_benchmarks 0.0.7 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3642061c18ead9883256159af91104b1a56becba
4
- data.tar.gz: af9b8707c2b2bc6b78d54c363c1cf30b4c0e636c
2
+ SHA256:
3
+ metadata.gz: '05582989c51ab88e71317ed23866a5dd9c34a956b96791faa86f775c8c09e227'
4
+ data.tar.gz: c5a713f96b930b2b914674fd57390dd68d0cc8fc9269e3b3503514637d07d746
5
5
  SHA512:
6
- metadata.gz: 669a13f59e8f6a1e880c76c0687e639ccbf5426cd1c66bf13939553e2f7674e4298223ff9039b515dd987af1d1c30012dba2a85174bc6f05549e01b0011772b2
7
- data.tar.gz: 8664ae27c2de59d065f639f4e50b4f7351da9f5487c5d9776488dc7587a6b74af94a52fb370d7e614c9609ac2526f6fde3c20033cde036dde645676145ebd769
6
+ metadata.gz: a1cf494c1bde537ad7c8f6f6755ad71196b8946b2087479cefe4c9d5c0fee7c7f4657c14853fd8d26b01c2cd864ed8bb99e931404bd1b7c12ee1d8786b30f087
7
+ data.tar.gz: bb5bfe960f0182139852c1bc27ccda0cce0ae150e81aa8682c1090390d0412b6326f0000a587d948acee8d05363a9233b5466d7c9686cf27ce6fb7e8ecf66ad0
@@ -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,80 @@
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
+ 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
- )
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)
27
40
  end
28
41
  end
29
42
 
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"])
43
+ def authorities
44
+ raw_facet("document.publication.authorities").map do |a|
45
+ AcademicBenchmarks::Standards::Authority.from_hash(a["data"])
45
46
  end
46
47
  end
47
48
 
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"])
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"])
55
52
  end
56
53
  end
57
54
 
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"])
61
- end
62
- end
63
-
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
60
  def authority_tree(authority_or_auth_code_guid_or_desc, include_obsolete_standards: true)
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)
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)
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)
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,41 @@ 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
+ end
162
+
163
+ def request_facet(query_params)
164
+ odata_filters query_params
165
+ page = request_page(
166
+ query_params: query_params,
167
+ limit: 0, # return no standards since facets are separate
168
+ offset: 0
169
+ ).parsed_response
170
+
171
+ page.dig("meta", "facets", 0, "details")
172
+ end
173
+
149
174
  def request_search_pages_and_concat_resources(query_params)
175
+ query_params['fields[standards]'] = STANDARDS_FIELDS.join(',')
176
+ odata_filters query_params
150
177
  query_params.reverse_merge!({limit: DEFAULT_PER_PAGE})
151
178
 
152
179
  if !query_params[:limit] || query_params[:limit] <= 0
@@ -161,10 +188,9 @@ module AcademicBenchmarks
161
188
  offset: 0
162
189
  ).parsed_response
163
190
 
164
- resources = first_page["resources"]
165
- count = first_page["count"]
191
+ resources = first_page["data"]
192
+ count = first_page["meta"]["count"]
166
193
  offset = query_params[:limit]
167
-
168
194
  while offset < count
169
195
  page = request_page(
170
196
  query_params: query_params,
@@ -172,7 +198,7 @@ module AcademicBenchmarks
172
198
  offset: offset
173
199
  )
174
200
  offset += query_params[:limit]
175
- resources.push(page.parsed_response["resources"])
201
+ resources.push(page.parsed_response["data"])
176
202
  end
177
203
 
178
204
  resources.flatten
@@ -183,19 +209,29 @@ module AcademicBenchmarks
183
209
  limit: limit,
184
210
  offset: offset,
185
211
  })
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:"
212
+ 1.times do
213
+ resp = @handle.class.get(
214
+ '/standards',
215
+ query: query_params.merge({
216
+ limit: limit,
217
+ offset: offset,
218
+ })
196
219
  )
220
+ if resp.code == 429
221
+ sleep retry_after(resp)
222
+ redo
223
+ end
224
+ if resp.code != 200
225
+ raise RuntimeError.new(
226
+ "Received response '#{resp.code}: #{resp.message}' requesting standards from Academic Benchmarks:"
227
+ )
228
+ end
229
+ return resp
197
230
  end
198
- resp
231
+ end
232
+
233
+ def retry_after(response)
234
+ ENV['ACADEMIC_BENCHMARKS_TOO_MANY_REQUESTS_RETRY']&.to_f || 5
199
235
  end
200
236
  end
201
237
  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