Migrate workflow from page numbers to query parameters #9

Merged
S1SYPHOS merged 6 commits from caching into main 4 months ago
  1. 5
      .gitignore
  2. 17
      README.md
  3. 235
      ajum/ajum.py
  4. 81
      ajum/cli.py
  5. 94
      ajum/utils.py
  6. 0
      tests/fixtures/cache/results/.gitkeep
  7. 21
      tests/fixtures/cache/reviews/invalid.json
  8. 34
      tests/fixtures/cache/reviews/irgendwo-woanders.json
  9. 31
      tests/fixtures/cache/reviews/millo-baut-einen-schneemann.json
  10. 32
      tests/fixtures/cache/reviews/reineke-fuchs.json
  11. 3
      tests/fixtures/config.json
  12. 11
      tests/fixtures/database.json
  13. 105
      tests/fixtures/expected/both.json
  14. 128
      tests/fixtures/expected/full.json
  15. 14
      tests/fixtures/expected/index.json
  16. 11
      tests/fixtures/expected/strict.json
  17. 0
      tests/fixtures/results/.gitkeep
  18. 101
      tests/test_cli.py

5
.gitignore vendored

@ -146,5 +146,6 @@ cython_debug/
/.db/
# AJuM
database.json
config.json
/database.json
/config.json
/tests/fixtures/results/*.json

@ -71,7 +71,6 @@ Commands:
query Queries remote database
show Shows data for given ISBN
stats Shows statistics
update Updates local database
```
@ -89,23 +88,7 @@ Usage: ajum backup [OPTIONS]
Options:
-p, --parallel INTEGER Number of parallel downloads.
--help Show this message and exit.
```
### `update`
.. local database:
```text
$ ajum update --help
Usage: ajum update [OPTIONS]
Updates local database
Options:
-n, --number INTEGER Number of results pages to be scraped.
-p, --parallel INTEGER Number of parallel downloads.
--help Show this message and exit.
```

@ -1,4 +1,5 @@
import asyncio
import json
import os
from copy import deepcopy
@ -9,12 +10,12 @@ from time import sleep
from typing import Dict, List, Optional, Union
from urllib.parse import parse_qs, unquote, urlparse
from aiofiles import open as async_open
from aiohttp import ClientSession, ClientTimeout, request
from bs4 import BeautifulSoup as bs
from bs4.element import Tag
from .utils import create_path, data2hash, flatten, get_logger
from .utils import load_json, dump_json, load_html, dump_html
class Ajum():
@ -26,13 +27,13 @@ class Ajum():
headers: dict = {'User-Agent': 'we-love-ajum'}
# Maximum number for open files
# Maximum number of open files
#
# Check current limit with `ulimit -n`
#
# For more information,
# see https://github.com/Tinche/aiofiles/issues/83#issuecomment-761208062
max_open_files: int = 512
file_limit: int = 1024
# Request timeout
@ -63,6 +64,7 @@ class Ajum():
Creates 'Ajum' instance
:param cache_dir: dict Database directory
:param file_limit: int Limit of open files
:return: None
"""
@ -79,15 +81,100 @@ class Ajum():
self.logger = get_logger(log_dir, 'ajum.log')
# FiLE I/O
async def load_html(self, html_file: str, lock: asyncio.locks.Semaphore) -> str:
"""
Loads data from HTML
:param html_file: str Path to HTML file
:param lock: asyncio.locks.Semaphore Lock preventing too many open files
:return: str HTML data
"""
# Respect file limit when ..
async with lock:
# .. loading data
async with async_open(html_file, 'r') as file:
return await file.read()
async def dump_html(self, data: str, html_file: str, lock: asyncio.locks.Semaphore) -> None:
"""
Dumps HTML data to file
:param data: str Data
:param html_file: str Path to HTML file
:param lock: asyncio.locks.Semaphore Lock preventing too many open files
:return: None
"""
# Create path (if needed)
create_path(html_file)
# Respect file limit when ..
async with lock:
# .. storing data
async with async_open(html_file, 'w') as file:
await file.write(data)
async def load_json(self, json_file: str, lock: asyncio.locks.Semaphore) -> Union[dict, list]:
"""
Loads data from JSON
:param json_file: str Path to JSON file
:param lock: asyncio.locks.Semaphore Lock preventing too many open files
:return: dict|list Data
:raises: Exception Decoding error
"""
try:
# Respect file limit when ..
async with lock:
# .. loading data
async with async_open(json_file, 'r') as file:
return json.loads(await file.read())
# If something goes wrong ..
except json.decoder.JSONDecodeError as e:
# .. report back
raise Exception('Loading JSON file "{}" failed: "{}"'.format(json_file, e))
async def dump_json(self, data: Union[dict, list], json_file: str, lock: asyncio.locks.Semaphore) -> None:
"""
Dumps JSON data to file
:param data: dict|list Data
:param json_file: str Path to JSON file
:param lock: asyncio.locks.Semaphore Lock preventing too many open files
:return: None
"""
# Create path (if needed)
create_path(json_file)
# Respect file limit when ..
async with lock:
# .. storing data
async with async_open(json_file, 'w') as file:
await file.write(json.dumps(data, ensure_ascii=False, indent=4))
# GENERAL
async def fetch(self, session: ClientSession, base_url: str, params: dict = {}) -> str:
async def fetch(self, session: ClientSession, base_url: str = 'https://www.ajum.de/rezension-suche/', params: Optional[Dict[str, Dict[str, Union[list, str]]]] = None) -> str:
"""
Fetches HTML content
:param session: ClientSession Session object
:param session: aiohttp.client.ClientSession Session object
:param base_url: str Base URL
:param params: dict Query parameters
:param params: dict<str,dict<str,list|str>> Query parameters
:return: str HTML content
"""
@ -125,54 +212,12 @@ class Ajum():
sleep(self.wait)
async def query_api(self, session: ClientSession, params: dict) -> str:
"""
Connects to database API
:param session: ClientSession Session object
:param params: dict Query parameters
:return: str HTML content
"""
# Define base URL
base_url = 'https://www.ajum.de/rezension-suche/'
# Make API call
return await self.fetch(session, base_url, params)
def queries(self, params: List[dict]) -> List[str]:
"""
Connects to database API using multiple queries
:param params: list<dict> List of query parameters
:return: list<str> HTML contents for all queries
"""
async def helper(params: List[dict]) -> List[str]:
"""
Connects to database API (async helper function)
:param params: list<dict> List of query parameters
:return: list<str> HTML contents for all queries
"""
async with ClientSession() as session:
return await asyncio.gather(*[self.query_api(session, param) for param in params])
return flatten(asyncio.run(helper(params)))
def hash2file(self, path: str, data: Union[int, str], extension: str) -> str:
def hash2file(self, path: str, data: Union[dict, str], extension: str) -> str:
"""
Builds path to data file
:param path: str Subpath ('results' or 'reviews')
:param data: int | str
:param data: dict|str
:param extension: str File extension
:return: str Path to data file
@ -181,7 +226,7 @@ class Ajum():
# Build data path
data_path = '{}/{}/{}.{}'.format(self.cache_dir, path, data2hash(data), extension)
# Create directories (if needed)
# Create directory (if needed)
create_path(data_path)
return data_path
@ -189,13 +234,13 @@ class Ajum():
# RESULTS PAGES
async def fetch_results(self, session: ClientSession, page: int, max_files: asyncio.locks.Semaphore) -> List[str]:
async def fetch_results(self, session: ClientSession, params: Dict[str, Dict[str, Union[list, str]]], lock: asyncio.locks.Semaphore) -> List[str]:
"""
Fetches results page & caches slugs of its reviews
:param session: ClientSession Session object
:param page: int Results page number
:param max_files: asyncio.locks.Semaphore File opening limit
:param session: aiohttp.client.ClientSession Session object
:param params: dict<str,dict<str,list|str>> Query parameters
:param lock: asyncio.locks.Semaphore Lock preventing too many open files
:return: list<str> Review slugs
"""
@ -204,31 +249,31 @@ class Ajum():
data = []
# Determine JSON file
json_file = self.hash2file('results', page, 'json')
json_file = self.hash2file('results', params, 'json')
try:
# If cached ..
if os.path.exists(json_file):
# .. load review slugs
data = await load_json(json_file, max_files)
data = await self.load_json(json_file, lock)
# .. otherwise ..
else:
# .. determine HTML file
html_file = self.hash2file('results', page, 'html')
html_file = self.hash2file('results', params, 'html')
# If cached ..
if os.path.exists(html_file):
# .. load HTML from file
html = await load_html(html_file, max_files)
html = await self.load_html(html_file, lock)
# .. otherwise ..
else:
# .. send request
html = await self.query_api(session, {'tx_solr[page]': str(page)})
html = await self.fetch(session, params=params)
# .. store response text
await dump_html(html, html_file, max_files)
await self.dump_html(html, html_file, lock)
# Attempt to ..
try:
@ -236,7 +281,7 @@ class Ajum():
data = self.extract_slugs(html)
# .. store them
await dump_json(data, json_file, max_files)
await self.dump_json(data, json_file, lock)
# .. but if no reviews slugs are found ..
except:
@ -249,7 +294,8 @@ class Ajum():
# .. otherwise ..
except Exception as e:
# .. report back
self.logger.error('Results page "{}" failed: "{}"'.format(page, e))
raise
self.logger.error('Results page for query "{}" failed: "{}"'.format(params, e))
return data
@ -292,46 +338,43 @@ class Ajum():
return list((extract_slug(card) for card in cards))
def get_slugs(self, pages: Union[int, List[int]] = 1) -> List[str]:
def get_slugs(self, params_list: List[dict]) -> List[str]:
"""
Fetches multiple results pages at once & caches slugs of their reviews
:param pages: int|list<int> Index OR multiple indices of results pages
:param params_list: list<dict<str,list|str>> List of query parameters
:return: list<str> Review slugs
"""
async def helper(pages: List[int]) -> List[str]:
async def helper(params_list: List[dict]) -> List[str]:
"""
Fetches multiple results pages (async helper function)
:param pages: list<int> Indices of results pages
:param params_list: list<dict<str,list|str>> List of query parameters
:return: list<str> Review slugs
"""
# Create semaphore dictating maximum open files
semaphore = asyncio.Semaphore(self.max_open_files)
# Impose file opening limit
lock = asyncio.Semaphore(self.file_limit)
async with ClientSession() as session:
return await asyncio.gather(*[self.fetch_results(session, page, semaphore) for page in pages])
return await asyncio.gather(*[self.fetch_results(session, params, lock) for params in params_list])
# Convert integer to list (if needed)
if isinstance(pages, int):
pages = [pages]
return flatten(asyncio.run(helper(pages)))
return flatten(asyncio.run(helper(params_list)))
# REVIEW PAGE
async def fetch_review(self, session: ClientSession, slug: str, max_files: asyncio.locks.Semaphore) -> dict:
async def fetch_review(self, session: ClientSession, slug: str, lock: asyncio.locks.Semaphore) -> dict:
"""
Fetches single review & caches its data
:param session: ClientSession Session object
:param session: aiohttp.client.ClientSession Session object
:param slug: str Review slug
:param max_files: asyncio.locks.Semaphore File opening limit
:param lock: asyncio.locks.Semaphore Lock preventing too many open files
:return: dict Review data
"""
@ -346,7 +389,7 @@ class Ajum():
# If cached ..
if os.path.exists(json_file):
# .. load review slugs
data = await load_json(json_file, max_files)
data = await self.load_json(json_file)
# .. otherwise ..
else:
@ -356,7 +399,7 @@ class Ajum():
# If cached ..
if os.path.exists(html_file):
# .. load HTML from file
html = await load_html(html_file, max_files)
html = await self.load_html(html_file)
# .. otherwise ..
else:
@ -364,7 +407,7 @@ class Ajum():
html = await self.fetch(session, 'https://www.ajum.de/rezension/' + slug)
# .. store response text
await dump_html(html, html_file, max_files)
await self.dump_html(html, html_file)
# Get review slug
data = {'URL': slug}
@ -375,7 +418,7 @@ class Ajum():
data.update(self.extract_review(html))
# .. store it
await dump_json(data, json_file, max_files)
await self.dump_json(data, json_file)
# .. but if no review data present ..
except:
@ -408,7 +451,7 @@ class Ajum():
:param tag: bs4.element.Tag Selected 'card' element
:return: list<str> | str Processed output
:return: list<str>|str Processed output
"""
# Strip text
@ -496,12 +539,12 @@ class Ajum():
:return: list<dict> Review data
"""
# Create semaphore dictating maximum open files
semaphore = asyncio.Semaphore(self.max_open_files)
# Impose file opening limit
lock = asyncio.Semaphore(self.file_limit)
# Gather results
async with ClientSession() as session:
return await asyncio.gather(*[self.fetch_review(session, slug, semaphore) for slug in slugs])
return await asyncio.gather(*[self.fetch_review(session, slug, lock) for slug in slugs])
# Convert string to list (if needed)
if isinstance(slugs, str):
@ -562,21 +605,21 @@ class Ajum():
:return: dict Review data
"""
# Create semaphore dictating maximum open files
semaphore = asyncio.Semaphore(self.max_open_files)
# Impose file opening limit
lock = asyncio.Semaphore(self.file_limit)
# Gather results
return await asyncio.gather(*[load_json(file, semaphore) for file in glob(self.cache_dir + '/reviews/*.json')])
return await asyncio.gather(*[self.load_json(file, lock) for file in glob(self.cache_dir + '/reviews/*.json')])
return asyncio.run(helper())
def clear_cache(self, hard_reset: bool = False) -> None:
def clear_cache(self, reset: bool = False) -> None:
"""
Removes cached index files
:param hard_reset: bool Whether to clear cached results pages
:param reset: bool Whether to clear cached results pages
:return: None
"""
@ -592,18 +635,18 @@ class Ajum():
os.remove(file)
# If selected ..
if hard_reset:
if reset:
# .. loop over cached results pages ..
for file in results:
# .. deleting each one
os.remove(file)
def is_cached(self, data: Union[int, str, List[int], List[str]]) -> bool:
def is_cached(self, data: Union[dict, str, List[dict], List[str]]) -> bool:
"""
Checks whether given data exists in cache
:param data: int|str|list<int>|list<str> Results page(s) OR review slug(s)
:param data: dict|str|list<dict>|list<str> Results page(s) OR review slug(s)
:return: bool Cache status
"""
@ -618,7 +661,7 @@ class Ajum():
files = []
# Check whether data represents ..
if isinstance(data, int):
if isinstance(data, dict):
# (1) .. results page
files = [
self.hash2file('results', data, 'json'),
@ -676,7 +719,7 @@ class Ajum():
"""
async with ClientSession() as session:
return await self.query_api(session, params)
return await self.fetch(session, params=params)
def build_filter(data: Union[List[str], str], filters: list) -> list:
@ -690,7 +733,7 @@ class Ajum():
"""
if isinstance(data, list):
return [item for item in data if item in filters]
return sorted([item for item in data if item in filters])
if data in filters:
return [data]

@ -1,5 +1,3 @@
import json
from glob import glob
from itertools import groupby
from multiprocessing import Pool, Manager, cpu_count
@ -14,6 +12,7 @@ import isbnlib
from .ajum import Ajum
from .utils import create_path, list2chunks
from .utils import load_json, dump_json
@click.group()
@ -39,7 +38,7 @@ def cli(ctx, config: Optional[click.Path] = None, ua: Optional[click.Path] = Non
# Load user settings
# (1) Determine config file
config_file = config if config else join(cache_dir, 'config.json')
config_file = config or join(cache_dir, 'config.json')
# (2) If it exists ..
if exists(config_file):
@ -48,7 +47,7 @@ def cli(ctx, config: Optional[click.Path] = None, ua: Optional[click.Path] = Non
# Load UA strings
# (1) Determine file containing user agents
ua_file = ua if ua else join(cache_dir, 'user-agents.txt')
ua_file = ua or join(cache_dir, 'user-agents.txt')
# (2) If it exists ..
if exists(ua_file):
@ -60,7 +59,8 @@ def cli(ctx, config: Optional[click.Path] = None, ua: Optional[click.Path] = Non
@cli.command()
@click.pass_context
@click.option('-p', '--parallel', default=32, help='Number of parallel downloads.')
def backup(ctx: click.Context, parallel: int = 32) -> None:
@click.option('-n', '--number', type=int, help='Number of results pages to be scraped.')
def backup(ctx: click.Context, parallel: int = 32, number: Optional[int] = None) -> None:
"""
Backs up remote database
"""
@ -72,17 +72,17 @@ def backup(ctx: click.Context, parallel: int = 32) -> None:
slugs = []
# Loop over results pages in chunks ..
for chunk in list2chunks(range(ajum.max_page()), parallel):
for chunk in list2chunks(range(number or ajum.max_page()), parallel):
# If already cached ..
if ajum.is_cached(chunk):
# .. move on to next chunk
continue
# .. reporting back
if ctx.obj['verbose'] > 0: click.echo('Fetching results pages "{}" ..'.format(', '.join([str(i) for i in chunk])))
if ctx.obj['verbose'] > 0: click.echo('Fetching results pages "{}" ..'.format(', '.join([str(i + 1) for i in chunk])))
# .. fetching their data
for slug in ajum.get_slugs([page + 1 for page in chunk]):
for slug in ajum.get_slugs([{'tx_solr[page]': str(page + 1), 'tx_solr[sort]': 'datum_desc+desc'} for page in chunk]):
# .. extracting review slugs
slugs.append(slug)
@ -103,58 +103,12 @@ def backup(ctx: click.Context, parallel: int = 32) -> None:
sleep(ajum.wait)
@cli.command()
@click.pass_context
@click.option('-n', '--number', default=10, help='Number of results pages to be scraped.')
@click.option('-p', '--parallel', default=32, help='Number of parallel downloads.')
def update(ctx, number: int, parallel: int = 32) -> None:
"""
Updates local database
"""
# Initialize object
ajum = init(ctx.obj)
# Create data array
slugs = []
# Loop over query parameters ..
for chunk in list2chunks([{'tx_solr[page]': str(page + 1), 'tx_solr[sort]': 'datum_desc+desc'} for page in range(number)], parallel):
# .. reporting back
if ctx.obj['verbose'] > 0: click.echo('Fetching results pages "{}" ..'.format(', '.join([str(i) for i in chunk])))
# .. fetching their HTML
for html in ajum.queries(chunk):
# Extract review slugs
slugs.extend(ajum.extract_slugs(html))
# .. wait for it
sleep(ajum.wait)
# Loop over results pages in chunks ..
for chunk in list2chunks(slugs, parallel):
# If already cached ..
if ajum.is_cached(chunk):
# .. move on to next chunk
continue
# .. reporting back
if ctx.obj['verbose'] > 0: click.echo('Fetching review pages "{}" ..'.format(', '.join(chunk)))
# .. fetching their data
ajum.get_reviews(chunk)
# .. wait for it
sleep(ajum.wait)
@cli.command()
@click.pass_context
@click.argument('file', default='index.json', type=click.File('w'))
@click.option('-s', '--strict', is_flag=True, help='Whether to skip invalid ISBNs.')
@click.option('-f', '--full', is_flag=True, help='Whether to export full database.')
@click.option('-j', '--jobs', type=int, help='Number of jobs.')
def export(ctx, file: click.File, strict: bool = False, full: bool = False, jobs: Optional[int] = None) -> None:
def export(ctx, file: click.File, strict: bool = False, full: bool = False) -> None:
"""
Exports review data to FILE
"""
@ -246,14 +200,14 @@ def export(ctx, file: click.File, strict: bool = False, full: bool = False, jobs
# Create database file
# (1) Sort data by ISBN
# (2) Dump data to JSON file
json.dump(dict(sorted(isbns.items())), file, ensure_ascii=False, indent=4)
dump_json(dict(sorted(isbns.items())), file)
else:
# Create index file
# (1) Sort data by ISBN
# (2) Sort lists of review slugs
# (3) Dump data to JSON file
json.dump(dict(sorted({k: sorted(v) for k, v in isbns.items()}.items())), file, ensure_ascii=False, indent=4)
dump_json(dict(sorted({k: sorted(v) for k, v in isbns.items()}.items())), file)
@cli.command()
@ -349,7 +303,7 @@ def stats(ctx, file: click.File) -> None:
# If index file exists ..
if file:
# .. count indexed ISBNs & reviews
index = json.load(file)
index = load_json(file)
index_count = len(index.keys())
review_count = sum([len(item) for item in index.values()])
@ -462,14 +416,3 @@ def show_reviews(reviews: List[Dict[str, Dict[str, Union[list, str]]]]) -> None:
if not click.confirm('Weiterlesen?', True):
# .. stop!
break
def load_json(json_file):
try:
with open(json_file, 'r') as file:
return json.load(file)
except json.decoder.JSONDecodeError:
raise Exception
return {}

@ -2,13 +2,11 @@ import os
import json
import logging
from asyncio.locks import Semaphore
from io import TextIOWrapper
from hashlib import md5
from logging.handlers import RotatingFileHandler
from typing import Any, List, Union
from aiofiles import open as async_open
def create_path(path: str) -> None:
"""
@ -90,96 +88,62 @@ def data2hash(data: Any) -> str:
return md5(str(data).encode('utf-8')).hexdigest()
async def load_html(html_file: str, max_files: Semaphore) -> str:
"""
Loads data from HTML file (async version)
:param html_file: str Path to HTML file
:param max_files: asyncio.locks.Semaphore File opening limit
:return: str HTML data
"""
# Respect file opening limit when ..
async with max_files:
# .. loading data
async with async_open(html_file, 'r') as file:
return await file.read()
async def dump_html(data: str, html_file: str, max_files: Semaphore) -> None:
def list2chunks(data: list, size: int) -> List[list]:
"""
Dumps HTML data to file
Splits list into chunks
:param data: str Data
:param html_file: str Path to HTML file
:param max_files: asyncio.locks.Semaphore File opening limit
:param data: list Data to be split
:param size: int Chunk size
:return: None
:return: list<list> Chunks
"""
# Create path (if needed)
create_path(html_file)
# Split data into smaller chunks
return [data[i:i + size] for i in range(0, len(data), size)]
# Respect file opening limit when ..
async with max_files:
# .. storing data
async with async_open(html_file, 'w') as file:
await file.write(data)
# FILE I/O
async def load_json(json_file: str, max_files: Semaphore) -> Union[dict, list]:
def load_json(json_file: Union[str, TextIOWrapper]) -> Union[list, dict]:
"""
Loads data from JSON file (async version)
Loads data from JSON
:param json_file: str Path to JSON file
:param max_files: asyncio.locks.Semaphore File opening limit
:param json_file: str|io.TextIOWrapper Path to JSON file OR text stream
:return: dict|list Data
:raises: Exception Decoding error
"""
try:
# Respect file opening limit when ..
async with max_files:
# .. loading data
async with async_open(json_file, 'r') as file:
return json.loads(await file.read())
if isinstance(json_file, str):
with open(json_file, 'r') as file:
return json.load(file)
return json.load(json_file)
except json.decoder.JSONDecodeError:
raise Exception
return {}
async def dump_json(data: Union[dict, list], json_file: str, max_files: Semaphore) -> None:
def dump_json(data: Union[dict, list], json_file: Union[str, TextIOWrapper]) -> None:
"""
Dumps JSON data to file
:param data: dict|list Data
:param json_file: str Path to JSON file
:param max_files: asyncio.locks.Semaphore File opening limit
:param json_file: str|io.TextIOWrapper Path to JSON file OR text stream
:return: None
"""
# Create path (if needed)
create_path(json_file)
# Respect file opening limit when ..
async with max_files:
# .. storing data
async with async_open(json_file, 'w') as file:
await file.write(json.dumps(data, ensure_ascii=False, indent=4))
if isinstance(json_file, str):
# Create path (if needed)
create_path(json_file)
def list2chunks(data: list, size: int) -> List[list]:
"""
Splits list into chunks
# Store data
with open(json_file, 'w') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
:param data: list Data to be split
:param size: int Chunk size
:return: list<list> Chunks
"""
# Split data into smaller chunks
return [data[i:i + size] for i in range(0, len(data), size)]
else:
json.dump(data, json_file, ensure_ascii=False, indent=4)

@ -0,0 +1,21 @@
{
"URL": "invalid",
"Titel": "Invalid",
"Datum": "01.01.1900",
"Autor*in": "",
"ISBN": "XXX-X-XXX-XXXXX-X",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "",
"Seitenanzahl": "",
"Verlag": "",
"Gattung": "",
"Jahr": "",
"Lesealter": "",
"Einsatzmöglichkeiten": "",
"Preis": "",
"Bewertung": "",
"Schlagwörter": [],
"Teaser": [],
"Beurteilungstext": []
}

@ -0,0 +1,34 @@
{
"URL": "irgendwo-woanders",
"Titel": "irgendwo woanders",
"Datum": "01.01.2010",
"Autor*in": "Hänel, Wolfram",
"ISBN": "978-3-407-78924-2",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "",
"Seitenanzahl": "392",
"Verlag": "Beltz",
"Gattung": "",
"Jahr": "2004",
"Lesealter": [
"12-13 Jahre",
"14-15 Jahre",
"16-17 Jahre"
],
"Einsatzmöglichkeiten": "",
"Preis": "8,00 €",
"Bewertung": "sehr empfehlenswert",
"Schlagwörter": [
"Familie"
],
"Teaser": [
"Marei durchquert mit ihrem Vater in einem alten VW-Bus halb Europa. Die beiden sind auf der Suche nach Mareis Mutter, einer bekannten Theaterregisseurin. Sie erleben gemeinsam nicht nur Abenteuer, sondern besuchen auch viele Theater, lernen interessante Leute kennen und bauen nicht zuletzt eine Beziehung zueinander auf."
],
"Beurteilungstext": [
"Mareis Eltern leben schon lange getrennt, und die 14-Jährige wächst bei ihrer Patentante Sabine in Hildesheim auf. Bei ihnen erscheint auch dann und wann Susanne, Mareis Mutter, für einige Wochen. Sie ist eine bekannte Theaterregisseurin und in ganz Europa unterwegs. Ihren Vater Burkhard hat Marei schon seit Jahren nicht mehr gesehen. Doch eines Tages sitzt er bei ihnen in der Wohnung Er ist mit seinem Hund Poodle und einem klapprigen VW-Bus gekommen, um zusammen mit seiner Tochter Mareis Mutter zu suchen. Die lockt sie von einem Ort zum anderen. So durchqueren die beiden halb Europa, reisen von Bologna über Wien nach Weimar, kommen nach Dänemark und Schweden und landen schließlich in Paris. Doch auch dort ist Susanne schon nicht mehr. Ist es den beiden wirklich ernst und wichtig, sie zu finden und wieder zusammen mit ihr zu leben? Sie schickt sie über London nach Dublin. In allen Städten besuchen Vater und Tochter Theatervorstellungen, die die bekannte Regisseurin inszeniert hat. Theaterstücke von Dario Fo, Ibsen, Shakespeare, Beckett lernt Marei kennen, und ihr Vater, ein Bühnenbildner, kann ausführlich und interessant über diese Aufführungen und den tieferen Sinn des Theaters erzählen. Und endlich in Irland am Leuchtturm Galley Head treffen Marei und Burkhard auf Susanne und ihre neue kleine Theatergruppe. Ob es den dreien gelingt, künftig wieder gemeinsam als Familie zu leben, bleibt offen.",
"In diesem spannend geschriebenen Jugendroman erlebt der Leser, wie sich das Verhältnis zwischen der 14-Jährigen und ihrem Vater von anfänglicher Distanz zu einer echten Vater-Tochter-Beziehung entwickelt. Gefühle und Gedanken der zwei Protagonisten werden deutlich und einfühlsam dargestellt, so dass man sich in sie hineinversetzen kann und mit ihnen alle Hochs und Tiefs der Reise miterlebt.",
"Gleichzeitig ist das Buch eine Liebeserklärung ans Theater. Hintergründe und Erläuterungen zu vielen Theaterstücken und Inszenierungen öffnen die Augen und lassen den Leser spätere Theaterbesuche anders erleben. Und dann die Fahrt durch ganz Europa! Diese Route zu verfolgen wird sehr erleichtert durch den Abdruck einer Europakarte, in der man alle kleineren und größeren Reiseziele verzeichnet findet.",
"Für Jugendliche und Erwachsene und vor allem für Theaterfans sehr lesenswert!"
]
}

@ -0,0 +1,31 @@
{
"URL": "millo-baut-einen-schneemann",
"Titel": "Millo baut einen Schneemann",
"Datum": "01.01.2010",
"Autor*in": "Wenniges, Oliver",
"ISBN": "978-3-596-85193-5",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "Wenniges, Oliver",
"Seitenanzahl": "30",
"Verlag": "Fischer Schatzinsel",
"Gattung": "",
"Jahr": "2005",
"Lesealter": [
"4-5 Jahre",
"6-7 Jahre"
],
"Einsatzmöglichkeiten": "Bücherei",
"Preis": "12,50 €",
"Bewertung": "empfehlenswert",
"Schlagwörter": [
"Abenteuer",
"Gender/Geschlecht"
],
"Teaser": [
"Mirko Miloczewski nennen alle Millo. An einem kalten Wintertag macht sich Millo auf zu einer Expedition und zieht sich warm an. Millo sucht Yeti, den Schneemenschen. Da er weiß, wo er den Yeti finden wird, macht er sich zielstrebig auf die Reise. Er nimmt Greta und Peter mit, denn so eine Expedition ist nicht ganz ungefährlich. Alle 3 sind mit dem Schlitten unterwegs und kommen ihrem Ziel näher, wie sie meinen. Und dann haben sie ihn - im Stadtpark war er...."
],
"Beurteilungstext": [
"Ein Kinderbuch, was über Mut und Selbstbewusstsein eines ca. 5jährigen Jungens erzählt. Der Autor spricht eben diese Zielgruppe an und beschreibt einen Ausflugtag des Jungen Millo. Der hat Großes vor und läßt den Leser und Zuhörer auch genauestens daran teilhaben. Verblüffend offen und selbstbewusst erzählt er nämlich von einer Expedition zum Yeti, die im Grunde genommen ein Ausflug zum Schneemannbauen ist. Die Kleinen erfahren allerhand Sachinformationen und können so den eigenen Horizont vergrößern. Einfach, aber schön sind die Illustrationen. Die Figuren sind übernatürlich groß dargestellt und der Zeichner verzichtet auf kleine Details. Einfach und klar ist die Botschaft der Bilder. Die Schrift ist als Erstleserschrift groß gewählt und wird eingerahmt in die Zeichnungen. Diese wiederum unterstreichen die Textbotschaft. Herrlich einfach und lustig, diese Geschichte. Für kleine Abenteurer genau richtig."
]
}

@ -0,0 +1,32 @@
{
"URL": "reineke-fuchs",
"Titel": "Reineke Fuchs",
"Datum": "01.01.2010",
"Autor*in": "Goethe, Johann Wolfgang von",
"ISBN": "978-3-446-23090-3",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "Wiesmüller, Dieter",
"Seitenanzahl": "211",
"Verlag": "Hanser",
"Gattung": "Märchen/Fabel/Sage",
"Jahr": "2008",
"Lesealter": [
"12-13 Jahre",
"14-15 Jahre",
"16-17 Jahre",
"ab 18 Jahre"
],
"Einsatzmöglichkeiten": "",
"Preis": "19,90 €",
"Bewertung": "sehr empfehlenswert",
"Teaser": [
"Schläue schützt vor Strafe nicht. Und so steht er vor Gericht, der Gauner Reineke. Doch was ein richtiger Fuchs ist, der lässt sich nicht so einfach fangen. Und verurteilen schon gar nicht. Goethes Klassiker begleitet von Illustrationen des Künstlers Dieter Wiesmüller."
],
"Beurteilungstext": [
"Goethe ist ein Muss im Deutschunterricht. Das ändern all die Jahrzehnte nicht, die vergehen. Immer und immer steht er auf dem Lehrplan. Nun gibt es aber Mittel und Wege diese unübertroffene Literatur, diese spannenden und hinreißend gesponnenen Geschichten den Heranwachsenden von Heute trotz der antiquierten Sprache nahe zu bringen. Ein solches Mittel sind interessante Illustrationen. Ein brillantes Beispiel ist Goethes \"Hexen-Einmal-Eins\", bebildert vom genialen Wolf Erlbruch. Der Kniff ist, Bilder zu schaffen, die Kinder - kleine wie größere - faszinieren und neugierig machen auf die Geschichte hinter diesen Abbildungen. Den Text begleitend öffnen sie Türen zu einer Literatur, die nur scheinbar nicht mehr zeitgemäß ist.",
"Die Geschichte Reineke Fuchs' ist bestimmt zeitgemäß. Durch List und Charme gelingt es dem Fuchs trotz größtem Unrecht seiner Strafe zu entfliehen. Mehr noch: Der König spricht \"Ich werde künftig die Klagen / Über euch weiter nicht hören. Und ihr sollt immer an meiner / Stelle reden und handeln als Kanzler des Reiches.\" Da lassen sich zur heutigen Gesellschaft und ihrer Politik so manche Parallele finden.",
"Lange schon lässt sich die Dekadenz und die Selbstgefälligkeit der oberen Gesellschaftsschicht beobachten. Die Figur des Reineke Fuchs stammt bereits aus dem Mittelalter. Goethe griff sie auf und bearbeitete sie neu. Wilhelm von Kaulbach illustrierte Goethes Ausgabe damals. Dieter Wiesmüller schuf nun neue Bilder. Mit dem Bleistift skizzierte er wie ein Gerichtsmaler die Szenen des Geschehens und der Verhandlung. Er gestaltete detailreich, der Moderne entsprechend mit gestriegeltem, schlankem Personal. Begleitet werden die Szenen von entsprechenden Textpassagen, sodass sich das Geschehen mit Hilfe der Text-Bild-Kombination leicht verfolgen lässt.",
"Anders als etwa Wolf Erlbruch mit seinen künstlerisch ‚verrückten' Collagen bleiben Wiesmüllers Illustrationen sehr realistisch. Sie gleichen Fotos einer journalistischen Berichterstattung, wodurch die Nähe zum politischen Tagesgeschehen verstärkt wird. Das Lesen dieses dichten, spannungsreichen und beißend ironischen Textes ersetzen sie selbstverständlich nicht. Lust machen wollen, Neugier wecken sollen sie. Eine neue, eine aktuelle Perspektive - nicht nur für junge Leser, sondern auch für alte Kenner des Textes eröffnen sie."
]
}

@ -0,0 +1,3 @@
{
"cache_dir": "tests/fixtures/cache"
}

@ -0,0 +1,11 @@
{
"978-3-407-78924-2": [
"irgendwo-woanders"
],
"978-3-446-23090-3": [
"reineke-fuchs"
],
"978-3-596-85193-5": [
"millo-baut-einen-schneemann"
]
}

@ -0,0 +1,105 @@
{
"978-3-407-78924-2": [
{
"URL": "irgendwo-woanders",
"Titel": "irgendwo woanders",
"Datum": "01.01.2010",
"Autor*in": "Hänel, Wolfram",
"ISBN": "978-3-407-78924-2",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "",
"Seitenanzahl": "392",
"Verlag": "Beltz",
"Gattung": "",
"Jahr": "2004",
"Lesealter": [
"12-13 Jahre",
"14-15 Jahre",
"16-17 Jahre"
],
"Einsatzmöglichkeiten": "",
"Preis": "8,00 €",
"Bewertung": "sehr empfehlenswert",
"Schlagwörter": [
"Familie"
],
"Teaser": [
"Marei durchquert mit ihrem Vater in einem alten VW-Bus halb Europa. Die beiden sind auf der Suche nach Mareis Mutter, einer bekannten Theaterregisseurin. Sie erleben gemeinsam nicht nur Abenteuer, sondern besuchen auch viele Theater, lernen interessante Leute kennen und bauen nicht zuletzt eine Beziehung zueinander auf."
],
"Beurteilungstext": [
"Mareis Eltern leben schon lange getrennt, und die 14-Jährige wächst bei ihrer Patentante Sabine in Hildesheim auf. Bei ihnen erscheint auch dann und wann Susanne, Mareis Mutter, für einige Wochen. Sie ist eine bekannte Theaterregisseurin und in ganz Europa unterwegs. Ihren Vater Burkhard hat Marei schon seit Jahren nicht mehr gesehen. Doch eines Tages sitzt er bei ihnen in der Wohnung Er ist mit seinem Hund Poodle und einem klapprigen VW-Bus gekommen, um zusammen mit seiner Tochter Mareis Mutter zu suchen. Die lockt sie von einem Ort zum anderen. So durchqueren die beiden halb Europa, reisen von Bologna über Wien nach Weimar, kommen nach Dänemark und Schweden und landen schließlich in Paris. Doch auch dort ist Susanne schon nicht mehr. Ist es den beiden wirklich ernst und wichtig, sie zu finden und wieder zusammen mit ihr zu leben? Sie schickt sie über London nach Dublin. In allen Städten besuchen Vater und Tochter Theatervorstellungen, die die bekannte Regisseurin inszeniert hat. Theaterstücke von Dario Fo, Ibsen, Shakespeare, Beckett lernt Marei kennen, und ihr Vater, ein Bühnenbildner, kann ausführlich und interessant über diese Aufführungen und den tieferen Sinn des Theaters erzählen. Und endlich in Irland am Leuchtturm Galley Head treffen Marei und Burkhard auf Susanne und ihre neue kleine Theatergruppe. Ob es den dreien gelingt, künftig wieder gemeinsam als Familie zu leben, bleibt offen.",
"In diesem spannend geschriebenen Jugendroman erlebt der Leser, wie sich das Verhältnis zwischen der 14-Jährigen und ihrem Vater von anfänglicher Distanz zu einer echten Vater-Tochter-Beziehung entwickelt. Gefühle und Gedanken der zwei Protagonisten werden deutlich und einfühlsam dargestellt, so dass man sich in sie hineinversetzen kann und mit ihnen alle Hochs und Tiefs der Reise miterlebt.",
"Gleichzeitig ist das Buch eine Liebeserklärung ans Theater. Hintergründe und Erläuterungen zu vielen Theaterstücken und Inszenierungen öffnen die Augen und lassen den Leser spätere Theaterbesuche anders erleben. Und dann die Fahrt durch ganz Europa! Diese Route zu verfolgen wird sehr erleichtert durch den Abdruck einer Europakarte, in der man alle kleineren und größeren Reiseziele verzeichnet findet.",
"Für Jugendliche und Erwachsene und vor allem für Theaterfans sehr lesenswert!"
]
}
],
"978-3-446-23090-3": [
{
"URL": "reineke-fuchs",
"Titel": "Reineke Fuchs",
"Datum": "01.01.2010",
"Autor*in": "Goethe, Johann Wolfgang von",
"ISBN": "978-3-446-23090-3",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "Wiesmüller, Dieter",
"Seitenanzahl": "211",
"Verlag": "Hanser",
"Gattung": "Märchen/Fabel/Sage",
"Jahr": "2008",
"Lesealter": [
"12-13 Jahre",
"14-15 Jahre",
"16-17 Jahre",
"ab 18 Jahre"
],
"Einsatzmöglichkeiten": "",
"Preis": "19,90 €",
"Bewertung": "sehr empfehlenswert",
"Teaser": [
"Schläue schützt vor Strafe nicht. Und so steht er vor Gericht, der Gauner Reineke. Doch was ein richtiger Fuchs ist, der lässt sich nicht so einfach fangen. Und verurteilen schon gar nicht. Goethes Klassiker begleitet von Illustrationen des Künstlers Dieter Wiesmüller."
],
"Beurteilungstext": [
"Goethe ist ein Muss im Deutschunterricht. Das ändern all die Jahrzehnte nicht, die vergehen. Immer und immer steht er auf dem Lehrplan. Nun gibt es aber Mittel und Wege diese unübertroffene Literatur, diese spannenden und hinreißend gesponnenen Geschichten den Heranwachsenden von Heute trotz der antiquierten Sprache nahe zu bringen. Ein solches Mittel sind interessante Illustrationen. Ein brillantes Beispiel ist Goethes \"Hexen-Einmal-Eins\", bebildert vom genialen Wolf Erlbruch. Der Kniff ist, Bilder zu schaffen, die Kinder - kleine wie größere - faszinieren und neugierig machen auf die Geschichte hinter diesen Abbildungen. Den Text begleitend öffnen sie Türen zu einer Literatur, die nur scheinbar nicht mehr zeitgemäß ist.",
"Die Geschichte Reineke Fuchs' ist bestimmt zeitgemäß. Durch List und Charme gelingt es dem Fuchs trotz größtem Unrecht seiner Strafe zu entfliehen. Mehr noch: Der König spricht \"Ich werde künftig die Klagen / Über euch weiter nicht hören. Und ihr sollt immer an meiner / Stelle reden und handeln als Kanzler des Reiches.\" Da lassen sich zur heutigen Gesellschaft und ihrer Politik so manche Parallele finden.",
"Lange schon lässt sich die Dekadenz und die Selbstgefälligkeit der oberen Gesellschaftsschicht beobachten. Die Figur des Reineke Fuchs stammt bereits aus dem Mittelalter. Goethe griff sie auf und bearbeitete sie neu. Wilhelm von Kaulbach illustrierte Goethes Ausgabe damals. Dieter Wiesmüller schuf nun neue Bilder. Mit dem Bleistift skizzierte er wie ein Gerichtsmaler die Szenen des Geschehens und der Verhandlung. Er gestaltete detailreich, der Moderne entsprechend mit gestriegeltem, schlankem Personal. Begleitet werden die Szenen von entsprechenden Textpassagen, sodass sich das Geschehen mit Hilfe der Text-Bild-Kombination leicht verfolgen lässt.",
"Anders als etwa Wolf Erlbruch mit seinen künstlerisch ‚verrückten' Collagen bleiben Wiesmüllers Illustrationen sehr realistisch. Sie gleichen Fotos einer journalistischen Berichterstattung, wodurch die Nähe zum politischen Tagesgeschehen verstärkt wird. Das Lesen dieses dichten, spannungsreichen und beißend ironischen Textes ersetzen sie selbstverständlich nicht. Lust machen wollen, Neugier wecken sollen sie. Eine neue, eine aktuelle Perspektive - nicht nur für junge Leser, sondern auch für alte Kenner des Textes eröffnen sie."
]
}
],
"978-3-596-85193-5": [
{
"URL": "millo-baut-einen-schneemann",
"Titel": "Millo baut einen Schneemann",
"Datum": "01.01.2010",
"Autor*in": "Wenniges, Oliver",
"ISBN": "978-3-596-85193-5",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "Wenniges, Oliver",
"Seitenanzahl": "30",
"Verlag": "Fischer Schatzinsel",
"Gattung": "",
"Jahr": "2005",
"Lesealter": [
"4-5 Jahre",
"6-7 Jahre"
],
"Einsatzmöglichkeiten": "Bücherei",
"Preis": "12,50 €",
"Bewertung": "empfehlenswert",
"Schlagwörter": [
"Abenteuer",
"Gender/Geschlecht"
],
"Teaser": [
"Mirko Miloczewski nennen alle Millo. An einem kalten Wintertag macht sich Millo auf zu einer Expedition und zieht sich warm an. Millo sucht Yeti, den Schneemenschen. Da er weiß, wo er den Yeti finden wird, macht er sich zielstrebig auf die Reise. Er nimmt Greta und Peter mit, denn so eine Expedition ist nicht ganz ungefährlich. Alle 3 sind mit dem Schlitten unterwegs und kommen ihrem Ziel näher, wie sie meinen. Und dann haben sie ihn - im Stadtpark war er...."
],
"Beurteilungstext": [
"Ein Kinderbuch, was über Mut und Selbstbewusstsein eines ca. 5jährigen Jungens erzählt. Der Autor spricht eben diese Zielgruppe an und beschreibt einen Ausflugtag des Jungen Millo. Der hat Großes vor und läßt den Leser und Zuhörer auch genauestens daran teilhaben. Verblüffend offen und selbstbewusst erzählt er nämlich von einer Expedition zum Yeti, die im Grunde genommen ein Ausflug zum Schneemannbauen ist. Die Kleinen erfahren allerhand Sachinformationen und können so den eigenen Horizont vergrößern. Einfach, aber schön sind die Illustrationen. Die Figuren sind übernatürlich groß dargestellt und der Zeichner verzichtet auf kleine Details. Einfach und klar ist die Botschaft der Bilder. Die Schrift ist als Erstleserschrift groß gewählt und wird eingerahmt in die Zeichnungen. Diese wiederum unterstreichen die Textbotschaft. Herrlich einfach und lustig, diese Geschichte. Für kleine Abenteurer genau richtig."
]
}
]
}

@ -0,0 +1,128 @@
{
"978-3-407-78924-2": [
{
"URL": "irgendwo-woanders",
"Titel": "irgendwo woanders",
"Datum": "01.01.2010",
"Autor*in": "Hänel, Wolfram",
"ISBN": "978-3-407-78924-2",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "",
"Seitenanzahl": "392",
"Verlag": "Beltz",
"Gattung": "",
"Jahr": "2004",
"Lesealter": [
"12-13 Jahre",
"14-15 Jahre",
"16-17 Jahre"
],
"Einsatzmöglichkeiten": "",
"Preis": "8,00 €",
"Bewertung": "sehr empfehlenswert",
"Schlagwörter": [
"Familie"
],
"Teaser": [
"Marei durchquert mit ihrem Vater in einem alten VW-Bus halb Europa. Die beiden sind auf der Suche nach Mareis Mutter, einer bekannten Theaterregisseurin. Sie erleben gemeinsam nicht nur Abenteuer, sondern besuchen auch viele Theater, lernen interessante Leute kennen und bauen nicht zuletzt eine Beziehung zueinander auf."
],
"Beurteilungstext": [
"Mareis Eltern leben schon lange getrennt, und die 14-Jährige wächst bei ihrer Patentante Sabine in Hildesheim auf. Bei ihnen erscheint auch dann und wann Susanne, Mareis Mutter, für einige Wochen. Sie ist eine bekannte Theaterregisseurin und in ganz Europa unterwegs. Ihren Vater Burkhard hat Marei schon seit Jahren nicht mehr gesehen. Doch eines Tages sitzt er bei ihnen in der Wohnung Er ist mit seinem Hund Poodle und einem klapprigen VW-Bus gekommen, um zusammen mit seiner Tochter Mareis Mutter zu suchen. Die lockt sie von einem Ort zum anderen. So durchqueren die beiden halb Europa, reisen von Bologna über Wien nach Weimar, kommen nach Dänemark und Schweden und landen schließlich in Paris. Doch auch dort ist Susanne schon nicht mehr. Ist es den beiden wirklich ernst und wichtig, sie zu finden und wieder zusammen mit ihr zu leben? Sie schickt sie über London nach Dublin. In allen Städten besuchen Vater und Tochter Theatervorstellungen, die die bekannte Regisseurin inszeniert hat. Theaterstücke von Dario Fo, Ibsen, Shakespeare, Beckett lernt Marei kennen, und ihr Vater, ein Bühnenbildner, kann ausführlich und interessant über diese Aufführungen und den tieferen Sinn des Theaters erzählen. Und endlich in Irland am Leuchtturm Galley Head treffen Marei und Burkhard auf Susanne und ihre neue kleine Theatergruppe. Ob es den dreien gelingt, künftig wieder gemeinsam als Familie zu leben, bleibt offen.",
"In diesem spannend geschriebenen Jugendroman erlebt der Leser, wie sich das Verhältnis zwischen der 14-Jährigen und ihrem Vater von anfänglicher Distanz zu einer echten Vater-Tochter-Beziehung entwickelt. Gefühle und Gedanken der zwei Protagonisten werden deutlich und einfühlsam dargestellt, so dass man sich in sie hineinversetzen kann und mit ihnen alle Hochs und Tiefs der Reise miterlebt.",
"Gleichzeitig ist das Buch eine Liebeserklärung ans Theater. Hintergründe und Erläuterungen zu vielen Theaterstücken und Inszenierungen öffnen die Augen und lassen den Leser spätere Theaterbesuche anders erleben. Und dann die Fahrt durch ganz Europa! Diese Route zu verfolgen wird sehr erleichtert durch den Abdruck einer Europakarte, in der man alle kleineren und größeren Reiseziele verzeichnet findet.",
"Für Jugendliche und Erwachsene und vor allem für Theaterfans sehr lesenswert!"
]
}
],
"978-3-446-23090-3": [
{
"URL": "reineke-fuchs",
"Titel": "Reineke Fuchs",
"Datum": "01.01.2010",
"Autor*in": "Goethe, Johann Wolfgang von",
"ISBN": "978-3-446-23090-3",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "Wiesmüller, Dieter",
"Seitenanzahl": "211",
"Verlag": "Hanser",
"Gattung": "Märchen/Fabel/Sage",
"Jahr": "2008",
"Lesealter": [
"12-13 Jahre",
"14-15 Jahre",
"16-17 Jahre",
"ab 18 Jahre"
],
"Einsatzmöglichkeiten": "",
"Preis": "19,90 €",
"Bewertung": "sehr empfehlenswert",
"Teaser": [
"Schläue schützt vor Strafe nicht. Und so steht er vor Gericht, der Gauner Reineke. Doch was ein richtiger Fuchs ist, der lässt sich nicht so einfach fangen. Und verurteilen schon gar nicht. Goethes Klassiker begleitet von Illustrationen des Künstlers Dieter Wiesmüller."
],
"Beurteilungstext": [
"Goethe ist ein Muss im Deutschunterricht. Das ändern all die Jahrzehnte nicht, die vergehen. Immer und immer steht er auf dem Lehrplan. Nun gibt es aber Mittel und Wege diese unübertroffene Literatur, diese spannenden und hinreißend gesponnenen Geschichten den Heranwachsenden von Heute trotz der antiquierten Sprache nahe zu bringen. Ein solches Mittel sind interessante Illustrationen. Ein brillantes Beispiel ist Goethes \"Hexen-Einmal-Eins\", bebildert vom genialen Wolf Erlbruch. Der Kniff ist, Bilder zu schaffen, die Kinder - kleine wie größere - faszinieren und neugierig machen auf die Geschichte hinter diesen Abbildungen. Den Text begleitend öffnen sie Türen zu einer Literatur, die nur scheinbar nicht mehr zeitgemäß ist.",
"Die Geschichte Reineke Fuchs' ist bestimmt zeitgemäß. Durch List und Charme gelingt es dem Fuchs trotz größtem Unrecht seiner Strafe zu entfliehen. Mehr noch: Der König spricht \"Ich werde künftig die Klagen / Über euch weiter nicht hören. Und ihr sollt immer an meiner / Stelle reden und handeln als Kanzler des Reiches.\" Da lassen sich zur heutigen Gesellschaft und ihrer Politik so manche Parallele finden.",
"Lange schon lässt sich die Dekadenz und die Selbstgefälligkeit der oberen Gesellschaftsschicht beobachten. Die Figur des Reineke Fuchs stammt bereits aus dem Mittelalter. Goethe griff sie auf und bearbeitete sie neu. Wilhelm von Kaulbach illustrierte Goethes Ausgabe damals. Dieter Wiesmüller schuf nun neue Bilder. Mit dem Bleistift skizzierte er wie ein Gerichtsmaler die Szenen des Geschehens und der Verhandlung. Er gestaltete detailreich, der Moderne entsprechend mit gestriegeltem, schlankem Personal. Begleitet werden die Szenen von entsprechenden Textpassagen, sodass sich das Geschehen mit Hilfe der Text-Bild-Kombination leicht verfolgen lässt.",
"Anders als etwa Wolf Erlbruch mit seinen künstlerisch ‚verrückten' Collagen bleiben Wiesmüllers Illustrationen sehr realistisch. Sie gleichen Fotos einer journalistischen Berichterstattung, wodurch die Nähe zum politischen Tagesgeschehen verstärkt wird. Das Lesen dieses dichten, spannungsreichen und beißend ironischen Textes ersetzen sie selbstverständlich nicht. Lust machen wollen, Neugier wecken sollen sie. Eine neue, eine aktuelle Perspektive - nicht nur für junge Leser, sondern auch für alte Kenner des Textes eröffnen sie."
]
}
],
"978-3-596-85193-5": [
{
"URL": "millo-baut-einen-schneemann",
"Titel": "Millo baut einen Schneemann",
"Datum": "01.01.2010",
"Autor*in": "Wenniges, Oliver",
"ISBN": "978-3-596-85193-5",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "Wenniges, Oliver",
"Seitenanzahl": "30",
"Verlag": "Fischer Schatzinsel",
"Gattung": "",
"Jahr": "2005",
"Lesealter": [
"4-5 Jahre",
"6-7 Jahre"
],
"Einsatzmöglichkeiten": "Bücherei",
"Preis": "12,50 €",
"Bewertung": "empfehlenswert",
"Schlagwörter": [
"Abenteuer",
"Gender/Geschlecht"
],
"Teaser": [
"Mirko Miloczewski nennen alle Millo. An einem kalten Wintertag macht sich Millo auf zu einer Expedition und zieht sich warm an. Millo sucht Yeti, den Schneemenschen. Da er weiß, wo er den Yeti finden wird, macht er sich zielstrebig auf die Reise. Er nimmt Greta und Peter mit, denn so eine Expedition ist nicht ganz ungefährlich. Alle 3 sind mit dem Schlitten unterwegs und kommen ihrem Ziel näher, wie sie meinen. Und dann haben sie ihn - im Stadtpark war er...."
],
"Beurteilungstext": [
"Ein Kinderbuch, was über Mut und Selbstbewusstsein eines ca. 5jährigen Jungens erzählt. Der Autor spricht eben diese Zielgruppe an und beschreibt einen Ausflugtag des Jungen Millo. Der hat Großes vor und läßt den Leser und Zuhörer auch genauestens daran teilhaben. Verblüffend offen und selbstbewusst erzählt er nämlich von einer Expedition zum Yeti, die im Grunde genommen ein Ausflug zum Schneemannbauen ist. Die Kleinen erfahren allerhand Sachinformationen und können so den eigenen Horizont vergrößern. Einfach, aber schön sind die Illustrationen. Die Figuren sind übernatürlich groß dargestellt und der Zeichner verzichtet auf kleine Details. Einfach und klar ist die Botschaft der Bilder. Die Schrift ist als Erstleserschrift groß gewählt und wird eingerahmt in die Zeichnungen. Diese wiederum unterstreichen die Textbotschaft. Herrlich einfach und lustig, diese Geschichte. Für kleine Abenteurer genau richtig."
]
}
],
"XXX-X-XXX-XXXXX-X": [
{
"URL": "invalid",
"Titel": "Invalid",
"Datum": "01.01.1900",
"Autor*in": "",
"ISBN": "XXX-X-XXX-XXXXX-X",
"Übersetzer*in": "",
"Ori. Sprache": "",
"Illustrator*in": "",
"Seitenanzahl": "",
"Verlag": "",
"Gattung": "",
"Jahr": "",
"Lesealter": "",
"Einsatzmöglichkeiten": "",
"Preis": "",
"Bewertung": "",
"Schlagwörter": [],
"Teaser": [],
"Beurteilungstext": []
}
]
}

@ -0,0 +1,14 @@
{
"978-3-407-78924-2": [
"irgendwo-woanders"
],
"978-3-446-23090-3": [
"reineke-fuchs"
],
"978-3-596-85193-5": [
"millo-baut-einen-schneemann"
],
"XXX-X-XXX-XXXXX-X": [
"invalid"
]
}

@ -0,0 +1,11 @@
{
"978-3-407-78924-2": [
"irgendwo-woanders"
],
"978-3-446-23090-3": [
"reineke-fuchs"
],
"978-3-596-85193-5": [
"millo-baut-einen-schneemann"
]
}

@ -1,14 +1,105 @@
import filecmp
from click.testing import CliRunner
from ajum.cli import cli
# Global setup
# (1) Initialize CLI runner
runner = CliRunner()
# (2) Test config
config = '-c tests/fixtures/config.json'
def test_cli():
# Run function
result = runner.invoke(cli)
# Assert result
assert result.exit_code == 0
def test_export():
# Setup
# (1) Output file
fixture = 'tests/fixtures/results/index.json'
# (2) Expected output
expected = 'tests/fixtures/expected/index.json'
# Run function
<