howkast 0.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.
@@ -0,0 +1,2 @@
1
+ 0.0.0 [UNRELEASED]
2
+ - Initial release.
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "howkast"
16
+ gem.homepage = "http://github.com/jurisgalang/howkast"
17
+ gem.summary = %Q{Ruby bindings for the [Howcast](http://howcast.com) API}
18
+ gem.description = %Q{
19
+ This gem implements Ruby bindings for the [Howcast](http://howcast.com) API
20
+ Howcast empowers people with engaging, useful how-to information wherever, whenever they need to know how.
21
+ }
22
+ gem.email = "jurisgalang@gmail.com"
23
+ gem.authors = ["Juris Galang"]
24
+ gem.license = ["MIT", "GPL"]
25
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
26
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
27
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
28
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
29
+ end
30
+ Jeweler::RubygemsDotOrgTasks.new
31
+
32
+ require 'rspec/core'
33
+ require 'rspec/core/rake_task'
34
+ RSpec::Core::RakeTask.new(:spec) do |spec|
35
+ spec.pattern = FileList['spec/**/*_spec.rb']
36
+ end
37
+
38
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
39
+ spec.pattern = 'spec/**/*_spec.rb'
40
+ spec.rcov = true
41
+ end
42
+
43
+ require 'cucumber/rake/task'
44
+ Cucumber::Rake::Task.new(:features)
45
+
46
+ task :default => :spec
47
+
48
+ require 'yard'
49
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,9 @@
1
+ Feature: something something
2
+ In order to something something
3
+ A user something something
4
+ something something something
5
+
6
+ Scenario: something something
7
+ Given inspiration
8
+ When I create a sweet new gem
9
+ Then everyone should see how awesome I am
@@ -0,0 +1,13 @@
1
+ require 'bundler'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts "Run `bundle install` to install missing gems"
7
+ exit e.status_code
8
+ end
9
+
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
11
+ require 'howkast'
12
+
13
+ require 'rspec/expectations'
@@ -0,0 +1,109 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{howkast}
8
+ s.version = "0.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Juris Galang"]
12
+ s.date = %q{2011-01-03}
13
+ s.description = %q{
14
+ This gem implements Ruby bindings for the [Howcast](http://howcast.com) API
15
+ Howcast empowers people with engaging, useful how-to information wherever, whenever they need to know how.
16
+ }
17
+ s.email = %q{jurisgalang@gmail.com}
18
+ s.extra_rdoc_files = [
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".rspec",
24
+ "GPL-LICENSE",
25
+ "Gemfile",
26
+ "Gemfile.lock",
27
+ "MIT-LICENSE",
28
+ "README.md",
29
+ "RELEASENOTES",
30
+ "Rakefile",
31
+ "VERSION",
32
+ "features/howkast.feature",
33
+ "features/step_definitions/howkast_steps.rb",
34
+ "features/support/env.rb",
35
+ "howkast.gemspec",
36
+ "lib/howkast.rb",
37
+ "lib/howkast/api.rb",
38
+ "lib/howkast/base.rb",
39
+ "lib/howkast/configuration.rb",
40
+ "lib/howkast/errors.rb",
41
+ "lib/howkast/ext/icebox.rb",
42
+ "lib/howkast/ext/string.rb",
43
+ "lib/howkast/model.rb",
44
+ "lib/howkast/processors/base.rb",
45
+ "lib/howkast/processors/categories.rb",
46
+ "lib/howkast/processors/playlists.rb",
47
+ "lib/howkast/processors/search.rb",
48
+ "lib/howkast/processors/users.rb",
49
+ "lib/howkast/processors/videos.rb",
50
+ "sample/adhoc.rb",
51
+ "sample/quickstart.rb",
52
+ "spec/howkast/api_service_spec.rb",
53
+ "spec/howkast/api_spec.rb",
54
+ "spec/howkast/model_spec.rb",
55
+ "spec/howkast/processors/categories_spec.rb",
56
+ "spec/howkast/processors/playlists_spec.rb",
57
+ "spec/howkast/processors/search_spec.rb",
58
+ "spec/howkast/processors/users_spec.rb",
59
+ "spec/howkast/processors/videos_spec.rb",
60
+ "spec/howkast_spec.rb",
61
+ "spec/spec_helper.rb"
62
+ ]
63
+ s.homepage = %q{http://github.com/jurisgalang/howkast}
64
+ s.licenses = [["MIT", "GPL"]]
65
+ s.require_paths = ["lib"]
66
+ s.rubygems_version = %q{1.3.7}
67
+ s.summary = %q{Ruby bindings for the [Howcast](http://howcast.com) API}
68
+ s.test_files = [
69
+ "spec/howkast/api_service_spec.rb",
70
+ "spec/howkast/api_spec.rb",
71
+ "spec/howkast/model_spec.rb",
72
+ "spec/howkast/processors/categories_spec.rb",
73
+ "spec/howkast/processors/playlists_spec.rb",
74
+ "spec/howkast/processors/search_spec.rb",
75
+ "spec/howkast/processors/users_spec.rb",
76
+ "spec/howkast/processors/videos_spec.rb",
77
+ "spec/howkast_spec.rb",
78
+ "spec/spec_helper.rb"
79
+ ]
80
+
81
+ if s.respond_to? :specification_version then
82
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
83
+ s.specification_version = 3
84
+
85
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
86
+ s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
87
+ s.add_development_dependency(%q<yard>, ["~> 0.6.0"])
88
+ s.add_development_dependency(%q<cucumber>, [">= 0"])
89
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
90
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
91
+ s.add_development_dependency(%q<rcov>, [">= 0"])
92
+ else
93
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
94
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
95
+ s.add_dependency(%q<cucumber>, [">= 0"])
96
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
97
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
98
+ s.add_dependency(%q<rcov>, [">= 0"])
99
+ end
100
+ else
101
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
102
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
103
+ s.add_dependency(%q<cucumber>, [">= 0"])
104
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
105
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
106
+ s.add_dependency(%q<rcov>, [">= 0"])
107
+ end
108
+ end
109
+
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ require 'time'
6
+ require 'singleton'
7
+ require 'active_support/memoizable'
8
+ require 'httparty'
9
+ require 'named-parameters'
10
+
11
+ require 'howkast/ext/string'
12
+ #require 'howkast/ext/icebox'
13
+
14
+ require 'howkast/configuration'
15
+ require 'howkast/errors'
16
+ require 'howkast/model'
17
+ require 'howkast/base'
18
+ require 'howkast/api'
19
+
20
+ Dir[File.join(File.dirname(__FILE__), 'howkast', 'processors', '*')].each do |processor|
21
+ require processor
22
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ module Howkast
4
+ class API
5
+ service :video,
6
+ processor: :videos,
7
+ options: { :required => :id }
8
+
9
+ service :videos,
10
+ options: { :required => [ :sort, :filter ],
11
+ :optional => [ :category, :page ] }
12
+
13
+ service :search,
14
+ options: { :required => :query,
15
+ :optional => { :view => :video } }
16
+
17
+ service :user,
18
+ processor: :users,
19
+ options: { :required => [ :id, :resource ] }
20
+
21
+ service :playlist,
22
+ processor: :playlists,
23
+ options: { :required => :id }
24
+
25
+ service :category,
26
+ processor: :categories,
27
+ options: { :required => :id }
28
+
29
+ service :categories
30
+
31
+ recognizes :api_key
32
+ def initialize opts = { }
33
+ key = opts[:api_key]
34
+ configuration.api_key = key if key
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+
3
+ module Howkast
4
+ def self.configure config = { }
5
+ configuration = Configuration.instance
6
+ config.each{ |k, v| configuration.send "#{k}=", v }
7
+ end
8
+
9
+ module Base
10
+ HOWCAST_BASE_URI = 'www.howcast.com'
11
+ extend ActiveSupport::Memoizable
12
+
13
+ def self.included base
14
+ base.send :include, HTTParty
15
+ #base.send :include, HTTParty::Icebox
16
+
17
+ base.base_uri HOWCAST_BASE_URI
18
+ base.format :xml
19
+ #base.cache :store => Configuration.instance.cache_store,
20
+ # :timeout => Configuration.instance.cache_timeout,
21
+ # :location => Configuration.instance.cache_location
22
+
23
+ base.extend ClassMethods
24
+ end
25
+
26
+ def configuration
27
+ Configuration.instance
28
+ end
29
+
30
+ private
31
+ def request *args
32
+ r = self.class.get *args
33
+ raise Howkast::Error::RequestError(r.code, r.parsed_response['howcast']) \
34
+ unless r.code == 200
35
+ r.parsed_response['howcast']
36
+ end
37
+ memoize :request
38
+
39
+ module ClassMethods
40
+ def configuration
41
+ Configuration.instance
42
+ end
43
+
44
+ def services
45
+ @services
46
+ end
47
+
48
+ def service name, spec = { }
49
+ arguments_guard = ->(argcount) do
50
+ maxarity = spec[:maxarity] || 1
51
+ raise ArgumentError, "wrong number of arguments (#{argcount} for #{maxarity})" \
52
+ unless maxarity == argcount
53
+ end
54
+
55
+ has_named_parameters name, spec[:options] || { }
56
+ (@services ||= []) << name.to_sym
57
+
58
+ define_method name do |*args, &block|
59
+ arguments_guard[args.count]
60
+
61
+ self.class.default_params api_key: configuration.api_key
62
+ procname = (spec[:processor] || name).to_s.modulize
63
+ processor = Processor.const_defined?(procname) ?
64
+ Processor.const_get(procname) :
65
+ Processor::Base
66
+
67
+ path = "/#{processor.path || name}"
68
+ query = args.last
69
+ args = args[0..-2] if query.instance_of? Hash
70
+ query = { } unless query.instance_of? Hash
71
+
72
+ processor.filter args, query
73
+
74
+ path << "/#{args.join('/')}" unless args.empty?
75
+ path << '.xml'
76
+
77
+ data = request(path, query: query)
78
+ processor.parse_element data
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ class API
85
+ include Base
86
+ end
87
+
88
+ module Processor
89
+ end
90
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ module Howkast
4
+ class Configuration
5
+ include Singleton
6
+ attr_accessor :api_key
7
+ attr_accessor :cache_timeout
8
+ attr_accessor :cache_location
9
+ attr_accessor :cache_store
10
+
11
+ def initialize
12
+ @cache_timeout = 600
13
+ @cache_location = '/tmp/howkast/cache'
14
+ @cache_store = 'file'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ module Howkast
4
+ module Error
5
+ class RequestError < StandardError; end
6
+
7
+ def self.RequestError(code, data)
8
+ msg = data['response']['err']['msg'] unless data.nil?
9
+ const = msg ? "#{msg.gsub(/\w+/){ $&.modulize }.gsub(/ /, '')}" : "HTTP#{code}"
10
+ klass = if Howkast::Error.const_defined? const
11
+ Howkast::Error.const_get const
12
+ else
13
+ Howkast::Error.const_set const, Class.new(RequestError)
14
+ end
15
+ klass.new(msg)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,261 @@
1
+ # encoding: utf-8
2
+ #
3
+ # See: https://gist.github.com/209605
4
+ #
5
+ # = Icebox : Caching for HTTParty
6
+ #
7
+ # Cache responses in HTTParty models [http://github.com/jnunemaker/httparty]
8
+ #
9
+ # === Usage
10
+ #
11
+ # class Foo
12
+ # include HTTParty
13
+ # include HTTParty::Icebox
14
+ # cache :store => 'file', :timeout => 600, :location => MY_APP_ROOT.join('tmp', 'cache')
15
+ # end
16
+ #
17
+ # Modeled after Martyn Loughran's APICache [http://github.com/newbamboo/api_cache]
18
+ # and Ruby On Rails's caching [http://api.rubyonrails.org/classes/ActiveSupport/Cache.html]
19
+ #
20
+ # Author: Karel Minarik [www.karmi.cz]
21
+ #
22
+ # === Notes
23
+ #
24
+ # Thanks to Amit Chakradeo to point out objects have to be stored marhalled on FS
25
+ # Thanks to Marlin Forbes to point out query parameters have to be include in the cache key
26
+ #
27
+ require 'logger'
28
+ #require 'ftools'
29
+ require 'tmpdir'
30
+ require 'pathname'
31
+ require 'digest/md5'
32
+
33
+ module HTTParty #:nodoc:
34
+ module Icebox
35
+
36
+ module ClassMethods
37
+
38
+ # Enable caching and set cache options
39
+ # Returns memoized cache object
40
+ #
41
+ # Following options are available, default values are in []:
42
+ #
43
+ # +store+:: Storage mechanism for cached data (memory, filesystem, your own) [memory]
44
+ # +timeout+:: Cache expiration in seconds [60]
45
+ # +logger+:: Path to logfile or logger instance [STDOUT]
46
+ #
47
+ # Any additional options are passed to the Cache constructor
48
+ #
49
+ # Usage:
50
+ #
51
+ # # Enable caching in HTTParty, in memory, for 1 minute
52
+ # cache # Use default values
53
+ #
54
+ # # Enable caching in HTTParty, on filesystem (/tmp), for 10 minutes
55
+ # cache :store => 'file', :timeout => 600, :location => '/tmp/'
56
+ #
57
+ # # Use your own cache store (see AbstractStore class below)
58
+ # cache :store => 'memcached', :timeout => 600, :server => '192.168.1.1:1001'
59
+ #
60
+ def cache(options={})
61
+ options[:store] ||= 'memory'
62
+ options[:timeout] ||= 60
63
+ logger = options[:logger]
64
+ @cache ||= Cache.new( options.delete(:store), options )
65
+ end
66
+
67
+ end
68
+
69
+ # When included, extend class with +cache+ method
70
+ # and redefine +get+ method to use cache
71
+ #
72
+ def self.included(receiver) #:nodoc:
73
+ receiver.extend ClassMethods
74
+ receiver.class_eval do
75
+
76
+ # Get reponse from network
77
+ # TODO: Why alias :new :old is not working here? Returns NoMethodError
78
+ #
79
+ def self.get_without_caching(path, options={})
80
+ perform_request Net::HTTP::Get, path, options
81
+ end
82
+
83
+ # Get response from cache, if available
84
+ #
85
+ def self.get_with_caching(path, options={})
86
+ key = path.clone
87
+ key << options[:query].to_s if defined? options[:query]
88
+
89
+ if cache.exists?(key) and not cache.stale?(key)
90
+ Cache.logger.debug "CACHE -- GET #{path}#{options[:query]}"
91
+ return cache.get(key)
92
+ else
93
+ Cache.logger.debug "/!\\ NETWORK -- GET #{path}#{options[:query]}"
94
+ response = get_without_caching(path, options)
95
+ cache.set(key, response) if response.code == 200
96
+ return response
97
+ end
98
+ end
99
+
100
+ # Redefine original HTTParty +get+ method to use cache
101
+ #
102
+ def self.get(path, options={})
103
+ self.get_with_caching(path, options)
104
+ end
105
+
106
+ end
107
+ end
108
+
109
+ # === Cache container
110
+ #
111
+ # Pass a store name ('memory', etc) to initializer
112
+ #
113
+ class Cache
114
+ attr_accessor :store
115
+
116
+ def initialize(store, options={})
117
+ self.class.logger = options[:logger]
118
+ @store = self.class.lookup_store(store).new(options)
119
+ end
120
+
121
+ def get(key); @store.get encode(key) unless stale?(key); end
122
+ def set(key, value); @store.set encode(key), value; end
123
+ def exists?(key); @store.exists? encode(key); end
124
+ def stale?(key); @store.stale? encode(key); end
125
+
126
+ def self.logger; @logger || default_logger; end
127
+ def self.default_logger; logger = ::Logger.new(STDERR); end
128
+
129
+ # Pass a filename (String), IO object, Logger instance or +nil+ to silence the logger
130
+ def self.logger=(device); @logger = device.kind_of?(::Logger) ? device : ::Logger.new(device); end
131
+
132
+ private
133
+
134
+ # Return store class based on passed name
135
+ def self.lookup_store(name)
136
+ store_name = "#{name.capitalize}Store"
137
+ return Store::const_get(store_name)
138
+ rescue NameError => e
139
+ raise Store::StoreNotFound, "The cache store '#{store_name}' was not found. Did you loaded any such class?"
140
+ end
141
+
142
+ def encode(key); Digest::MD5.hexdigest(key); end
143
+ end
144
+
145
+
146
+ # === Cache stores
147
+ #
148
+ module Store
149
+
150
+ class StoreNotFound < StandardError; end #:nodoc:
151
+
152
+ # ==== Abstract Store
153
+ # Inherit your store from this class
154
+ # *IMPORTANT*: Do not forget to call +super+ in your +initialize+ method!
155
+ #
156
+ class AbstractStore
157
+ def initialize(options={})
158
+ raise ArgumentError, "You need to set the :timeout parameter" unless options[:timeout]
159
+ @timeout = options[:timeout]
160
+ message = "Cache: Using #{self.class.to_s.split('::').last}"
161
+ message << " in location: #{options[:location]}" if options[:location]
162
+ message << " with timeout #{options[:timeout]} sec"
163
+ Cache.logger.info message unless options[:logger].nil?
164
+ return self
165
+ end
166
+ %w{set get exists? stale?}.each do |method_name|
167
+ define_method(method_name) { raise NoMethodError, "Please implement method set in your store class" }
168
+ end
169
+ end
170
+
171
+ # ===== Store objects in memory
172
+ #
173
+ Struct.new("Response", :code, :body, :headers) { def to_s; self.body; end }
174
+ class MemoryStore < AbstractStore
175
+ def initialize(options={})
176
+ super; @store = {}; self
177
+ end
178
+ def set(key, value)
179
+ Cache.logger.info("Cache: set (#{key})")
180
+ @store[key] = [Time.now, value]; true
181
+ end
182
+ def get(key)
183
+ data = @store[key][1]
184
+ Cache.logger.info("Cache: #{data.nil? ? "miss" : "hit"} (#{key})")
185
+ data
186
+ end
187
+ def exists?(key)
188
+ !@store[key].nil?
189
+ end
190
+ def stale?(key)
191
+ return true unless exists?(key)
192
+ Time.now - created(key) > @timeout
193
+ end
194
+ private
195
+ def created(key)
196
+ @store[key][0]
197
+ end
198
+ end
199
+
200
+ # ===== Store objects on the filesystem
201
+ #
202
+ class FileStore < AbstractStore
203
+ def initialize(options={})
204
+ super
205
+ options[:location] ||= Dir::tmpdir
206
+ @path = Pathname.new( options[:location] )
207
+ FileUtils.mkdir_p( @path )
208
+ self
209
+ end
210
+ def set(key, value)
211
+ Cache.logger.info("Cache: set (#{key})")
212
+ File.open( @path.join(key), 'w' ) { |file| file << Marshal.dump(value) }
213
+ true
214
+ end
215
+ def get(key)
216
+ data = Marshal.load(File.read( @path.join(key)))
217
+ Cache.logger.info("Cache: #{data.nil? ? "miss" : "hit"} (#{key})")
218
+ data
219
+ end
220
+ def exists?(key)
221
+ File.exists?( @path.join(key) )
222
+ end
223
+ def stale?(key)
224
+ return true unless exists?(key)
225
+ Time.now - created(key) > @timeout
226
+ end
227
+ private
228
+ def created(key)
229
+ File.mtime( @path.join(key) )
230
+ end
231
+ end
232
+ end
233
+
234
+ end
235
+ end
236
+ #
237
+ # Major parts of this code are based on architecture of ApiCache.
238
+ # Copyright (c) 2008 Martyn Loughran
239
+ #
240
+ # Other parts are inspired by the ActiveSupport::Cache in Ruby On Rails.
241
+ # Copyright (c) 2005-2009 David Heinemeier Hansson
242
+ #
243
+ # Permission is hereby granted, free of charge, to any person obtaining
244
+ # a copy of this software and associated documentation files (the
245
+ # "Software"), to deal in the Software without restriction, including
246
+ # without limitation the rights to use, copy, modify, merge, publish,
247
+ # distribute, sublicense, and/or sell copies of the Software, and to
248
+ # permit persons to whom the Software is furnished to do so, subject to
249
+ # the following conditions:
250
+ #
251
+ # The above copyright notice and this permission notice shall be
252
+ # included in all copies or substantial portions of the Software.
253
+ #
254
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
255
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
256
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
257
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
258
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
259
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
260
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
261
+ #