Better doc
This commit is contained in:
parent
c8ad274fc5
commit
e7659166be
19
LICENSE.txt
Normal file
19
LICENSE.txt
Normal 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
64
README.md
Normal 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.
|
@ -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,
|
||||||
|
@ -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
|
|
||||||
|
@ -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])
|
||||||
|
22
cuizin/db.py
22
cuizin/db.py
@ -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
|
||||||
|
@ -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
56
cuizin/scraping.py
Normal 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
|
@ -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'))
|
||||||
|
Loading…
Reference in New Issue
Block a user