ps_utilities 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6b53435e5d2f6da0cd21f54f5780806986b87e7ff36b963702e4cd1009862e7d
4
+ data.tar.gz: 0f3133a755d4f2f67e8bde12bbb3769969afe60015f609b672166582b06d2898
5
+ SHA512:
6
+ metadata.gz: 0b39f2d31153c8fc135d6fc39a415c55f8a347f4c97f0de18b91bec030f99d182e9baa655de000a29996d01997b2e44023f6d55d4f0b02e6187f254fd3f555b3
7
+ data.tar.gz: 87407650b7062b75f4ae4158bd48103a2acc302472923ad4d0456fbb900dc4a07191588df67621f028bcdf6e8d18aadf908dcfe8c666467335ecb872f68a6ea1
@@ -0,0 +1,166 @@
1
+ module PsUtilities
2
+
3
+ module ApiEndpoints
4
+
5
+ API_PATHS = {
6
+ ws: '/ws/v1',
7
+ ptg: '/powerschool-ptg-api/v2/',
8
+ xte: '/ws/xte'
9
+ }
10
+
11
+ def initialize(api_credentials, options = {})
12
+ self.client = Class.new(Powerschool::Client) do |klass|
13
+ uri = api_credentials['base_uri'] || Powerschool::Client::BASE_URI
14
+ klass.base_uri(uri)
15
+
16
+ # options like `verify: false` (to disable ssl verification)
17
+ options.each do |k, v|
18
+ default_options.update({k => v})
19
+ end
20
+ end.new(api_credentials)
21
+ end
22
+
23
+ class << self
24
+ [:get, :post, :put, :delete].each do |command|
25
+ define_method(command.to_s) do |method, api, path = nil|
26
+ if path.nil?
27
+ path, api = api, nil
28
+ end
29
+ define_method(method) do |options = {}|
30
+ return self.client.class.send(command, prepare_path(path.dup, api, options), self.client.options.merge(options))
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def prepare_path(path, api, options)
37
+ options = options.dup
38
+ options.each_pair do |key, value|
39
+ regexp_path_option = /(:#{key}$|:#{key}([:&\/-_]))/
40
+ if path.match(regexp_path_option)
41
+ if value.blank?
42
+ raise "Blank value for parameter '%s' in '%s'" % [key, path]
43
+ end
44
+ path.gsub!(regexp_path_option, "#{value}\\2")
45
+ options.delete(key)
46
+ end
47
+ end
48
+ if parameter = path.match(/:(\w*)/)
49
+ raise "Missing parameter '%s' in '%s'. Parameters: %s" % [parameter[1], path, options]
50
+ end
51
+ if api
52
+ path = (API_PATHS[api] + path).gsub('//', '/')
53
+ end
54
+ path
55
+ end
56
+
57
+ # retreive max_page_size from metadata. Defaults to 100
58
+ def get_page_size(resource)
59
+ @metadata ||= self.metadata()
60
+ @metadata['%s_max_page_size' % resource.split('/').last.singularize] rescue 100
61
+ end
62
+
63
+ # Process every object for a resource.
64
+ def all(resource, options = {}, &block)
65
+ page_size = (options[:query][:pagesize] rescue nil) || get_page_size(resource)
66
+ _options = options.dup
67
+ _options[:query] ||= {}
68
+ _options[:query][:pagesize] ||= page_size
69
+
70
+ page = 1
71
+ results = []
72
+ begin
73
+ _options[:query][:page] = page
74
+ response = self.send(resource, _options)
75
+ results = response.parsed_response || {}
76
+ if !response.parsed_response
77
+ if response.headers['www-authenticate'].match(/Bearer error/)
78
+ raise response.headers['www-authenticate'].to_s
79
+ end
80
+ end
81
+
82
+ if results.is_a?(Hash)
83
+ plural = results.keys.first
84
+ results = results[plural][plural.singularize] || []
85
+ end
86
+ if results.is_a?(Hash)
87
+ # a rare(?) case has been observed where (in this case section_enrollment) did return a single
88
+ # data object as a hash rather than as a hash inside an array
89
+ results = [results]
90
+ end
91
+ results.each do |result|
92
+ block.call(result, response)
93
+ end
94
+ page += 1
95
+ end while results.any? && results.size == page_size
96
+ end
97
+
98
+ # client is set up per district so it returns only one district
99
+ # for urls with parameters
100
+ get :district, :ws, '/district'
101
+ get :schools, :ws, '/district/school'
102
+ get :teachers, :ws, '/staff'
103
+ get :student, :ws, '/student/:student_id'
104
+ get :students, :ws, '/student'
105
+ get :school_teachers, :ws, '/school/:school_id/staff'
106
+ get :school_students, :ws, '/school/:school_id/student'
107
+ get :school_sections, :ws, '/school/:school_id/section'
108
+ get :school_courses, :ws, '/school/:school_id/course'
109
+ get :school_terms, :ws, '/school/:school_id/term'
110
+ get :section_enrollment, :ws, '/section/:section_id/section_enrollment'
111
+
112
+ # PowerTeacher Gradebook (pre Powerschool 10)
113
+ get :assignment, :ptg, 'assignment/:id'
114
+ post :post_section_assignment, :ptg, '/section/:section_id/assignment'
115
+ put :put_assignment_scores, :ptg, '/assignment/:assignment_id/score'
116
+ put :put_assignment_score, :ptg, '/assignment/:assignment_id/student/:student_id/score'
117
+
118
+ # PowerTeacher Pro
119
+ post :xte_post_section_assignment, :xte, '/section/assignment?users_dcid=:teacher_id'
120
+ put :xte_put_assignment_scores, :xte, '/score'
121
+ get :xte_section_assignments, :xte, '/section/assignment?users_dcid=:teacher_id&section_ids=:section_id'
122
+ get :xte_section_assignment, :xte, '/section/assignment/:assignment_id?users_dcid=:teacher_id'
123
+ get :xte_teacher_category, :xte, '/teacher_category'
124
+
125
+ get :metadata, :ws, '/metadata'
126
+ get :areas, '/ws/schema/area'
127
+ get :tables, '/ws/schema/table'
128
+ get :table_records, '/ws/schema/table/:table?projection=:projection'
129
+ get :table_metadata, '/ws/schema/table/:table/metadata'
130
+ get :area_table, '/ws/schema/area/:area/table'
131
+
132
+ get :queries, '/ws/schema/query/api'
133
+
134
+ def start_year
135
+ offset = Date.today.month <= 6 ? -1 : 0
136
+ year = self.client.api_credentials['start_year'] || (Date.today.year + offset)
137
+ end
138
+
139
+ # Special method to filter terms and find the current ones
140
+ def current_terms(options, today = nil)
141
+ terms = []
142
+ today ||= Date.today.to_s(:db)
143
+ self.all(:school_terms, options) do |term, response|
144
+ if term['end_date'] >= today
145
+ terms << term
146
+ end
147
+ end
148
+ if terms.empty?
149
+ options[:query] = {q: 'start_year==%s' % start_year}
150
+ self.all(:school_terms, options) do |term, response|
151
+ if term['end_date'] >= today
152
+ terms << term
153
+ end
154
+ end
155
+ end
156
+ # now filter again for the start date and if there isn't one matching we have to return the most recent one
157
+ in_two_weeks = (Date.parse(today) + 2.weeks).to_s(:db)
158
+ active_terms = terms.select{|term| term['start_date'] <= in_two_weeks }
159
+ if active_terms.any?
160
+ return active_terms
161
+ end
162
+ return terms
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,175 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'json'
4
+ require 'httparty'
5
+ require 'ps_utilities/pre_built_get'
6
+ require 'ps_utilities/pre_built_put'
7
+ require 'ps_utilities/pre_built_post'
8
+
9
+ # http://blog.honeybadger.io/ruby-custom-exceptions/
10
+ class AuthError < RuntimeError
11
+ attr_reader :url, :credentials
12
+ def initialize(msg="", url="", credentials={})
13
+ @url = url
14
+ @credentials = credentials
15
+ super(msg)
16
+ end
17
+ end
18
+
19
+ module PsUtilities
20
+
21
+ # The PsUtilities, makes it east to work with the Powerschool API
22
+ # @since 0.1.0
23
+ #
24
+ # @note You should use environment variables to initialize your server.
25
+ class Connection
26
+
27
+ attr_reader :credentials, :headers
28
+
29
+ include PsUtilities::PreBuiltGet
30
+ include PsUtilities::PreBuiltPut
31
+ include PsUtilities::PreBuiltPost
32
+
33
+ def initialize(attributes: {}, headers: {})
34
+ @credentials = attr_defaults.merge(attributes)
35
+ @headers = header_defaults.merge(headers)
36
+
37
+ raise ArgumentError, "missing client_secret" if credentials[:client_secret].nil? or
38
+ credentials[:client_secret].empty?
39
+ raise ArgumentError, "missing client_id" if credentials[:client_id].nil? or
40
+ credentials[:client_id].empty?
41
+ raise ArgumentError, "missing base_uri" if credentials[:base_uri].nil? or
42
+ credentials[:base_uri].empty?
43
+ end
44
+
45
+ # with no command it just authenticates
46
+ def run(command: nil, params: {}, url: nil, options: {})
47
+ authenticate unless token_valid?
48
+ case command
49
+ when nil, :authenticate
50
+ # authenticate unless token_valid?
51
+ when :get, :put, :post
52
+ send(command, url, options) unless url.empty?
53
+ else
54
+ send(command, params)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # options = {query: {}}
61
+ def get(url, options={})
62
+ max_retries = 3
63
+ times_retried = 0
64
+ options = options.merge(headers)
65
+ ps_url = credentials[:base_uri] + url
66
+ begin
67
+ HTTParty.get(ps_url, options)
68
+ # self.class.get(url, query: options[:query], headers: options[:headers])
69
+ rescue Net::ReadTimeout, Net::OpenTimeout
70
+ if times_retried < max_retries
71
+ times_retried += 1
72
+ retry
73
+ else
74
+ { error: "no response (timeout) from URL: #{url}" }
75
+ end
76
+ end
77
+ end
78
+
79
+ # options = {body: {}}
80
+ def put(url, options={})
81
+ max_retries = 3
82
+ times_retried = 0
83
+ options = options.merge(headers)
84
+ ps_url = credentials[:base_uri] + url
85
+ begin
86
+ HTTParty.put(ps_url, options )
87
+ # self.class.get(url, body: options[:body], headers: options[:headers])
88
+ rescue Net::ReadTimeout, Net::OpenTimeout
89
+ if times_retried < max_retries
90
+ times_retried += 1
91
+ retry
92
+ else
93
+ { error: "no response (timeout) from URL: #{url}" }
94
+ end
95
+ end
96
+ end
97
+
98
+ # options = {body: {}}
99
+ def post(url, options={})
100
+ max_retries = 3
101
+ times_retried = 0
102
+ options = options.merge(headers)
103
+ ps_url = credentials[:base_uri] + url
104
+ begin
105
+ HTTParty.post(ps_url, options )
106
+ # self.class.get(url, body: options[:body], headers: options[:headers])
107
+ rescue Net::ReadTimeout, Net::OpenTimeout
108
+ if times_retried < max_retries
109
+ times_retried += 1
110
+ retry
111
+ else
112
+ { error: "no response (timeout) from URL: #{url}" }
113
+ end
114
+ end
115
+ end
116
+
117
+ # In PowerSchool go to System>System Settings>Plugin Management Configuration>your plugin>Data Provider Configuration to manually check plugin expiration date
118
+ def authenticate
119
+ @headers[:headers] ||= {}
120
+ ps_url = credentials[:base_uri] + credentials[:auth_endpoint]
121
+ response = HTTParty.post(ps_url, {headers: auth_headers,
122
+ body: 'grant_type=client_credentials'})
123
+
124
+ @credentials[:token_expires] = Time.now + response.parsed_response['expires_in'].to_i
125
+ @credentials[:access_token] = response.parsed_response['access_token'].to_s
126
+ @headers[:headers].merge!('Authorization' => 'Bearer ' + credentials[:access_token])
127
+
128
+ # throw error if no token returned -- nothing else will work
129
+ raise AuthError.new("No Auth Token Returned",
130
+ ps_url, credentials
131
+ ) if credentials[:access_token].empty?
132
+ end
133
+
134
+ def token_valid?(tokens = credentials)
135
+ return false if tokens[:access_token].nil?
136
+ return false if tokens[:access_token].empty?
137
+ return false if tokens[:token_expires].nil?
138
+ return false if tokens[:token_expires] <= Time.now
139
+ return true
140
+ end
141
+
142
+ def auth_headers
143
+ { 'ContentType' => 'application/x-www-form-urlencoded;charset=UTF-8',
144
+ 'Accept' => 'application/json',
145
+ 'Authorization' => 'Basic ' + encode_credentials
146
+ }
147
+ end
148
+
149
+ def encode_credentials
150
+ ps_auth_text = [ credentials[:client_id],
151
+ credentials[:client_secret]
152
+ ].join(':')
153
+ Base64.encode64(ps_auth_text).gsub(/\n/, '')
154
+ end
155
+
156
+ def attr_defaults
157
+ { base_uri: ENV['PS_URL'],
158
+ auth_endpoint: ENV['PS_AUTH_ENDPOINT'] || '/oauth/access_token',
159
+ client_id: ENV['PS_CLIENT_ID'],
160
+ client_secret: ENV['PS_CLIENT_SECRET'],
161
+ # not recommended here - it changes (ok as a parameter though)
162
+ # access_token: ENV['PS_ACCESS_TOKEN'] || nil,
163
+ }
164
+ end
165
+
166
+ def header_defaults
167
+ { headers:
168
+ { 'User-Agent' => "PsUtilitiesGem - v#{PsUtilities::Version::VERSION}",
169
+ 'Accept' => 'application/json',
170
+ 'Content-Type' => 'application/json'}
171
+ }
172
+ end
173
+
174
+ end
175
+ end
@@ -0,0 +1,36 @@
1
+ module PsUtilities
2
+
3
+ module PreBuiltGet
4
+
5
+ def get_active_students_count(params={})
6
+ # url = "/ws/v1/district/student/count?q=school_enrollment.enroll_status_code==0"
7
+ url = "/ws/v1/district/student/count"
8
+ options = { query: {"q" => "school_enrollment.enroll_status_code==0"} }
9
+ get(url, options)
10
+ end
11
+
12
+ def get_active_students_info(params={})
13
+ # url = "/ws/v1/district/student?q=school_enrollment.enroll_status==a&pagesize=500"
14
+ url = "/ws/v1/district/student"
15
+ options = { query:
16
+ {"q" => "school_enrollment.enroll_status_code==0",
17
+ "pagesize" => "500"}
18
+ }
19
+ get(url, options)
20
+ end
21
+
22
+ # las-test.powerschool.com/ws/v1/district/student?expansions=school_enrollment&q=student_username==xxxxx237
23
+ # params = {username: "xxxxxxx"}
24
+ def get_one_student_record(params)
25
+ # url = "/ws/v1/district/student?expansions=school_enrollment,contact&q=student_username==xxxxxx237"
26
+ url = "/ws/v1/district/student"
27
+ options = { query:
28
+ {"q" => "student_username==#{params[:username]}",
29
+ "expansions" => "school_enrollment,contact,contact_info"}
30
+ }
31
+ get(url, options)
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,7 @@
1
+ module PsUtilities
2
+
3
+ module PreBuiltPost
4
+
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ module PsUtilities
2
+
3
+ module PreBuiltPut
4
+
5
+ end
6
+
7
+ end
@@ -0,0 +1,5 @@
1
+ module PsUtilities
2
+ module Version
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ require "ps_utilities/version"
2
+ require "ps_utilities/connection"
3
+
4
+ module PsUtilities
5
+ # Your code goes here...
6
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ps_utilities
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Lee Weisbecker
8
+ - Bill Tihen
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2018-06-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httparty
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.16'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0.16'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.16'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.16'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '10.5'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '10.5'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.7'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '3.7'
70
+ description: 'Uses oauth2 to connection to the Powerschool API. Heavily refactored
71
+ code (not dependent on Rails) starting with: https://github.com/TomK32/powerschool'
72
+ email:
73
+ - leeweisbecker@gmail.com
74
+ - btihen@gmail.com
75
+ executables: []
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - lib/ps_utilities.rb
80
+ - lib/ps_utilities/api_endpoints.rb
81
+ - lib/ps_utilities/connection.rb
82
+ - lib/ps_utilities/pre_built_get.rb
83
+ - lib/ps_utilities/pre_built_post.rb
84
+ - lib/ps_utilities/pre_built_put.rb
85
+ - lib/ps_utilities/version.rb
86
+ homepage: https://github.com/LAS-IT/ps_utilities
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.7.6
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Simple ruby wrapper for Powerschool API interaction.
110
+ test_files: []