food_info 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f8b2e3a8689708f445b743d999bb38af47fe07a5
4
+ data.tar.gz: 2e705705c0d40d234460f3efd16edb076bda8bf9
5
+ SHA512:
6
+ metadata.gz: 786ff75020061bb4c9e5737bf8b9dc8fc946cf11c060e00ffe340777c72fbb906244fdd0559f64d2a6b07382d8e070d35c5ed8ba7c61a75f321e15d8f234cd39
7
+ data.tar.gz: a50c3920280484191fcb2f8ab56b5ad4dc3e4d295f33cc9169e6ae47aeeb49a2fe3483bc16efe361df36a64dd69f992c9f13b469d56a504b9de50e42affe9f68
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.7 December 5, 2013
4
+ * Pulling in misc bugfixes from eclipxe
5
+ * Adding caching: can pass in memcache-compatible object to use caching
6
+
3
7
  ## 0.0.6 October 21, 2011
4
8
  * FatSecret: Retry once if timestamp invalid (API only supports single request/second)
5
9
 
@@ -29,6 +29,16 @@ Once that's done, the first step is to tell FoodInfo which adapter you want to u
29
29
  FoodInfo.establish_connection(:fat_secret, :key => 'YOUR-KEY', :secret => 'YOUR-KEY')
30
30
 
31
31
 
