bright 0.2.0 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90f5aa090aca3509db5190f180780fab41aaca20cad1b2c8ad87d840ebc069e4
4
- data.tar.gz: 2371c2a573db38ff164e6f89e9721793d4c3e73be4bf76f090990f68964590d1
3
+ metadata.gz: 21ec0c637efbc8a7cf6f4089fc7e5357dc9e0a40616164058ca09b75183b420f
4
+ data.tar.gz: f20c064ba1a5a3aa292874b2ae2ce82274e5b426cdd3f71c8d705e93c4c73b65
5
5
  SHA512:
6
- metadata.gz: c1e87336d8567425a1a18f002ac4b3e25fc07ebac5e4b4248b6b280ddcce7d5aa7c578fb8927e5b8abbf8603f78f88f4dccfece3ced374903328769c17604559
7
- data.tar.gz: 9c1a8f380871e59f68d2d3733ac59f4796ae46031f9f5726a4c8707ff7585c7e4d860ff8643b8bb0550792e97751b868295dc12d967702241feb0168e9d9a595
6
+ metadata.gz: 57e8c1aa2f91505b5d51c2c6b5d0b19502517a9990095904f96f8c4e9b6870b7ae701f8c6d6a9bb750d829dce4341facfcf80be19018dae1633b4296fd5c1376
7
+ data.tar.gz: 2260684a855a642a93688037db1ceaa521f6e7f33615c77b95d8e440cd68aad25e5f78073ed170275f22dac779846488e7ba3a51948fa348d8954754608c2884
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2015 Stephen Heuer and Steven Novotny
1
+ Copyright (c) 2022 Arux Software, Inc.
2
2
 
3
3
  MIT License
4
4
 
data/bright.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Arux Software"]
10
10
  spec.email = ["sheuer@aruxsoftware.com"]
11
11
  spec.summary = "Framework and tools for dealing with Student Information Systems"
12
- spec.description = "Bright is a simple Student Information System API abstraction library used in and sponsored by Eleyo. It is written by Stephen Heuer, Steven Novotny, and contributors. The aim of the project is to abstract as many parts as possible away from the user to offer a consistent interface across all supported Student Information System APIs."
12
+ spec.description = "Bright is a simple Student Information System API abstraction library used in and sponsored by Arux Software. It is written by Stephen Heuer, Steven Novotny, and contributors. The aim of the project is to abstract as many parts as possible away from the user to offer a consistent interface across all supported Student Information System APIs."
13
13
  spec.homepage = ""
14
14
  spec.license = "MIT"
15
15
 
@@ -22,6 +22,6 @@ Gem::Specification.new do |spec|
22
22
  spec.add_runtime_dependency "json", ">= 0"
23
23
  spec.add_runtime_dependency 'oauth', ">= 0.5.4"
24
24
 
25
- spec.add_development_dependency "bundler", "~> 1.7"
26
- spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "bundler", ">= 2.2.10"
26
+ spec.add_development_dependency "rake", "~> 12.3.3"
27
27
  end
@@ -1,17 +1,15 @@
1
1
  module Bright
2
2
  class Address < Model
3
- @attribute_names = [:street, :apt, :city, :state, :postal_code, :lattitude, :longitude, :type]
3
+ @attribute_names = [:street, :apt, :city, :state, :postal_code, :latitude, :longitude, :type]
4
4
  attr_accessor *@attribute_names
5
-
6
- alias lat lattitude
5
+
6
+ alias lat latitude
7
7
  alias lng longitude
8
-
8
+
9
9
  def geographical_coordinates
10
- if self.lattitude and self.longitude
11
- "#{self.lattitude},#{self.longitude}"
10
+ if self.latitude and self.longitude
11
+ "#{self.latitude},#{self.longitude}"
12
12
  end
13
13
  end
14
14
  end
15
15
  end
16
-
17
-
@@ -31,53 +31,65 @@ module Bright
31
31
  @ssl_version = nil
32
32
  @proxy_address = nil
33
33
  @proxy_port = nil
34
+
35
+ if Bright.devmode && !@logger
36
+ @logger = Logger.new(STDOUT)
37
+ @logger.level = Logger::INFO
38
+ end
39
+
34
40
  end
35
41
 
36
42
  def request(method, body, headers = {})
37
43
  request_start = Time.now.to_f
44
+ info "connection_http_method=#{method.to_s.upcase} connection_uri=#{endpoint} headers=#{headers.inspect}", tag
45
+
46
+ result = nil
38
47
 
