ecoportal-api-v2 1.1.8 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,132 @@
1
+ module Ecoportal
2
+ module API
3
+ class V2
4
+ # @attr_reader client [Common::Client] a `Common::Client` object that
5
+ # holds the configuration of the api connection.
6
+ class S3
7
+ class Files
8
+ class FailedPollRequest < StandardError; end
9
+ class CantCheckStatus < StandardError; end
10
+
11
+ extend Common::BaseClass
12
+
13
+ POLL_TIMEOUT = 240
14
+ DELAY_STATUS_CHECK = 5
15
+
16
+ class_resolver :poll_class, "Ecoportal::API::V2::S3::Files::Poll"
17
+ class_resolver :poll_status_class, "Ecoportal::API::V2::S3::Files::PollStatus"
18
+
19
+ attr_reader :client, :s3_api
20
+
21
+ # @param client [Common::Client] a `Common::Client` object that holds the configuration of the api connection.
22
+ # @param s3_api [V2::S3] the parent S3 api object.
23
+ # @return [Schemas] an instance object ready to make schema api requests.
24
+ def initialize(client, s3_api:)
25
+ @client = client
26
+ @s3_api = s3_api
27
+ end
28
+
29
+ # Helper to upload multiple files at once
30
+ def upload!(files, **kargs, &block)
31
+ BatchUpload.new(files, files_api: self).upload!(**kargs, &block)
32
+ end
33
+
34
+ # Tell ecoPortal to register the file in an actual File Container
35
+ # @note this is essential to do for the file to be attachable.
36
+ # - eP will scan the file and give create the proper data model that
37
+ # refers to the file (FileContainer).
38
+ # - FileContainers also have a file versioning sytem.
39
+ # - A file container belongs only to one single organization.
40
+ # @yield [poll] block early, when the poll is created
41
+ # @yieldparam poll [Ecoportal::API::V2::S3::Files::Poll, NilClass] the creatd poll
42
+ # @param s3_file_reference [Hash]
43
+ # @return [Ecoportal::API::V2::S3::Files::PollStatus, NilClass]
44
+ # when finalized (success, failed or timeout)
45
+ def poll!(
46
+ s3_file_reference,
47
+ check_delay: DELAY_STATUS_CHECK,
48
+ raise_timeout: false
49
+ )
50
+ raise ArgumentError, "Block required. None given" unless block_given?
51
+
52
+ poll = create_poll(s3_file_reference)
53
+ yield(poll) if block_given?
54
+
55
+ wait_for_poll_completion(poll.id, check_delay: check_delay).tap do |status|
56
+ poll.status = status
57
+
58
+ next unless raise_timeout
59
+ next if status&.complete?
60
+
61
+ msg = "File poll `#{poll.id}` not complete. "
62
+ msg << "Probably timeout after #{POLL_TIMEOUT} seconds. "
63
+ msg << "Current status: #{poll.status.status}"
64
+
65
+ raise Ecoportal::API::Errors::TimeOut, msg
66
+ end
67
+ end
68
+
69
+ # Create Poll Request to ecoPortal so it registers the file
70
+ # in an actual File Container
71
+ # @param s3_file_reference [Hash]
72
+ # @return [Ecoportal::API::V2::S3::Files::Poll, NilClass]
73
+ def create_poll(s3_file_reference)
74
+ response = client.post("/s3/files", data: s3_file_reference)
75
+ body = body_data(response.body)
76
+
77
+ return poll_class.new(body) if response.success?
78
+
79
+ msg = "Could not create poll request for\n"
80
+ msg << JSON.pretty_generate(s3_file_reference)
81
+ msg << "\n - Error #{response.status}: #{body}"
82
+ raise FailedPollRequest, msg
83
+ end
84
+
85
+ # Check progress on the File Poll status
86
+ # @param poll_id [String]
87
+ def poll_status(poll_id)
88
+ response = client.get("/s3/files/poll/#{poll_id}")
89
+ body = body_data(response.body)
90
+
91
+ return poll_status_class.new(body) if response.success?
92
+
93
+ msg = "Could not get status for poll '#{poll_id}' "
94
+ msg << "- Error #{response.status}: #{body}"
95
+ raise CantCheckStatus, msg
96
+ end
97
+
98
+ private
99
+
100
+ def body_data(body)
101
+ return body unless body.is_a?(Hash)
102
+ return body unless body.key?("data")
103
+
104
+ body["data"]
105
+ end
106
+
107
+ # @note timeout library is evil. So we make poor-man timeout.
108
+ # https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
109
+ def wait_for_poll_completion(poll_id, check_delay: DELAY_STATUS_CHECK)
110
+ before = Time.now
111
+
112
+ loop do
113
+ status = poll_status(poll_id)
114
+ break status if status&.complete?
115
+
116
+ if Time.now >= before + POLL_TIMEOUT
117
+ status&.status = 'timeout'
118
+ break status
119
+ end
120
+
121
+ sleep(check_delay)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ require 'ecoportal/api/v2/s3/files/poll'
131
+ require 'ecoportal/api/v2/s3/files/poll_status'
132
+ require 'ecoportal/api/v2/s3/files/batch_upload'
@@ -0,0 +1,154 @@
1
+ module Ecoportal
2
+ module API
3
+ class V2
4
+ class S3
5
+ class Upload
6
+ class MissingFile < ArgumentError; end
7
+
8
+ DEFAULT_MIME_TYPE = 'application/octet-stream'.freeze
9
+ EXPECTED_CODE = "204".freeze # No Content
10
+ MAX_RETRIES = 5
11
+
12
+ attr_reader :filename
13
+
14
+ def initialize(credentials, file:)
15
+ msg = "Expecting #{credentials_class}. Given: #{credentials.class}"
16
+ raise ArgumentError, msg unless credentials.is_a?(credentials_class)
17
+
18
+ @credentials = credentials
19
+ @filename = file
20
+ end
21
+
22
+ def upload!
23
+ require_deps!
24
+
25
+ @response = Net::HTTP.start(
26
+ uri.hostname,
27
+ uri.port,
28
+ use_ssl: true
29
+ ) do |https|
30
+ https.max_retries = self.class::MAX_RETRIES
31
+ https.request(request)
32
+ end
33
+
34
+ return unless success?
35
+
36
+ yield(s3_file_reference) if block_given?
37
+ s3_file_reference
38
+ end
39
+
40
+ def failed?
41
+ return false unless response
42
+
43
+ response.code != EXPECTED_CODE
44
+ end
45
+
46
+ def success?
47
+ return false unless response
48
+
49
+ response.code == EXPECTED_CODE
50
+ end
51
+
52
+ def s3_file_reference
53
+ return unless success?
54
+
55
+ @s3_file_reference ||= {
56
+ 'filename' => file_basename,
57
+ 'filetype' => mime_type,
58
+ 'filesize' => file_size,
59
+ 'path' => s3_file_path
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ attr_reader :credentials
66
+ attr_reader :response
67
+
68
+ def request
69
+ Net::HTTP::Post.new(uri).tap do |req|
70
+ req.set_form form_data, 'multipart/form-data'
71
+ end
72
+ end
73
+
74
+ def uri
75
+ @uri ||= URI(credentials[:s3_endpoint])
76
+ end
77
+
78
+ def form_data
79
+ [
80
+ ['key', s3_file_path],
81
+ *aws_params,
82
+ *file_params
83
+ ]
84
+ end
85
+
86
+ def s3_file_path
87
+ "#{user_tmpdir}/#{file_basename}"
88
+ end
89
+
90
+ def user_tmpdir
91
+ credentials[:user_tmpdir]
92
+ end
93
+
94
+ def aws_params
95
+ params = %i[AWSAccessKeyId policy signature x-amz-server-side-encryption]
96
+ params.map do |key|
97
+ [key.to_s, credentials[key]]
98
+ end
99
+ end
100
+
101
+ def file_params
102
+ [
103
+ ['content-type', mime_type],
104
+ ['file', io_stream]
105
+ ]
106
+ end
107
+
108
+ def mime_type
109
+ @mime_type ||= lib_mime_type || DEFAULT_MIME_TYPE
110
+ end
111
+
112
+ def lib_mime_type
113
+ MIME::Types.type_for(file_extension).first&.content_type
114
+ end
115
+
116
+ def io_stream
117
+ require_file!
118
+ File.open(file_fullname)
119
+ end
120
+
121
+ def credentials_class
122
+ Ecoportal::API::V2::S3::Data
123
+ end
124
+
125
+ def file_size
126
+ @file_size ||= File.size(file_fullname)
127
+ end
128
+
129
+ def file_extension
130
+ @file_extension ||= File.extname(filename)[1..]
131
+ end
132
+
133
+ def file_basename
134
+ @file_basename ||= File.basename(filename)
135
+ end
136
+
137
+ def file_fullname
138
+ @file_fullname ||= File.expand_path(filename)
139
+ end
140
+
141
+ def require_file!
142
+ msg = "File '#{file_basename}' doesn't exist"
143
+ raise MissingFile, msg unless File.exist?(file_fullname)
144
+ end
145
+
146
+ def require_deps!
147
+ require 'net/http'
148
+ require 'mime/types'
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,66 @@
1
+ module Ecoportal
2
+ module API
3
+ class V2
4
+ # @attr_reader client [Common::Client] a `Common::Client` object that holds the configuration of the api connection.
5
+ class S3
6
+ class MissingLocalFile < ArgumentError; end
7
+ class CredentialsGetFailed < StandardError; end
8
+
9
+ extend Common::BaseClass
10
+
11
+ class_resolver :data_class, "Ecoportal::API::V2::S3::Data"
12
+ class_resolver :files_class, "Ecoportal::API::V2::S3::Files"
13
+
14
+ attr_reader :client
15
+
16
+ # @param client [Common::Client] a `Common::Client` object that holds the configuration of the api connection.
17
+ # @return [Schemas] an instance object ready to make schema api requests.
18
+ def initialize(client)
19
+ @client = client
20
+ end
21
+
22
+ # Gets the S3 credentials to upload files
23
+ # @return [Ecoportal::API::V2::S3::Data]
24
+ def data
25
+ response = client.get("/s3/data")
26
+ body = body_data(response.body)
27
+
28
+ return data_class.new(body) if response.success?
29
+
30
+ msg = "Could not get S3 credentials - Error #{response.status}: #{body}"
31
+ raise CredentialsGetFailed, msg
32
+ end
33
+
34
+ # @param file [String] full path to existing local file
35
+ # @param credentials [Ecoportal::API::V2::S3::Data, NilClass]
36
+ # @return [Hash, NilClass] with the s3_file_reference on success, and `nil` otherwise
37
+ def upload_file(file, credentials: data, &block)
38
+ msg = "The file '#{file}' does not exist"
39
+ raise MissingLocalFile, msg unless File.exist?(file)
40
+
41
+ credentials ||= data
42
+ Upload.new(credentials, file: file).upload!(&block)
43
+ end
44
+
45
+ # Obtain specific object for eP file api requests.
46
+ # @return [Files] an instance object ready to make eP file api requests.
47
+ def files
48
+ files_class.new(client, s3_api: self)
49
+ end
50
+
51
+ private
52
+
53
+ def body_data(body)
54
+ return body unless body.is_a?(Hash)
55
+ return body unless body.key?("data")
56
+
57
+ body["data"]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ require 'ecoportal/api/v2/s3/data'
65
+ require 'ecoportal/api/v2/s3/upload'
66
+ require 'ecoportal/api/v2/s3/files'
@@ -8,10 +8,10 @@ module Ecoportal
8
8
  extend Common::BaseClass
9
9
  include Common::Logging
10
10
 
11
- VERSION = "v2"
11
+ VERSION = "v2".freeze
12
12
 
13
13
  class << self
14
- def v2key (ukey, gkey)
14
+ def v2key(ukey, gkey)
15
15
  Base64.urlsafe_encode64({
16
16
  organization: gkey,
17
17
  user: ukey
@@ -22,6 +22,7 @@ module Ecoportal
22
22
  class_resolver :people_class, "Ecoportal::API::V2::People"
23
23
  class_resolver :registers_class, "Ecoportal::API::V2::Registers"
24
24
  class_resolver :pages_class, "Ecoportal::API::V2::Pages"
25
+ class_resolver :s3_class, "Ecoportal::API::V2::S3"
25
26
 
26
27
  attr_reader :client, :logger
27
28
 
@@ -64,6 +65,12 @@ module Ecoportal
64
65
  pages_class.new(client)
65
66
  end
66
67
 
68
+ # Obtain specific object for file api requests.
69
+ # @return [S3] an instance object ready to make files api requests.
70
+ def s3 # rubocop:disable Naming/VariableNumber
71
+ s3_class.new(client)
72
+ end
73
+
67
74
  private
68
75
 
69
76
  def get_key(api_key: nil, user_key: nil, org_key: nil)
@@ -72,7 +79,6 @@ module Ecoportal
72
79
  raise "You need to provide either an api_key or user_key" unless user_key
73
80
  raise "You need to provide an org_key as well (not just a user_key)" unless org_key
74
81
  end
75
-
76
82
  end
77
83
  end
78
84
  end
@@ -80,3 +86,4 @@ end
80
86
  require 'ecoportal/api/v2/people'
81
87
  require 'ecoportal/api/v2/pages'
82
88
  require 'ecoportal/api/v2/registers'
89
+ require 'ecoportal/api/v2/s3'
@@ -1,5 +1,5 @@
1
1
  module Ecoportal
2
2
  module API
3
- GEM2_VERSION = "1.1.8".freeze
3
+ GEM2_VERSION = "2.0.0".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,55 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecoportal-api-v2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.8
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-22 00:00:00.000000000 Z
11
+ date: 2024-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 2.4.12
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '3'
23
- type: :development
24
- prerelease: false
25
- version_requirements: !ruby/object:Gem::Requirement
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 2.4.12
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '3'
33
- - !ruby/object:Gem::Dependency
34
- name: rspec
14
+ name: pry
35
15
  requirement: !ruby/object:Gem::Requirement
36
16
  requirements:
37
17
  - - ">="
38
18
  - !ruby/object:Gem::Version
39
- version: 3.12.0
40
- - - "<"
41
- - !ruby/object:Gem::Version
42
- version: '4'
19
+ version: '0.14'
43
20
  type: :development
44
21
  prerelease: false
45
22
  version_requirements: !ruby/object:Gem::Requirement
46
23
  requirements:
47
24
  - - ">="
48
25
  - !ruby/object:Gem::Version
49
- version: 3.12.0
50
- - - "<"
51
- - !ruby/object:Gem::Version
52
- version: '4'
26
+ version: '0.14'
53
27
  - !ruby/object:Gem::Dependency
54
28
  name: rake
55
29
  requirement: !ruby/object:Gem::Requirement
@@ -71,32 +45,32 @@ dependencies:
71
45
  - !ruby/object:Gem::Version
72
46
  version: '14'
73
47
  - !ruby/object:Gem::Dependency
74
- name: yard
48
+ name: redcarpet
75
49
  requirement: !ruby/object:Gem::Requirement
76
50
  requirements:
77
51
  - - ">="
78
52
  - !ruby/object:Gem::Version
79
- version: 0.9.34
53
+ version: 3.6.0
80
54
  - - "<"
81
55
  - !ruby/object:Gem::Version
82
- version: '1'
56
+ version: '4'
83
57
  type: :development
84
58
  prerelease: false
85
59
  version_requirements: !ruby/object:Gem::Requirement
86
60
  requirements:
87
61
  - - ">="
88
62
  - !ruby/object:Gem::Version
89
- version: 0.9.34
63
+ version: 3.6.0
90
64
  - - "<"
91
65
  - !ruby/object:Gem::Version
92
- version: '1'
66
+ version: '4'
93
67
  - !ruby/object:Gem::Dependency
94
- name: redcarpet
68
+ name: rspec
95
69
  requirement: !ruby/object:Gem::Requirement
96
70
  requirements:
97
71
  - - ">="
98
72
  - !ruby/object:Gem::Version
99
- version: 3.6.0
73
+ version: 3.12.0
100
74
  - - "<"
101
75
  - !ruby/object:Gem::Version
102
76
  version: '4'
@@ -106,44 +80,58 @@ dependencies:
106
80
  requirements:
107
81
  - - ">="
108
82
  - !ruby/object:Gem::Version
109
- version: 3.6.0
83
+ version: 3.12.0
110
84
  - - "<"
111
85
  - !ruby/object:Gem::Version
112
86
  version: '4'
113
87
  - !ruby/object:Gem::Dependency
114
- name: pry
88
+ name: yard
115
89
  requirement: !ruby/object:Gem::Requirement
116
90
  requirements:
117
- - - ">="
91
+ - - "~>"
118
92
  - !ruby/object:Gem::Version
119
- version: '0.14'
93
+ version: 0.9.34
120
94
  type: :development
121
95
  prerelease: false
122
96
  version_requirements: !ruby/object:Gem::Requirement
123
97
  requirements:
124
- - - ">="
98
+ - - "~>"
125
99
  - !ruby/object:Gem::Version
126
- version: '0.14'
100
+ version: 0.9.34
127
101
  - !ruby/object:Gem::Dependency
128
102
  name: ecoportal-api
129
103
  requirement: !ruby/object:Gem::Requirement
130
104
  requirements:
131
- - - ">="
132
- - !ruby/object:Gem::Version
133
- version: 0.9.6
134
- - - "<"
105
+ - - "~>"
135
106
  - !ruby/object:Gem::Version
136
107
  version: '0.10'
137
108
  type: :runtime
138
109
  prerelease: false
139
110
  version_requirements: !ruby/object:Gem::Requirement
140
111
  requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '0.10'
115
+ - !ruby/object:Gem::Dependency
116
+ name: mime-types
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '3.5'
141
122
  - - ">="
142
123
  - !ruby/object:Gem::Version
143
- version: 0.9.6
144
- - - "<"
124
+ version: 3.5.2
125
+ type: :runtime
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
145
130
  - !ruby/object:Gem::Version
146
- version: '0.10'
131
+ version: '3.5'
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: 3.5.2
147
135
  description:
148
136
  email:
149
137
  - rien@ecoportal.co.nz
@@ -154,8 +142,10 @@ extensions: []
154
142
  extra_rdoc_files: []
155
143
  files:
156
144
  - ".gitignore"
145
+ - ".markdownlint.json"
157
146
  - ".rspec"
158
147
  - ".rubocop.yml"
148
+ - ".ruby-version"
159
149
  - ".yardopts"
160
150
  - CHANGELOG.md
161
151
  - Gemfile
@@ -169,6 +159,7 @@ files:
169
159
  - lib/ecoportal/api/common.v2.rb
170
160
  - lib/ecoportal/api/common/concerns.rb
171
161
  - lib/ecoportal/api/common/concerns/benchmarkable.rb
162
+ - lib/ecoportal/api/common/concerns/threadable.rb
172
163
  - lib/ecoportal/api/common/content.rb
173
164
  - lib/ecoportal/api/common/content/array_model.rb
174
165
  - lib/ecoportal/api/common/content/class_helpers.rb
@@ -249,11 +240,19 @@ files:
249
240
  - lib/ecoportal/api/v2/registers/stage_result.rb
250
241
  - lib/ecoportal/api/v2/registers/stages_result.rb
251
242
  - lib/ecoportal/api/v2/registers/template.rb
243
+ - lib/ecoportal/api/v2/s3.rb
244
+ - lib/ecoportal/api/v2/s3/data.rb
245
+ - lib/ecoportal/api/v2/s3/files.rb
246
+ - lib/ecoportal/api/v2/s3/files/batch_upload.rb
247
+ - lib/ecoportal/api/v2/s3/files/poll.rb
248
+ - lib/ecoportal/api/v2/s3/files/poll_status.rb
249
+ - lib/ecoportal/api/v2/s3/upload.rb
252
250
  - lib/ecoportal/api/v2_version.rb
253
251
  homepage: https://www.ecoportal.com
254
252
  licenses:
255
253
  - MIT
256
- metadata: {}
254
+ metadata:
255
+ rubygems_mfa_required: 'true'
257
256
  post_install_message:
258
257
  rdoc_options: []
259
258
  require_paths:
@@ -262,14 +261,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
262
261
  requirements:
263
262
  - - ">="
264
263
  - !ruby/object:Gem::Version
265
- version: 2.7.2
264
+ version: 3.2.2
266
265
  required_rubygems_version: !ruby/object:Gem::Requirement
267
266
  requirements:
268
267
  - - ">="
269
268
  - !ruby/object:Gem::Version
270
269
  version: '0'
271
270
  requirements: []
272
- rubygems_version: 3.4.12
271
+ rubygems_version: 3.5.6
273
272
  signing_key:
274
273
  specification_version: 4
275
274
  summary: A collection of helpers for interacting with the ecoPortal MS's V2 API