ed_fi_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +78 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +5 -0
  7. data/.yardopts +1 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +65 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +113 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/docs/EdFi.html +127 -0
  16. data/docs/EdFi/Client.html +1204 -0
  17. data/docs/EdFi/Client/AccessToken.html +541 -0
  18. data/docs/EdFi/Client/ArgumentError.html +143 -0
  19. data/docs/EdFi/Client/Auth.html +440 -0
  20. data/docs/EdFi/Client/Error.html +139 -0
  21. data/docs/EdFi/Client/Proxy.html +521 -0
  22. data/docs/EdFi/Client/Response.html +479 -0
  23. data/docs/EdFi/Client/UnableToAuthenticateError.html +145 -0
  24. data/docs/_index.html +203 -0
  25. data/docs/class_list.html +51 -0
  26. data/docs/css/common.css +1 -0
  27. data/docs/css/full_list.css +58 -0
  28. data/docs/css/style.css +499 -0
  29. data/docs/file.README.html +124 -0
  30. data/docs/file_list.html +56 -0
  31. data/docs/frames.html +17 -0
  32. data/docs/index.html +124 -0
  33. data/docs/js/app.js +248 -0
  34. data/docs/js/full_list.js +216 -0
  35. data/docs/js/jquery.js +4 -0
  36. data/docs/method_list.html +235 -0
  37. data/docs/top-level-namespace.html +110 -0
  38. data/ed_fi_client.gemspec +32 -0
  39. data/lib/ed_fi/client.rb +244 -0
  40. data/lib/ed_fi/client/access_token.rb +63 -0
  41. data/lib/ed_fi/client/auth.rb +99 -0
  42. data/lib/ed_fi/client/errors.rb +19 -0
  43. data/lib/ed_fi/client/proxy.rb +65 -0
  44. data/lib/ed_fi/client/response.rb +150 -0
  45. data/lib/ed_fi/client/version.rb +18 -0
  46. data/lib/ed_fi_client.rb +1 -0
  47. metadata +201 -0
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.9.12
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
16
+
17
+ <script type="text/javascript" charset="utf-8">
18
+ pathId = "";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="class_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+
41
+
42
+ <span class="title">Top Level Namespace</span>
43
+
44
+ </div>
45
+
46
+ <div id="search">
47
+
48
+ <a class="full_list_link" id="class_list_link"
49
+ href="class_list.html">
50
+
51
+ <svg width="24" height="24">
52
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
53
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
54
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
55
+ </svg>
56
+ </a>
57
+
58
+ </div>
59
+ <div class="clear"></div>
60
+ </div>
61
+
62
+ <div id="content"><h1>Top Level Namespace
63
+
64
+
65
+
66
+ </h1>
67
+ <div class="box_info">
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+ </div>
80
+
81
+ <h2>Defined Under Namespace</h2>
82
+ <p class="children">
83
+
84
+
85
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="EdFi.html" title="EdFi (module)">EdFi</a></span>
86
+
87
+
88
+
89
+
90
+ </p>
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <div id="footer">
103
+ Generated on Thu May 24 18:35:37 2018 by
104
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
+ 0.9.12 (ruby-2.5.1).
106
+ </div>
107
+
108
+ </div>
109
+ </body>
110
+ </html>
@@ -0,0 +1,32 @@
1
+
2
+ lib = File.expand_path('lib', __dir__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ed_fi/client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ed_fi_client'
8
+ spec.version = EdFi::Client::VERSION
9
+ spec.authors = ['Nestor Custodio']
10
+ spec.email = ['sakimorix@gmail.com']
11
+
12
+ spec.summary = 'A simple API wrapper for Ed-Fi ODS access.'
13
+ spec.homepage = 'https://www.github.com/nestor-custodio/ed_fi_client'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.16'
24
+ spec.add_development_dependency 'pry', '~> 0.11'
25
+ spec.add_development_dependency 'pry-byebug', '~> 3.6'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.0'
28
+ spec.add_development_dependency 'rspec_junit_formatter', '~> 0.3'
29
+
30
+ spec.add_dependency 'activesupport', '~> 5.2.0'
31
+ spec.add_dependency 'crapi', '~> 0.1'
32
+ end
@@ -0,0 +1,244 @@
1
+ require 'active_support/all'
2
+ require 'crapi'
3
+
4
+ require 'ed_fi/client/auth'
5
+ require 'ed_fi/client/errors'
6
+ require 'ed_fi/client/proxy'
7
+ require 'ed_fi/client/response'
8
+ require 'ed_fi/client/version'
9
+
10
+ ## The main container defined by the **ed_fi_client** gem. Provides a connection mechanism, an
11
+ ## authentication mechanism, simple CRUD methods ({#delete} / {#get} / {#patch} / {#post} / {#put}),
12
+ ## and proxy generators.
13
+ ##
14
+ ## All other classes defined in this gem (including gem-specific `::Error` derivatives) are
15
+ ## subclasses of {EdFi::Client EdFi::Client}.
16
+ ##
17
+ class EdFi::Client < Crapi::Client
18
+ ## The "profile" header content-type template.
19
+ PROFILE_MIME_TYPE = 'application/vnd.ed-fi.%<resource>s.%<profile>s.%<access>s+json'.freeze
20
+
21
+ ## @param base_uri [URI, String]
22
+ ## The base URI the client should use for determining the host to connect to, whether SSH is
23
+ ## applicable, and the path to the target API.
24
+ ##
25
+ ## @param opts [Hash]
26
+ ## Method options. All options not explicitly listed below are passed on to Crapi::Client.
27
+ ##
28
+ ## @option opts [String] :profile
29
+ ## The profile for which {EdFi::Client#read} and {EdFi::Client#write} will generate headers, if
30
+ ## any.
31
+ ##
32
+ ## @option opts [String] :client_id
33
+ ## The client id to use for authentication. This is AKA the "api key" / "username".
34
+ ##
35
+ ## @option opts [String] :client_secret
36
+ ## The client secret to use for authentication. This is AKA the "api secret" / "password".
37
+ ##
38
+ ##
39
+ ## @raise [EdFi::Client::ArgumentError]
40
+ ##
41
+ def initialize(base_uri, opts = {})
42
+ required_opts = %i[client_id client_secret]
43
+ required_opts.each { |opt| raise ArgumentError, "missing keyword: #{opt}" unless opts.key? opt }
44
+
45
+ super(base_uri, opts)
46
+ self.profile = opts[:profile]
47
+
48
+ ## Giving the EdFi::Client::Auth instance its own Crapi client lets us do fancy things with the
49
+ ## API segmenting stuff ...
50
+ ##
51
+ auth_client = Crapi::Client.new(base_uri, opts)
52
+ @auth = EdFi::Client::Auth.new(client: auth_client,
53
+ client_id: opts[:client_id],
54
+ client_secret: opts[:client_secret])
55
+ end
56
+
57
+ ## Sets the profile to use for {EdFi::Client#read} / {EdFi::Client#write} calls.
58
+ ##
59
+ ##
60
+ ## @param profile [String, Symbol]
61
+ ## The profile for which {EdFi::Client#read} and {EdFi::Client#write} will generate headers.
62
+ ##
63
+ def profile=(profile)
64
+ @profile = profile&.to_s&.downcase
65
+ end
66
+
67
+ ## rubocop:disable Naming/UncommunicativeMethodParamName
68
+
69
+ ## Returns the header needed to {EdFi::Client#get} a resource with a profile.
70
+ ##
71
+ ##
72
+ ## @param resource [String, Symbol]
73
+ ## The resource to be read.
74
+ ##
75
+ ## @param as [String, Symbol]
76
+ ## The profile to use. If one has already been set via {EdFi::Client#initialize} or
77
+ ## {EdFi::Client#profile=}, this value is optional.
78
+ ##
79
+ ##
80
+ ## @return [HashWithIndifferentAccess]
81
+ ##
82
+ def read(resource, as: nil)
83
+ self.profile = as if as.present?
84
+ mime_type = format(PROFILE_MIME_TYPE, resource: resource, profile: @profile, access: :readable)
85
+
86
+ { 'Accept': mime_type }.with_indifferent_access
87
+ end
88
+ ## rubocop:enable Naming/UncommunicativeMethodParamName
89
+
90
+ ## rubocop:disable Naming/UncommunicativeMethodParamName
91
+
92
+ ## Returns the header needed to {EdFi::Client#delete} / {EdFi::Client#patch} / {EdFi::Client#post}
93
+ ## / {EdFi::Client#put} a resource with a profile.
94
+ ##
95
+ ##
96
+ ## @param resource [String, Symbol]
97
+ ## The resource to be written.
98
+ ##
99
+ ## @param as [String, Symbol]
100
+ ## The profile to use. If one has already been set via {EdFi::Client#initialize} or
101
+ ## {EdFi::Client#profile=}, this value is optional.
102
+ ##
103
+ ##
104
+ ## @return [HashWithIndifferentAccess]
105
+ ##
106
+ def write(resource, as: nil)
107
+ self.profile = as if as.present?
108
+ mime_type = format(PROFILE_MIME_TYPE, resource: resource, profile: @profile, access: :writable)
109
+
110
+ { 'Content-Type': mime_type }.with_indifferent_access
111
+ end
112
+ ## rubocop:enable Naming/UncommunicativeMethodParamName
113
+
114
+ ## CRUD methods ...
115
+
116
+ ## CRUD method: DELETE
117
+ ##
118
+ ## *headers* and *query* are preprocessed for auth and case conversion, but all parameters are
119
+ ## otherwise passed through to Crapi::Proxy#delete.
120
+ ##
121
+ def delete(path, headers: {}, query: {})
122
+ (headers, query) = preprocess(headers, query)
123
+ respond_with super(path, headers: headers, query: query)
124
+ end
125
+
126
+ ## CRUD method: GET
127
+ ##
128
+ ## *headers* and *query* are preprocessed for auth and case conversion, but all parameters are
129
+ ## otherwise passed through to Crapi::Proxy#get.
130
+ ##
131
+ def get(path, headers: {}, query: {})
132
+ (headers, query) = preprocess(headers, query)
133
+ respond_with super(path, headers: headers, query: query)
134
+ end
135
+
136
+ ## CRUD method: PATCH
137
+ ##
138
+ ## *headers*, *query*, and *payload* are preprocessed for auth and case conversion, but all
139
+ ## parameters are otherwise passed through to Crapi::Proxy#patch.
140
+ ##
141
+ def patch(path, headers: {}, query: {}, payload: {})
142
+ (headers, query, payload) = preprocess(headers, query, payload)
143
+ respond_with super(path, headers: headers, query: query, payload: payload)
144
+ end
145
+
146
+ ## CRUD method: POST
147
+ ##
148
+ ## *headers*, *query*, and *payload* are preprocessed for auth and case conversion, but all
149
+ ## parameters are otherwise passed through to Crapi::Proxy#post.
150
+ ##
151
+ def post(path, headers: {}, query: {}, payload: {})
152
+ (headers, query, payload) = preprocess(headers, query, payload)
153
+ respond_with super(path, headers: headers, query: query, payload: payload)
154
+ end
155
+
156
+ ## CRUD method: PUT
157
+ ##
158
+ ## *headers*, *query*, and *payload* are preprocessed for auth and case conversion, but all
159
+ ## parameters are otherwise passed through to Crapi::Proxy#put.
160
+ ##
161
+ def put(path, headers: {}, query: {}, payload: {})
162
+ (headers, query, payload) = preprocess(headers, query, payload)
163
+ respond_with super(path, headers: headers, query: query, payload: payload)
164
+ end
165
+
166
+ ## API segment proxies ...
167
+
168
+ ## Convenience proxy generator for v2.0 API access, also addomg the school year you'd like to
169
+ ## access, if given.
170
+ ##
171
+ ##
172
+ ## @param period [Integer]
173
+ ## The school year to be accessed. (To access the v2.0 API for school year 2017-2018, call
174
+ ## `v2(2017)`.)
175
+ ##
176
+ ##
177
+ ## @return [EdFi::Client::Proxy]
178
+ ##
179
+ def v2(period = nil)
180
+ period = period.to_i
181
+
182
+ @v2 = {} if @v2.nil?
183
+ @v2[period] ||= begin
184
+ path = '/api/v2.0'
185
+ path += "/#{period}" if period.nonzero?
186
+ EdFi::Client::Proxy.new(add: path, to: self)
187
+ end
188
+ end
189
+
190
+ ##
191
+
192
+ private
193
+
194
+ ##
195
+
196
+ ## Generates an auth header, with a valid Bearer token.
197
+ ##
198
+ ##
199
+ ## @return [HashWithIndifferentAccess]
200
+ ##
201
+ def auth_header
202
+ { 'Authorization': "Bearer #{@auth.token}" }.with_indifferent_access
203
+ end
204
+
205
+ ## Carries out preprocessing for headers, query data, and payload data, returning processed
206
+ ## *copies* of the given values.
207
+ ##
208
+ ##
209
+ ## @param headers [Hash]
210
+ ## The headers to preprocess. A copy of this value is returned with auth headers added, so long
211
+ ## as no key conflicts arise.
212
+ ##
213
+ ## @param query [Hash]
214
+ ## The querystring values to preprocess. Keys will be case-converted where necessary.
215
+ ##
216
+ ## @param payload [Hash]
217
+ ## The payload values to preprocess. Keys will be case-converted where necessary.
218
+ ##
219
+ ##
220
+ ## @return [(Hash, Hash, Hash)]
221
+ ##
222
+ def preprocess(headers, query = nil, payload = nil)
223
+ payload = payload.as_json if payload.is_a? EdFi::Client::Response
224
+
225
+ headers = auth_header.merge(headers)
226
+ query = query.deep_transform_keys { |key| key.to_s.camelize(:lower) } if query.is_a? Hash
227
+ payload = payload.deep_transform_keys { |key| key.to_s.camelize(:lower) } if payload.is_a? Hash
228
+
229
+ [headers, query, payload]
230
+ end
231
+
232
+ ## Returns an {EdFi::Client::Response EdFi::Client::Response} for the given API resonse value.
233
+ ##
234
+ ##
235
+ ## @param response [Hash, Array]
236
+ ## The API response Hash/Array to convert to an {EdFi::Client::Response EdFi::Client::Response}.
237
+ ##
238
+ ##
239
+ ## @return [EdFi::Client::Response]
240
+ ##
241
+ def respond_with(response)
242
+ EdFi::Client::Response.new(response, client: self)
243
+ end
244
+ end
@@ -0,0 +1,63 @@
1
+ require 'crapi'
2
+
3
+ class EdFi::Client < Crapi::Client
4
+ ## The {EdFi::Client::AccessToken EdFi::Client::AccessToken} represents an access token, as
5
+ ## returned by "/oauth/token" calls.
6
+ ##
7
+ class AccessToken
8
+ ## An {EdFi::Client::AccessToken EdFi::Client::AccessToken} can be initiialized with the
9
+ ## "/oauth/token" response Hash. If given, an additional "issued_at" Time value helps to more
10
+ ## accurately calculate the token's expiration Time.
11
+ ##
12
+ ##
13
+ ## @param access_token [String]
14
+ ## The actual token value to use as the Bearer token in subsequent requests.
15
+ ##
16
+ ## @param token_type [String]
17
+ ## The token type (e.g. "bearer").
18
+ ##
19
+ ## @param issued_at [Time]
20
+ ## An optional value denoting the Time at which the token was issued.
21
+ ## If unset, defaults to `Time.current`.
22
+ ##
23
+ ## @param expires_in [Numeric]
24
+ ## The token's lifetime, in seconds.
25
+ ##
26
+ def initialize(access_token:, token_type:, issued_at: Time.current, expires_in:)
27
+ @access_token = access_token.dup
28
+ @token_type = token_type.dup
29
+ @issued_at = issued_at.dup
30
+ @expires_in = expires_in.dup
31
+ end
32
+
33
+ ## Gives a copy of the token *value*.
34
+ ##
35
+ ##
36
+ ## @return [String]
37
+ ##
38
+ def token
39
+ @access_token.dup
40
+ end
41
+
42
+ ## Gives the token's *calculated* expiration Time.
43
+ ##
44
+ ##
45
+ ## @return [Time]
46
+ ##
47
+ def expires_at
48
+ return 1.second.ago if @access_token.blank?
49
+ (@issued_at + @expires_in.seconds)
50
+ end
51
+
52
+ ## Denotes whether the token is still "valid", per its (calculated) expiration timesstamp. Note
53
+ ## that a 5-second window is allotted for the request using the token to complete.
54
+ ##
55
+ ##
56
+ ## @return [true,false]
57
+ ##
58
+ def valid?
59
+ safety_window = 5.seconds
60
+ Time.current <= (expires_at - safety_window)
61
+ end
62
+ end
63
+ end