├── .python-version ├── runtime.txt ├── Procfile ├── .github ├── FUNDING.yml ├── CODEOWNERS ├── workflows │ ├── auto-assign-pr.yml │ ├── release-please.yml │ ├── semantic-pr.yml │ ├── codeql-analysis.yml │ └── ci.yml └── dependabot.yml ├── runApiRESTServer.py ├── requirements.txt ├── flask_rest_service ├── static │ ├── css │ │ └── custom.css │ └── js │ │ └── graphs.js ├── resources_stats.py ├── resources_root.py ├── __init__.py ├── templates │ └── index.html └── resources_products.py ├── app.json ├── .gitignore ├── LICENSE └── README.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.6.6 2 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.2 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn runApiRESTServer:app --log-file - 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: openfoodfacts 4 | -------------------------------------------------------------------------------- /runApiRESTServer.py: -------------------------------------------------------------------------------- 1 | from flask_rest_service import app 2 | import os 3 | 4 | if __name__ == '__main__': 5 | port = int(os.environ.get('PORT', 5000)) 6 | app.run(host='0.0.0.0', port=port, debug=os.environ.get('DEBUG', True)) 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.0.2 2 | Flask-PyMongo==2.3.0 3 | Flask-RESTful==0.3.9 4 | Jinja2==3.0.3 5 | MarkupSafe==2.0.1 6 | Werkzeug==2.0.2 7 | aniso8601==9.0.1 8 | itsdangerous==2.0.1 9 | pymongo==4.0.1 10 | pytz==2022.7.1 11 | six==1.16.0 12 | requests==2.26.0 13 | gunicorn==20.1.0 14 | -------------------------------------------------------------------------------- /flask_rest_service/static/css/custom.css: -------------------------------------------------------------------------------- 1 | #date-range { 2 | font-weight: bold; 3 | font-size: 20px; 4 | } 5 | 6 | #number-products { 7 | font-size: 40px; 8 | } 9 | 10 | #number-products-total { 11 | font-size: 40px; 12 | } 13 | 14 | .dc-chart g.row text { 15 | fill: black; 16 | font-size: 12px; 17 | } -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # review when someone opens a pull request. 4 | # For more on how to customize the CODEOWNERS file - https://help.github.com/en/articles/about-code-owners 5 | 6 | * @openfoodfacts/openfoodfacts-python 7 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign-pr.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/auto-author-assign.yml 2 | name: 'Auto Author Assign' 3 | 4 | on: 5 | pull_request_target: 6 | types: [opened, reopened] 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | assign-author: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: toshimaru/auto-author-assign@v2.1.1 16 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Run release-please 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: GoogleCloudPlatform/release-please-action@v4 11 | with: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | release-type: simple 14 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Semantic PRs" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v6 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "monthly" 7 | commit-message: 8 | prefix: "chore" 9 | include: "scope" 10 | open-pull-requests-limit: 300 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | commit-message: 17 | prefix: "chore" 18 | include: "scope" 19 | open-pull-requests-limit: 300 20 | -------------------------------------------------------------------------------- /flask_rest_service/resources_stats.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pymongo 3 | from flask import request, abort, json, render_template, Response 4 | import flask_restful as restful 5 | from flask_rest_service import app, api, mongo 6 | from bson.objectid import ObjectId 7 | from bson.code import Code 8 | 9 | 10 | # ----- /stats ----- 11 | class Stats(restful.Resource): 12 | 13 | # ----- GET Request ----- 14 | def get(self): 15 | return Response(render_template("index.html") , mimetype='text/html') 16 | 17 | 18 | 19 | api.add_resource(Stats, '/stats') -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenFoodFacts-APIRestPython", 3 | "description": "API for Open Food Facts", 4 | "keywords": [ 5 | "api", 6 | "food", 7 | "data", 8 | "open" 9 | ], 10 | "website": "https://openfoodfacts.org/", 11 | "logo": "https://openfoodfacts.org/images/misc/openfoodfacts-logo-356.png", 12 | "repository": "https://github.com/openfoodfacts/OpenFoodFacts-APIRestPython", 13 | "image": "heroku/python", 14 | "env": { 15 | "MONGO_URL": { 16 | "description": "mongodb://:@:/", 17 | "required": true 18 | }, 19 | "DEBUG": { 20 | "value": "False" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /flask_rest_service/resources_root.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pymongo 3 | from flask import request, abort, json, Flask, render_template 4 | from flask_restful import Api, Resource 5 | from flask_restful import reqparse 6 | from flask_rest_service import app, api, mongo 7 | from bson.objectid import ObjectId 8 | from bson.code import Code 9 | 10 | # ----- / returns status OK and the MongoDB instance if the API is running ----- 11 | class Root(Resource): 12 | 13 | # ----- GET Request ----- 14 | def get(self): 15 | return { 16 | 'status': 'OK', 17 | 'mongo': str(mongo.db), 18 | } 19 | 20 | 21 | api.add_resource(Root, '/') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /flask_rest_service/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from flask_restful import Api 4 | from flask_pymongo import PyMongo 5 | from flask import make_response 6 | from bson.json_util import dumps 7 | import logging 8 | 9 | # ----- Define MongoDB variables ----- 10 | MONGO_URL = os.environ.get('MONGO_URL') 11 | if not MONGO_URL: 12 | #MONGO_URL = "mongodb://localhost:27017/off-fr"; 13 | MONGO_URL = "mongodb://offread:offapiread@localhost:27017/off-fr"; 14 | 15 | app = Flask(__name__) 16 | 17 | app.config['MONGO_URI'] = MONGO_URL 18 | app.config['MONGO_DBNAME'] = 'off-fr' 19 | 20 | log = logging.getLogger('werkzeug') 21 | log.setLevel(logging.ERROR) 22 | 23 | mongo = PyMongo(app) 24 | 25 | # ----- Output JSON function ----- 26 | def output_json(obj, code, headers=None): 27 | headers['Access-Control-Allow-Origin'] = '*' 28 | resp = make_response(dumps(obj), code) 29 | resp.headers.extend(headers or {}) 30 | return resp 31 | 32 | DEFAULT_REPRESENTATIONS = {'application/json': output_json} 33 | api = Api(app) 34 | api.representations = DEFAULT_REPRESENTATIONS 35 | 36 | # ----- Import all the WebServices ----- 37 | import flask_rest_service.resources_root 38 | import flask_rest_service.resources_products 39 | import flask_rest_service.resources_stats 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 9 * * 1' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v6.0.0 17 | with: 18 | # We must fetch at least the immediate parents so that if this is 19 | # a pull request then we can checkout the head. 20 | fetch-depth: 2 21 | 22 | # If this run was triggered by a pull request event, then checkout 23 | # the head of the pull request instead of the merge commit. 24 | - run: git checkout HEAD^2 25 | if: ${{ github.event_name == 'pull_request' }} 26 | 27 | # Initializes the CodeQL tools for scanning. 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v4 30 | # Override language selection by uncommenting this and choosing your languages 31 | # with: 32 | # languages: go, javascript, csharp, python, cpp, java 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v4 38 | 39 | # ℹ️ Command-line programs to run using the OS shell. 40 | # 📚 https://git.io/JvXDl 41 | 42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 43 | # and modify them (or add more) to build your code if your project 44 | # uses a compiled language 45 | 46 | #- run: | 47 | # make bootstrap 48 | # make release 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v4 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ '*' ] 10 | pull_request: 11 | branches: [ '*' ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - uses: actions/checkout@v6.0.0 23 | - uses: actions/setup-python@v6.1.0 24 | with: 25 | python-version: '3.8' # Version range or exact version of a Python version to use, using SemVer's version range syntax 26 | architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | # Runs a single command using the runners shell 39 | - name: Test with pytest 40 | run: | 41 | pytest 42 | - name: Download the full OFF database 43 | run: curl https://world.openfoodfacts.org/data/openfoodfacts-mongodbdump.tar.gz 44 | - name: Import to local mongodb 45 | run: mongorestore -d off -c products /foldertobsonfile/products.bson 46 | - name: Launch API 47 | run: python3 runApiRESTServer.py 48 | -------------------------------------------------------------------------------- /flask_rest_service/static/js/graphs.js: -------------------------------------------------------------------------------- 1 | queue() 2 | .defer(d3.json, "/products/stats/info?dateby=1") 3 | .await(makeGraphs); 4 | 5 | function makeGraphs(error, productsJSON, statesJson) { 6 | 7 | //Clean productsJSON data 8 | var products = productsJSON; 9 | var dateFormat = d3.time.format("%Y-%m"); 10 | var totalProd = 0; 11 | products.forEach(function(d) { 12 | var dateyear = d["dateyear"]; 13 | var datemonth = d["datemonth"]; 14 | d["date"] = dateFormat.parse(dateyear + '-' + datemonth); 15 | totalProd += d['count']; 16 | if(!d['saltlevels']) d['saltlevels'] = 'undifined'; 17 | if(!d['fatlevels']) d['fatlevels'] = 'undifined'; 18 | if(!d['saturatedfatlevels']) d['saturatedfatlevels'] = 'undifined'; 19 | if(!d['sugarslevels']) d['sugarslevels'] = 'undifined'; 20 | }); 21 | 22 | //Create a Crossfilter instance 23 | var ndx = crossfilter(products); 24 | 25 | //Define Dimensions 26 | var dateDim = ndx.dimension(function(d) { return d["date"]; }); 27 | var numberProductsDim = ndx.dimension(function(d) { return d["count"]; }); 28 | var saltLevelsProductsDim = ndx.dimension(function(d) { return d["saltlevels"]; }); 29 | var fatLevelsProductsDim = ndx.dimension(function(d) { return d["fatlevels"]; }); 30 | var saturatedFatLevelsProductsDim = ndx.dimension(function(d) { return d["saturatedfatlevels"]; }); 31 | var sugarsLevelsProductsDim = ndx.dimension(function(d) { return d["sugarslevels"]; }); 32 | 33 | //Calculate metrics 34 | var numProductsByDate = dateDim.group().reduceSum(function(d){return d.count;}); 35 | var numProductsSaltLevels = saltLevelsProductsDim.group().reduceSum(function(d){return d.count;}); 36 | var numProductsFatLevels = fatLevelsProductsDim.group().reduceSum(function(d){return d.count;}); 37 | var numProductsSaturedFatLevels = saturatedFatLevelsProductsDim.group().reduceSum(function(d){return d.count;}); 38 | var numProductsSugarsLevels = sugarsLevelsProductsDim.group().reduceSum(function(d){return d.count;}); 39 | 40 | var all = ndx.groupAll(); 41 | var totalProducts = ndx.groupAll().reduceSum(function(d) {return d["count"];}); 42 | 43 | //Define values (to be used in charts) 44 | var minDate = dateDim.bottom(1)[0]["date"]; 45 | var maxDate = dateDim.top(1)[0]["date"]; 46 | maxDate = new Date(new Date(maxDate).setMonth(maxDate.getMonth()+1)); 47 | 48 | //Charts 49 | var timeChart = dc.lineChart("#time-chart"); 50 | var saltChart = dc.pieChart("#salt-levels"); 51 | var fatChart = dc.pieChart("#fat-levels"); 52 | var sugarsChart = dc.pieChart("#sugars-levels"); 53 | var saturedFatChart = dc.pieChart("#saturated-fat-levels"); 54 | var productsDisp = dc.numberDisplay("#number-products"); 55 | var totalProductsDisp = document.getElementById('number-products-total'); 56 | var dateRange = document.getElementById('date-range'); 57 | totalProductsDisp.innerText = totalProd; 58 | 59 | productsDisp 60 | .formatNumber(d3.format("d")) 61 | .valueAccessor(function(d){return d; }) 62 | .group(totalProducts); 63 | 64 | saltChart 65 | .width(250) 66 | .height(250) 67 | .dimension(saltLevelsProductsDim) 68 | .group(numProductsSaltLevels) 69 | .innerRadius(30); 70 | 71 | saturedFatChart 72 | .width(250) 73 | .height(250) 74 | .dimension(saturatedFatLevelsProductsDim) 75 | .group(numProductsSaturedFatLevels) 76 | .innerRadius(30); 77 | 78 | fatChart 79 | .width(250) 80 | .height(250) 81 | .dimension(fatLevelsProductsDim) 82 | .group(numProductsFatLevels) 83 | .innerRadius(30); 84 | 85 | sugarsChart 86 | .width(250) 87 | .height(250) 88 | .dimension(numProductsSugarsLevels) 89 | .group(numProductsSugarsLevels) 90 | .innerRadius(30); 91 | 92 | timeChart 93 | .width(680) 94 | .height(300) 95 | .margins({top: 10, right: 10, bottom: 20, left: 40}) 96 | .dimension(dateDim) 97 | .group(numProductsByDate) 98 | .renderlet(function(chart){ 99 | dc.events.trigger(function(){ 100 | var firstDateNonParsed = timeChart.brush().extent()[0].toISOString().split('T')[0]; 101 | var lastDateNonParsed = timeChart.brush().extent()[1].toISOString().split('T')[0]; 102 | if(firstDateNonParsed == '2012-01-01' && lastDateNonParsed == '2012-01-01') { 103 | lastDateNonParsed = dateDim.top(1)[0]["date"].toISOString().split('T')[0]; 104 | } 105 | dateRange.innerText = firstDateNonParsed + ' / ' + lastDateNonParsed; 106 | }); 107 | }) 108 | .renderArea(true) 109 | .transitionDuration(500) 110 | .x(d3.time.scale().domain([minDate, maxDate])) 111 | .xUnits(d3.time.month) 112 | .elasticY(true); 113 | 114 | dc.renderAll(); 115 | 116 | }; -------------------------------------------------------------------------------- /flask_rest_service/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 | Number of Products 28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 | Number total of products 40 |
41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 |
49 |
50 |
51 | Range date 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Number of products added 62 |
63 |
64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 | 74 |
75 |
76 |
77 | Salt levels 78 |
79 |
80 |
81 |
82 |
83 |
84 | 85 | 86 |
87 |
88 |
89 | Sugars levels 90 |
91 |
92 |
93 |
94 |
95 |
96 | 97 | 98 |
99 |
100 |
101 | Fat levels 102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 | 110 |
111 |
112 |
113 | Saturated-fat levels 114 |
115 |
116 |
117 |
118 |
119 |
120 | 121 |
122 |
123 |
124 |
125 |