32
+ ### Caching
33
+
34
+ To cache results, FoodInfo supports passing in an instance of a memcache-API-compatible (i.e. responds to <code>get</code> and <code>set</code>) caching object. I recommend using the [Dalli gem](https://github.com/mperham/dalli).
35
+
36
+ require 'dalli'
37
+ client = Dalli::Client.new('localhost:11211')
38
+ FoodInfo.establish_connection(:fat_secret, :key => 'YOUR-KEY', :secret => 'YOUR-KEY', :cache => client)
39
+
40
+ With that in place repeated <code>search</code> or <code>details</code> requests will pull from the cache, and not the API endpoint.
41
+
32
42
  ### Searching
33
43
 
34
44
  Now we can search for foods.
data/Rakefile CHANGED
@@ -1,2 +1,8 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.pattern = "test/*_test.rb"
8
+ end
data/TODO.txt CHANGED
@@ -1,2 +1,5 @@
1
- * Add DB caching (or is that better left to the client, who can control the caching layer completely?)
2
1
  * It'd be great to have a connection pool, so we could process multiple requests concurrently. I've added a skeleton for it, but actually using it would require a non-blocking HTTP library in HTTParty.
2
+
3
+ FatSecret ratelimits requests to one/second.
4
+ - Could add queue and only process one per second (then separate event loop running while pool full, can't necessarily handle additional requests in meantime)
5
+ - Could roundrobin between multiple API keys, but presumably violates TOS
@@ -11,6 +11,9 @@ Gem::Specification.new do |gem|
11
11
  gem.add_dependency('httparty', '>= 0.7.7')
12
12
  gem.add_dependency('hashie', '>= 1.1.0')
13
13
  gem.add_dependency('ruby-hmac')
14
+
15
+ gem.add_development_dependency "rake"
16
+ gem.add_development_dependency "minitest"
14
17
 
15
18
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
19
  gem.files = `git ls-files`.split("\n")
@@ -3,6 +3,7 @@ require 'hashie'
3
3
  require "food_info/utils"
4
4
  require "food_info/errors"
5
5
  require "food_info/adapters"
6
+ require "food_info/cache_adapters"
6
7
  require "food_info/version"
7
8
 
8
9
 
@@ -15,18 +16,35 @@ module FoodInfo
15
16
  class << self
16
17
 
17
18
  # Sets the adapter we'll be pulling data from.
19
+ #
20
+ # Example usage:
21
+ # FoodInfo.establish_connection(:fat_secret, :key => '...', :secret => '...')
22
+ #
23
+ # Example with cache:
24
+ # require 'dalli' # Use dalli gem to interface with memcache
25
+ # client = Dalli::Client.new('localhost:11211')
26
+ # FoodInfo.establish_connection(:fat_secret, :key => '...', :secret => '...', :cache => client)
18
27
  def establish_connection(adapter_name, opts = {})
19
28
  klass = ADAPTERS[adapter_name.to_sym]
20
29
  raise UnsupportedAdapter.new("Requested adapter ('#{adapter_name}') is unknown") unless klass
30
+
31
+ # Set up the pool of workers (net yet implemented, so defaulting to "pool" of one)
21
32
  @@pool = []
22
33
  @@cursor = 0
23
34
  (opts.delete(:pool) || 1).to_i.times do
24
35
  @@pool << klass.new(opts)
25
36
  end
26
37
 
38
+ # Set up the cache, if any provided
39
+ obj = opts.delete(:cache)
40
+ @cache = obj ? FoodInfo::CacheAdapters::MemCacheCompatible.new(obj) : FoodInfo::CacheAdapters::Default.new
41
+
27
42
  true
28
43
  end
29
44
 
45
+ # ======================
46
+ # = Public API Methods =
47
+ # ======================
30
48
  def search(q, opts = {})
31
49
  cached(:search, q, opts)
32
50
  end
@@ -36,11 +54,13 @@ module FoodInfo
36
54
  end
37
55
 
38
56
 
39
-
40
-
41
- def cached(method, param, opts = {}, &block)
42
- # TODO - implement caching strategy
43
- next_adapter.send(method, param, opts)
57
+ # ==========================
58
+ # = Implementation details =
59
+ # ==========================
60
+ def cached(method, param, opts = {})
61
+ key = request_key(method, param, opts)
62
+
63
+ @cache.get(key) || @cache.set(key, next_adapter.send(method, param, opts))
44
64
  end
45
65
 
46
66
  # FUTURE: This connection pool code won't do much good until HTTParty is non-blocking
@@ -50,6 +70,14 @@ module FoodInfo
50
70
  @@pool[@@cursor]
51
71
  end
52
72
 
73
+ # Convert method + args into a string for use as the cache key
74
+ def request_key(method, param, opts = {})
75
+ # {:param => 123, :other => "something"} # => "other=something:param=123"
76
+ str_opts = opts.sort{|a,b| a.to_s <=> b.to_s}.map{|pair| pair.join('=')}.join(':')
77
+
78
+ "FoodInfo:#{method}:#{param}:#{str_opts}"
79
+ end
80
+
53
81
  end
54
82
  end
55
83
 
@@ -19,7 +19,7 @@ module FoodInfo
19
19
 
20
20
  def search(q, opts = {})
21
21
  params = {
22
- :search_expression => q,
22
+ :search_expression => URI.escape(q),
23
23
  :page_number => opts[:page] || 1,
24
24
  :max_results => opts[:per_page] || 20
25
25
  }
@@ -42,7 +42,7 @@ module FoodInfo
42
42
  data = self.class.get( query_url )
43
43
 
44
44
  if data['error'] # Invalid timestamp can happen if more than one request/second. Allow retrying once.
45
- if data['error']['message'] =~ 'Invalid/expired timestamp' && retried < RETRY_INVALID_TIMESTAMP
45
+ if data['error']['message'] =~ /Invalid\/expired timestamp/ && retried < RETRY_INVALID_TIMESTAMP
46
46
  return query(method, opts, retried + 1)
47
47
  else
48
48
  raise DataSourceException.new(data['error']['message'])
@@ -0,0 +1,9 @@
1
+ require "food_info/cache_adapters/default"
2
+ require "food_info/cache_adapters/mem_cache_compatible"
3
+
4
+ module FoodInfo
5
+ # All FoodInfo CacheAdapters must expose two public methods, +set+ and +get+, and will need to
6
+ # behave compatibly with the memcache API.
7
+ module CacheAdapters
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module FoodInfo
2
+ module CacheAdapters
3
+ class Default
4
+
5
+ def get(key)
6
+ nil
7
+ end
8
+
9
+ def set(key, val)
10
+ val
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ module FoodInfo
2
+ module CacheAdapters
3
+ class MemCacheCompatible
4
+
5
+ def initialize(obj = nil)
6
+ if obj && obj.respond_to?(:get) && obj.respond_to?(:set)
7
+ @cache = obj
8
+ else
9
+ raise "FoodInfo::CacheAdapters::MemCacheCompatible must be initialized with an object that responds to get and set (look into the Dalli gem)"
10
+ end
11
+ end
12
+
13
+ def set(key, val)
14
+ begin
15
+ @cache.set(key, val)
16
+ rescue Exception => e
17
+ STDERR.puts "FoodInfo Cache Error (set): #{e}"
18
+ end
19
+
20
+ val
21
+ end
22
+
23
+ def get(key)
24
+ begin
25
+ @cache.get(key)
26
+ rescue Exception => e
27
+ STDERR.puts "FoodInfo Cache Error (get): #{e}"
28
+ return nil
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module FoodInfo
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
@@ -0,0 +1,14 @@
1
+ require 'test_helper'
2
+
3
+ class FatSecretTest < Minitest::Test
4
+
5
+ def setup
6
+ FoodInfo.establish_connection(:fat_secret, :key => ENV['FS_KEY'], :secret => ENV['FS_SECRET'])
7
+ end
8
+
9
+ def test_retreives_details
10
+ f = FoodInfo.details("33689")
11
+ assert_equal "Cheddar Cheese", f.name
12
+ end
13
+
14
+ end
@@ -0,0 +1,3 @@
1
+ require 'food_info'
2
+ require 'minitest/autorun'
3
+ require 'minitest/pride'
metadata CHANGED
@@ -1,49 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: food_info
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
5
- prerelease:
4
+ version: 0.0.7
6
5
  platform: ruby
7
6
  authors:
8
7
  - Kali Donovan
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2011-10-21 00:00:00.000000000Z
11
+ date: 2013-12-05 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: httparty
16
- requirement: &70256719087420 !ruby/object:Gem::Requirement
17
- none: false
15
+ requirement: !ruby/object:Gem::Requirement
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
20
18
  - !ruby/object:Gem::Version
21
19
  version: 0.7.7
22
20
  type: :runtime
23
21
  prerelease: false
24
- version_requirements: *70256719087420
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.7.7
25
27
  - !ruby/object:Gem::Dependency
26
28
  name: hashie
27
- requirement: &70256719086900 !ruby/object:Gem::Requirement
28
- none: false
29
+ requirement: !ruby/object:Gem::Requirement
29
30
  requirements:
30
- - - ! '>='
31
+ - - '>='
31
32
  - !ruby/object:Gem::Version
32
33
  version: 1.1.0
33
34
  type: :runtime
34
35
  prerelease: false
35
- version_requirements: *70256719086900
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.0
36
41
  - !ruby/object:Gem::Dependency
37
42
  name: ruby-hmac
38
- requirement: &70256719086520 !ruby/object:Gem::Requirement
39
- none: false
43
+ requirement: !ruby/object:Gem::Requirement
40
44
  requirements:
41
- - - ! '>='
45
+ - - '>='
42
46
  - !ruby/object:Gem::Version
43
47
  version: '0'
44
48
  type: :runtime
45
49
  prerelease: false
46
- version_requirements: *70256719086520
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
47
83
  description: Generic Ruby interface to look up nutritional information on food. Design
48
84
  is modular so other adapters can be plugged in, but only data source currently implemented
49
85
  is FatSecret.
@@ -69,32 +105,37 @@ files:
69
105
  - lib/food_info/adapters/fat_secret/data/search_result.rb
70
106
  - lib/food_info/adapters/fat_secret/data/search_results.rb
71
107
  - lib/food_info/adapters/fat_secret/request.rb
72
- - lib/food_info/cache_adapters/mem_cache.rb
108
+ - lib/food_info/cache_adapters.rb
109
+ - lib/food_info/cache_adapters/default.rb
110
+ - lib/food_info/cache_adapters/mem_cache_compatible.rb
73
111
  - lib/food_info/errors.rb
74
112
  - lib/food_info/utils.rb
75
113
  - lib/food_info/version.rb
114
+ - test/fat_secret_test.rb
115
+ - test/test_helper.rb
76
116
  homepage: https://github.com/deviantech/food_info
77
117
  licenses: []
118
+ metadata: {}
78
119
  post_install_message:
79
120
  rdoc_options: []
80
121
  require_paths:
81
122
  - lib
82
123
  required_ruby_version: !ruby/object:Gem::Requirement
83
- none: false
84
124
  requirements:
85
- - - ! '>='
125
+ - - '>='
86
126
  - !ruby/object:Gem::Version
87
127
  version: '0'
88
128
  required_rubygems_version: !ruby/object:Gem::Requirement
89
- none: false
90
129
  requirements:
91
- - - ! '>='
130
+ - - '>='
92
131
  - !ruby/object:Gem::Version
93
132
  version: '0'
94
133
  requirements: []
95
134
  rubyforge_project:
96
- rubygems_version: 1.8.10
135
+ rubygems_version: 2.1.11
97
136
  signing_key:
98
- specification_version: 3
137
+ specification_version: 4
99
138
  summary: API for researching nutritional information of various foods
100
- test_files: []
139
+ test_files:
140
+ - test/fat_secret_test.rb
141
+ - test/test_helper.rb
@@ -1,7 +0,0 @@
1
- module FoodInfo
2
- module CacheAdapters
3
- class MemCache
4
-
5
- end
6
- end
7
- end