shotgrid_api_ruby 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
data/bin/console ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'shotgrid_api_ruby'
6
+
7
+ require 'dotenv/load'
8
+
9
+ if ENV['SHOTGUN_SITE_NAME'] && ENV['SCRIPT_NAME'] && ENV['SCRIPT_KEY']
10
+ $cl =
11
+ ShotgridApiRuby.new(
12
+ shotgun_site: ENV['SHOTGUN_SITE_NAME'],
13
+ auth: {
14
+ client_id: ENV['SCRIPT_NAME'],
15
+ client_secret: ENV['SCRIPT_KEY'],
16
+ },
17
+ )
18
+ end
19
+ if ENV['SITE_NAME'] && ENV['SCRIPT_NAME'] && ENV['SCRIPT_KEY']
20
+ $cl =
21
+ ShotgridApiRuby.new(
22
+ shotgrid_site: ENV['SITE_NAME'],
23
+ auth: {
24
+ client_id: ENV['SCRIPT_NAME'],
25
+ client_secret: ENV['SCRIPT_KEY'],
26
+ },
27
+ )
28
+ end
29
+ if ENV['SITE_URL'] && ENV['SCRIPT_NAME'] && ENV['SCRIPT_KEY']
30
+ $cl =
31
+ ShotgridApiRuby.new(
32
+ site_url: ENV['SITE_URL'],
33
+ auth: {
34
+ client_id: ENV['SCRIPT_NAME'],
35
+ client_secret: ENV['SCRIPT_KEY'],
36
+ },
37
+ )
38
+ end
39
+
40
+ # You can add fixtures and/or initialization code here to make experimenting
41
+ # with your gem easier. You can also use a different console, if you like.
42
+
43
+ # (If you use this, don't forget to add pry to your Gemfile!)
44
+ # require "pry"
45
+ # Pry.start
46
+
47
+ require 'pry'
48
+ Pry.start(__FILE__)
data/bin/prettirun ADDED
@@ -0,0 +1 @@
1
+ yarn prettier -c './**/*.rb'
data/bin/ruborun ADDED
@@ -0,0 +1 @@
1
+ bundle exec rubocop -P
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+ overcommit --install
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # zeitwerk will take care of auto loading files based on their name :)
4
+ require 'zeitwerk'
5
+ require 'active_support/core_ext/string/inflections'
6
+ require 'ostruct'
7
+ require 'faraday'
8
+ require 'json'
9
+
10
+ loader = Zeitwerk::Loader.for_gem
11
+ loader.setup # ready!
12
+
13
+ module ShotgridApiRuby
14
+ def self.new(**args)
15
+ Client.new(**args)
16
+ end
17
+ end
18
+
19
+ loader.eager_load
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShotgridApiRuby
4
+ # Faraday middleware responsible for authentication with
5
+ # the shotgrid site
6
+ class Auth < Faraday::Middleware
7
+ # Validate auth parameters format
8
+ module Validator
9
+ # Validate auth parameters format
10
+ #
11
+ # @param []
12
+ def self.valid?(
13
+ client_id: nil,
14
+ client_secret: nil,
15
+ username: nil,
16
+ password: nil,
17
+ session_token: nil,
18
+ refresh_token: nil
19
+ )
20
+ (client_id && client_secret) || (password && username) ||
21
+ session_token || refresh_token
22
+ end
23
+ end
24
+
25
+ def initialize(app = nil, options = {})
26
+ raise 'missing auth' unless options[:auth]
27
+ raise 'missing site_url' unless options[:site_url]
28
+ unless Validator.valid?(**options[:auth]&.transform_keys(&:to_sym))
29
+ raise 'Auth not valid'
30
+ end
31
+
32
+ super(app)
33
+
34
+ @site_url = options[:site_url]
35
+ @client_id = options[:auth][:client_id]
36
+ @client_secret = options[:auth][:client_secret]
37
+ @username = options[:auth][:username]
38
+ @password = options[:auth][:password]
39
+ @session_token = options[:auth][:session_token]
40
+ @refresh_token = options[:auth][:refresh_token]
41
+ end
42
+
43
+ attr_reader :client_id,
44
+ :client_secret,
45
+ :site_url,
46
+ :username,
47
+ :password,
48
+ :session_token,
49
+ :refresh_token
50
+
51
+ def auth_type
52
+ @auth_type ||=
53
+ begin
54
+ if refresh_token
55
+ 'refresh_token'
56
+ elsif client_id
57
+ 'client_credentials'
58
+ elsif username
59
+ 'password'
60
+ elsif session_token
61
+ 'session_token'
62
+ end
63
+ end
64
+ end
65
+
66
+ def call(request_env)
67
+ request_env[:request_headers].merge!(std_headers)
68
+
69
+ @app.call(request_env)
70
+ end
71
+
72
+ private
73
+
74
+ def auth_params
75
+ @auth_params ||=
76
+ begin
77
+ case auth_type
78
+ when 'refresh_token'
79
+ "refresh_token=#{refresh_token}&grant_type=refresh_token"
80
+ when 'client_credentials'
81
+ "client_id=#{client_id}&client_secret=#{
82
+ client_secret
83
+ }&grant_type=client_credentials"
84
+ when 'password'
85
+ "username=#{username}&password=#{password}&grant_type=password"
86
+ when 'session_token'
87
+ "session_token=#{session_token}&grant_type=session_token"
88
+ else
89
+ raise 'Not a valid/implemented auth type'
90
+ end
91
+ end
92
+ end
93
+
94
+ def auth_url
95
+ @auth_url ||= "#{site_url}/auth/access_token?#{auth_params}"
96
+ end
97
+
98
+ def access_token
99
+ ((@access_token && Time.now < @token_expiry) || sign_in) && @access_token
100
+ end
101
+
102
+ def sign_in
103
+ resp =
104
+ Faraday.post(auth_url) do |req|
105
+ req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
106
+ req.headers['Accept'] = 'application/json'
107
+ end
108
+ resp_body = JSON.parse(resp.body)
109
+
110
+ raise "Can't login: #{resp_body['errors']}" if resp.status >= 300
111
+
112
+ @access_token = resp_body['access_token']
113
+ @token_expiry = Time.now + resp_body['expires_in']
114
+ @refresh_token = resp_body['refresh_token']
115
+ end
116
+
117
+ def std_headers
118
+ {
119
+ 'Accept' => 'application/json',
120
+ 'Authorization' => "Bearer #{access_token}",
121
+ }
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShotgridApiRuby
4
+ # Main class for connection.
5
+ #
6
+ # This should be only instanciated once to re-use tokens
7
+ class Client
8
+ # Faraday connection
9
+ attr_reader :connection
10
+
11
+ def initialize(auth:, site_url: nil, shotgun_site: nil, shotgrid_site: nil)
12
+ raise 'No site given' unless site_url || shotgun_site || shotgrid_site
13
+ raise 'auth param not valid' unless auth && Auth::Validator.valid?(**auth)
14
+
15
+ site_url ||=
16
+ if shotgun_site
17
+ "https://#{shotgun_site}.shotgunstudio.com/api/v1"
18
+ elsif shotgrid_site
19
+ "https://#{shotgrid_site}.shotgrid.autodesk.com/api/v1"
20
+ end
21
+
22
+ @connection =
23
+ Faraday.new(url: site_url) do |faraday|
24
+ faraday.use(ShotgridApiRuby::Auth, auth: auth, site_url: site_url)
25
+ faraday.adapter Faraday.default_adapter
26
+ end
27
+ end
28
+
29
+ # Access preferences APIs
30
+ def preferences
31
+ @preferences = Preferences.new(connection)
32
+ end
33
+
34
+ # Access server_info APIs
35
+ def server_info
36
+ @server_info || ServerInfo.new(connection)
37
+ end
38
+
39
+ # Access entities related APIs
40
+ def entities(type)
41
+ public_send(type)
42
+ end
43
+
44
+ def respond_to_missing?(_name, _include_private = false)
45
+ true
46
+ end
47
+
48
+ def method_missing(name, *args, &block)
49
+ if args.empty?
50
+ fname = formated_name(name)
51
+ self
52
+ .class
53
+ .define_method(fname) do
54
+ if entities_client = instance_variable_get("@#{fname}")
55
+ entities_client
56
+ else
57
+ entities_client = entities_aux(fname)
58
+ instance_variable_set("@#{fname}", entities_client)
59
+ end
60
+ end
61
+ self.class.instance_eval { alias_method name, fname }
62
+ send(fname)
63
+ else
64
+ super
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def formated_name(name)
71
+ name.to_s.camelize.singularize
72
+ end
73
+
74
+ def entities_aux(type)
75
+ type = formated_name(type)
76
+ @entity_caller = Entities.new(connection, type)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShotgridApiRuby
4
+ class Entities
5
+ def initialize(connection, type)
6
+ @connection = connection.dup
7
+ @type = type
8
+ @base_url_prefix = @connection.url_prefix
9
+ @connection.url_prefix = "#{@connection.url_prefix}/entity/#{type}"
10
+ end
11
+
12
+ attr_reader :connection, :type
13
+
14
+ def first(
15
+ fields: nil,
16
+ sort: nil,
17
+ filter: nil,
18
+ retired: nil,
19
+ include_archived_projects: nil,
20
+ logical_operator: 'and'
21
+ )
22
+ all(
23
+ fields: fields,
24
+ sort: sort,
25
+ filter: filter,
26
+ retired: retired,
27
+ logical_operator: logical_operator,
28
+ include_archived_projects: include_archived_projects,
29
+ page_size: 1,
30
+ ).first
31
+ end
32
+
33
+ def find(id, fields: nil, retired: nil, include_archived_projects: nil)
34
+ params = Params.new
35
+
36
+ params.add_fields(fields)
37
+ params.add_options(retired, include_archived_projects)
38
+
39
+ resp = @connection.get(id.to_s, params)
40
+ resp_body = JSON.parse(resp.body)
41
+
42
+ if resp.status >= 300
43
+ raise "Error while getting #{type}: #{resp_body['errors']}"
44
+ end
45
+
46
+ entity = resp_body['data']
47
+ Entity.new(
48
+ entity['type'],
49
+ OpenStruct.new(entity['attributes']),
50
+ entity['relationships'],
51
+ entity['id'],
52
+ entity['links'],
53
+ )
54
+ end
55
+
56
+ def create(attributes)
57
+ resp =
58
+ @connection.post('', attributes.to_json) do |req|
59
+ req.headers['Content-Type'] = 'application/json'
60
+ end
61
+
62
+ resp_body = JSON.parse(resp.body)
63
+
64
+ if resp.status >= 300
65
+ raise "Error while creating #{type} with #{attributes}: #{
66
+ resp_body['errors']
67
+ }"
68
+ end
69
+
70
+ entity = resp_body['data']
71
+ Entity.new(
72
+ entity['type'],
73
+ OpenStruct.new(entity['attributes']),
74
+ entity['relationships'],
75
+ entity['id'],
76
+ entity['links'],
77
+ )
78
+ end
79
+
80
+ def update(id, changes)
81
+ return find(id) if changes.empty?
82
+
83
+ resp =
84
+ @connection.put(id.to_s, changes.to_json) do |req|
85
+ req.headers['Content-Type'] = 'application/json'
86
+ end
87
+
88
+ resp_body = JSON.parse(resp.body)
89
+
90
+ if resp.status >= 300
91
+ raise "Error while updating #{type}##{id} with #{changes}: #{
92
+ resp_body['errors']
93
+ }"
94
+ end
95
+
96
+ entity = resp_body['data']
97
+ Entity.new(
98
+ entity['type'],
99
+ OpenStruct.new(entity['attributes']),
100
+ entity['relationships'],
101
+ entity['id'],
102
+ entity['links'],
103
+ )
104
+ end
105
+
106
+ def delete(id)
107
+ resp =
108
+ @connection.delete(id.to_s) do |req|
109
+ req.headers['Content-Type'] = 'application/json'
110
+ end
111
+
112
+ if resp.status >= 300
113
+ resp_body = JSON.parse(resp.body)
114
+ raise "Error while deleting #{type}##{id}: #{resp_body['errors']}"
115
+ end
116
+
117
+ true
118
+ end
119
+
120
+ def revive(id)
121
+ resp = @connection.post("#{id}?revive=true")
122
+
123
+ if resp.status >= 300
124
+ resp_body = JSON.parse(resp.body)
125
+ raise "Error while reviving #{type}##{id}: #{resp_body['errors']}"
126
+ end
127
+
128
+ true
129
+ end
130
+
131
+ def all(
132
+ fields: nil,
133
+ logical_operator: 'and',
134
+ sort: nil,
135
+ filter: nil,
136
+ page: nil,
137
+ page_size: nil,
138
+ retired: nil,
139
+ include_archived_projects: nil
140
+ )
141
+ if filter && !Params.filters_are_simple?(filter)
142
+ return(
143
+ search(
144
+ fields: fields,
145
+ logical_operator: logical_operator,
146
+ sort: sort,
147
+ filter: filter,
148
+ page: page,
149
+ page_size: page_size,
150
+ retired: retired,
151
+ include_archived_projects: include_archived_projects,
152
+ )
153
+ )
154
+ end
155
+
156
+ params = Params.new
157
+
158
+ params.add_fields(fields)
159
+ params.add_sort(sort)
160
+ params.add_filter(filter)
161
+ params.add_page(page, page_size)
162
+ params.add_options(retired, include_archived_projects)
163
+
164
+ resp = @connection.get('', params)
165
+ resp_body = JSON.parse(resp.body)
166
+
167
+ if resp.status >= 300
168
+ raise "Error while getting #{type}: #{resp_body['errors']}"
169
+ end
170
+
171
+ resp_body['data'].map do |entity|
172
+ Entity.new(
173
+ entity['type'],
174
+ OpenStruct.new(entity['attributes']),
175
+ entity['relationships'],
176
+ entity['id'],
177
+ entity['links'],
178
+ )
179
+ end
180
+ end
181
+
182
+ def search(
183
+ fields: nil,
184
+ logical_operator: 'and',
185
+ sort: nil,
186
+ filter: nil,
187
+ page: nil,
188
+ page_size: nil,
189
+ retired: nil,
190
+ include_archived_projects: nil
191
+ )
192
+ if filter.nil? || Params.filters_are_simple?(filter)
193
+ return(
194
+ all(
195
+ fields: fields,
196
+ logical_operator: logical_operator,
197
+ sort: sort,
198
+ filter: filter,
199
+ page: page,
200
+ page_size: page_size,
201
+ retired: retired,
202
+ include_archived_projects: include_archived_projects,
203
+ )
204
+ )
205
+ end
206
+ params = Params.new
207
+
208
+ params.add_fields(fields)
209
+ params.add_sort(sort)
210
+ params.add_page(page, page_size)
211
+ params.add_options(retired, include_archived_projects)
212
+ params.add_filter(filter, logical_operator)
213
+
214
+ # In search: The name is filters and not filter
215
+ params[:filters] = params[:filter] if params[:filter]
216
+ params.delete(:filter)
217
+
218
+ resp =
219
+ @connection.post('_search', params) do |req|
220
+ req.headers['Content-Type'] =
221
+ if params[:filters].is_a? Array
222
+ 'application/vnd+shotgun.api3_array+json'
223
+ else
224
+ 'application/vnd+shotgun.api3_hash+json'
225
+ end
226
+ req.body = params.to_h.to_json
227
+ end
228
+ resp_body = JSON.parse(resp.body)
229
+
230
+ if resp.status >= 300
231
+ raise "Error while getting #{type}: #{resp_body['errors']}"
232
+ end
233
+
234
+ resp_body['data'].map do |entity|
235
+ Entity.new(
236
+ entity['type'],
237
+ OpenStruct.new(entity['attributes']),
238
+ entity['relationships'],
239
+ entity['id'],
240
+ entity['links'],
241
+ )
242
+ end
243
+ end
244
+
245
+ def schema_client
246
+ @schema_client ||= Schema.new(connection, type, @base_url_prefix)
247
+ end
248
+
249
+ def schema
250
+ schema_client.read
251
+ end
252
+
253
+ def fields
254
+ schema_client.fields
255
+ end
256
+
257
+ def summary_client
258
+ @summary_client ||= Summarize.new(connection, type, @base_url_prefix)
259
+ end
260
+
261
+ def count(filter: nil, logical_operator: 'and')
262
+ summary_client.count(filter: filter, logical_operator: logical_operator)
263
+ end
264
+
265
+ def summarize(
266
+ filter: nil,
267
+ grouping: nil,
268
+ summary_fields: nil,
269
+ logical_operator: 'and',
270
+ include_archived_projects: nil
271
+ )
272
+ summary_client.summarize(
273
+ filter: filter,
274
+ grouping: grouping,
275
+ summary_fields: summary_fields,
276
+ logical_operator: logical_operator,
277
+ include_archived_projects: include_archived_projects,
278
+ )
279
+ end
280
+ end
281
+ end