39
- begin
40
- info "connection_http_method=#{method.to_s.upcase} connection_uri=#{endpoint}", tag
41
-
42
- result = nil
43
-
44
- realtime = Benchmark.realtime do
45
- request = HTTPI::Request.new(endpoint.to_s)
46
- request.headers = headers
47
- request.body = body if body
48
- request.auth.ssl.verify_mode = :none if !@verify_peer
49
- configure_proxy(request)
50
- configure_timeouts(request)
51
-
52
- result = case method
53
- when :get
54
- raise ArgumentError, "GET requests do not support a request body" if body
55
- HTTPI.get(request)
56
- when :post
57
- debug body
58
- HTTPI.post(request)
59
- when :put
60
- debug body
61
- HTTPI.put(request)
62
- when :patch
63
- debug body
64
- HTTPI.patch(request)
65
- when :delete
66
- HTTPI.delete(request)
67
- else
68
- raise ArgumentError, "Unsupported request method #{method.to_s.upcase}"
69
- end
48
+ if !Bright.devmode
49
+ HTTPI.log = false
50
+ end
51
+
52
+ realtime = Benchmark.realtime do
53
+ request = HTTPI::Request.new(endpoint.to_s)
54
+ request.headers = headers
55
+ request.body = body if body
56
+ request.auth.ssl.verify_mode = :none if !@verify_peer
57
+ configure_proxy(request)
58
+ configure_timeouts(request)
59
+
60
+ result = case method
61
+ when :get
62
+ raise ArgumentError, "GET requests do not support a request body" if body
63
+ HTTPI.get(request)
64
+ when :post
65
+ debug(body) if Bright.devmode
66
+ HTTPI.post(request)
67
+ when :put
68
+ debug(body) if Bright.devmode
69
+ HTTPI.put(request)
70
+ when :patch
71
+ debug(body) if Bright.devmode
72
+ HTTPI.patch(request)
73
+ when :delete
74
+ HTTPI.delete(request)
75
+ else
76
+ raise ArgumentError, "Unsupported request method #{method.to_s.upcase}"
70
77
  end
78
+ end
71
79
 
72
- info "--> %d (%d %.4fs)" % [result.code, result.body ? result.body.length : 0, realtime], tag
73
- debug result.body
74
- result
80
+ if Bright.devmode
81
+ info("--> %d (%d %.4fs)" % [result.code, result.body ? result.body.length : 0, realtime], tag)
82
+ debug(result.body)
75
83
  end
84
+
85
+ handle_response(result)
86
+
76
87
  ensure
77
88
  info "connection_request_total_time=%.4fs" % [Time.now.to_f - request_start], tag
78
89
  end
79
90
 
80
91
  private
92
+
81
93
  def configure_proxy(http)
82
94
  http.proxy = "#{proxy_address}:#{proxy_port}" if proxy_address
83
95
  end
@@ -88,15 +100,10 @@ module Bright
88
100
  end
89
101
 
90
102
  def handle_response(response)
91
- if @ignore_http_status then
92
- return response.body
103
+ if @ignore_http_status or !response.error?
104
+ return response
93
105
  else
94
- case response.code.to_i
95
- when 200...300
96
- response.body
97
- else
98
- raise ResponseError.new(response)
99
- end
106
+ raise ResponseError.new(response, endpoint.to_s)
100
107
  end
101
108
  end
102
109
 
