shotgrid_api_ruby 0.1.2

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