panda 0.6.4 → 1.0.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.
- data/Gemfile +10 -0
- data/README.md +388 -21
- data/Rakefile +4 -17
- data/VERSION +1 -1
- data/lib/panda.rb +21 -1
- data/lib/panda/api_authentication.rb +4 -4
- data/lib/panda/base.rb +102 -0
- data/lib/panda/connection.rb +189 -0
- data/lib/panda/error.rb +29 -0
- data/lib/panda/modules/associations.rb +55 -0
- data/lib/panda/modules/builders.rb +34 -0
- data/lib/panda/modules/cloud_connection.rb +7 -0
- data/lib/panda/modules/finders.rb +62 -0
- data/lib/panda/modules/router.rb +55 -0
- data/lib/panda/modules/short_status.rb +13 -0
- data/lib/panda/modules/updatable.rb +27 -0
- data/lib/panda/panda.rb +21 -170
- data/lib/panda/proxies/encoding_scope.rb +48 -0
- data/lib/panda/proxies/profile_scope.rb +7 -0
- data/lib/panda/proxies/proxy.rb +25 -0
- data/lib/panda/proxies/scope.rb +87 -0
- data/lib/panda/proxies/video_scope.rb +28 -0
- data/lib/panda/resources/cloud.rb +49 -0
- data/lib/panda/resources/encoding.rb +30 -0
- data/lib/panda/resources/profile.rb +22 -0
- data/lib/panda/resources/resource.rb +52 -0
- data/lib/panda/resources/video.rb +13 -0
- data/panda.gemspec +36 -12
- data/spec/cloud_spec.rb +80 -0
- data/spec/encoding_spec.rb +232 -0
- data/spec/heroku_spec.rb +22 -0
- data/spec/panda_spec.rb +17 -4
- data/spec/profile_spec.rb +117 -0
- data/spec/spec_helper.rb +8 -1
- data/spec/video_spec.rb +305 -0
- metadata +33 -23
- data/log/debug.log +0 -0
data/Rakefile
CHANGED
@@ -10,10 +10,7 @@ begin
|
|
10
10
|
gem.email = "info@pandastream.com"
|
11
11
|
gem.homepage = "http://github.com/newbamboo/panda_gem"
|
12
12
|
gem.authors = ["New Bamboo"]
|
13
|
-
gem.
|
14
|
-
gem.add_development_dependency "webmock"
|
15
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
-
gem.add_dependency "ruby-hmac", ">= 0.3.2"
|
13
|
+
gem.add_dependency "ruby-hmac", ">= 0.3.2"
|
17
14
|
gem.add_dependency "rest-client", ">= 1.4"
|
18
15
|
gem.add_dependency "json", ">= 1.2"
|
19
16
|
end
|
@@ -22,19 +19,9 @@ rescue LoadError
|
|
22
19
|
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
23
20
|
end
|
24
21
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
spec.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
-
spec.ruby_opts = ['-rrubygems']
|
22
|
+
desc "Run all the specs"
|
23
|
+
task :spec do
|
24
|
+
system "bundle exec spec -O spec/spec.opts spec"
|
30
25
|
end
|
31
26
|
|
32
|
-
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
33
|
-
spec.libs << 'lib' << 'spec'
|
34
|
-
spec.pattern = 'spec/**/*_spec.rb'
|
35
|
-
spec.rcov = true
|
36
|
-
end
|
37
|
-
|
38
|
-
task :spec => :check_dependencies
|
39
|
-
|
40
27
|
task :default => :spec
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
data/lib/panda.rb
CHANGED
@@ -1,2 +1,22 @@
|
|
1
1
|
require 'panda/api_authentication'
|
2
|
-
require 'panda/
|
2
|
+
require 'panda/connection'
|
3
|
+
require 'panda/modules/router'
|
4
|
+
require 'panda/modules/finders'
|
5
|
+
require 'panda/modules/builders'
|
6
|
+
require 'panda/modules/associations'
|
7
|
+
require 'panda/modules/updatable'
|
8
|
+
require 'panda/modules/short_status'
|
9
|
+
require 'panda/modules/cloud_connection'
|
10
|
+
require 'panda/proxies/proxy'
|
11
|
+
require 'panda/proxies/scope'
|
12
|
+
require 'panda/proxies/encoding_scope'
|
13
|
+
require 'panda/proxies/video_scope'
|
14
|
+
require 'panda/proxies/profile_scope'
|
15
|
+
require 'panda/error'
|
16
|
+
require 'panda/base'
|
17
|
+
require 'panda/resources/resource'
|
18
|
+
require 'panda/resources/cloud'
|
19
|
+
require 'panda/resources/encoding'
|
20
|
+
require 'panda/resources/profile'
|
21
|
+
require 'panda/resources/video'
|
22
|
+
require 'panda/panda'
|
@@ -5,19 +5,19 @@ require 'hmac'
|
|
5
5
|
require 'hmac-sha2'
|
6
6
|
require 'base64'
|
7
7
|
|
8
|
-
|
8
|
+
module Panda
|
9
9
|
class ApiAuthentication
|
10
10
|
def self.generate_signature(verb, request_uri, host, secret_key, params_given={})
|
11
11
|
# Ensure all param keys are strings
|
12
12
|
params = {}; params_given.each {|k,v| params[k.to_s] = v }
|
13
|
-
|
13
|
+
|
14
14
|
query_string = canonical_querystring(params)
|
15
|
-
|
15
|
+
|
16
16
|
string_to_sign = verb.to_s.upcase + "\n" +
|
17
17
|
host.downcase + "\n" +
|
18
18
|
request_uri + "\n" +
|
19
19
|
query_string
|
20
|
-
|
20
|
+
|
21
21
|
hmac = HMAC::SHA256.new( secret_key )
|
22
22
|
hmac.update( string_to_sign )
|
23
23
|
# chomp is important! the base64 encoded version will have a newline at the end
|
data/lib/panda/base.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
module Panda
|
2
|
+
class Base
|
3
|
+
attr_accessor :attributes, :errors
|
4
|
+
include Panda::Router
|
5
|
+
|
6
|
+
def initialize(attributes = {})
|
7
|
+
init_load
|
8
|
+
load(attributes)
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def id(this_id)
|
13
|
+
find(this_id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def end_class_name
|
17
|
+
"#{name.split('::').last}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def changed?
|
22
|
+
!@changed_attributes.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
def new?
|
26
|
+
id.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def delete
|
30
|
+
response = connection.delete(object_url_map(self.class.one_path))
|
31
|
+
!!response['deleted']
|
32
|
+
end
|
33
|
+
|
34
|
+
def id
|
35
|
+
attributes['id']
|
36
|
+
end
|
37
|
+
|
38
|
+
def id=(id)
|
39
|
+
attributes['id'] = id
|
40
|
+
end
|
41
|
+
|
42
|
+
def reload
|
43
|
+
perform_reload
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_json
|
48
|
+
attributes.to_json
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def perform_reload(args={})
|
54
|
+
raise "RecordNotFound" if new?
|
55
|
+
|
56
|
+
url = self.class.object_url(self.class.one_path, :id => id)
|
57
|
+
response = connection.get(url)
|
58
|
+
init_load
|
59
|
+
load_response(response.merge(args))
|
60
|
+
end
|
61
|
+
|
62
|
+
def init_load
|
63
|
+
@attributes = {}
|
64
|
+
@changed_attributes = {}
|
65
|
+
@errors = []
|
66
|
+
end
|
67
|
+
|
68
|
+
def load(attributes)
|
69
|
+
attributes.each do |key, value|
|
70
|
+
@attributes[key.to_s] = value
|
71
|
+
@changed_attributes[key.to_s] = value if !(attributes['id'] || attributes[:id])
|
72
|
+
end
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
def load_response(response)
|
77
|
+
if response['error'] || response['id'].nil?
|
78
|
+
!(@errors << Error.new(response))
|
79
|
+
else
|
80
|
+
@errors=[]
|
81
|
+
load(response)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def method_missing(method_symbol, *arguments)
|
86
|
+
method_name = method_symbol.to_s
|
87
|
+
if method_name =~ /(=|\?)$/
|
88
|
+
case $1
|
89
|
+
when '='
|
90
|
+
attributes[$`] = arguments.first
|
91
|
+
@changed_attributes[$`] = arguments.first
|
92
|
+
when '?'
|
93
|
+
!! attributes[$`]
|
94
|
+
end
|
95
|
+
else
|
96
|
+
return attributes[method_name] if attributes.include?(method_name)
|
97
|
+
super
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module Panda
|
2
|
+
class Connection
|
3
|
+
attr_accessor :api_host, :api_port, :access_key, :secret_key, :api_version, :cloud_id, :format
|
4
|
+
|
5
|
+
API_PORT=80
|
6
|
+
US_API_HOST="api.pandastream.com"
|
7
|
+
EU_API_HOST="api.eu.pandastream.com"
|
8
|
+
|
9
|
+
def initialize(auth_params={}, options={})
|
10
|
+
@raise_error = false
|
11
|
+
@api_version = 2
|
12
|
+
@format = "hash"
|
13
|
+
|
14
|
+
if auth_params.class == String
|
15
|
+
self.format = options[:format] || options["format"]
|
16
|
+
init_from_uri(auth_params)
|
17
|
+
else
|
18
|
+
self.format = auth_params[:format] || auth_params["format"]
|
19
|
+
init_from_hash(auth_params)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Set the correct api_host for US/EU
|
24
|
+
def region=(region)
|
25
|
+
if(region.to_s == "us")
|
26
|
+
self.api_host = US_API_HOST
|
27
|
+
elsif(region.to_s == "eu")
|
28
|
+
self.api_host = EU_API_HOST
|
29
|
+
else
|
30
|
+
raise "Region Unknown"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Setup connection for Heroku
|
35
|
+
def heroku=(url)
|
36
|
+
init_from_uri(url)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Raise exception on non JSON parsable response if set
|
40
|
+
def raise_error=(bool)
|
41
|
+
@raise_error = bool
|
42
|
+
end
|
43
|
+
|
44
|
+
# Setup respond type JSON / Hash
|
45
|
+
def format=(ret_format)
|
46
|
+
if ret_format
|
47
|
+
raise "Format unknown" if !["json", "hash"].include?(ret_format.to_s)
|
48
|
+
@format = ret_format.to_s
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Authenticated requests
|
53
|
+
|
54
|
+
def get(request_uri, params={})
|
55
|
+
@connection = RestClient::Resource.new(api_url)
|
56
|
+
rescue_restclient_exception do
|
57
|
+
query = signed_query("GET", request_uri, params)
|
58
|
+
body_of @connection[request_uri + '?' + query].get
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def post(request_uri, params={})
|
63
|
+
@connection = RestClient::Resource.new(api_url)
|
64
|
+
rescue_restclient_exception do
|
65
|
+
body_of @connection[request_uri].post(signed_params("POST", request_uri, params))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def put(request_uri, params={})
|
70
|
+
@connection = RestClient::Resource.new(api_url)
|
71
|
+
rescue_restclient_exception do
|
72
|
+
body_of @connection[request_uri].put(signed_params("PUT", request_uri, params))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def delete(request_uri, params={})
|
77
|
+
@connection = RestClient::Resource.new(api_url)
|
78
|
+
rescue_restclient_exception do
|
79
|
+
query = signed_query("DELETE", request_uri, params)
|
80
|
+
body_of @connection[request_uri + '?' + query].delete
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Signing methods
|
85
|
+
def signed_query(*args)
|
86
|
+
ApiAuthentication.hash_to_query(signed_params(*args))
|
87
|
+
end
|
88
|
+
|
89
|
+
def signed_params(verb, request_uri, params = {}, timestamp_str = nil)
|
90
|
+
auth_params = stringify_keys(params)
|
91
|
+
auth_params['cloud_id'] = @cloud_id
|
92
|
+
auth_params['access_key'] = @access_key
|
93
|
+
auth_params['timestamp'] = timestamp_str || Time.now.iso8601(6)
|
94
|
+
|
95
|
+
params_to_sign = auth_params.reject{|k,v| ['file'].include?(k.to_s)}
|
96
|
+
auth_params['signature'] = ApiAuthentication.generate_signature(verb, request_uri, @api_host, @secret_key, params_to_sign)
|
97
|
+
auth_params
|
98
|
+
end
|
99
|
+
|
100
|
+
def api_url
|
101
|
+
"http://#{@api_host}:#{@api_port}/#{@prefix}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Shortcut to setup your bucket
|
105
|
+
def setup_bucket(params={})
|
106
|
+
granting_params = {
|
107
|
+
:s3_videos_bucket => params[:bucket],
|
108
|
+
:user_aws_key => params[:access_key],
|
109
|
+
:user_aws_secret => params[:secret_key]
|
110
|
+
}
|
111
|
+
|
112
|
+
put("/clouds/#{@cloud_id}.json", granting_params)
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_hash
|
116
|
+
hash = {}
|
117
|
+
[:api_host, :api_port, :access_key, :secret_key, :api_version, :cloud_id].each do |a|
|
118
|
+
hash[a] = send(a)
|
119
|
+
end
|
120
|
+
hash
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
def stringify_keys(params)
|
125
|
+
params.inject({}) do |options, (key, value)|
|
126
|
+
options[key.to_s] = value
|
127
|
+
options
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def rescue_restclient_exception(&block)
|
132
|
+
begin
|
133
|
+
yield
|
134
|
+
rescue RestClient::Exception => e
|
135
|
+
format_to(e.http_body)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# API change on rest-client 1.4
|
140
|
+
def body_of(response)
|
141
|
+
json_response = response.respond_to?(:body) ? response.body : response
|
142
|
+
format_to(json_response)
|
143
|
+
end
|
144
|
+
|
145
|
+
def format_to(response)
|
146
|
+
begin
|
147
|
+
if self.format == "json"
|
148
|
+
response
|
149
|
+
elsif defined?(ActiveSupport::JSON)
|
150
|
+
ActiveSupport::JSON.decode(response)
|
151
|
+
else
|
152
|
+
JSON.parse(response)
|
153
|
+
end
|
154
|
+
rescue JSON::ParserError => e
|
155
|
+
# if not used with PandaResources
|
156
|
+
# don't raise Service Not Available because
|
157
|
+
# maybe the host, the url, or anything is wrongly setup
|
158
|
+
if @raise_error
|
159
|
+
raise ServiceNotAvailable.new
|
160
|
+
else
|
161
|
+
raise e
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def init_from_uri(uri)
|
167
|
+
heroku_uri = URI.parse(uri)
|
168
|
+
|
169
|
+
@access_key = heroku_uri.user
|
170
|
+
@secret_key = heroku_uri.password
|
171
|
+
@cloud_id = heroku_uri.path[1..-1]
|
172
|
+
@api_host = heroku_uri.host
|
173
|
+
@api_port = heroku_uri.port
|
174
|
+
@prefix = "v#{@api_version}"
|
175
|
+
end
|
176
|
+
|
177
|
+
def init_from_hash(hash_params)
|
178
|
+
params = { :api_host => US_API_HOST, :api_port => API_PORT }.merge!(hash_params)
|
179
|
+
|
180
|
+
@cloud_id = params["cloud_id"] || params[:cloud_id]
|
181
|
+
@access_key = params["access_key"] || params[:access_key]
|
182
|
+
@secret_key = params["secret_key"] || params[:secret_key]
|
183
|
+
@api_host = params["api_host"] || params[:api_host]
|
184
|
+
@api_port = params["api_port"] || params[:api_port]
|
185
|
+
@prefix = params["prefix_url"] || "v#{@api_version}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
data/lib/panda/error.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Panda
|
2
|
+
class Error
|
3
|
+
attr_reader :message
|
4
|
+
attr_reader :error_class
|
5
|
+
attr_reader :original_hash
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@original_hash = options
|
9
|
+
@message = options['message']
|
10
|
+
@error_class = options['error']
|
11
|
+
end
|
12
|
+
|
13
|
+
def raise!
|
14
|
+
raise(self.to_s)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"#{@error_class}: #{@message}"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class ServiceNotAvailable < StandardError
|
24
|
+
def initialize
|
25
|
+
super("ServiceNotAvailable")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Panda
|
2
|
+
module Associations
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
def has_one(relation_name)
|
10
|
+
# for example creates : @video ||= VideoScope.new(self)
|
11
|
+
|
12
|
+
define_method relation_name do
|
13
|
+
param_id = "#{relation_name}_id"
|
14
|
+
if instance_var = instance_variable_get("@#{relation_name}")
|
15
|
+
instance_var
|
16
|
+
else
|
17
|
+
@associations ||= []
|
18
|
+
@associations << relation_name
|
19
|
+
instance_variable_set("@#{relation_name}",
|
20
|
+
Panda::const_get(relation_name.to_s.capitalize).find(send(param_id.to_sym)))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_many(relation_name)
|
26
|
+
# for example creates : @encodings ||= EncodingScope.new(self)
|
27
|
+
|
28
|
+
define_method relation_name do
|
29
|
+
model_name = "#{relation_name.to_s[0..-2].capitalize}"
|
30
|
+
if instance_var = instance_variable_get("@#{relation_name}")
|
31
|
+
instance_var
|
32
|
+
else
|
33
|
+
@associations ||= []
|
34
|
+
@associations << relation_name
|
35
|
+
instance_variable_set("@#{relation_name}",
|
36
|
+
Panda::const_get("#{model_name}Scope").new(self))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
alias :belongs_to :has_one
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def reset_associations
|
47
|
+
if @associations
|
48
|
+
@associations.each do |a|
|
49
|
+
instance_variable_set("@#{a}",nil)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|