@@ -0,0 +1,53 @@
1
+ require 'securerandom'
2
+
3
+ module Bright
4
+ class Contact < Model
5
+ @attribute_names = [:client_id, :api_id, :first_name, :middle_name, :last_name, :nick_name,
6
+ :birth_date, :gender, :relationship_type,
7
+ :hispanic_ethnicity, :race, :image,
8
+ :sis_student_id, :state_student_id, :last_modified]
9
+ attr_accessor *@attribute_names
10
+
11
+ def self.attribute_names
12
+ @attribute_names
13
+ end
14
+
15
+ attr_accessor :phone_numbers, :addresses, :email_address
16
+
17
+ def phone_numbers=(array)
18
+ if array.size <= 0 or array.first.is_a?(PhoneNumber)
19
+ @phone_numbers = array
20
+ elsif array.first.is_a?(Hash)
21
+ @phone_numbers = array.collect{|a| PhoneNumber.new(a)}
22
+ end
23
+ @phone_numbers ||= []
24
+ end
25
+
26
+ def phone_numbers
27
+ @phone_numbers ||= []
28
+ end
29
+
30
+ def addresses=(array)
31
+ if array.size <= 0 or array.first.is_a?(Address)
32
+ @addresses = array
33
+ elsif array.first.is_a?(Hash)
34
+ @addresses = array.collect{|a| Address.new(a)}
35
+ end
36
+ @addresses ||= []
37
+ end
38
+
39
+ def addresses
40
+ @addresses ||= []
41
+ end
42
+
43
+ def email_address=(email)
44
+ if email.is_a?(EmailAddress)
45
+ @email_address = email
46
+ elsif email.is_a?(Hash)
47
+ @email_address = EmailAddress.new(email)
48
+ end
49
+ @email_address
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ class CursorResponseCollection < ResponseCollection
2
+
3
+ attr_accessor :collected_objects
4
+
5
+ def initialize(options = {})
6
+ @collected_objects = [options[:seed_page]].flatten
7
+ @per_page = options[:per_page].to_i
8
+ @load_more_call = options[:load_more_call]
9
+ @next_cursor = options[:next_cursor]
10
+ end
11
+
12
+ def each
13
+ while (!@next_cursor.blank?) do
14
+ objects_hsh = load_more_call.call(@next_cursor)
15
+ objects = objects_hsh[:objects]
16
+ @next_cursor = objects_hsh[:next_cursor]
17
+ objects.each do |obj|
18
+ yield obj
19
+ end
20
+ @collected_objects += objects
21
+ end
22
+ end
23
+
24
+ def last
25
+ self.to_a
26
+ return @collected_objects.last
27
+ end
28
+
29
+ def loaded_results
30
+ @collected_objects.flatten
31
+ end
32
+
33
+ def total
34
+ self.to_a
35
+ self.loaded_results.size
36
+ end
37
+
38
+ alias size total
39
+ alias length total
40
+
41
+ def empty?
42
+ self.to_a.empty?
43
+ end
44
+
45
+ end
data/lib/bright/errors.rb CHANGED
@@ -1,15 +1,25 @@
1
1
  module Bright
2
2
  class ResponseError < StandardError
3
3
  attr_reader :response
4
+ attr_reader :uri
4
5
 
5
- def initialize(response, message = nil)
6
+ def initialize(response, uri = nil)
6
7
  @response = response
7
- @message = message
8
+ @uri = uri
8
9
  end
9
10
 
10
11
  def to_s
11
- "Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
12
+ "Failed with #{response.code} #{response.message if response.respond_to?(:message)}".strip
12
13
  end
14
+
15
+ def body
16
+ response.body
17
+ end
18
+
19
+ def server_error?
20
+ (500..599).include?(response&.code.to_i)
21
+ end
22
+
13
23
  end
14
24
 
15
25
  class UnknownAttributeError < NoMethodError
@@ -0,0 +1,13 @@
1
+ module Kernel
2
+ def Boolean(string)
3
+ return true if string == true || string =~ /^true$/i || string == "1" || string == 1 || string.to_s.downcase == "yes"
4
+ return false if string == false || string.nil? || string =~ /^false$/i || string == "0" || string == 0 || string.to_s.downcase == "no" || string.blank?
5
+ raise ArgumentError.new("invalid value for Boolean: \"#{string}\"")
6
+ end
7
+ end
8
+
9
+ class Object
10
+ def to_bool
11
+ Boolean(self)
12
+ end
13
+ end
@@ -1,8 +1,20 @@
1
1
  module Bright
2
2
  class PhoneNumber < Model
3
- @attribute_names = [:phone_number, :type]
3
+ @attribute_names = [:phone_number, :extension, :type]
4
4
  attr_accessor *@attribute_names
5
5
  TYPES = ["Cell", "Home", "Work", "Other"]
6
-
6
+
7
+ def phone_number=(number)
8
+ number_a = number.to_s.split(/x|X/)
9
+ if number_a.size == 2
10
+ @extension = number_a.last.gsub(/[^0-9]/, "").strip
11
+ end
12
+ @phone_number = number_a.first.gsub(/[^0-9]/, "").strip
13
+ end
14
+
15
+ def extension=(number)
16
+ @extension = number.gsub(/[^0-9]/, "").strip
17
+ end
18
+
7
19
  end
8
20
  end
@@ -1,11 +1,11 @@
1
1
  class ResponseCollection
