openbeautyfacts 0.1.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +111 -0
  4. data/Rakefile +19 -0
  5. data/lib/openbeautyfacts.rb +60 -0
  6. data/lib/openbeautyfacts/additive.rb +51 -0
  7. data/lib/openbeautyfacts/allergen.rb +33 -0
  8. data/lib/openbeautyfacts/brand.rb +33 -0
  9. data/lib/openbeautyfacts/category.rb +33 -0
  10. data/lib/openbeautyfacts/city.rb +33 -0
  11. data/lib/openbeautyfacts/contributor.rb +33 -0
  12. data/lib/openbeautyfacts/country.rb +33 -0
  13. data/lib/openbeautyfacts/faq.rb +50 -0
  14. data/lib/openbeautyfacts/ingredient_that_may_be_from_palm_oil.rb +33 -0
  15. data/lib/openbeautyfacts/label.rb +33 -0
  16. data/lib/openbeautyfacts/locale.rb +30 -0
  17. data/lib/openbeautyfacts/manufacturing_place.rb +33 -0
  18. data/lib/openbeautyfacts/origin.rb +33 -0
  19. data/lib/openbeautyfacts/packager_code.rb +33 -0
  20. data/lib/openbeautyfacts/packaging.rb +33 -0
  21. data/lib/openbeautyfacts/press.rb +56 -0
  22. data/lib/openbeautyfacts/product.rb +185 -0
  23. data/lib/openbeautyfacts/product_state.rb +33 -0
  24. data/lib/openbeautyfacts/purchase_place.rb +33 -0
  25. data/lib/openbeautyfacts/store.rb +33 -0
  26. data/lib/openbeautyfacts/trace.rb +33 -0
  27. data/lib/openbeautyfacts/user.rb +41 -0
  28. data/lib/openbeautyfacts/version.rb +3 -0
  29. data/test/fixtures/additives.yml +1422 -0
  30. data/test/fixtures/additives_locale.yml +1422 -0
  31. data/test/fixtures/brands.yml +2144 -0
  32. data/test/fixtures/brands_locale.yml +2024 -0
  33. data/test/fixtures/faq.yml +1031 -0
  34. data/test/fixtures/fetch_product_3600550362626.yml +104 -0
  35. data/test/fixtures/index.yml +169 -0
  36. data/test/fixtures/login_user.yml +40 -0
  37. data/test/fixtures/press.yml +986 -0
  38. data/test/fixtures/product_3600550362626.yml +104 -0
  39. data/test/fixtures/product_states.yml +936 -0
  40. data/test/fixtures/product_states_locale.yml +976 -0
  41. data/test/fixtures/products_for_brand.yml +1707 -0
  42. data/test/fixtures/products_for_state.yml +2260 -0
  43. data/test/fixtures/products_with_additive.yml +1723 -0
  44. data/test/fixtures/search_doux.yml +538 -0
  45. data/test/fixtures/search_doux_1_000_000.yml +257 -0
  46. data/test/minitest_helper.rb +12 -0
  47. data/test/test_openbeautyfacts.rb +193 -0
  48. metadata +188 -0
