Better doc

This commit is contained in:
Lucas Verney 2018-03-01 17:57:45 +01:00
parent c8ad274fc5
commit e7659166be
9 changed files with 202 additions and 51 deletions

19
LICENSE.txt Normal file
View File

@ -0,0 +1,19 @@
Copyright 2018 - Lucas Verney (Phyks)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

64
README.md Normal file
View File

@ -0,0 +1,64 @@
Cuizin
======
Cuizin is a tool wrapping around [Web Outside Of Browsers](http://weboob.org/)
to help you sort and organize recipes you find online. You can also manually
add new recipes.
## Installation
```
$ git clone … # Clone this repository
$ pip install -r requirements.txt # Install Python dependencies
$ npm install # Install JS dependencies for the frontend
$ npm run build # Build the JS dependencies
```
Ideally, Python dependencies should be installed in a virtual environment.
TODO:
- `npm run build` should handle prefix
## Usage
Run `python -m cuizin` to run the built-in webserver. Conversely, you can use
WSGI to serve the application directly (`application` variable is exported in
`cuizin/__main__.py`).
You can customize the behavior of the app by passing environment variables:
* `HOST` to set the host on which the webserver should listen to (defaults to
`localhost` only). Use `HOST=0.0.0.0` to make it world-accessible.
* `PORT` to set the port on which the webserver should listen. Defaults to
`8080`.
* `DEBUG` to enable or disable the debug from Bottle (defaults to `False`).
* `WEBOOB_MODULES_PATH` to set the path to the local clone of the Weboob
modules. Default to using the `weboob-modules` package installed by `pip`
(and which you should regularly update with `pip install --upgrade
weboob-modules`).
If you serve the app with a reverse proxy, you should serve the content of
`cuizin/dist` (static frontend files) directly, without passing it down to the
Bottle webserver.
## Contributing
All contributions are welcome, feel free to open a MR. Just in case, if you
plan on working on some major new feature, please open an issue before to get
some feedbacks.
All the code lies under `cuizin` directory. Frontend code lies under
`cuizin/js_src`. `build` and `config` folders at the root of this repository
are used for the frontend build.
Additionnally, you can use `make dev` to spawn a development webserver to
serve the JS frontend and auto-update/auto-reload when you make changes. The
spawned JS server will be set up at `localhost:8081` and you should start the
backend Python server at `localhost:8080` with `python -m cuizin` along it.
## License
This software is released under an MIT License.

View File

@ -14,7 +14,7 @@ module.exports = {
// Various Dev Server settings // Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined port: 8081, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false, autoOpenBrowser: false,
errorOverlay: true, errorOverlay: true,
notifyOnErrors: true, notifyOnErrors: true,

View File

@ -1,41 +1,3 @@
from __future__ import absolute_import, print_function, unicode_literals """
import json Cuizin is a lightweight tool to help you manage your recipes.
import sys """
from weboob.core.ouiboube import WebNip
from weboob.tools.json import WeboobEncoder
from cuizin import db
BACKENDS = ['750g', 'allrecipes', 'cuisineaz', 'marmiton', 'supertoinette']
def add_recipe(url, modules_path=None):
webnip = WebNip(modules_path=modules_path)
backends = [
webnip.load_backend(
module,
module,
params={}
)
for module in BACKENDS
]
recipe = None
for backend in backends:
browser = backend.browser
if url.startswith(browser.BASEURL):
browser.location(url)
recipe = db.Recipe.from_weboob(browser.page.get_recipe())
# Ensure URL is set
recipe.url = url
break
if not recipe:
# TODO
recipe = db.Recipe()
recipe.url = url
recipe.save()
return recipe

View File

@ -1,3 +1,6 @@
"""
Main entry point for Cuizin.
"""
import os import os
import peewee import peewee
@ -11,7 +14,7 @@ app = application = web.app
if __name__ == '__main__': if __name__ == '__main__':
HOST = os.environ.get('CUIZIN_HOST', 'localhost') HOST = os.environ.get('CUIZIN_HOST', 'localhost')
PORT = os.environ.get('CUIZIN_PORT', '8080') PORT = os.environ.get('CUIZIN_PORT', '8080')
DEBUG = os.environ.get('CUIZIN_DEBUG', False) DEBUG = bool(os.environ.get('CUIZIN_DEBUG', False))
try: try:
db.database.create_tables([db.Recipe]) db.database.create_tables([db.Recipe])

View File

@ -1,3 +1,6 @@
"""
Database definition
"""
import base64 import base64
import json import json
@ -15,6 +18,9 @@ database.connect()
class JSONField(TextField): class JSONField(TextField):
"""
A Peewee database field with transparent JSON dump/load.
"""
def db_value(self, value): def db_value(self, value):
return json.dumps(value) return json.dumps(value)
@ -24,6 +30,9 @@ class JSONField(TextField):
class Recipe(Model): class Recipe(Model):
"""
Our base model for a recipe.
"""
title = CharField() title = CharField()
url = CharField(null=True, unique=True) url = CharField(null=True, unique=True)
author = CharField(null=True) author = CharField(null=True)
@ -41,25 +50,32 @@ class Recipe(Model):
@staticmethod @staticmethod
def from_weboob(obj): def from_weboob(obj):
recipe = Recipe() recipe = Recipe()
# Set fields
for field in ['title', 'url', 'author', 'picture_url', for field in ['title', 'url', 'author', 'picture_url',
'short_description', 'preparation_time', 'cooking_time', 'short_description', 'preparation_time', 'cooking_time',
'ingredients', 'instructions']: 'ingredients', 'instructions']:
value = getattr(obj, field) value = getattr(obj, field)
if value: if value:
setattr(recipe, field, value) setattr(recipe, field, value)
# Serialize number of person
recipe.nb_person = '-'.join(str(num) for num in obj.nb_person) recipe.nb_person = '-'.join(str(num) for num in obj.nb_person)
# Download picture and save it as a blob
recipe.picture = requests.get(obj.picture_url).content recipe.picture = requests.get(obj.picture_url).content
return recipe return recipe
def to_dict(self): def to_dict(self):
"""
Dict conversion function, for serialization in the API.
"""
serialized = model_to_dict(self) serialized = model_to_dict(self)
prepend_info = ( # Dump picture as a base64 string, compatible with HTML `src` attribute
# for images.
picture_mime = (
'data:%s;base64' % magic.from_buffer(serialized['picture'], 'data:%s;base64' % magic.from_buffer(serialized['picture'],
mime=True) mime=True)
) )
serialized['picture'] = '%s,%s' % ( serialized['picture'] = '%s,%s' % (
prepend_info, picture_mime,
base64.b64encode(serialized['picture']).decode('utf-8') base64.b64encode(serialized['picture']).decode('utf-8')
) )
return serialized return serialized

View File

@ -1,2 +1,2 @@
export const API_URL = 'http://localhost:8080'; export const API_URL = 'http://localhost:8080'; // TODO: Should be coming from an env variable
export const FOOBAR = 'TODO'; // TODO export const FOOBAR = 'TODO'; // TODO

56
cuizin/scraping.py Normal file
View File

@ -0,0 +1,56 @@
"""
Scraping code, to fetch and add a recipe.
This code wraps around [Web Outside Of Browsers](http://weboob.org/).
"""
from __future__ import absolute_import, print_function, unicode_literals
import os
from weboob.core.ouiboube import WebNip
from cuizin import db
# List of backends with recipe abilities in Weboob
BACKENDS = ['750g', 'allrecipes', 'cuisineaz', 'marmiton', 'supertoinette']
def add_recipe(url):
"""
Add a recipe, trying to scrape from a given URL.
:param url: URL of the recipe.
:return: A ``cuizin.db.Recipe`` model.
"""
# Eventually load modules from a local clone
MODULES_PATH = os.environ.get('WEBOOB_MODULES_PATH', None)
# Get all backends with recipe abilities
webnip = WebNip(modules_path=MODULES_PATH)
backends = [
webnip.load_backend(
module,
module,
params={}
)
for module in BACKENDS
]
# Try to fetch the recipe with a Weboob backend
recipe = None
for backend in backends:
browser = backend.browser
if url.startswith(browser.BASEURL):
browser.location(url)
recipe = db.Recipe.from_weboob(browser.page.get_recipe())
# Ensure URL is set
recipe.url = url
break
if not recipe:
# TODO
recipe = db.Recipe()
recipe.url = url
recipe.save()
return recipe

View File

@ -1,9 +1,12 @@
import json import json
import os
import bottle import bottle
from cuizin import add_recipe
from cuizin import db from cuizin import db
from cuizin.scraping import add_recipe
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
app = bottle.Bottle() app = bottle.Bottle()
@ -26,13 +29,20 @@ def enable_cors():
@app.route('/api/v1', ['GET', 'OPTIONS']) @app.route('/api/v1', ['GET', 'OPTIONS'])
def api_v1_index(): def api_v1_index():
"""
API index route
"""
return { return {
'recipes': '/api/v1/recipes' 'recipes': '/api/v1/recipes',
'recipe': '/api/v1/recipe/:id'
} }
@app.route('/api/v1/recipes', ['GET', 'OPTIONS']) @app.route('/api/v1/recipes', ['GET', 'OPTIONS'])
def api_v1_recipes(): def api_v1_recipes():
"""
List all recipes
"""
# CORS # CORS
if bottle.request.method == 'OPTIONS': if bottle.request.method == 'OPTIONS':
return '' return ''
@ -46,6 +56,9 @@ def api_v1_recipes():
@app.post('/api/v1/recipes') @app.post('/api/v1/recipes')
def api_v1_recipes_post(): def api_v1_recipes_post():
"""
Create a new recipe from URL
"""
data = json.load(bottle.request.body) data = json.load(bottle.request.body)
if 'url' not in data: if 'url' not in data:
return { return {
@ -69,6 +82,9 @@ def api_v1_recipes_post():
@app.route('/api/v1/recipe/:id', ['GET', 'OPTIONS']) @app.route('/api/v1/recipe/:id', ['GET', 'OPTIONS'])
def api_v1_recipe(id): def api_v1_recipe(id):
"""
Get a given recipe from db
"""
# CORS # CORS
if bottle.request.method == 'OPTIONS': if bottle.request.method == 'OPTIONS':
return '' return ''
@ -84,6 +100,9 @@ def api_v1_recipe(id):
@app.delete('/api/v1/recipe/:id', ['DELETE', 'OPTIONS']) @app.delete('/api/v1/recipe/:id', ['DELETE', 'OPTIONS'])
def api_v1_recipe_delete(id): def api_v1_recipe_delete(id):
"""
Delete a given recipe from db
"""
# CORS # CORS
if bottle.request.method == 'OPTIONS': if bottle.request.method == 'OPTIONS':
return '' return ''
@ -97,7 +116,19 @@ def api_v1_recipe_delete(id):
} }
@app.get('/')
def index():
"""
Return built index.html file
"""
return bottle.static_file('index.html',
root=os.path.join(MODULE_DIR, 'dist'))
@app.get('/static/<filename:path>') @app.get('/static/<filename:path>')
def get_static_files(filename): def get_static_files(filename):
"""Get Static files""" """
return bottle.static_file(filename) # TODO: root= Get Static files
"""
return bottle.static_file(filename,
root=os.path.join(MODULE_DIR, 'dist', 'static'))