mloughran-api_cache 0.1.1

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/README.markdown ADDED
@@ -0,0 +1,59 @@
1
+ api_cache
2
+ =========
3
+ You want to use the Twitter API but you don't want to die? I have the solution to API caching:
4
+
5
+ APICache.get("http://twitter.com/statuses/public_timeline.rss")
6
+
7
+ You get the following functionality for free:
8
+
9
+ * New data every 10 minutes
10
+ * If the twitter API dies then keep using the last data received for a day. Then assume it's invalid and announce that Twitter has FAILED (optional).
11
+ * Don't hit the rate limit (70 requests per 60 minutes)
12
+
13
+ So what exactly does `APICache` do? Given cached data less than 10 minutes old, it returns that. Otherwise, assuming it didn't try to request the URL within the last minute (to avoid the rate limit), it makes a get request to the Twitter API. If the Twitter API timeouts or doesn't return a 2xx code (very likely) we're still fine: it just returns the last data fetched (as long as it's less than a day old). In the exceptional case that all is lost and no data can be returned, it raises an `APICache::NotAvailableError` exception. You're responsible for catching this exception and complaining bitterly to the internet.
14
+
15
+ All very simple. What if you need to do something more complicated? Say you need authentication or the silly API you're using doesn't follow a nice convention of returning 2xx for success. Then you need a block:
16
+
17
+ APICache.get('twitter_replies', :cache => 3600) do
18
+ Net::HTTP.start('twitter.com') do |http|
19
+ req = Net::HTTP::Get.new('/statuses/replies.xml')
20
+ req.basic_auth 'username', 'password'
21
+ response = http.request(req)
22
+ case response
23
+ when Net::HTTPSuccess
24
+ # 2xx response code
25
+ response.body
26
+ else
27
+ raise APICache::Invalid
28
+ end
29
+ end
30
+ end
31
+
32
+ All the caching is still handled for you. If you supply a block then the first argument to `APICache.get` is assumed to be a unique key rather than a URL. Throwing `APICache::Invalid` signals to `APICache` that the request was not successful.
33
+
34
+ You can send any of the following options to `APICache.get(url, options = {}, &block)`. These are the default values (times are all in seconds):
35
+
36
+ {
37
+ :cache => 600, # 10 minutes After this time fetch new data
38
+ :valid => 86400, # 1 day Maximum time to use old data
39
+ # :forever is a valid option
40
+ :period => 60, # 1 minute Maximum frequency to call API
41
+ :timeout => 5 # 5 seconds API response timeout
42
+ }
43
+
44
+ Before using the APICache you need to initialize the caches. In merb, for example, put this in your `init.rb`:
45
+
46
+ APICache.start
47
+
48
+ Currently there are two stores available: `MemcacheStore` and `MemoryStore`. `MemcacheStore` is the default but if you'd like to use `MemoryStore`, or another store - see `AbstractStore`, just supply it to the start method:
49
+
50
+ APICache.start(APICache::MemoryStore)
51
+
52
+ I suppose you'll want to get your hands on this magic. For now just grab the source from [github](http://github.com/mloughran/api_cache/tree/master) and `rake install`. I'll get a gem sorted soon.
53
+
54
+ Please send feedback if you think of any other functionality that would be handy.
55
+
56
+ Copyright
57
+ =========
58
+
59
+ Copyright (c) 2008 Martyn Loughran. See LICENSE for details.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 0
4
+ :minor: 1
@@ -0,0 +1,25 @@
1
+ class APICache::AbstractStore
2
+ def initialize
3
+ raise "Method not implemented. Called abstract class."
4
+ end
5
+
6
+ # Set value. Returns true if success.
7
+ def set(key, value)
8
+ raise "Method not implemented. Called abstract class."
9
+ end
10
+
11
+ # Get value.
12
+ def get(key)
13
+ raise "Method not implemented. Called abstract class."
14
+ end
15
+
16
+ # Does a given key exist in the cache?
17
+ def exists?(key)
18
+ raise "Method not implemented. Called abstract class."
19
+ end
20
+
21
+ # Has a given time passed since the key was set?
22
+ def expired?(key, timeout)
23
+ raise "Method not implemented. Called abstract class."
24
+ end
25
+ end
@@ -0,0 +1,65 @@
1
+ require 'net/http'
2
+
3
+ # This class wraps up querying the API and remembers when each API was
4
+ # last queried in case there is a limit to the number that can be made.
5
+ class APICache::API
6
+ def initialize
7
+ @query_times = {}
8
+ end
9
+
10
+ # Checks whether the API can be queried (i.e. whether retry_time has passed
11
+ # since the last query to the API).
12
+ #
13
+ # If retry_time is 0 then there is no limit on how frequently queries can
14
+ # be made to the API.
15
+ def queryable?(key, retry_time)
16
+ if @query_times[key]
17
+ if Time.now - @query_times[key] > retry_time
18
+ APICache.logger.log "Queryable: true - retry_time has passed"
19
+ true
20
+ else
21
+ APICache.logger.log "Queryable: false - queried too recently"
22
+ false
23
+ end
24
+ else
25
+ APICache.logger.log "Queryable: true - never used API before"
26
+ true
27
+ end
28
+ end
29
+
30
+ # Fetch data from the API.
31
+ #
32
+ # If no block is given then the key is assumed to be a URL and which will
33
+ # be queried expecting a 200 response. Otherwise the return value of the
34
+ # block will be used.
35
+ #
36
+ # If the block is unable to fetch the value from the API it should raise
37
+ # APICache::Invalid.
38
+ def get(key, timeout, &block)
39
+ APICache.logger.log "Fetching data from the API"
40
+ @query_times[key] = Time.now
41
+ Timeout::timeout(timeout) do
42
+ if block_given?
43
+ # This should raise APICache::Invalid if it is not correct
44
+ yield
45
+ else
46
+ get_via_http(key, timeout)
47
+ end
48
+ end
49
+ rescue Timeout::Error, APICache::Invalid
50
+ raise APICache::CannotFetch
51
+ end
52
+
53
+ private
54
+
55
+ def get_via_http(key, timeout)
56
+ response = Net::HTTP.get_response(URI.parse(key))
57
+ case response
58
+ when Net::HTTPSuccess
59
+ # 2xx response code
60
+ response.body
61
+ else
62
+ raise APICache::Invalid
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,41 @@
1
+ # Cache performs calculations relating to the status of items stored in the
2
+ # cache and delegates storage to the various cache stores.
3
+ require 'digest/md5'
4
+ class APICache::Cache
5
+ attr_accessor :store
6
+ def initialize(store)
7
+ @store = store.send(:new)
8
+ end
9
+
10
+ # Returns one of the following options depending on the state of the key:
11
+ #
12
+ # * :current (key has been set recently)
13
+ # * :refetch (data should be refetched but is still available for use)
14
+ # * :invalid (data is too old to be useful)
15
+ # * :missing (do data for this key)
16
+ def state(key, refetch_time, invalid_time)
17
+ if @store.exists?(encode(key))
18
+ if !@store.expired?(encode(key), refetch_time)
19
+ :current
20
+ elsif (invalid_time == :forever) || !@store.expired?(encode(key), invalid_time)
21
+ :refetch
22
+ else
23
+ :invalid
24
+ end
25
+ else
26
+ :missing
27
+ end
28
+ end
29
+
30
+ def get(key)
31
+ @store.get(encode(key))
32
+ end
33
+
34
+ def set(key, value)
35
+ @store.set(encode(key), value)
36
+ true
37
+ end
38
+ def encode(key)
39
+ Digest::MD5.hexdigest key
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ class APICache::Logger
2
+ def initialize
3
+
4
+ end
5
+
6
+ def log(message)
7
+ # puts "APICache: #{message}"
8
+ end
9
+ end
@@ -0,0 +1,54 @@
1
+ require 'memcache'
2
+
3
+ class APICache::MemcacheStore < APICache::AbstractStore
4
+ class NotReady < Exception #:nodoc:
5
+ def initialize
6
+ super("Memcache server is not ready")
7
+ end
8
+ end
9
+
10
+ class NotDefined < Exception #:nodoc:
11
+ def initialize
12
+ super("Memcache is not defined")
13
+ end
14
+ end
15
+
16
+ def initialize
17
+ APICache.logger.log "Using memcached store"
18
+ namespace = 'api_cache'
19
+ host = '127.0.0.1:11211'
20
+ @memcache = MemCache.new(host, {:namespace => namespace})
21
+ raise NotReady unless @memcache.active?
22
+ true
23
+ # rescue NameError
24
+ # raise NotDefined
25
+ end
26
+
27
+ def set(key, data)
28
+ @memcache.set(key, data)
29
+ @memcache.set("#{key}_created_at", Time.now)
30
+ APICache.logger.log("cache: set (#{key})")
31
+ true
32
+ end
33
+
34
+ def get(key)
35
+ data = @memcache.get(key)
36
+ APICache.logger.log("cache: #{data.nil? ? "miss" : "hit"} (#{key})")
37
+ data
38
+ end
39
+
40
+ def exists?(key)
41
+ # TODO: inefficient - is there a better way?
42
+ !@memcache.get(key).nil?
43
+ end
44
+
45
+ def expired?(key, timeout)
46
+ Time.now - created(key) > timeout
47
+ end
48
+
49
+ private
50
+
51
+ def created(key)
52
+ @memcache.get("#{key}_created_at")
53
+ end
54
+ end
@@ -0,0 +1,33 @@
1
+ class APICache::MemoryStore < APICache::AbstractStore
2
+ def initialize
3
+ APICache.logger.log "Using memory store"
4
+ @cache = {}
5
+ true
6
+ end
7
+
8
+ def set(key, value)
9
+ APICache.logger.log("cache: set (#{key})")
10
+ @cache[key] = [Time.now, value]
11
+ true
12
+ end
13
+
14
+ def get(key)
15
+ data = @cache[key][1]
16
+ APICache.logger.log("cache: #{data.nil? ? "miss" : "hit"} (#{key})")
17
+ data
18
+ end
19
+
20
+ def exists?(key)
21
+ !@cache[key].nil?
22
+ end
23
+
24
+ def expired?(key, timeout)
25
+ Time.now - created(key) > timeout
26
+ end
27
+
28
+ private
29
+
30
+ def created(key)
31
+ @cache[key][0]
32
+ end
33
+ end
data/lib/api_cache.rb ADDED
@@ -0,0 +1,73 @@
1
+ class APICache
2
+ class NotAvailableError < RuntimeError; end
3
+ class Invalid < RuntimeError; end
4
+ class CannotFetch < RuntimeError; end
5
+
6
+ class << self
7
+ attr_accessor :cache
8
+ attr_accessor :api
9
+ attr_accessor :logger
10
+ end
11
+
12
+ # Initializes the cache
13
+ def self.start(store = APICache::MemcacheStore, logger = APICache::Logger.new)
14
+ APICache.logger = logger
15
+ APICache.cache = APICache::Cache.new(store)
16
+ APICache.api = APICache::API.new
17
+ end
18
+
19
+ # Raises an APICache::NotAvailableError if it can't get a value. You should rescue this
20
+ # if your application code.
21
+ #
22
+ # Optionally call with a block. The value of the block is then used to
23
+ # set the cache rather than calling the url. Use it for example if you need
24
+ # to make another type of request, catch custom error codes etc. To signal
25
+ # that the call failed just raise APICache::Invalid - the value will then
26
+ # not be cached and the api will not be called again for options[:timeout]
27
+ # seconds. If an old value is available in the cache then it will be used.
28
+ #
29
+ # For example:
30
+ # APICache.get("http://twitter.com/statuses/user_timeline/6869822.atom")
31
+ #
32
+ # APICache.get \
33
+ # "http://twitter.com/statuses/user_timeline/6869822.atom",
34
+ # :cache => 60, :valid => 600
35
+ def self.get(key, options = {}, &block)
36
+ options = {
37
+ :cache => 600, # 10 minutes After this time fetch new data
38
+ :valid => 86400, # 1 day Maximum time to use old data
39
+ # :forever is a valid option
40
+ :period => 60, # 1 minute Maximum frequency to call API
41
+ :timeout => 5 # 5 seconds API response timeout
42
+ }.merge(options)
43
+
44
+ cache_state = cache.state(key, options[:cache], options[:valid])
45
+
46
+ if cache_state == :current
47
+ cache.get(key)
48
+ else
49
+ begin
50
+ raise APICache::CannotFetch unless api.queryable?(key, options[:period])
51
+ value = api.get(key, options[:timeout], &block)
52
+ cache.set(key, value)
53
+ value
54
+ rescue APICache::CannotFetch
55
+ APICache.logger.log "Failed to fetch new data from API"
56
+ if cache_state == :refetch
57
+ cache.get(key)
58
+ else
59
+ APICache.logger.log "Data not available in the cache or from API"
60
+ raise APICache::NotAvailableError
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ require 'api_cache/cache'
68
+ require 'api_cache/api'
69
+ require 'api_cache/logger'
70
+
71
+ APICache.autoload 'AbstractStore', 'api_cache/abstract_store'
72
+ APICache.autoload 'MemoryStore', 'api_cache/memory_store'
73
+ APICache.autoload 'MemcacheStore', 'api_cache/memcache_store'
@@ -0,0 +1,73 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe APICache do
4
+ before :each do
5
+ APICache.start(APICache::MemoryStore)
6
+ @key = 'random_key'
7
+ @encoded_key = "1520171c64bfb71a95c97d310eea3492"
8
+ @data = 'some bit of data'
9
+ end
10
+
11
+ describe "get method" do
12
+
13
+ it "should MD5 encode the cache key" do
14
+ APICache::Cache.new(APICache::MemoryStore).encode(@key).should == @encoded_key
15
+ end
16
+ it "should encode the cache key before calling the store" do
17
+ APICache.cache.should_receive(:state).and_return(:current)
18
+ APICache.cache.store.should_receive(:get).with(@encoded_key).and_return(@data)
19
+ APICache.cache.should_receive(:encode).with(@key).and_return(@encoded_key)
20
+ APICache.get(@key).should != @data
21
+ end
22
+
23
+ it "should fetch data from the cache if the state is :current" do
24
+ APICache.cache.should_receive(:state).and_return(:current)
25
+ APICache.cache.should_receive(:get).and_return(@data)
26
+
27
+ APICache.get(@key).should == @data
28
+ end
29
+
30
+ it "should make new request to API if the state is :refetch" do
31
+ APICache.cache.should_receive(:state).and_return(:refetch)
32
+ APICache.api.should_receive(:get).with(@key, 5).and_return(@data)
33
+
34
+ APICache.get(@key).should == @data
35
+ end
36
+
37
+ it "should return the cached value if the api cannot fetch data and state is :refetch" do
38
+ APICache.cache.should_receive(:state).and_return(:refetch)
39
+ APICache.api.should_receive(:get).with(@key, 5).and_raise(APICache::CannotFetch)
40
+ APICache.cache.should_receive(:get).and_return(@data)
41
+
42
+ APICache.get(@key).should == @data
43
+ end
44
+
45
+ it "should make new request to API if the state is :invalid" do
46
+ APICache.cache.should_receive(:state).and_return(:invalid)
47
+ APICache.api.should_receive(:get).with(@key, 5).and_return(@data)
48
+
49
+ APICache.get(@key).should == @data
50
+ end
51
+
52
+ it "should raise an exception if the api cannot fetch data and state is :invalid" do
53
+ APICache.cache.should_receive(:state).and_return(:invalid)
54
+ APICache.api.should_receive(:get).with(@key, 5).and_raise(APICache::CannotFetch)
55
+
56
+ lambda { APICache.get(@key).should }.should raise_error(APICache::NotAvailableError)
57
+ end
58
+
59
+ it "should make new request to API if the state is :missing" do
60
+ APICache.cache.should_receive(:state).and_return(:missing)
61
+ APICache.api.should_receive(:get).with(@key, 5).and_return(@data)
62
+
63
+ APICache.get(@key).should == @data
64
+ end
65
+
66
+ it "should raise an exception if the api cannot fetch data and state is :missing" do
67
+ APICache.cache.should_receive(:state).and_return(:missing)
68
+ APICache.api.should_receive(:get).with(@key, 5).and_raise(APICache::CannotFetch)
69
+
70
+ lambda { APICache.get(@key).should }.should raise_error(APICache::NotAvailableError)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,5 @@
1
+ $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require "rubygems"
4
+ require "api_cache"
5
+ require "spec"
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mloughran-api_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Martyn Loughran
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-17 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Library to handle caching external API calls
17
+ email: me@mloughran.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README.markdown
26
+ - VERSION.yml
27
+ - lib/api_cache
28
+ - lib/api_cache/abstract_store.rb
29
+ - lib/api_cache/api.rb
30
+ - lib/api_cache/cache.rb
31
+ - lib/api_cache/logger.rb
32
+ - lib/api_cache/memcache_store.rb
33
+ - lib/api_cache/memory_store.rb
34
+ - lib/api_cache.rb
35
+ - spec/api_cache_spec.rb
36
+ - spec/spec_helper.rb
37
+ has_rdoc: true
38
+ homepage: http://github.com/mloughran/api_cache
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --inline-source
42
+ - --charset=UTF-8
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project:
60
+ rubygems_version: 1.2.0
61
+ signing_key:
62
+ specification_version: 2
63
+ summary: Library to handle caching external API calls
64
+ test_files: []
65
+