mirror of
https://github.com/shouptech/humulus.git
synced 2026-02-03 16:09:44 +00:00
Add ability to import styles into couch.
This commit is contained in:
parent
e11fd79deb
commit
6b918b3d90
3 changed files with 301 additions and 0 deletions
|
|
@ -33,6 +33,9 @@ def create_app(test_config=None):
|
||||||
from . import couch
|
from . import couch
|
||||||
couch.init_app(app)
|
couch.init_app(app)
|
||||||
|
|
||||||
|
from . import styles
|
||||||
|
styles.init_app(app)
|
||||||
|
|
||||||
# Register blueprint for index page
|
# Register blueprint for index page
|
||||||
from . import home
|
from . import home
|
||||||
app.register_blueprint(home.bp)
|
app.register_blueprint(home.bp)
|
||||||
|
|
|
||||||
116
src/humulus/styles.py
Normal file
116
src/humulus/styles.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"""This module has functions for working with BJCP styles."""
|
||||||
|
|
||||||
|
# Copyright 2019 Mike Shoup
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
from flask import current_app
|
||||||
|
from flask.cli import with_appcontext
|
||||||
|
|
||||||
|
from humulus.couch import get_db, put_doc
|
||||||
|
|
||||||
|
def sub_to_doc(sub):
|
||||||
|
"""Coverts sub (XML) to a dictionary document.
|
||||||
|
|
||||||
|
The returned dictionary can be placed right into CouchDB if you want.
|
||||||
|
"""
|
||||||
|
doc = {
|
||||||
|
'_id': 'style_{}'.format(sub.attrib['id']),
|
||||||
|
'$type': 'style',
|
||||||
|
'id': sub.attrib['id'],
|
||||||
|
'name': sub.find('name').text,
|
||||||
|
'aroma': sub.find('aroma').text,
|
||||||
|
'appearance': sub.find('appearance').text,
|
||||||
|
'flavor': sub.find('flavor').text,
|
||||||
|
'mouthfeel': sub.find('mouthfeel').text,
|
||||||
|
'impression': sub.find('impression').text,
|
||||||
|
'ibu': {},
|
||||||
|
'og': {},
|
||||||
|
'fg': {},
|
||||||
|
'srm': {},
|
||||||
|
'abv': {}
|
||||||
|
}
|
||||||
|
if sub.find('comments') is not None:
|
||||||
|
doc['comments'] = sub.find('comments').text
|
||||||
|
if sub.find('history') is not None:
|
||||||
|
doc['history'] = sub.find('history').text
|
||||||
|
if sub.find('ingredients') is not None:
|
||||||
|
doc['ingredients'] = sub.find('ingredients').text
|
||||||
|
if sub.find('comparison') is not None:
|
||||||
|
doc['comparison'] = sub.find('comparison').text
|
||||||
|
if sub.find('examples') is not None:
|
||||||
|
doc['examples'] = sub.find('examples').text
|
||||||
|
if sub.find('tags') is not None:
|
||||||
|
doc['tags'] = sub.find('tags').text.split(', ')
|
||||||
|
|
||||||
|
doc['ibu']['low'] = (sub.find('./stats/ibu/low').text
|
||||||
|
if sub.find('./stats/ibu/low') is not None else '0')
|
||||||
|
doc['ibu']['high'] = (sub.find('./stats/ibu/high').text
|
||||||
|
if sub.find('./stats/ibu/high') is not None else '100')
|
||||||
|
doc['og']['low'] = (sub.find('./stats/og/low').text
|
||||||
|
if sub.find('./stats/og/low') is not None else '1.0')
|
||||||
|
doc['og']['high'] = (sub.find('./stats/og/high').text
|
||||||
|
if sub.find('./stats/og/high') is not None else '1.2')
|
||||||
|
doc['fg']['low'] = (sub.find('./stats/fg/low').text
|
||||||
|
if sub.find('./stats/fg/low') is not None else '1.0')
|
||||||
|
doc['fg']['high'] = (sub.find('./stats/fg/high').text
|
||||||
|
if sub.find('./stats/fg/high') is not None else '1.2')
|
||||||
|
doc['srm']['low'] = (sub.find('./stats/srm/low').text
|
||||||
|
if sub.find('./stats/srm/low') is not None else '0')
|
||||||
|
doc['srm']['high'] = (sub.find('./stats/srm/high').text
|
||||||
|
if sub.find('./stats/srm/high') is not None else '100')
|
||||||
|
doc['abv']['low'] = (sub.find('./stats/abv/low').text
|
||||||
|
if sub.find('./stats/abv/low') is not None else '0')
|
||||||
|
doc['abv']['high'] = (sub.find('./stats/abv/high').text
|
||||||
|
if sub.find('./stats/abv/high') is not None else '100')
|
||||||
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
def import_styles(url):
|
||||||
|
"""Parses BJCP styles in XML format from `url`.
|
||||||
|
|
||||||
|
`url` defaults to the official BJCP XML styleguide.
|
||||||
|
|
||||||
|
Each subcategory is converted to JSON and then put to a couchdb. The _id of
|
||||||
|
the subsequent document will be `style_<number><letter>`, i.e., style_1A.
|
||||||
|
If the style already exists in the database, it will be skipped.
|
||||||
|
"""
|
||||||
|
db = get_db()
|
||||||
|
root = ET.fromstring(requests.get(url).text)
|
||||||
|
subs = root.findall('./class[@type="beer"]/category/subcategory')
|
||||||
|
for sub in subs:
|
||||||
|
doc = sub_to_doc(sub)
|
||||||
|
if doc['_id'] not in db:
|
||||||
|
put_doc(doc)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command('import-styles')
|
||||||
|
@with_appcontext
|
||||||
|
def import_command():
|
||||||
|
"""CLI command to import BJCP styles."""
|
||||||
|
url = current_app.config.get(
|
||||||
|
'BJCP_STYLES_URL',
|
||||||
|
('https://raw.githubusercontent.com/meanphil'
|
||||||
|
'/bjcp-guidelines-2015/master/styleguide.xml')
|
||||||
|
)
|
||||||
|
import_styles(url)
|
||||||
|
click.echo("Imported BJCP styles.")
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
"""Register the CLI command with the app."""
|
||||||
|
app.cli.add_command(import_command)
|
||||||
182
tests/test_styles.py
Normal file
182
tests/test_styles.py
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
# Copyright 2019 Mike Shoup
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from humulus.styles import import_styles, sub_to_doc
|
||||||
|
|
||||||
|
COMPLETE_STYLE = '''<subcategory id="1A">
|
||||||
|
<name>Test Style</name>
|
||||||
|
<aroma>Smelly</aroma>
|
||||||
|
<appearance>Good looking</appearance>
|
||||||
|
<flavor>Good tasting</flavor>
|
||||||
|
<mouthfeel>Good feeling</mouthfeel>
|
||||||
|
<impression>Refreshing</impression>
|
||||||
|
<comments>Comments</comments>
|
||||||
|
<history>Old</history>
|
||||||
|
<ingredients>Grains, Hops, and Water</ingredients>
|
||||||
|
<comparison>Comparison</comparison>
|
||||||
|
<examples>Examples</examples>
|
||||||
|
<tags>one, two</tags>
|
||||||
|
<stats>
|
||||||
|
<ibu flexible="false">
|
||||||
|
<low>1</low>
|
||||||
|
<high>2</high>
|
||||||
|
</ibu>
|
||||||
|
<og flexible="false">
|
||||||
|
<low>1.010</low>
|
||||||
|
<high>1.020</high>
|
||||||
|
</og>
|
||||||
|
<fg flexible="false">
|
||||||
|
<low>1.000</low>
|
||||||
|
<high>1.010</high>
|
||||||
|
</fg>
|
||||||
|
<srm flexible="false">
|
||||||
|
<low>1</low>
|
||||||
|
<high>2</high>
|
||||||
|
</srm>
|
||||||
|
<abv flexible="false">
|
||||||
|
<low>1</low>
|
||||||
|
<high>2</high>
|
||||||
|
</abv>
|
||||||
|
</stats>
|
||||||
|
</subcategory>
|
||||||
|
'''
|
||||||
|
|
||||||
|
INCOMPLETE_STYLE = '''<subcategory id="2B">
|
||||||
|
<name>Test Style</name>
|
||||||
|
<aroma>Smelly</aroma>
|
||||||
|
<appearance>Good looking</appearance>
|
||||||
|
<flavor>Good tasting</flavor>
|
||||||
|
<mouthfeel>Good feeling</mouthfeel>
|
||||||
|
<impression>Refreshing</impression>
|
||||||
|
</subcategory>
|
||||||
|
'''
|
||||||
|
|
||||||
|
TEST_XML = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<styleguide>
|
||||||
|
<class type="beer">
|
||||||
|
<category id="1">
|
||||||
|
<subcategory id="1A">
|
||||||
|
<name>Test Style</name>
|
||||||
|
<aroma>Smelly</aroma>
|
||||||
|
<appearance>Good looking</appearance>
|
||||||
|
<flavor>Good tasting</flavor>
|
||||||
|
<mouthfeel>Good feeling</mouthfeel>
|
||||||
|
<impression>Refreshing</impression>
|
||||||
|
</subcategory>
|
||||||
|
</category>
|
||||||
|
</class>
|
||||||
|
</styleguide>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def test_sub_to_doc():
|
||||||
|
assert sub_to_doc(ET.fromstring(COMPLETE_STYLE)) == {
|
||||||
|
'_id': 'style_1A',
|
||||||
|
'$type': 'style',
|
||||||
|
'id': '1A',
|
||||||
|
'name': 'Test Style',
|
||||||
|
'aroma': 'Smelly',
|
||||||
|
'appearance': 'Good looking',
|
||||||
|
'flavor': 'Good tasting',
|
||||||
|
'mouthfeel': 'Good feeling',
|
||||||
|
'impression': 'Refreshing',
|
||||||
|
'comments': 'Comments',
|
||||||
|
'history': 'Old',
|
||||||
|
'ingredients': 'Grains, Hops, and Water',
|
||||||
|
'comparison': 'Comparison',
|
||||||
|
'examples': 'Examples',
|
||||||
|
'tags': ['one', 'two'],
|
||||||
|
'ibu': {'low': '1', 'high': '2'},
|
||||||
|
'og': {'low': '1.010', 'high': '1.020'},
|
||||||
|
'fg': {'low': '1.000', 'high': '1.010'},
|
||||||
|
'srm': {'low': '1', 'high': '2'},
|
||||||
|
'abv': {'low': '1', 'high': '2'}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert sub_to_doc(ET.fromstring(INCOMPLETE_STYLE)) == {
|
||||||
|
'_id': 'style_2B',
|
||||||
|
'$type': 'style',
|
||||||
|
'id': '2B',
|
||||||
|
'name': 'Test Style',
|
||||||
|
'aroma': 'Smelly',
|
||||||
|
'appearance': 'Good looking',
|
||||||
|
'flavor': 'Good tasting',
|
||||||
|
'mouthfeel': 'Good feeling',
|
||||||
|
'impression': 'Refreshing',
|
||||||
|
'ibu': {'low': '0', 'high': '100'},
|
||||||
|
'og': {'low': '1.0', 'high': '1.2'},
|
||||||
|
'fg': {'low': '1.0', 'high': '1.2'},
|
||||||
|
'srm': {'low': '0', 'high': '100'},
|
||||||
|
'abv': {'low': '0', 'high': '100'}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_styles(monkeypatch):
|
||||||
|
class PutRecorder:
|
||||||
|
doc = None
|
||||||
|
|
||||||
|
class MockDB:
|
||||||
|
db = {}
|
||||||
|
|
||||||
|
def fake_get_db():
|
||||||
|
return MockDB.db
|
||||||
|
|
||||||
|
def fake_put_doc(doc):
|
||||||
|
PutRecorder.doc = doc
|
||||||
|
|
||||||
|
def fake_requests_get(url):
|
||||||
|
class TestXML:
|
||||||
|
text = TEST_XML
|
||||||
|
return TestXML
|
||||||
|
|
||||||
|
monkeypatch.setattr('requests.get', fake_requests_get)
|
||||||
|
monkeypatch.setattr('humulus.styles.get_db', fake_get_db)
|
||||||
|
monkeypatch.setattr('humulus.styles.put_doc', fake_put_doc)
|
||||||
|
|
||||||
|
import_styles(None)
|
||||||
|
assert PutRecorder.doc == {'$type': 'style',
|
||||||
|
'_id': 'style_1A',
|
||||||
|
'abv': {'high': '100', 'low': '0'},
|
||||||
|
'appearance': 'Good looking',
|
||||||
|
'aroma': 'Smelly',
|
||||||
|
'fg': {'high': '1.2', 'low': '1.0'},
|
||||||
|
'flavor': 'Good tasting',
|
||||||
|
'ibu': {'high': '100', 'low': '0'},
|
||||||
|
'id': '1A',
|
||||||
|
'impression': 'Refreshing',
|
||||||
|
'mouthfeel': 'Good feeling',
|
||||||
|
'name': 'Test Style',
|
||||||
|
'og': {'high': '1.2', 'low': '1.0'},
|
||||||
|
'srm': {'high': '100', 'low': '0'}
|
||||||
|
}
|
||||||
|
|
||||||
|
MockDB.db = {'style_1A': ''}
|
||||||
|
PutRecorder.doc = None
|
||||||
|
import_styles(None)
|
||||||
|
assert PutRecorder.doc is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_command(runner, monkeypatch):
|
||||||
|
class Recorder:
|
||||||
|
called = False
|
||||||
|
|
||||||
|
def fake_import_styles(url):
|
||||||
|
Recorder.called = True
|
||||||
|
|
||||||
|
monkeypatch.setattr('humulus.styles.import_styles', fake_import_styles)
|
||||||
|
result = runner.invoke(args=['import-styles'])
|
||||||
|
assert Recorder.called
|
||||||
|
assert 'Imported BJCP styles.' in result.output
|
||||||
Loading…
Add table
Reference in a new issue