2
2
  include Enumerable
3
-
3
+
4
4
  attr_accessor :paged_objects
5
5
  attr_accessor :total
6
6
  attr_accessor :per_page
7
7
  attr_accessor :load_more_call
8
-
8
+
9
9
  # seed_page, total, per_page, load_more_call
10
10
  def initialize(options = {})
11
11
  @paged_objects = {0 => options[:seed_page]}
@@ -14,7 +14,7 @@ class ResponseCollection
14
14
  @pages = @per_page > 0 ? (@total.to_f / @per_page.to_f).ceil : 0
15
15
  @load_more_call = options[:load_more_call]
16
16
  end
17
-
17
+
18
18
  def each
19
19
  current_page = -1
20
20
  while (current_page += 1) < @pages do
@@ -33,7 +33,7 @@ class ResponseCollection
33
33
  @paged_objects[next_page_no] = next_page_thread.value if next_page_thread
34
34
  end
35
35
  end
36
-
36
+
37
37
  def last
38
38
  last_page_no = @pages - 1
39
39
  if load_more_call and (last_page = @paged_objects[last_page_no]).nil?
@@ -41,15 +41,15 @@ class ResponseCollection
41
41
  end
42
42
  last_page.last
43
43
  end
44
-
44
+
45
45
  def loaded_results
46
46
  @paged_objects.values.flatten
47
47
  end
48
-
48
+
49
49
  alias size total
50
50
  alias length total
51
51
 
52
52
  def empty?
53
53
  total <= 0
54
54
  end
55
- end
55
+ end
@@ -2,29 +2,29 @@ module Bright
2
2
  module SisApi
3
3
  class Aeries < Base
4
4
  DATE_FORMAT = '%Y-%m-%dT%H:%M:%S'
5
-
5
+
6
6
  @@description = "Connects to the Aeries API for accessing student information"
7
7
  @@doc_url = "http://www.aeries.com/downloads/docs.1234/TechnicalSpecs/Aeries_API_Documentation.pdf"
8
8
  @@api_version = ""
9
-
9
+
10
10
  attr_accessor :connection_options
11
-
11
+
12
12
  def initialize(options = {})
13
13
  self.connection_options = options[:connection] || {}
14
14
  # {
15
- # :certficate => "",
15
+ # :certficate => "",
16
16
  # :uri => ""
17
17
  # }
18
18
  end
19
-
19
+
20
20
  def get_student_by_api_id(api_id)
21
21
  get_students({:api_id => api_id, :limit => 1}).first
22
22
  end
23
-
23
+
24
24
  def get_student(params)
25
25
  get_students(params.merge(:limit => 1)).first
26
26
  end
27
-
27
+
28
28
  def get_students(params)
29
29
  if params.has_key?(:school) or params.has_key?(:school_api_id)
30
30
  school_api_id = params.delete(:school) || params.delete(:school_api_id)
@@ -32,7 +32,7 @@ module Bright
32
32
  else
33
33
  threads = []
34
34
  get_schools.each do |school|
35
- threads << Thread.new do
35
+ threads << Thread.new do
36
36
  get_students_by_school(school, params)
37
37
  end
38
38
  end
@@ -40,7 +40,7 @@ module Bright
40
40
  end
41
41
  filter_students_by_params(students, params)
42
42
  end
43
-
43
+
44
44
  def get_students_by_school(school, params = {})
45
45
  school_api_id = school.is_a?(School) ? school.api_id : school
46
46
  if params.has_key?(:api_id)
@@ -53,21 +53,21 @@ module Bright
53
53
  students_response_hash = self.request(:get, path, self.map_student_search_params(params))
54
54
  students_response_hash.collect{|shsh| Student.new(convert_to_student_data(shsh))}
55
55
  end
56
-
56
+
57
57
  def create_student(student)
58
58
  raise NotImplementedError
59
59
  end
60
-
60
+
61
61
  def update_student(student)
62
62
  raise NotImplementedError
63
63
  end
64
-
64
+
65
65
  def get_schools(params = {})
66
66
  schools_response_hash = self.request(:get, 'api/v2/schools', params)
67
-
67
+
68
68
  schools_response_hash.collect{|h| School.new(convert_to_school_data(h))}
69
69
  end
70
-
70
+
71
71
  def request(method, path, params = {})
72
72
  uri = "#{self.connection_options[:uri]}/#{path}"
73
73
  body = nil
