ed_fi_client 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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +78 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +113 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/EdFi.html +127 -0
- data/docs/EdFi/Client.html +1204 -0
- data/docs/EdFi/Client/AccessToken.html +541 -0
- data/docs/EdFi/Client/ArgumentError.html +143 -0
- data/docs/EdFi/Client/Auth.html +440 -0
- data/docs/EdFi/Client/Error.html +139 -0
- data/docs/EdFi/Client/Proxy.html +521 -0
- data/docs/EdFi/Client/Response.html +479 -0
- data/docs/EdFi/Client/UnableToAuthenticateError.html +145 -0
- data/docs/_index.html +203 -0
- data/docs/class_list.html +51 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +499 -0
- data/docs/file.README.html +124 -0
- data/docs/file_list.html +56 -0
- data/docs/frames.html +17 -0
- data/docs/index.html +124 -0
- data/docs/js/app.js +248 -0
- data/docs/js/full_list.js +216 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +235 -0
- data/docs/top-level-namespace.html +110 -0
- data/ed_fi_client.gemspec +32 -0
- data/lib/ed_fi/client.rb +244 -0
- data/lib/ed_fi/client/access_token.rb +63 -0
- data/lib/ed_fi/client/auth.rb +99 -0
- data/lib/ed_fi/client/errors.rb +19 -0
- data/lib/ed_fi/client/proxy.rb +65 -0
- data/lib/ed_fi/client/response.rb +150 -0
- data/lib/ed_fi/client/version.rb +18 -0
- data/lib/ed_fi_client.rb +1 -0
- 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
|
+
— 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> »
|
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
|
data/lib/ed_fi/client.rb
ADDED
@@ -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
|