flatfile_api 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.
@@ -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