Built with ♥ by Scot Scriven

126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 Scriven Scot 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /flask_rest_service/resources_products.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pymongo 3 | from flask import request, abort, json 4 | import flask_restful as restful 5 | from flask_rest_service import app, api, mongo 6 | from bson.objectid import ObjectId 7 | from bson.code import Code 8 | 9 | # ----- /products ----- 10 | class ProductsList(restful.Resource): 11 | 12 | # ----- GET Request ----- 13 | def get(self): 14 | # ----- Get limit # in the request, 50 by default ----- 15 | limit = request.args.get('limit',default=50,type=int) 16 | # ----- Get skip # in the request, 0 by default ----- 17 | skip = request.args.get('skip',default=0,type=int) 18 | # ----- Get count # in the request, 0 by default ----- 19 | count = request.args.get('count',default=0, type=int) 20 | # ----- See if we want short response or not, 0 by default ----- 21 | short = request.args.get('short',default=0, type=int) 22 | # ----- Query ----- 23 | query = request.args.get('q') 24 | 25 | 26 | # ----- The regex param makes it search "like" things, the options one tells to be case insensitive 27 | data = dict((key, {'$regex' : request.args.get(key), '$options' : '-i'}) for key in request.args.keys()) 28 | 29 | # ----- Delete custom parameters from the request, since they are not in MongoDB ----- 30 | # ----- And add filters to custom parameters : ----- 31 | if request.args.get('limit'): 32 | del data['limit'] 33 | 34 | if request.args.get('skip'): 35 | del data['skip'] 36 | 37 | if request.args.get('count'): 38 | del data['count'] 39 | 40 | if request.args.get('short'): 41 | del data['short'] 42 | 43 | if request.args.get('q'): 44 | del data['q'] 45 | 46 | # ----- Filter data returned ----- 47 | # ----- If we just want a short response ----- 48 | # ----- Tell which fields we want ----- 49 | fieldsNeeded = {'code':1, 'lang':1, 'product_name':1} 50 | if request.args.get('short') and short == 1: 51 | if request.args.get('count') and count == 1 and not request.args.get('q'): 52 | return mongo.db.products.find(data, fieldsNeeded).count() 53 | elif not request.args.get('count') and not request.args.get('q'): 54 | return mongo.db.products.find(data, fieldsNeeded).sort('created_t', pymongo.DESCENDING).skip(skip).limit(limit) 55 | elif request.args.get('count') and count == 1 and request.args.get('q'): 56 | return mongo.db.products.find({ "$text" : { "$search": query } }, fieldsNeeded).count() 57 | else: 58 | return mongo.db.products.find({ "$text" : { "$search": query } }, fieldsNeeded).sort('created_t', pymongo.DESCENDING).skip(skip).limit(limit) 59 | else: 60 | if request.args.get('count') and count == 1 and not request.args.get('q'): 61 | return mongo.db.products.find(data).count() 62 | elif not request.args.get('count') and not request.args.get('q'): 63 | return mongo.db.products.find(data).sort('created_t', pymongo.DESCENDING).skip(skip).limit(limit) 64 | elif request.args.get('count') and count == 1 and request.args.get('q'): 65 | return mongo.db.products.find({ "$text" : { "$search": query } }).count() 66 | else: 67 | return mongo.db.products.find({ "$text" : { "$search": query } }).sort('created_t', pymongo.DESCENDING).skip(skip).limit(limit) 68 | 69 | 70 | # ----- /products/stats/info----- 71 | class ProductsStats(restful.Resource): 72 | 73 | # ----- GET Request ----- 74 | def get(self): 75 | 76 | map = Code("function () {" 77 | " var date = new Date(this.created_t*1000);" 78 | " var parsedDateMonth = date.getMonth();" 79 | " var parsedDateYear = date.getFullYear();" 80 | " var saltLevelsValue = null;" 81 | " var fatLevelsValue = null;" 82 | " var saturatedfatLevelsValue = null;" 83 | " var sugarsLevelsValue = null;" 84 | " if(!parsedDateYear || !parsedDateMonth) return;" 85 | " if(this.hasOwnProperty('nutrient_levels')) {" 86 | " saltLevelsValue = this.nutrient_levels.salt;" 87 | " fatLevelsValue = this.nutrient_levels.fat;" 88 | " saturatedfatLevelsValue = this['nutrient_levels']['saturated-fat'];" 89 | " sugarsLevelsValue = this.nutrient_levels.sugars;" 90 | " } else { saltLevelsValue = null; fatLevelsValue = null; saturatedfatLevelsValue = null, sugarsLevelsValue = null}" 91 | " emit({year : parsedDateYear, month : parsedDateMonth, saltlevels : saltLevelsValue, fatlevels : fatLevelsValue, saturatedfatlevels : saturatedfatLevelsValue, sugarslevels : sugarsLevelsValue}, {count:1});" 92 | "};") 93 | 94 | reduce = Code("function (key, values) {" 95 | " var count = 0;" 96 | " var ret = {count : 0};" 97 | " for (index in values) {" 98 | " ret.count += values[index].count;" 99 | " }" 100 | " return ret;" 101 | " };") 102 | 103 | listRes = [] 104 | result = mongo.db.products.map_reduce(map, reduce, "stats_products") 105 | for doc in result.find(): 106 | res = {} 107 | res['dateyear'] = doc['_id']['year'] 108 | res['datemonth'] = doc['_id']['month'] 109 | res['count'] = doc['value']['count'] 110 | res['saltlevels'] = doc['_id']['saltlevels'] 111 | res['saturatedfatlevels'] = doc['_id']['saturatedfatlevels'] 112 | res['sugarslevels'] = doc['_id']['sugarslevels'] 113 | res['fatlevels'] = doc['_id']['fatlevels'] 114 | listRes.append(res) 115 | return listRes 116 | 117 | 118 | # ----- /product/ ----- 119 | class ProductId(restful.Resource): 120 | 121 | # ----- GET Request ----- 122 | def get(self, barcode): 123 | return mongo.db.products.find_one({"code":barcode}) 124 | 125 | 126 | # ----- /products/brands ----- 127 | class ProductsBrands(restful.Resource): 128 | 129 | # ----- GET Request ----- 130 | def get(self): 131 | # ----- Get count # in the request, 0 by default ----- 132 | count = request.args.get('count',default=0, type=int) 133 | query = request.args.get('query') 134 | 135 | if request.args.get('query'): 136 | map = Code("function () {" 137 | " if (!this.brands) return;" 138 | " emit(this.brands.trim(), 1);" 139 | "};") 140 | 141 | reduce = Code("function (key, values) {" 142 | " var count = 0;" 143 | " for (index in values) {" 144 | " count += values[index];" 145 | " }" 146 | " return count;" 147 | " };") 148 | 149 | result = mongo.db.products.map_reduce(map, reduce, "brands_products") 150 | 151 | res = [] 152 | for doc in result.find({"_id": {'$regex' : query, '$options' : '-i'}}): 153 | res.append(doc['_id']) 154 | if request.args.get('count') and count == 1: 155 | return len(res) 156 | else: 157 | return res 158 | else: 159 | if request.args.get('count') and count == 1: 160 | return len(mongo.db.products.distinct('brands')) 161 | else: 162 | return mongo.db.products.distinct('brands') 163 | 164 | 165 | # ----- /products/categories ----- 166 | class ProductsCategories(restful.Resource): 167 | 168 | # ----- GET Request ----- 169 | def get(self): 170 | # ----- Get count # in the request, 0 by default ----- 171 | count = request.args.get('count',default=0, type=int) 172 | query = request.args.get('query') 173 | 174 | if request.args.get('query'): 175 | map = Code("function () {" 176 | " if (!this.categories) return;" 177 | " emit(this.categories.trim(), 1);" 178 | "};") 179 | 180 | reduce = Code("function (key, values) {" 181 | " var count = 0;" 182 | " for (index in values) {" 183 | " count += values[index];" 184 | " }" 185 | " return count;" 186 | " };") 187 | 188 | result = mongo.db.products.map_reduce(map, reduce, "categories_products") 189 | 190 | res = [] 191 | for doc in result.find({"_id": {'$regex' : query, '$options' : '-i'}}): 192 | res.append(doc['_id']) 193 | if request.args.get('count') and count == 1: 194 | return len(res) 195 | else: 196 | return res 197 | else: 198 | if request.args.get('count') and count == 1: 199 | return len(mongo.db.products.distinct('categories')) 200 | else: 201 | return mongo.db.products.distinct('categories') 202 | 203 | 204 | # ----- /products/countries ----- 205 | class ProductsCountries(restful.Resource): 206 | 207 | # ----- GET Request ----- 208 | def get(self): 209 | # ----- Get count # in the request, 0 by default ----- 210 | count = request.args.get('count',default=0, type=int) 211 | query = request.args.get('query') 212 | 213 | if request.args.get('query'): 214 | map = Code("function () {" 215 | " if (!this.countries) return;" 216 | " emit(this.countries.trim(), 1);" 217 | "};") 218 | 219 | reduce = Code("function (key, values) {" 220 | " var count = 0;" 221 | " for (index in values) {" 222 | " count += values[index];" 223 | " }" 224 | " return count;" 225 | " };") 226 | 227 | result = mongo.db.products.map_reduce(map, reduce, "countries_products") 228 | 229 | res = [] 230 | for doc in result.find({"_id": {'$regex' : query, '$options' : '-i'}}): 231 | res.append(doc['_id']) 232 | if request.args.get('count') and count == 1: 233 | return len(res) 234 | else: 235 | return res 236 | else: 237 | if request.args.get('count') and count == 1: 238 | return len(mongo.db.products.distinct('countries')) 239 | else: 240 | return mongo.db.products.distinct('countries') 241 | 242 | 243 | # ----- /products/additives ----- 244 | class ProductsAdditives(restful.Resource): 245 | 246 | # ----- GET Request ----- 247 | def get(self): 248 | # ----- Get count # in the request, 0 by default ----- 249 | count = request.args.get('count',default=0, type=int) 250 | query = request.args.get('query') 251 | 252 | if request.args.get('query'): 253 | map = Code("function () {" 254 | " if (!this.colours) return;" 255 | " this.additives_tags.forEach(function (c){" 256 | " emit(c.trim(), 1)" 257 | " });" 258 | "};") 259 | 260 | reduce = Code("function (key, values) {" 261 | " var count = 0;" 262 | " for (index in values) {" 263 | " count += values[index];" 264 | " }" 265 | " return count;" 266 | " };") 267 | 268 | result = mongo.db.products.map_reduce(map, reduce, "additives_products") 269 | 270 | res = [] 271 | for doc in result.find({"_id": {'$regex' : query, '$options' : '-i'}}): 272 | res.append(doc['_id']) 273 | if request.args.get('count') and count == 1: 274 | return len(res) 275 | else: 276 | return res 277 | else: 278 | if request.args.get('count') and count == 1: 279 | return len(mongo.db.products.distinct('additives_tags')) 280 | else: 281 | return mongo.db.products.distinct('additives_tags') 282 | 283 | 284 | # ----- /products/allergens ----- 285 | class ProductsAllergens(restful.Resource): 286 | 287 | # ----- GET Request ----- 288 | def get(self): 289 | # ----- Get count # in the request, 0 by default ----- 290 | count = request.args.get('count',default=0, type=int) 291 | query = request.args.get('query') 292 | 293 | if request.args.get('query'): 294 | map = Code("function () {" 295 | " if (!this.colours) return;" 296 | " this.allergens_tags.forEach(function (c){" 297 | " emit(c.trim(), 1)" 298 | " });" 299 | "};") 300 | 301 | reduce = Code("function (key, values) {" 302 | " var count = 0;" 303 | " for (index in values) {" 304 | " count += values[index];" 305 | " }" 306 | " return count;" 307 | " };") 308 | 309 | result = mongo.db.products.map_reduce(map, reduce, "allergens_products") 310 | 311 | res = [] 312 | for doc in result.find({"_id": {'$regex' : query, '$options' : '-i'}}): 313 | res.append(doc['_id']) 314 | if request.args.get('count') and count == 1: 315 | return len(res) 316 | else: 317 | return res 318 | else: 319 | if request.args.get('count') and count == 1: 320 | return len(mongo.db.products.distinct('allergens_tags')) 321 | else: 322 | return mongo.db.products.distinct('allergens_tags') 323 | 324 | 325 | 326 | api.add_resource(ProductsList, '/products') 327 | api.add_resource(ProductsStats, '/products/stats/info') 328 | api.add_resource(ProductId, '/product/') 329 | api.add_resource(ProductsBrands, '/products/brands') 330 | api.add_resource(ProductsCategories, '/products/categories') 331 | api.add_resource(ProductsCountries, '/products/countries') 332 | api.add_resource(ProductsAdditives, '/products/additives') 333 | api.add_resource(ProductsAllergens, '/products/allergens') -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Food Facts API Rest Python 2 | 3 | OFF API provides programmatic access to Open Food Facts functionality and content.
4 | 5 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/openfoodfacts/OpenFoodFacts-APIRestPython) 6 | 7 | To try the API : https://openfoodfacts-api.herokuapp.com/
8 | Or if you want to try in localhost, see below. 9 | 10 | The API is [REST API](https://en.wikipedia.org/wiki/Representational_State_Transfer "RESTful"). 11 | Currently, return format for all endpoints is [JSON](https://json.org/ "JSON"). 12 | 13 | ## Status 14 | 15 | 16 | [![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges) 17 | [![Build Status](https://travis-ci.org/openfoodfacts/OpenFoodFacts-APIRestPython.svg?branch=master)](https://travis-ci.org/openfoodfacts/OpenFoodFacts-APIRestPython) 18 | [![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/openfoodfacts/OpenFoodFacts-APIRestPython.svg)](https://isitmaintained.com/project/openfoodfacts/OpenFoodFacts-APIRestPython "Average time to resolve an issue") 19 | [![Percentage of issues still open](https://isitmaintained.com/badge/open/openfoodfacts/OpenFoodFacts-APIRestPython.svg)](https://isitmaintained.com/project/openfoodfacts/OpenFoodFacts-APIRestPython "Percentage of issues still open") 20 | 21 | ## What is this repository for? 22 | 23 | This piece of software’s main goals are: 24 | * To make it easy to retrieve data using HTTP requests 25 | * To provide filters in the API 26 | * To provide custom filters 27 | 28 | 29 | ## Setup for localhost 30 | 31 | * Install python 3 32 | * Install mongodb 33 | * Install pip 34 | * Install requirements: `$ pip install -r requirements.txt` 35 | * Download the database from : https://world.openfoodfacts.org/data/openfoodfacts-mongodbdump.tar.gz 36 | * Import to local mongodb: `$ mongorestore -d off -c products /foldertobsonfile/products.bson` 37 | * Launch api: `$ python3 runApiRESTServer.py` 38 | * That's all! 39 | 40 | ## Documentation 41 | ### How to use it? 42 | 43 | Simple filter: `/products?origins=United Kingdom`
44 | Complex filter: `/products?nutrition_grade_fr=a&origins=United Kingdom`
45 | For arrays, a “.” will be used as a separator like so: `/products?nutrient_levels.salt=low` 46 | Searchs can be inexact like :`/products?ingredients_text=beef`
47 | It will retrieve tags like “beef braising steak”, “beef steak”... 48 | 49 | /!\ By default the objects will be sorted by `created_t` in order to have the most important objects first 50 | 51 | URL to query | Description 52 | ------------------------------ | --------------------------- 53 | GET `/product/` | Get a product by barcode eg. `/product/737628064502` 54 | GET `/products/brands` | Return a list of `Brands`. If you want to query brands, to do for example an autocomplete field in ajax, query the API like: `/products/brands?query=Auch` or `/products/brands?query=Sains`. 55 | GET `/products/categories` | Return a list of `Categories`. If you want to query categories, to do for example an autocomplete field in ajax, query the API like: `/products/categories?query=Ric` or `/products/categories?query=plant`. 56 | GET `/products/countries` | Return a list of `Countries`. If you want to query countries, to do for example an autocomplete field in ajax, query the API like: `/products/countries?query=Fra` or `/products/countries?query=Aus`. 57 | GET `/products/additives` | Return a list of `Additives`. If you want to query additives, to do for example an autocomplete field in ajax, query the API like: `/products/additives?query=Citric` or `/products/additives?query=acid`. 58 | GET `/products/allergens` | Return a list of `Allergens`. If you want to query allergens, to do for example an autocomplete field in ajax, query the API like: `/products/allergens?query=milk` or `/products/allergens?query=oil`. 59 | 60 | ### Options 61 | 62 | Field | Value by default | Value type 63 | ------------- | ---------------- | --------- 64 | limit= | 50 | limit the number of products returned 65 | skip= | 0 | skips the specified number of products returned 66 | count= | 0 | if 1 then returns the number of rows 67 | short= | 0 | Filters rows retrieved, make it faster for lists for example, if 1 columns projection on `code`, `lang` and `product_name` 68 | q= | none | search text on indexed fields 69 | 70 | ### Indexed fields 71 | 72 | Some fields are described here: https://world.openfoodfacts.org/data/data-fields.txt 73 | 74 | ### Example 75 | **Request** 76 | 77 | GET /product/737628064502 78 | 79 | **Return** 80 | ``` json 81 | { 82 | "status_verbose":"product found", 83 | "status":1, 84 | "product":{ 85 | "last_edit_dates_tags":[ 86 | "2012-12-11", 87 | "2012-12", 88 | "2012" 89 | ], 90 | "labels_hierarchy":[ 91 | "en:gluten-free" 92 | ], 93 | "_id":"737628064502", 94 | "categories_hierarchy":[ 95 | "en:Rice Noodles" 96 | ], 97 | "pnns_groups_1":"unknown", 98 | "states_tags":[ 99 | "en:to-be-checked", 100 | "en:complete", 101 | "en:nutrition-facts-completed", 102 | "en:ingredients-completed", 103 | "en:expiration-date-to-be-completed", 104 | "en:characteristics-completed", 105 | "en:photos-validated", 106 | "en:photos-uploaded" 107 | ], 108 | "checkers_tags":[ 109 | ], 110 | "labels_tags":[ 111 | "en:gluten-free" 112 | ], 113 | "image_small_url":"https://world.openfoodfacts.org/images/products/737/628/064/502/front.6.200.jpg", 114 | "code":"737628064502", 115 | "additives_tags_n":null, 116 | "traces_tags":[ 117 | "peanuts" 118 | ], 119 | "lang":"en", 120 | "photographers":[ 121 | "andre" 122 | ], 123 | "generic_name":"Rice Noodles", 124 | "ingredients_that_may_be_from_palm_oil_tags":[ 125 | ], 126 | "old_additives_tags":[ 127 | "en:e330" 128 | ], 129 | "_keywords":[ 130 | "thailand", 131 | "stir-fry", 132 | "kitchen", 133 | "thai", 134 | "free", 135 | "gluten", 136 | "rice", 137 | "noodle" 138 | ], 139 | "rev":15, 140 | "editors":[ 141 | "", 142 | "thierrym", 143 | "manu1400", 144 | "andre" 145 | ], 146 | "interface_version_created":"20120622", 147 | "emb_codes":"", 148 | "max_imgid":"5", 149 | "additives_tags":[ 150 | "en:e330" 151 | ], 152 | "emb_codes_orig":"", 153 | "informers_tags":[ 154 | "andre", 155 | "manu1400", 156 | "thierrym" 157 | ], 158 | "nutrient_levels_tags":[ 159 | "脂肪-in-moderate-quantity", 160 | "饱和脂肪-in-low-quantity", 161 | "糖-in-high-quantity", 162 | "食盐-in-high-quantity" 163 | ], 164 | "photographers_tags":[ 165 | "andre" 166 | ], 167 | "additives_n":1, 168 | "pnns_groups_2_tags":[ 169 | "unknown" 170 | ], 171 | "unknown_nutrients_tags":[ 172 | ], 173 | "packaging_tags":[ 174 | "cellophane", 175 | "carton" 176 | ], 177 | "nutriments":{ 178 | "sodium":"0.629", 179 | "sugars":10, 180 | "carbohydrates_unit":"g", 181 | "fat_unit":"g", 182 | "proteins_unit":"g", 183 | "nutrition-score-fr_100g":15, 184 | "fat":7, 185 | "proteins_serving":7, 186 | "sodium_serving":0.629, 187 | "salt":1.59766, 188 | "proteins":7, 189 | "nutrition-score-fr":15, 190 | "sugars_unit":"g", 191 | "fat_serving":7, 192 | "sodium_unit":"mg", 193 | "sugars_100g":"12.8", 194 | "saturated-fat_unit":"g", 195 | "sodium_100g":0.806, 196 | "saturated-fat_serving":1, 197 | "fiber_unit":"g", 198 | "energy":1297, 199 | "energy_unit":"kcal", 200 | "sugars_serving":10, 201 | "carbohydrates_100g":70.5, 202 | "nutrition-score-uk":15, 203 | "proteins_100g":8.97, 204 | "fiber_serving":0, 205 | "carbohydrates_serving":55, 206 | "energy_serving":1297, 207 | "fat_100g":"8.97", 208 | "saturated-fat_100g":"1.28", 209 | "nutrition-score-uk_100g":15, 210 | "fiber":0, 211 | "salt_serving":1.59766, 212 | "salt_100g":"2.05", 213 | "carbohydrates":55, 214 | "fiber_100g":0, 215 | "energy_100g":1660, 216 | "saturated-fat":1 217 | }, 218 | "countries_tags":[ 219 | "en:france" 220 | ], 221 | "ingredients_from_palm_oil_tags":[ 222 | ], 223 | "emb_codes_tags":[ 224 | ], 225 | "brands_tags":[ 226 | "thai-kitchen" 227 | ], 228 | "purchase_places":"", 229 | "pnns_groups_2":"unknown", 230 | "countries_hierarchy":[ 231 | "en:france" 232 | ], 233 | "traces":"Peanuts", 234 | "categories":"Rice Noodles", 235 | "ingredients_text":"RICE NOODLES (RICE, WATER), SEASONING PACKET (PEANUT, SUGAR, SALT, CORN STARCH, SPICES [CHILI, CINNAMON, PEPPER, CUMIN, CLOVE], HYDRDLYZED SOY PROTEIN, GREEN ONIONS, CITRIC ACID, PEANUT OIL, SESAME OIL, NATURAL FLAVOR). ", 236 | "created_t":1345799269, 237 | "product_name":"Stir-Fry Rice Noodles", 238 | "ingredients_from_or_that_may_be_from_palm_oil_n":0, 239 | "creator":"andre", 240 | "no_nutrition_data":null, 241 | "serving_size":"78 g", 242 | "completed_t":1355184837, 243 | "last_modified_by":"thierrym", 244 | "new_additives_n":1, 245 | "origins":"Thailand", 246 | "stores":"", 247 | "nutrition_grade_fr":"d", 248 | "nutrient_levels":{ 249 | "salt":"high", 250 | "fat":"moderate", 251 | "sugars":"high", 252 | "saturated-fat":"low" 253 | }, 254 | "stores_tags":[ 255 | ], 256 | "id":"737628064502", 257 | "countries":"France", 258 | "purchase_places_tags":[ 259 | ], 260 | "interface_version_modified":"20120622", 261 | "fruits-vegetables-nuts_100g_estimate":0, 262 | "sortkey":1355184837, 263 | "last_modified_t":1355184837, 264 | "nutrition_score_debug":" -- energy 4 + sat-fat 1 + fr-sat-fat-for-fats 0 + sugars 2 + sodium 8 - fruits 0% 0 - fiber 0 - proteins 5 -- fsa 15 -- fr 15", 265 | "countries.20131227":null, 266 | "correctors_tags":[ 267 | "andre", 268 | "thierrym" 269 | ], 270 | "correctors":[ 271 | "andre", 272 | "thierrym" 273 | ], 274 | "new_additives_debug":"lc: en - [ rice-noodles -> en:rice-noodles ] [ rice -> en:rice ] [ water -> en:water ] [ seasoning-packet -> en:seasoning-packet ] [ peanut -> en:peanut ] [ sugar -> en:sugar ] [ salt -> en:salt ] [ corn-starch -> en:corn-starch ] [ spices-chili -> en:spices-chili ] [ cinnamon -> en:cinnamon ] [ pepper -> en:pepper ] [ cumin -> en:cumin ] [ clove -> en:clove ] [ hydrdlyzed-soy-protein -> en:hydrdlyzed-soy-protein ] [ green-onions -> en:green-onions ] [ citric-acid -> en:e330 -> exists ] [ peanut-oil -> en:peanut-oil ] [ sesame-oil -> en:sesame-oil ] [ natural-flavor -> en:natural-flavor ] ", 275 | "brands":"Thai Kitchen", 276 | "ingredients_tags":[ 277 | "rice-noodles", 278 | "seasoning-packet", 279 | "", 280 | "rice", 281 | "water", 282 | "peanut", 283 | "sugar", 284 | "salt", 285 | "corn-starch", 286 | "spices", 287 | "chili", 288 | "cinnamon", 289 | "pepper", 290 | "cumin", 291 | "clove", 292 | "hydrdlyzed-soy-protein", 293 | "green-onions", 294 | "citric-acid", 295 | "peanut-oil", 296 | "sesame-oil", 297 | "natural-flavor" 298 | ], 299 | "new_additives_tags":[ 300 | "en:e330" 301 | ], 302 | "states":"en:to-be-checked, en:complete, en:nutrition-facts-completed, en:ingredients-completed, en:expiration-date-to-be-completed, en:characteristics-completed, en:photos-validated, en:photos-uploaded", 303 | "informers":[ 304 | "andre", 305 | "manu1400", 306 | "thierrym" 307 | ], 308 | "entry_dates_tags":[ 309 | "2012-08-24", 310 | "2012-08", 311 | "2012" 312 | ], 313 | "nutrition_grades_tags":[ 314 | "d" 315 | ], 316 | "packaging":"Cellophane,Carton", 317 | "serving_quantity":78, 318 | "origins_tags":[ 319 | "thailand" 320 | ], 321 | "nutrition_data_per":"serving", 322 | "labels":"gluten free", 323 | "cities_tags":[ 324 | ], 325 | "emb_codes_20141016":"", 326 | "categories_tags":[ 327 | "en:rice-noodles" 328 | ], 329 | "quantity":"155 g", 330 | "expiration_date":"", 331 | "states_hierarchy":[ 332 | "en:to-be-checked", 333 | "en:complete", 334 | "en:nutrition-facts-completed", 335 | "en:ingredients-completed", 336 | "en:expiration-date-to-be-completed", 337 | "en:characteristics-completed", 338 | "en:photos-validated", 339 | "en:photos-uploaded" 340 | ], 341 | "ingredients_that_may_be_from_palm_oil_n":0, 342 | "ingredients_from_palm_oil_n":0, 343 | "image_url":"https://world.openfoodfacts.org/images/products/737/628/064/502/front.6.400.jpg", 344 | "ingredients":[ 345 | { 346 | "text":"RICE NOODLES", 347 | "id":"rice-noodles", 348 | "rank":1 349 | }, 350 | { 351 | "text":"SEASONING PACKET", 352 | "id":"seasoning-packet", 353 | "rank":2 354 | }, 355 | { 356 | "text":".", 357 | "id":"", 358 | "rank":3 359 | }, 360 | { 361 | "text":"RICE", 362 | "id":"rice" 363 | }, 364 | { 365 | "text":"WATER", 366 | "id":"water" 367 | }, 368 | { 369 | "text":"PEANUT", 370 | "id":"peanut" 371 | }, 372 | { 373 | "text":"SUGAR", 374 | "id":"sugar" 375 | }, 376 | { 377 | "text":"SALT", 378 | "id":"salt" 379 | }, 380 | { 381 | "text":"CORN STARCH", 382 | "id":"corn-starch" 383 | }, 384 | { 385 | "text":"SPICES", 386 | "id":"spices" 387 | }, 388 | { 389 | "text":"CHILI", 390 | "id":"chili" 391 | }, 392 | { 393 | "text":"CINNAMON", 394 | "id":"cinnamon" 395 | }, 396 | { 397 | "text":"PEPPER", 398 | "id":"pepper" 399 | }, 400 | { 401 | "text":"CUMIN", 402 | "id":"cumin" 403 | }, 404 | { 405 | "text":"CLOVE", 406 | "id":"clove" 407 | }, 408 | { 409 | "text":"HYDRDLYZED SOY PROTEIN", 410 | "id":"hydrdlyzed-soy-protein" 411 | }, 412 | { 413 | "text":"GREEN ONIONS", 414 | "id":"green-onions" 415 | }, 416 | { 417 | "text":"CITRIC ACID", 418 | "id":"citric-acid" 419 | }, 420 | { 421 | "text":"PEANUT OIL", 422 | "id":"peanut-oil" 423 | }, 424 | { 425 | "text":"SESAME OIL", 426 | "id":"sesame-oil" 427 | }, 428 | { 429 | "text":"NATURAL FLAVOR", 430 | "id":"natural-flavor" 431 | } 432 | ], 433 | "lc":"en", 434 | "pnns_groups_1_tags":[ 435 | "unknown" 436 | ], 437 | "checkers":[ 438 | ], 439 | "complete":1 440 | }, 441 | "code":"737628064502" 442 | } 443 | ``` 444 | 445 | ## Creators 446 | 447 | **Scot Scriven** 448 | - 449 | 450 | ## Copyright and license 451 | 452 | Copyright 2015 Scriven Scot 453 | 454 | Licensed under the Apache License, Version 2.0 (the "License"); 455 | you may not use this file except in compliance with the License. 456 | You may obtain a copy of the License at 457 | 458 | https://www.apache.org/licenses/LICENSE-2.0 459 | 460 | Unless required by applicable law or agreed to in writing, software 461 | distributed under the License is distributed on an "AS IS" BASIS, 462 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 463 | See the License for the specific language governing permissions and 464 | limitations under the License. 465 | --------------------------------------------------------------------------------