flatfile_api 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,447 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+
6
+ require_relative "flatfile_api/version"
7
+ require_relative "flatfile_api/response"
8
+ require_relative "flatfile_api/paginated_response"
9
+ require_relative "flatfile_api/string_tools"
10
+
11
+ class FlatfileApi
12
+ class Error < StandardError; end
13
+
14
+ using StringTools
15
+
16
+ PROTO = "https"
17
+ HOST = "api.us.flatfile.io"
18
+
19
+ def initialize(access_key_id: ENV['FLATFILE_ACCESS_KEY_ID'], secret_access_key: ENV['FLATFILE_SECRET_ACCESS_KEY'], debug: false)
20
+ @access_key_id = access_key_id
21
+ @secret_access_key = secret_access_key
22
+ @debug = debug
23
+ @redirect_limit = 5
24
+
25
+ @base_uri = URI "#{PROTO}://#{HOST}"
26
+ @agent = Net::HTTP.new @base_uri.host, @base_uri.port
27
+ @agent.use_ssl = PROTO == 'https'
28
+ @agent.keep_alive_timeout = 10
29
+ @agent.set_debug_output $stdout if @debug
30
+ end
31
+
32
+ def exchange_access_key_for_jwt(
33
+ access_key_id:,
34
+ expires_in:nil,
35
+ secret_access_key:
36
+ )
37
+
38
+ request(
39
+ :post,
40
+ "/auth/access-key-exchange",
41
+ body_params: {
42
+ accessKeyId: access_key_id, # Access Key generated in app
43
+ expiresIn: expires_in, # Sets an expiration (in seconds)
44
+ secretAccessKey: secret_access_key, # Secret Access Key generated in app
45
+ },
46
+ )
47
+ end
48
+
49
+ def download_an_upload(
50
+ batch_id:,
51
+ type:
52
+ )
53
+
54
+ request(
55
+ :get,
56
+ "/batch/%<batchId>s/export.csv",
57
+ path_params: {
58
+ batchId: batch_id, # A valid UUID
59
+ },
60
+ query_params: {
61
+ type: type, # File to download
62
+ },
63
+ )
64
+ end
65
+
66
+ def delete_an_upload(
67
+ batch_id:
68
+ )
69
+
70
+ request(
71
+ :delete,
72
+ "/batch/%<batchId>s",
73
+ path_params: {
74
+ batchId: batch_id, # A valid UUID
75
+ },
76
+ )
77
+ end
78
+
79
+ def bulk_delete_uploads(
80
+ older_than_quantity:,
81
+ older_than_unit:,
82
+ send_email:,
83
+ team_id:
84
+ )
85
+
86
+ request(
87
+ :get,
88
+ "/delete/batches",
89
+ query_params: {
90
+ olderThanQuantity: older_than_quantity,
91
+ olderThanUnit: older_than_unit,
92
+ sendEmail: send_email,
93
+ teamId: team_id,
94
+ },
95
+ )
96
+ end
97
+
98
+ def list_workspace_uploads(
99
+ end_user_id:nil,
100
+ environment_id:nil,
101
+ license_key:,
102
+ search:nil,
103
+ skip:nil,
104
+ take:nil,
105
+ workspace_id:nil
106
+ )
107
+
108
+ request(
109
+ :get,
110
+ "/rest/batches",
111
+ query_params: {
112
+ endUserId: end_user_id, # Valid endUserId for the Workspace
113
+ environmentId: environment_id, # Valid environmentId for the Workspace
114
+ licenseKey: license_key, # A valid licenseKey for the Workspace
115
+ search: search, # Searches fileName, originalFile, memo
116
+ skip: skip, # The rows to skip before listing
117
+ take: take, # The maximum number of rows to return
118
+ workspaceId: workspace_id, # Valid workspaceId for the Workspace
119
+ },
120
+ )
121
+ end
122
+
123
+ def file_upload_meta_data(
124
+ batch_id:
125
+ )
126
+
127
+ request(
128
+ :get,
129
+ "/rest/batch/%<batchId>s",
130
+ path_params: {
131
+ batchId: batch_id, # A valid UUID
132
+ },
133
+ )
134
+ end
135
+
136
+ def sheet_name_for_file_upload(
137
+ upload_id:,
138
+ license_key:nil
139
+ )
140
+
141
+ request(
142
+ :get,
143
+ "/upload/%<uploadId>s/dataSources",
144
+ path_params: {
145
+ uploadId: upload_id, # A valid UUID
146
+ },
147
+ query_params: {
148
+ licenseKey: license_key, # A valid licenseKey for the Workspace
149
+ },
150
+ )
151
+ end
152
+
153
+ def records_for_file_upload(
154
+ batch_id:,
155
+ created_at_end_date:nil,
156
+ created_at_start_date:nil,
157
+ deleted:nil,
158
+ skip:nil,
159
+ take:nil,
160
+ updated_at_end_date:nil,
161
+ updated_at_start_date:nil,
162
+ valid:nil
163
+ )
164
+
165
+ request(
166
+ :get,
167
+ "/rest/batch/%<batchId>s/rows",
168
+ path_params: {
169
+ batchId: batch_id, # A valid UUID
170
+ },
171
+ query_params: {
172
+ createdAtEndDate: created_at_end_date, # The maximum createdAt date to return
173
+ createdAtStartDate: created_at_start_date, # The minimum createdAt date to return
174
+ deleted: deleted, # Return only deleted rows
175
+ skip: skip, # The rows to skip before listing
176
+ take: take, # The maximum number of rows to return
177
+ updatedAtEndDate: updated_at_end_date, # The maximum updatedAt date to return
178
+ updatedAtStartDate: updated_at_start_date, # The minimum updatedAt date to return
179
+ valid: valid, # Return only valid rows
180
+ },
181
+ )
182
+ end
183
+
184
+ def upload_to_workspace_sheet(
185
+ sheet_id:,
186
+ workspace_id:
187
+ )
188
+
189
+ request(
190
+ :post,
191
+ "/workspace/%<workspaceId>s/sheet/%<sheetId>s/data",
192
+ path_params: {
193
+ sheetId: sheet_id, # A valid UUID
194
+ workspaceId: workspace_id, # A valid UUID
195
+ },
196
+ )
197
+ end
198
+
199
+ def fetch_workspace_sheet_records(
200
+ sheet_id:,
201
+ workspace_id:,
202
+ filter:nil,
203
+ merge_id:nil,
204
+ nested:nil,
205
+ record_ids:nil,
206
+ skip:nil,
207
+ take:nil,
208
+ valid:nil
209
+ )
210
+
211
+ request(
212
+ :get,
213
+ "/workspace/%<workspaceId>s/sheet/%<sheetId>s/records",
214
+ path_params: {
215
+ sheetId: sheet_id, # A valid UUID
216
+ workspaceId: workspace_id, # A valid UUID
217
+ },
218
+ query_params: {
219
+ filter: filter, # Return only the filtered rows
220
+ mergeId: merge_id,
221
+ nested: nested,
222
+ recordIds: record_ids,
223
+ skip: skip, # The rows to skip before listing
224
+ take: take, # The maximum number of rows to return
225
+ valid: valid, # Return only valid rows
226
+ },
227
+ )
228
+ end
229
+
230
+ def list_team_workspaces(
231
+ team_id:,
232
+ environment_id:nil,
233
+ skip:nil,
234
+ take:nil
235
+ )
236
+
237
+ request(
238
+ :get,
239
+ "/rest/teams/%<teamId>s/workspaces",
240
+ path_params: {
241
+ teamId: team_id,
242
+ },
243
+ query_params: {
244
+ environmentId: environment_id, # Valid environmentId for the Workspace
245
+ skip: skip, # The rows to skip before listing
246
+ take: take, # The maximum number of rows to return
247
+ },
248
+ )
249
+ end
250
+
251
+ def detail_workspace(
252
+ workspace_id:
253
+ )
254
+
255
+ request(
256
+ :get,
257
+ "/rest/workspace/%<workspaceId>s",
258
+ path_params: {
259
+ workspaceId: workspace_id, # A valid UUID
260
+ },
261
+ )
262
+ end
263
+
264
+ def invite_workspace_collaborator(
265
+ team_id:,
266
+ workspace_id:,
267
+ email:
268
+ )
269
+
270
+ request(
271
+ :post,
272
+ "/rest/teams/%<teamId>s/workspaces/%<workspaceId>s/invitations",
273
+ path_params: {
274
+ teamId: team_id,
275
+ workspaceId: workspace_id, # A valid UUID
276
+ },
277
+ body_params: {
278
+ email: email, # Email address of invited collaborator
279
+ },
280
+ )
281
+ end
282
+
283
+ def list_workspace_invitations(
284
+ team_id:,
285
+ workspace_id:
286
+ )
287
+
288
+ request(
289
+ :get,
290
+ "/rest/teams/%<teamId>s/workspaces/%<workspaceId>s/invitations",
291
+ path_params: {
292
+ teamId: team_id,
293
+ workspaceId: workspace_id, # A valid UUID
294
+ },
295
+ )
296
+ end
297
+
298
+ def list_workspace_collaborators(
299
+ team_id:,
300
+ workspace_id:
301
+ )
302
+
303
+ request(
304
+ :get,
305
+ "/rest/teams/%<teamId>s/workspaces/%<workspaceId>s/collaborators",
306
+ path_params: {
307
+ teamId: team_id,
308
+ workspaceId: workspace_id, # A valid UUID
309
+ },
310
+ )
311
+ end
312
+
313
+ def revoke_workspace_invitation(
314
+ team_id:,
315
+ workspace_id:,
316
+ email:
317
+ )
318
+
319
+ request(
320
+ :delete,
321
+ "/rest/teams/%<teamId>s/workspaces/%<workspaceId>s/invitations",
322
+ path_params: {
323
+ teamId: team_id,
324
+ workspaceId: workspace_id, # A valid UUID
325
+ },
326
+ body_params: {
327
+ email: email, # Email address of invited collaborator
328
+ },
329
+ )
330
+ end
331
+
332
+ def remove_workspace_collaborator(
333
+ team_id:,
334
+ user_id:,
335
+ workspace_id:,
336
+ email:
337
+ )
338
+
339
+ request(
340
+ :delete,
341
+ "/rest/teams/%<teamId>s/workspaces/%<workspaceId>s/collaborators/%<userId>s",
342
+ path_params: {
343
+ teamId: team_id,
344
+ userId: user_id,
345
+ workspaceId: workspace_id, # A valid UUID
346
+ },
347
+ body_params: {
348
+ email: email, # Email address of collaborator
349
+ },
350
+ )
351
+ end
352
+
353
+ # def inspect
354
+ # "#<#{self.class}:#{object_id}>"
355
+ # end
356
+
357
+ private
358
+
359
+ def ouroboros(node)
360
+ case node
361
+ when Array
362
+ node.map { |v| ouroboros(v) }
363
+ when Hash
364
+ node.each_with_object({}) { |(k, v), o| o[k.underscore.to_sym] = ouroboros(v) }
365
+ else
366
+ node
367
+ end
368
+ end
369
+
370
+ def request(method, path, path_params: {}, body_params: {}, query_params: nil)
371
+ uri = URI("#{PROTO}://#{HOST}")
372
+ uri.path = path % path_params
373
+ uri.query = URI.encode_www_form(query_params.compact) if query_params
374
+ request_uri(
375
+ method,
376
+ uri,
377
+ path_params: path_params,
378
+ body_params: body_params,
379
+ query_params: query_params
380
+ )
381
+ end
382
+
383
+ def request_uri(method, uri, path_params: {}, body_params: {}, query_params: nil, redirects: 0)
384
+ http = Net::HTTP.new(uri.host, uri.port)
385
+ http.use_ssl = true
386
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
387
+
388
+ case method
389
+ when :post
390
+ request = Net::HTTP::Post.new uri
391
+ request['Content-Type'] = 'application/json; charset=UTF-8'
392
+ request.body = JSON.generate body_params.compact
393
+ when :get
394
+ request = Net::HTTP::Get.new uri
395
+ when :delete
396
+ request = Net::HTTP::Delete.new uri
397
+ else
398
+ raise NotImplementedError.new "Bad http method: #{method}"
399
+ end
400
+
401
+ # puts "\e[1;32mDespatching request to #{uri.path}\e[0m" if @debug
402
+
403
+ request["X-Api-Key"] = "#{@access_key_id}+#{@secret_access_key}"
404
+
405
+ @session = @agent.start unless @agent.started?
406
+ response = @session.request request
407
+
408
+ # puts "\e[1;32mResponse received\e[0m" if @debug
409
+
410
+ case response
411
+ when Net::HTTPOK
412
+ # Continue
413
+ when Net::HTTPRedirection
414
+ raise "Too many redirects" if redirects > @redirect_limit
415
+ # It's probably a file
416
+ next_uri = URI response['location']
417
+ return request_uri(method, next_uri, redirects: redirects + 1)
418
+ else
419
+ raise Error.new "Error response from #{uri} -> #{response.read_body}"
420
+ end
421
+
422
+ case response['content-type']
423
+ when /^application\/json/
424
+ data = ouroboros JSON.parse response.read_body
425
+ when /^text\/csv/, /^application\//
426
+ return Response.new(response.body)
427
+ else
428
+ raise NotImplementedError.new "Don't know how to parse #{response['content-type']}"
429
+ end
430
+
431
+ if data.key?(:pagination)
432
+ PaginatedResponse.new(
433
+ data,
434
+ self,
435
+ method,
436
+ uri.path,
437
+ path_params: path_params,
438
+ body_params: body_params,
439
+ query_params: query_params
440
+ )
441
+ else
442
+ Response.new data
443
+ end
444
+
445
+ end
446
+
447
+ end
@@ -0,0 +1,54 @@
1
+ require 'yaml'
2
+
3
+ endpoints = YAML.load_file('spec_2022-12-02.yaml')
4
+
5
+ def camel_to_snake(key)
6
+ key.gsub(/(?<=[a-z0-9])([A-Z])/) { "_#{$1}" }.downcase
7
+ end
8
+
9
+ def snake_to_camel(key)
10
+ key.gsub(/_([a-z0-9])/) { $1.upcase }
11
+ end
12
+
13
+ endpoints.each do |endpoint|
14
+ method_name = endpoint[:description].gsub(/[^a-z0-9_]/i, '_').downcase
15
+ combined_params = endpoint.values_at(
16
+ :path_params,
17
+ :query_params,
18
+ :body_params
19
+ ).compact.flatten.each_with_object({}) do |param, o|
20
+ key = camel_to_snake param['Name']
21
+ puts "\e[31mOverwriting key: #{key}\e[0m" if o.key? key
22
+ o[camel_to_snake param['Name']] = /true/i === param['Required']
23
+ end
24
+
25
+ print "def #{method_name}("
26
+ puts "" if combined_params.any?
27
+ puts(combined_params.map do |k, v|
28
+ " #{k}:#{v ? '' : 'nil'}"
29
+ end.join(",\n"))
30
+ puts ")"
31
+ puts ""
32
+
33
+ path = endpoint[:path].gsub(/\/:([a-z]+)\b/i) { ?/ + "\%<#{$1}>s" }
34
+
35
+ puts " request("
36
+ puts " :#{endpoint[:method].downcase},"
37
+ puts " \"#{path}\","
38
+
39
+ [:path_params, :query_params, :body_params].each do |fields|
40
+ next unless endpoint.key? fields
41
+ puts " #{fields}: {"
42
+ endpoint[fields].each do |param|
43
+ key = camel_to_snake param["Name"]
44
+ print " #{param["Name"]}: #{key},"
45
+ print " # #{param["Description"]}" if param["Description"].length > 1
46
+ puts ""
47
+ end
48
+ puts " },"
49
+ end
50
+ puts " )"
51
+ puts "end"
52
+ puts ""
53
+
54
+ end
@@ -0,0 +1,38 @@
1
+ require 'yaml'
2
+
3
+ endpoints = YAML.load_file('spec_2022-12-02.yaml')
4
+
5
+ def camel_to_snake(key)
6
+ key.gsub(/(?<=[a-z0-9])([A-Z])/) { "_#{$1}" }.downcase
7
+ end
8
+
9
+ def snake_to_camel(key)
10
+ key.gsub(/_([a-z0-9])/) { $1.upcase }
11
+ end
12
+
13
+ endpoints.each do |endpoint|
14
+ method_name = endpoint[:description].gsub(/[^a-z0-9_]/i, '_').downcase
15
+
16
+ puts "\n\n### #{method_name}\n"
17
+ combined_params = endpoint.values_at(
18
+ :path_params,
19
+ :query_params,
20
+ :body_params
21
+ ).compact.flatten
22
+
23
+ combined_params.sort_by { |param|
24
+ camel_to_snake param['Name']
25
+ }.tap { |params|
26
+ if params.any?
27
+ puts "| Name | Required | Type | Value | Description |"
28
+ puts "| ---- | -------- | ---- | ----- | ----------- |"
29
+ end
30
+ }.each do |param, o|
31
+ key = camel_to_snake param['Name']
32
+ puts "| #{key} | #{param['Required']} | #{param['Type']} | #{param['Value']} | #{param['Description']} |"
33
+ end
34
+
35
+ paginated = combined_params.map { |x| x['Name'] }.include? 'take'
36
+ puts "\nResponse: `FlatfileApi::" + ( paginated ? 'Paginated' : '' ) + 'Response`'
37
+
38
+ end
@@ -0,0 +1,83 @@
1
+ require 'nokogiri'
2
+ require 'yaml'
3
+ require 'open-uri'
4
+
5
+ def body_params?(pointer)
6
+ /Request Body:/i === pointer.text
7
+ end
8
+
9
+ def example_response?(pointer)
10
+ /Example Reponse:/i === pointer.text
11
+ end
12
+
13
+ def extract_header(pointer)
14
+ /(POST|GET|DELETE|PATCH|PUT)\s+(.+)/ === pointer.text.strip.gsub(/[\u200B-\u200D\uFEFF]/, '')
15
+ $~.captures
16
+ end
17
+
18
+ def extract_params(pointer)
19
+ header, *rows = pointer.css('tr')
20
+ headers = header.css('th').map(&:text)
21
+ rows.each_with_object([]) do |row, o|
22
+ o << Hash[headers.zip(row.css('td').map(&:text))]
23
+ end
24
+ end
25
+
26
+ def extract_path(pointer)
27
+ /Endpoint:\s+(.*)/i === pointer.text&.strip
28
+ $~.captures.first
29
+ end
30
+
31
+ def header?(pointer)
32
+ /POST|GET|DELETE|PATCH|PUT/ === pointer.at_css('code')&.text
33
+ end
34
+
35
+ def param_table?(pointer)
36
+ /Name/i === pointer.at_css('th')&.text
37
+ end
38
+
39
+ def path?(pointer)
40
+ /Endpoint:\s+(.*)/i === pointer.text
41
+ end
42
+
43
+ def path_params?(pointer)
44
+ /Path Variables:/i === pointer.text
45
+ end
46
+
47
+ def query_params?(pointer)
48
+ /Query Params:/i === pointer.text
49
+ end
50
+
51
+ url = "https://flatfile.com/docs/api-reference/"
52
+ doc = Nokogiri::HTML(URI.open(url))
53
+ pointer = doc.at_css('#endpoints')
54
+
55
+ endpoints = []
56
+ context = nil
57
+ spec = nil
58
+ while pointer = pointer.next_sibling
59
+ case
60
+ when header?(pointer)
61
+ context = nil
62
+ spec = {}
63
+ endpoints << spec
64
+ spec[:method], spec[:description] = extract_header(pointer)
65
+ when path?(pointer)
66
+ spec[:path] = extract_path(pointer)
67
+ when body_params?(pointer)
68
+ context = :body_params
69
+ when path_params?(pointer)
70
+ context = :path_params
71
+ when query_params?(pointer)
72
+ context = :query_params
73
+ when param_table?(pointer)
74
+ spec[context] = extract_params(pointer)
75
+ context = nil
76
+ when example_response?(pointer)
77
+ context = nil
78
+ else
79
+ # puts "Don't know how to handle: #{pointer.text}"
80
+ end
81
+ end
82
+
83
+ puts endpoints.to_yaml