@@ -78,11 +78,12 @@ module Bright
78
78
  body = JSON.dump(params)
79
79
  end
80
80
 
81
- headers = self.headers_for_auth
81
+ response = connection_retry_wrapper {
82
+ connection = Bright::Connection.new(uri)
83
+ headers = self.headers_for_auth
84
+ connection.request(method, body, headers)
85
+ }
82
86
 
83
- connection = Bright::Connection.new(uri)
84
- response = connection.request(method, body, headers)
85
-
86
87
  if !response.error?
87
88
  response_hash = JSON.parse(response.body)
88
89
  else
@@ -91,55 +92,55 @@ module Bright
91
92
  end
92
93
  response_hash
93
94
  end
94
-
95
+
95
96
  protected
96
-
97
+
97
98
  def map_student_search_params(attrs)
98
99
  attrs
99
100
  end
100
-
101
+
101
102
  def convert_to_student_data(attrs)
102
103
  cattrs = {}
103
-
104
+
104
105
  cattrs[:first_name] = attrs["FirstName"]
105
106
  cattrs[:middle_name] = attrs["MiddleName"]
106
107
  cattrs[:last_name] = attrs["LastName"]
107
-
108
+
108
109
  cattrs[:api_id] = attrs["PermanentID"]
109
110
  cattrs[:sis_student_id] = attrs["StudentNumber"]
110
111
  cattrs[:state_student_id] = attrs["StateStudentID"]
111
-
112
+
112
113
  cattrs[:gender] = attrs["Sex"]
113
114
  if attrs["Birthdate"]
114
- begin
115
+ begin
115
116
  cattrs[:birth_date] = Date.strptime(attrs["Birthdate"], DATE_FORMAT)
116
117
  rescue => e
117
118
  puts "#{e.inspect} #{bd}"
118
119
  end
119
120
  end
120
-
121
+
121
122
  #SchoolCode
122
-
123
+
123
124
  cattrs.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
124
125
  end
125
-
126
+
126
127
  def convert_to_school_data(attrs)
127
128
  cattrs = {}
128
-
129
+
129
130
  cattrs[:api_id] = attrs["SchoolCode"]
130
131
  cattrs[:name] = attrs["Name"]
131
132
  cattrs[:number] = attrs["SchoolCode"]
132
-
133
+
133
134
  cattrs.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
134
135
  end
135
-
136
+
136
137
  def headers_for_auth
137
138
  {
138
139
  'AERIES-CERT' => self.connection_options[:certificate],
139
140
  'Content-Type' => "application/json"
140
141
  }
141
142
  end
142
-
143
+
143
144
  end
144
145
  end
145
- end
146
+ end
@@ -1,17 +1,17 @@
1
1
  module Bright
2
2
  module SisApi
3
3
  class Base
4
-
4
+
5
5
  def filter_students_by_params(students, params)
6
6
  total = params[:limit]
7
7
  count = 0
8
8
  found = []
9
-
9
+
10
10
  keys = (Student.attribute_names & params.keys.collect(&:to_sym))
11
11
  puts "filtering on #{keys.join(",")}"
12
12
  students.each do |student|
13
13
  break if total and count >= total
14
-
14
+
15
15
  should = (keys).all? do |m|
16
16
  student.send(m) =~ Regexp.new(Regexp.escape(params[m]), Regexp::IGNORECASE)
17
17
  end
@@ -20,7 +20,33 @@ module Bright
20
20
  end
21
21
  found
22
22
  end
23
-
23
+
24
+ def connection_retry_wrapper(&block)
25
+ retry_attempts = connection_options[:retry_attempts] || 2
26
+ retries = 0
27
+ begin
28
+ yield
29
+ rescue Bright::ResponseError => e
30
+ retries += 1
31
+ if e.server_error? && retries <= retry_attempts.to_i
32
+ puts "retrying #{retries}: #{e.class.to_s} - #{e.to_s}"
33
+ sleep(retries * 3)
34
+ retry
35
+ else
36
+ raise
37
+ end
38
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, EOFError => e
39
+ retries += 1
40
+ if retries <= retry_attempts.to_i
41
+ puts "retrying #{retries}: #{e.class.to_s} - #{e.to_s}"
42
+ sleep(retries * 3)
43
+ retry
44
+ else
45
+ raise
46
+ end
47
+ end
48
+ end
49
+
24
50
  end
25
51
  end
26
- end
52
+ end