@@ -0,0 +1,33 @@
1
+ require 'hashie'
2
+
3
+ module Openbeautyfacts
4
+ class Country < Hashie::Mash
5
+
6
+ # TODO: Add more locales
7
+ LOCALE_PATHS = {
8
+ 'fr' => 'pays',
9
+ 'uk' => 'countries',
10
+ 'us' => 'countries',
11
+ 'world' => 'countries'
12
+ }
13
+
14
+ class << self
15
+
16
+ # Get countries
17
+ #
18
+ def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
19
+ if path = LOCALE_PATHS[locale]
20
+ Product.tags_from_page(self, "http://#{locale}.#{domain}/#{path}")
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # Get products with country
27
+ #
28
+ def products(page: -1)
29
+ Product.from_website_page(url, page: page, products_count: products_count) if url
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,50 @@
1
+ require 'hashie'
2
+ require 'nokogiri'
3
+ require 'open-uri'
4
+
5
+ module Openbeautyfacts
6
+ class Faq < Hashie::Mash
7
+
8
+ # TODO: Add more locales
9
+ LOCALE_PATHS = {
10
+ 'fr' => 'questions-frequentes',
11
+ 'uk' => 'faq',
12
+ 'us' => 'faq',
13
+ 'world' => 'faq'
14
+ }
15
+
16
+ class << self
17
+ def items(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
18
+ if path = LOCALE_PATHS[locale]
19
+ html = open("http://#{locale}.#{domain}/#{path}").read
20
+ dom = Nokogiri::HTML.fragment(html)
21
+
22
+ titles = dom.css('#main_column h2')
23
+ titles.each_with_index.map do |item, index|
24
+ paragraphs = []
25
+
26
+ element = item.next_sibling
27
+ while !element.nil? && element.node_name != 'h2'
28
+ if element.node_name == 'p'
29
+ paragraphs.push(element)
30
+ end
31
+
32
+ element = element.next_sibling
33
+ end
34
+
35
+ if index == titles.length - 1
36
+ paragraphs = paragraphs[0..-3]
37
+ end
38
+
39
+ new({
40
+ "question" => item.text.strip,
41
+ "answer" => paragraphs.map { |paragraph| paragraph.text.strip.gsub(/\r?\n/, ' ') }.join("\n\n"),
42
+ "answer_html" => paragraphs.map(&:to_html).join
43
+ })
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,33 @@
1
+ require 'hashie'
2
+
3
+ module Openbeautyfacts
4
+ class IngredientThatMayBeFromPalmOil < Hashie::Mash
5
+
6
+ # TODO: Add more locales
7
+ LOCALE_PATHS = {
8
+ 'fr' => 'ingredients-pouvant-etre-issus-de-l-huile-de-palme',
9
+ 'uk' => 'ingredients-that-may-be-from-palm-oil',
10
+ 'us' => 'ingredients-that-may-be-from-palm-oil',
11
+ 'world' => 'ingredients-that-may-be-from-palm-oil'
12
+ }
13
+
14
+ class << self
15
+
16
+ # Get ingredients that may be from palm oil
17
+ #
18
+ def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
19
+ if path = LOCALE_PATHS[locale]
20
+ Product.tags_from_page(self, "http://#{locale}.#{domain}/#{path}")
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # Get products with ingredient that may be from palm oil
27
+ #
28
+ def products(page: -1)
29
+ Product.from_website_page(url, page: page, products_count: products_count) if url
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ require 'hashie'
2
+
3
+ module Openbeautyfacts
4
+ class Label < Hashie::Mash
5
+
6
+ # TODO: Add more locales
7
+ LOCALE_PATHS = {
8
+ 'fr' => 'labels',
9
+ 'uk' => 'labels',
10
+ 'us' => 'labels',
11
+ 'world' => 'labels'
12
+ }
13
+
14
+ class << self
15
+
16
+ # Get labels
17
+ #
18
+ def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
19
+ if path = LOCALE_PATHS[locale]
20
+ Product.tags_from_page(self, "http://#{locale}.#{domain}/#{path}")
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # Get products with label
27
+ #
28
+ def products(page: -1)
29
+ Product.from_website_page(url, page: page, products_count: products_count) if url
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ module Openbeautyfacts
2
+ class Locale < String
3
+
4
+ GLOBAL = 'world'
5
+
6
+ class << self
7
+
8
+ # Get locales
9
+ #
10
+ def all(domain: DEFAULT_DOMAIN)
11
+ url = "http://#{domain}/"
12
+ body = open(url).read
13
+ dom = Nokogiri.parse(body)
14
+
15
+ dom.css('ul li a').map { |locale_link|
16
+ locale_from_link(locale_link.attr('href'))
17
+ }.uniq.sort
18
+ end
19
+
20
+ # Return locale from link
21
+ #
22
+ def locale_from_link(link)
23
+ locale = link[/^https?:\/\/([^.]+)\./i, 1]
24
+ locale unless locale.nil? || locale == 'static'
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ require 'hashie'
2
+
3
+ module Openbeautyfacts
4
+ class ManufacturingPlace < Hashie::Mash
5
+
6
+ # TODO: Add more locales
7
+ LOCALE_PATHS = {
8
+ 'fr' => 'lieux-de-fabrication',
9
+ 'uk' => 'manufacturing-places',
10
+ 'us' => 'manufacturing-places',
11
+ 'world' => 'manufacturing-places'
12
+ }
13
+
14
+ class << self
15
+
16
+ # Get manufacturing places
17
+ #
18
+ def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
19
+ if path = LOCALE_PATHS[locale]
20
+ Product.tags_from_page(self, "http://#{locale}.#{domain}/#{path}")
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # Get products from manufacturing place
27
+ #
28
+ def products(page: -1)
29
+ Product.from_website_page(url, page: page, products_count: products_count) if url
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ require 'hashie'
2
+
3
+ module Openbeautyfacts
4
+ class Origin < Hashie::Mash
5
+
6
+ # TODO: Add more locales
7
+ LOCALE_PATHS = {
8
+ 'fr' => 'origines',
9
+ 'uk' => 'origins',
10
+ 'us' => 'origins',
11
+ 'world' => 'origins'
12
+ }
13
+
14
+ class << self
15
+
16
+ # Get origins
17
+ #
18
+ def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
19
+ if path = LOCALE_PATHS[locale]
20
+ Product.tags_from_page(self, "http://#{locale}.#{domain}/#{path}")
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # Get products with origin
27
+ #
28
+ def products(page: -1)
29
+ Product.from_website_page(url, page: page, products_count: products_count) if url
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ require 'hashie'
2
+
3
+ module Openbeautyfacts
4
+ class PackagerCode < Hashie::Mash
5
+
6
+ # TODO: Add more locales
7
+ LOCALE_PATHS = {
8
+ 'fr' => 'codes-emballeurs',
9
+ 'uk' => 'packager-codes',
10
+ 'us' => 'packager-codes',
11
+ 'world' => 'packager-codes'
12
+ }
13
+
14
+ class << self
15
+
16
+ # Get packager codes
17
+ #
18
+ def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
19
+ if path = LOCALE_PATHS[locale]
20
+ Product.tags_from_page(self, "http://#{locale}.#{domain}/#{path}")
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # Get products with packager code
27
+ #
28
+ def products(page: -1)
29
+ Product.from_website_page(url, page: page, products_count: products_count) if url
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ require 'hashie'
2
+
3
+ module Openbeautyfacts
4
+ class Packaging < Hashie::Mash
5
+
6
+ # TODO: Add more locales
7
+ LOCALE_PATHS = {
8
+ 'fr' => 'conditionnements',
9
+ 'uk' => 'packaging',
10
+ 'us' => 'packaging',
11
+ 'world' => 'packaging'
12
+ }
13
+
14
+ class << self
15
+
16
+ # Get packagings
17
+ #
18
+ def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
19
+ if path = LOCALE_PATHS[locale]
20
+ Product.tags_from_page(self, "http://#{locale}.#{domain}/#{path}")
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # Get products with packaging
27
+ #
28
+ def products(page: -1)
29
+ Product.from_website_page(url, page: page, products_count: products_count) if url
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,56 @@
1
+ require 'hashie'
2
+ require 'nokogiri'
3
+ require 'open-uri'
4
+ require 'time'
5
+
6
+ module Openbeautyfacts
7
+ class Press < Hashie::Mash
8
+
9
+ # TODO: Add more locales
10
+ LOCALE_PATHS = {
11
+ 'fr' => 'presse',
12
+ 'uk' => 'press',
13
+ 'us' => 'press',
14
+ 'world' => 'press'
15
+ }
16
+
17
+ LOCALE_DATE_FORMATS = {
18
+ 'fr' => '%d/%m/%Y',
19
+ 'uk' => '%m/%d/%Y',
20
+ 'us' => '%m/%d/%Y',
21
+ 'world' => '%m/%d/%Y'
22
+ }
23
+
24
+ class << self
25
+ def items(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
26
+ if path = LOCALE_PATHS[locale]
27
+ html = open("http://#{locale}.#{domain}/#{path}").read
28
+ dom = Nokogiri::HTML.fragment(html)
29
+
30
+ titles = dom.css('#main_column li')
31
+ titles.each_with_index.map do |item, index|
32
+ data = item.inner_html.split(' - ')
33
+
34
+ link = Nokogiri::HTML.fragment(data.first).css('a')
35
+ attributes = {
36
+ "title" => link.text.strip,
37
+ "url" => link.attr('href').value
38
+ }
39
+
40
+ last = Nokogiri::HTML.fragment(data.last)
41
+ if date_format = LOCALE_DATE_FORMATS[locale] and date = last.text.strip[/\d+\/\d+\/\d+\z/, 0]
42
+ attributes["date"] = DateTime.strptime(date, date_format)
43
+ end
44
+
45
+ if data.length >= 3
46
+ attributes["source"] = Nokogiri::HTML.fragment(data[-2]).text.strip
47
+ end
48
+
49
+ new(attributes)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,185 @@
1
+ require 'hashie'
2
+ require 'net/http'
3
+ require 'nokogiri'
4
+ require 'open-uri'
5
+
6
+ module Openbeautyfacts
7
+ class Product < Hashie::Mash
8
+
9
+ # TODO: Add more locales
10
+ LOCALE_WEBURL_PREFIXES = {
11
+ 'fr' => 'produit',
12
+ 'uk' => 'product',
13
+ 'us' => 'product',
14
+ 'world' => 'product'
15
+ }
16
+
17
+ class << self
18
+
19
+ # Get product
20
+ #
21
+ def get(code, locale: DEFAULT_LOCALE)
22
+ if code
23
+ product_url = url(code, locale: locale)
24
+ json = open(product_url).read
25
+ hash = JSON.parse(json)
26
+
27
+ new(hash["product"]) if !hash["status"].nil? && hash["status"] == 1
28
+ end
29
+ end
30
+ alias_method :find, :get
31
+
32
+ # Return product API URL
33
+ #
34
+ def url(code, locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
35
+ if code
36
+ path = "api/v0/produit/#{code}.json"
37
+ "http://#{locale}.#{domain}/#{path}"
38
+ end
39
+ end
40
+
41
+ # Search products
42
+ #
43
+ def search(terms, locale: DEFAULT_LOCALE, page: 1, page_size: 20, sort_by: 'unique_scans_n', domain: DEFAULT_DOMAIN)
44
+ terms = URI::encode(terms)
45
+ path = "cgi/search.pl?search_terms=#{terms}&jqm=1&page=#{page}&page_size=#{page_size}&sort_by=#{sort_by}"
46
+ url = "http://#{locale}.#{domain}/#{path}"
47
+ json = open(url).read
48
+ hash = JSON.parse(json)
49
+ html = hash["jqm"]
50
+
51
+ from_jquery_mobile_list(html)
52
+ end
53
+ alias_method :where, :search
54
+
55
+ def from_html_list(html, list_css_selector, code_from_link_regex, locale: 'world')
56
+ dom = Nokogiri::HTML.fragment(html)
57
+ dom.css(list_css_selector).map do |product|
58
+ attributes = {}
59
+
60
+ if link = product.css('a').first
61
+ attributes["product_name"] = link.inner_text.strip
62
+
63
+ if code = link.attr('href')[code_from_link_regex, 1]
64
+ attributes["_id"] = code
65
+ attributes["code"] = code
66
+ end
67
+ end
68
+
69
+ if image = product.css('img').first and image_url = image.attr('src')
70
+ attributes["image_small_url"] = image_url
71
+ attributes["lc"] = Locale.locale_from_link(image_url)
72
+ end
73
+ attributes["lc"] ||= locale
74
+
75
+ new(attributes)
76
+ end
77
+
78
+ end
79
+
80
+ def from_jquery_mobile_list(jqm_html)
81
+ from_html_list(jqm_html, 'ul li:not(#loadmore)', /code=(\d+)\Z/i)
82
+ end
83
+
84
+ def from_website_list(html, locale: 'world')
85
+ from_html_list(html, 'ul.products li', /\/(\d+)[\/|\Z]/i, locale: 'world')
86
+ end
87
+
88
+ # page -1 to fetch all pages
89
+ def from_website_page(page_url, page: -1, products_count: nil)
90
+ if page == -1
91
+ if products_count # Avoid one call
92
+ pages_count = (products_count.to_f / 20).ceil
93
+ (1..pages_count).map { |page| from_website_page(page_url, page: page) }.flatten
94
+ else
95
+ products = []
96
+
97
+ page = 1
98
+ begin
99
+ products_on_page = from_website_page(page_url, page: page)
100
+ products += products_on_page
101
+ page += 1
102
+ end while products_on_page.any?
103
+
104
+ products
105
+ end
106
+ else
107
+ html = open("#{page_url}/#{page}").read
108
+ from_website_list(html, locale: Locale.locale_from_link(page_url))
109
+ end
110
+ end
111
+
112
+ def tags_from_page(_klass, page_url, &custom_tag_parsing)
113
+ html = open(page_url).read
114
+ dom = Nokogiri::HTML.fragment(html)
115
+
116
+ dom.css('table#tagstable tbody tr').map do |tag|
117
+ if custom_tag_parsing
118
+ custom_tag_parsing.call(tag)
119
+ else
120
+ link = tag.css('a').first
121
+
122
+ _klass.new({
123
+ "name" => link.text.strip,
124
+ "url" => URI.join(page_url, link.attr('href')).to_s,
125
+ "products_count" => tag.css('td')[1].text.to_i
126
+ })
127
+ end
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ # Fetch product
134
+ #
135
+ def fetch
136
+ if (self.code)
137
+ product = self.class.get(self.code)
138
+ self.merge!(product)
139
+ end
140
+
141
+ self
142
+ end
143
+ alias_method :reload, :fetch
144
+
145
+ # Update product
146
+ # Only product_name, brands and quantity fields seems to be updatable throught app / API.
147
+ # User can be nil
148
+ # Tested not updatable fields: countries, ingredients_text, purchase_places, purchase_places_tag, purchase_places_tags
149
+ #
150
+ def update(user: nil, domain: DEFAULT_DOMAIN)
151
+ if self.code && self.lc
152
+ subdomain = self.lc == 'world' ? 'world' : "world-#{self.lc}"
153
+ path = 'cgi/product_jqm.pl'
154
+ uri = URI("http://#{subdomain}.#{domain}/#{path}")
155
+ params = self.to_hash
156
+ params.merge!("user_id" => user.user_id, "password" => user.password) if user
157
+ response = Net::HTTP.post_form(uri, params)
158
+
159
+ data = JSON.parse(response.body)
160
+ data["status"] == 1
161
+ else
162
+ false
163
+ end
164
+ end
165
+ alias_method :save, :update
166
+
167
+ # Return Product API URL
168
+ #
169
+ def url(locale: DEFAULT_LOCALE)
170
+ self.class.url(self.code, locale: locale)
171
+ end
172
+
173
+ # Return Product web URL according to locale
174
+ #
175
+ def weburl(locale: nil, domain: DEFAULT_DOMAIN)
176
+ locale ||= self.lc || DEFAULT_LOCALE
177
+
178
+ if self.code && prefix = LOCALE_WEBURL_PREFIXES[locale]
179
+ path = "#{prefix}/#{self.code}"
180
+ "http://#{locale}.#{domain}/#{path}"
181
+ end
182
+ end
183
+
184
+ end
185
+ end