Files
elwig-rest-backend/src/elwig-backend

782 lines
38 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Callable, Optional
from http.server import BaseHTTPRequestHandler, HTTPServer
import argparse
import datetime
import traceback
import re
import base64
import json
import sqlite3
import urllib.parse
import gzip
VERSION: str = '0.0.3'
CNX: sqlite3.Cursor
USER_FILE: str
class HttpError(Exception):
status_code: int
def __init__(self, status_code: int, message: str):
self.status_code = status_code
super().__init__(message)
class BadRequestError(HttpError):
def __init__(self, message: str = 'Bad request'):
super().__init__(400, message)
class UnauthorizedError(HttpError):
def __init__(self, message: str = 'Unauthorized'):
super().__init__(401, message)
class NotFoundError(HttpError):
def __init__(self, message: str = 'Not found'):
super().__init__(404, message)
class MethodNotAllowedError(HttpError):
def __init__(self, message: str = 'Method not allowed'):
super().__init__(405, message)
class Filter:
def __init__(self, name: str, values: list[int] or list[str] = None):
self.name = name
self.values = values
def is_int(self) -> bool:
return type(self.values[0]) is int
def is_str(self) -> bool:
return type(self.values[0]) is str
def is_single(self) -> bool:
return self.values is None
def to_sql_list(self) -> str:
if self.is_int():
return ', '.join(str(v) for v in self.values)
else:
return ', '.join(f"'{v}'" for v in self.values)
def __repr__(self) -> str:
if self.is_single():
return self.name
elif self.name == 'kgnr':
return f'kgnr={";".join(f"{v:05}" for v in self.values)}'
return f'{self.name}={";".join(str(v) for v in self.values)}'
def __str__(self) -> str:
return self.__repr__()
def __eq__(self, other) -> bool:
return self.__repr__() == other.__repr__()
@staticmethod
def from_str(string: str) -> Filter:
f = string.split('=', 1)
if len(f) == 2:
ps = f[1].split(';')
is_digit = all(p.isdigit() for p in ps)
return Filter(f[0], [int(p) for p in ps] if is_digit else ps)
return Filter(f[0])
def sqlite_regexp(pattern: str, value: Optional[str]) -> Optional[bool]:
return re.match(pattern, value) is not None if value is not None else None
def kmw_to_oe(kmw: float) -> float:
return kmw * (4.54 + 0.022 * kmw) if kmw is not None else None
def jdmp(value, is_bool: bool = False) -> str:
if is_bool and value:
return ' true'
elif is_bool and not value:
return 'false'
return json.dumps(value, ensure_ascii=False)
def get_delivery_schedule_filter_clauses(filters: list[Filter]) -> list[str]:
clauses = []
for f in filters:
if f.name == 'year' and f.is_int():
clauses.append(f"s.year IN ({f.to_sql_list()})")
elif f.name == 'sortid' and f.is_str() and all(len(v) == 2 and v.isalpha() and v.isupper() for v in f.values):
clauses.append(f"v.sortid IN ({f.to_sql_list()})")
elif f.name == 'date' and f.is_str() and all(re.match(r'[0-9]{4}-[0-9]{2}-[0-9]{2}', v) is not None for v in f.values):
clauses.append(f"s.date IN ({f.to_sql_list()})")
else:
raise BadRequestError(f"Invalid filter '{f}'")
return clauses
class ElwigApi(BaseHTTPRequestHandler):
def send(self, data: str, status_code: int = 200, url: str = None) -> None:
raw = data.encode('utf-8')
self.send_response(status_code)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Headers', 'Authorization')
self.send_header('Access-Control-Allow-Methods', 'HEAD, GET, OPTIONS')
if 300 <= status_code < 400 and status_code != 304 and url:
self.send_header('Location', url)
elif status_code == 401:
self.send_header('WWW-Authenticate', 'Basic realm=Elwig')
if 'Accept-Encoding' in self.headers and len(data) > 64:
accept_encoding = [e.strip() for e in self.headers.get('Accept-Encoding').split(',')]
if 'gzip' in accept_encoding:
raw = gzip.compress(raw)
self.send_header('Content-Encoding', 'gzip')
self.send_header('Content-Type', 'application/json; charset=UTF-8')
self.send_header('Content-Length', str(len(raw)))
self.end_headers()
if self.request.type != 'HEAD' and self.request.type != 'OPTIONS':
self.wfile.write(raw)
def error(self, status_code: int, message: str = None) -> None:
self.send(f'{{"message":{jdmp(message)}}}\n', status_code=status_code)
def see_other(self, url: str) -> None:
self.send(f'{{"url": {jdmp(url)}}}\n', status_code=303, url=url)
def authorize(self) -> tuple[str, str]:
try:
auth = self.headers.get('Authorization')
if auth is None or not auth.startswith('Basic '):
raise UnauthorizedError()
auth = base64.b64decode(auth[6:]).split(b':', 1)
if len(auth) != 2:
raise UnauthorizedError('Invalid Authorization header')
username, password = auth[0].decode('utf-8'), auth[1].decode('utf-8')
except:
raise UnauthorizedError('Invalid Authorization header')
with open(USER_FILE, 'r') as file:
for line in file:
(u, r, p) = line.strip().split(':', 2)
if u == username:
if p == password:
return u, r
raise UnauthorizedError()
def exec_collection(self, sql_query: str, fmt: Callable, filters: list[Filter],
offset: int = None, limit: int = None,
distinct: tuple[[str], [int]] = None, sub_fmt: Callable = None) -> None:
with_clause = re.findall(r'(WITH .*?\))[\s\n]*SELECT', sql_query, flags=re.DOTALL)
if len(with_clause) > 0 and '.*' not in with_clause[0]:
with_clause = with_clause[0]
count_query = sql_query.replace(with_clause, '')
else:
with_clause = None
count_query = sql_query
count = f"""SELECT COUNT(DISTINCT {" || '|' || ".join(distinct[0])}) FROM""" if distinct else "SELECT COUNT(*) FROM"
count_query = re.sub(r"SELECT [^*]+? FROM", count, count_query, count=1, flags=re.DOTALL)
count_query = re.sub(r"(OFFSET|LIMIT) [0-9-]+", '', count_query)
count_query = re.sub(r"GROUP BY .*", '', count_query)
if with_clause:
count_query = with_clause + ' ' + count_query
count = CNX.execute(count_query).fetchone()
count = count[0] if count is not None else 0
if limit is not None:
if "LIMIT " in sql_query:
sql_query = re.sub(r"LIMIT [0-9-]+", f"LIMIT {limit}", sql_query)
else:
sql_query += f" LIMIT {limit}"
if offset is not None:
if "OFFSET " in sql_query:
sql_query = re.sub(r"OFFSET [0-9-]+", f"OFFSET {offset}", sql_query)
else:
sql_query += f" OFFSET {offset}"
rows = CNX.execute(sql_query)
data = (f'''{{"filters":[{','.join(f'{{"filter":{jdmp(str(f))}}}' for f in filters)}],'''
f'"total":{count},"offset":{offset or 0},"limit":{jdmp(limit)},'
f'"data":[')
first, first_, cur, last = True, True, None, None
for r in rows or []:
cur = tuple([r[i] for i in distinct[1]]) if distinct else None
if not distinct or cur != last:
first_ = True
if first:
first = False
else:
if distinct and sub_fmt:
data += '\n ]}'
data += ','
data += f'\n ' + fmt(r)
if distinct and sub_fmt:
if first_:
data += '['
first_ = False
else:
data += ','
data += f'\n ' + sub_fmt(r)
last = cur
if distinct and sub_fmt and not first:
data += '\n ]}'
data += '\n]}\n'
self.send(data)
def do_GET_delivery_schedules(self, filters: list[Filter],
offset: int = None, limit: int = None,
order: str = None) -> None:
clauses = get_delivery_schedule_filter_clauses(filters)
sql = f"""
WITH announcements
AS (SELECT year, dsnr, SUM(weight) AS weight
FROM delivery_announcement
GROUP BY year, dsnr)
SELECT s.year, s.dsnr, s.date, s.description, s.max_weight, s.cancelled,
COALESCE(a.weight, 0) AS announced_weight,
COALESCE(SUM(p.weight), 0) AS delivered_weight,
STRFTIME('%Y-%m-%dT%H:%M:%SZ', DATETIME(s.ancmt_from, 'unixepoch')),
STRFTIME('%Y-%m-%dT%H:%M:%SZ', DATETIME(s.ancmt_to, 'unixepoch')),
b.zwstid, b.name,
s.attrid, s.cultid
FROM delivery_schedule s
LEFT JOIN branch b ON b.zwstid = s.zwstid
LEFT JOIN announcements a ON (a.year, a.dsnr) = (s.year, s.dsnr)
LEFT JOIN delivery_schedule_wine_variety v ON (v.year, v.dsnr) = (s.year, s.dsnr)
LEFT JOIN delivery d ON (d.date, d.zwstid) = (s.date, s.zwstid)
LEFT JOIN delivery_part p ON (p.year, p.did) = (d.year, d.did) AND p.sortid = v.sortid
"""
if len(clauses) > 0:
sql += f" WHERE {' AND '.join(clauses)}"
sql += " GROUP BY s.year, s.dsnr"
sql += " ORDER BY s.year, s.date, s.zwstid, s.description, s.dsnr"
rows1 = CNX.execute("""
SELECT date, zwstid, cultid, SUM(weight)
FROM delivery d
JOIN delivery_part p ON (p.year, p.did) = (d.year, d.did)
WHERE (d.date, d.zwstid, COALESCE(p.cultid, '')) IN
(SELECT date, zwstid, COALESCE(cultid, '') FROM delivery_schedule GROUP BY date, zwstid, cultid HAVING COUNT(*) = 1)
GROUP BY date, zwstid, cultid
""")
days1 = {(r[0], r[1], r[2]): r[3] for r in rows1}
rows2 = CNX.execute("""
SELECT date, zwstid, attrid, SUM(weight)
FROM delivery d
JOIN delivery_part p ON (p.year, p.did) = (d.year, d.did)
WHERE (d.date, d.zwstid, COALESCE(p.attrid, '')) IN
(SELECT date, zwstid, COALESCE(attrid, '') FROM delivery_schedule GROUP BY date, zwstid, attrid HAVING COUNT(*) = 1)
GROUP BY date, zwstid, attrid
""")
days2 = {(r[0], r[1], r[2]): r[3] for r in rows2}
self.exec_collection(
sql,
lambda r: f'{{"year":{r[0]:4},"dsnr":{r[1]:2},"date":"{r[2]}",'
f'"branch":{{"zwstid":{jdmp(r[10])},"name":{jdmp(r[11]):20}}},'
f'"description":{jdmp(r[3]):50},'
f'"max_weight":{jdmp(r[4]):>6},'
f'"is_cancelled":{jdmp(r[5], is_bool=True)},'
f'"announced_weight":{r[6]:6},'
f'"delivered_weight":{days1.get((r[2], r[10], r[13]), days2.get((r[2], r[10], r[12]), r[7] or 0)):6},'
f'"announcement_from":{jdmp(r[8])},'
f'"announcement_to":{jdmp(r[9])}}}',
filters, offset, limit, distinct=(['s.year', 's.dsnr'], [1, 2]))
def do_HEAD(self) -> None:
self.do_GET()
def do_OPTIONS(self) -> None:
self.send('')
def do_GET(self) -> None:
try:
if self.path == '/':
openapi_json = f'https://{self.headers.get("Host", "localhost")}/elwig/api/v1/openapi.json'
self.see_other(f'https://validator.swagger.io/?url={openapi_json}')
return
elif self.path == '/openapi.json':
self.send(OPEN_API_DOC)
return
username, role = self.authorize()
parts = self.path.split('?', 1)
if len(parts) == 1:
path, query = parts[0], {}
else:
path, query = parts[0], {urllib.parse.unquote(s[0]): urllib.parse.unquote(s[-1])
for s in [p.split('=', 1) for p in parts[1].split('&')]}
filters = [Filter.from_str(f) for f in (query['filters'].split(',') if 'filters' in query else [])]
try:
offset = int(query['offset']) if 'offset' in query else None
limit = int(query['limit']) if 'limit' in query else None
except ValueError:
raise BadRequestError('Invalid integer value in query')
order = query['order'] if 'order' in query else None
if path == '/wine/varieties':
self.exec_collection(
"SELECT sortid, type, name, comment FROM wine_variety",
lambda r: f'{{"sortid":{jdmp(r[0])},"type":{jdmp(r[1])},"name":{jdmp(r[2])},"comment":{jdmp(r[3])}}}',
[], offset, limit)
elif path == '/wine/quality_levels':
self.exec_collection(
"SELECT qualid, name, min_kmw, predicate FROM wine_quality_level",
lambda r: f'{{"qualid":{jdmp(r[0])},"name":{jdmp(r[1]):22},"min_kmw":{jdmp(r[2])},"is_predicate":{jdmp(r[3], is_bool=True)}}}',
[], offset, limit)
elif path == '/wine/attributes':
self.exec_collection(
"SELECT attrid, name FROM wine_attribute",
lambda r: f'{{"attrid":{jdmp(r[0]):4},"name":{jdmp(r[1])}}}',
[], offset, limit)
elif path == '/wine/cultivations':
self.exec_collection(
"SELECT cultid, name, description FROM wine_cultivation",
lambda r: f'{{"cultid":{jdmp(r[0]):5},"name":{jdmp(r[1])},"description":{jdmp(r[2])}}}',
[], offset, limit)
elif path == '/modifiers':
self.exec_collection(
"SELECT year, modid, name, ordering FROM modifier",
lambda r: f'{{"year":{jdmp(r[0])},"modid":{jdmp(r[1]):5},"name":{jdmp(r[2]):18},"ordering":{jdmp(r[3])}}}',
[], offset, limit)
elif path == '/delivery_schedules':
self.do_GET_delivery_schedules(filters, offset, limit, order)
else:
raise NotFoundError('Invalid path')
except HttpError as e:
self.error(e.status_code, str(e))
except Exception as e:
traceback.print_exception(e)
self.error(500, str(e))
def main() -> None:
global CNX
global USER_FILE
sqlite3.register_adapter(datetime.date, lambda d: d.strftime('%Y-%m-%d'))
sqlite3.register_adapter(datetime.time, lambda t: t.strftime('%H:%M:%S'))
parser = argparse.ArgumentParser()
parser.add_argument('db', type=str, metavar='DB')
parser.add_argument('user_file', type=str, metavar='USER-FILE')
parser.add_argument('-p', '--port', type=int, default=8080)
args = parser.parse_args()
USER_FILE = args.user_file
CNX = sqlite3.connect(f'file:{args.db}?mode=ro', uri=True)
CNX.create_function('REGEXP', 2, sqlite_regexp, deterministic=True)
server = HTTPServer(('localhost', args.port), ElwigApi)
print(f'Listening on http://localhost:{args.port}')
try:
server.serve_forever()
except InterruptedError | KeyboardInterrupt:
print()
server.server_close()
print('Good bye!')
OPEN_API_DOC: str = '''{
"openapi": "3.1.0",
"info": {
"title": "Elwig API",
"summary": "Elektronische Winzergenossenschaftsverwaltung (\\"Electronic Management for Vintners'/Winemakers' Cooperatives\\")",
"description": "",
"contact": {"email": "contact@necronda.net"},
"version": "[VERSION]"
},
"servers": [{
"url": "https://wgm.elwig.at/elwig/api/v1",
"description": "WG Matzen"
}],
"components": {"securitySchemes": {"basicAuth": {"type": "http", "scheme": "basic"}}},
"security": [{"basicAuth": []}],
"paths": {
"/wine/varieties": {
"get": {
"tags": ["Base Data"],
"summary": "Weinsorten",
"parameters": [
{"in": "query", "name": "limit", "schema": {"type": "integer"}, "required": false},
{"in": "query", "name": "offset", "schema": {"type": "integer"}, "required": false}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["filters", "total", "offset", "limit", "data"],
"properties": {
"filters": {
"type": "array",
"items": {"type": "object", "required": ["filter"], "properties": {"filter": {"type": "string"}}},
"description": "Applied filters"
},
"total": {"type": "integer", "description": "Total number of wine varieties in the database"},
"offset": {"type": "integer", "description": "Applied offset"},
"limit": {"oneOf": [{"type": "integer"}, {"type": "null"}], "description": "Applied limit"},
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["sortid", "type", "name"],
"properties": {
"sortid": {"type": "string"},
"type": {"type": "string", "enum": ["R", "W"]},
"name": {"type": "string"},
"comment": {"type": "string"}
}
}
}
}
},
"examples": {
"simple": {
"value": {"filters": [], "total": 4, "offset": 0, "limit": null, "data": [
{"sortid": "BL", "type": "R", "name": "Blauburger", "comment": null},
{"sortid": "GV", "type": "W", "name": "Grüner Veltliner", "comment": "Weißgipfler"},
{"sortid": "WR", "type": "W", "name": "Welschriesling", "comment": null},
{"sortid": "ZW", "type": "R", "name": "Zweigelt", "comment": "Blauer Zweigelt, Rotburger"}
]}
},
"limit": {
"value": {"filters": [], "total": 4, "offset": 0, "limit": 2, "data": [
{"sortid": "BL", "type": "R", "name": "Blauburger", "comment": null},
{"sortid": "GV", "type": "W", "name": "Grüner Veltliner", "comment": "Weißgipfler"}
]}
}
}
}
}
},
"400": {"description": "Bad Request", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Invalid integer value in query"}}}}}},
"401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unauthorized"}}}}}},
"500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unknown error"}}}}}}
}
}
},
"/wine/quality_levels": {
"get": {
"tags": ["Base Data"],
"summary": "Qualitätsstufen",
"parameters": [
{"in": "query", "name": "limit", "schema": {"type": "integer"}, "required": false},
{"in": "query", "name": "offset", "schema": {"type": "integer"}, "required": false}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["filters", "total", "offset", "limit", "data"],
"properties": {
"filters": {
"type": "array",
"items": {"type": "object", "required": ["filter"], "properties": {"filter": {"type": "string"}}},
"description": "Applied filters"
},
"total": {"type": "integer", "description": "Total number of wine quality levels in the database"},
"offset": {"type": "integer", "description": "Applied offset"},
"limit": {"oneOf": [{"type": "integer"}, {"type": "null"}], "description": "Applied limit"},
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["qualid", "name"],
"properties": {
"qualid": {"type": "string"},
"name": {"type": "string"},
"min_kmw": {"oneOf": [{"type": "number"}, {"type": "null"}]},
"is_predicate": {"type": "boolean"}
}
}
}
}
},
"examples": {
"simple": {
"value": {"filters": [], "total": 10, "offset": 0, "limit": null, "data": [
{"qualid": "WEI", "name": "Wein", "min_kmw": null, "is_predicate": false},
{"qualid": "RSW", "name": "Rebsortenwein", "min_kmw": 10.6, "is_predicate": false},
{"qualid": "LDW", "name": "Landwein", "min_kmw": 14.0, "is_predicate": false},
{"qualid": "QUW", "name": "Qualitätswein", "min_kmw": 15.0, "is_predicate": false},
{"qualid": "KAB", "name": "Kabinett", "min_kmw": 17.0, "is_predicate": false},
{"qualid": "SPL", "name": "Spätlese", "min_kmw": 19.0, "is_predicate": true},
{"qualid": "AUL", "name": "Auslese", "min_kmw": 21.0, "is_predicate": true},
{"qualid": "BAL", "name": "Beerenauslese", "min_kmw": 25.0, "is_predicate": true},
{"qualid": "TBA", "name": "Trockenbeerenauslese", "min_kmw": 30.0, "is_predicate": true},
{"qualid": "DAC", "name": "DAC", "min_kmw": 15.0, "is_predicate": true}
]}
},
"limit": {
"value": {"filters": [], "total": 10, "offset": 0, "limit": 4, "data": [
{"qualid": "WEI", "name": "Wein", "min_kmw": null, "is_predicate": false},
{"qualid": "RSW", "name": "Rebsortenwein", "min_kmw": 10.6, "is_predicate": false},
{"qualid": "LDW", "name": "Landwein", "min_kmw": 14.0, "is_predicate": false},
{"qualid": "QUW", "name": "Qualitätswein", "min_kmw": 15.0, "is_predicate": false}
]}
}
}
}
}
},
"400": {"description": "Bad Request", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Invalid integer value in query"}}}}}},
"401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unauthorized"}}}}}},
"500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unknown error"}}}}}}
}
}
},
"/wine/attributes": {
"get": {
"tags": ["Client Base Data"],
"summary": "(Sorten-)Attribute",
"parameters": [
{"in": "query", "name": "limit", "schema": {"type": "integer"}, "required": false},
{"in": "query", "name": "offset", "schema": {"type": "integer"}, "required": false}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["filters", "total", "offset", "limit", "data"],
"properties": {
"filters": {
"type": "array",
"items": {"type": "object", "required": ["filter"], "properties": {"filter": {"type": "string"}}},
"description": "Applied filters"
},
"total": {"type": "integer", "description": "Total number of wine attributes in the database"},
"offset": {"type": "integer", "description": "Applied offset"},
"limit": {"oneOf": [{"type": "integer"}, {"type": "null"}], "description": "Applied limit"},
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["attrid", "name"],
"properties": {
"attrid": {"type": "string"},
"name": {"type": "string"}
}
}
}
}
},
"examples": {
"simple": {
"value": {"filters": [], "total": 3, "offset": 0, "limit": null, "data": [
{"attrid": "S", "name": "Saft"},
{"attrid": "Z", "name": "Sekt"},
{"attrid": "D", "name": "DAC"}
]}
},
"limit": {
"value": {"filters": [], "total": 3, "offset": 0, "limit": 1, "data": [
{"attrid": "S", "name": "Saft"}
]}
}
}
}
}
},
"400": {"description": "Bad Request", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Invalid integer value in query"}}}}}},
"401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unauthorized"}}}}}},
"500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unknown error"}}}}}}
}
}
},
"/wine/cultivations": {
"get": {
"tags": ["Client Base Data"],
"summary": "Bewirtschaftungsarten",
"parameters": [
{"in": "query", "name": "limit", "schema": {"type": "integer"}, "required": false},
{"in": "query", "name": "offset", "schema": {"type": "integer"}, "required": false}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["filters", "total", "offset", "limit", "data"],
"properties": {
"filters": {
"type": "array",
"items": {"type": "object", "required": ["filter"], "properties": {"filter": {"type": "string"}}},
"description": "Applied filters"
},
"total": {"type": "integer", "description": "Total number of wine cultivations in the database"},
"offset": {"type": "integer", "description": "Applied offset"},
"limit": {"oneOf": [{"type": "integer"}, {"type": "null"}], "description": "Applied limit"},
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["cultid", "name"],
"properties": {
"cultid": {"type": "string"},
"name": {"type": "string"},
"description": {"oneOf": [{"type": "string"}, {"type": "null"}]}
}
}
}
}
},
"examples": {
"simple": {
"value": {"filters": [], "total": 2, "offset": 0, "limit": null, "data": [
{"cultid": "KIP", "name": "KIP", "description": "Kontrollierte Integrierte Produktion"},
{"cultid": "B", "name": "Bio", "description": "AT-BIO-302"}
]}
},
"limit": {
"value": {"filters": [], "total": 2, "offset": 0, "limit": 1, "data": [
{"cultid": "KIP", "name": "KIP", "description": "Kontrollierte Integrierte Produktion"}
]}
}
}
}
}
},
"400": {"description": "Bad Request", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Invalid integer value in query"}}}}}},
"401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unauthorized"}}}}}},
"500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unknown error"}}}}}}
}
}
},
"/modifiers": {
"get": {
"tags": ["Client Base Data"],
"summary": "Zu-/Abschläge",
"parameters": [
{"in": "query", "name": "limit", "schema": {"type": "integer"}, "required": false},
{"in": "query", "name": "offset", "schema": {"type": "integer"}, "required": false},
{"in": "query", "name": "filters", "schema": {"type": "string", "pattern": "^year=[0-9]+(;[0-9]+)*$"}, "required": false}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["filters", "total", "offset", "limit", "data"],
"properties": {
"filters": {
"type": "array",
"items": {"type": "object", "required": ["filter"], "properties": {"filter": {"type": "string"}}},
"description": "Applied filters"
},
"total": {"type": "integer", "description": "Total number of modifiers in the database (on which the filters apply to)"},
"offset": {"type": "integer", "description": "Applied offset"},
"limit": {"oneOf": [{"type": "integer"}, {"type": "null"}], "description": "Applied limit"},
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["year", "modid", "name"],
"properties": {
"year": {"type": "integer"},
"modid": {"type": "string"},
"name": {"type": "string"},
"ordering": {"type": "integer"}
}
}
}
}
},
"examples": {
"filter": {
"value": {"filters": [{"filter": "year=2020"}], "total": 4, "offset": 0, "limit": null, "data": [
{"year": 2020, "modid": "A", "name": "Klasse A", "ordering": 1},
{"year": 2020, "modid": "B", "name": "Klasse B", "ordering": 2},
{"year": 2020, "modid": "C", "name": "Klasse C", "ordering": 3},
{"year": 2020, "modid": "D", "name": "Klasse D", "ordering": 4}
]}
}
}
}
}
},
"400": {"description": "Bad Request", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Invalid filter"}}}}}},
"401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unauthorized"}}}}}},
"500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unknown error"}}}}}}
}
}
},
"/delivery_schedules": {
"get": {
"tags": ["Delivery Schedules"],
"summary": "Lesepläne",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["filters", "total", "offset", "limit", "data"],
"properties": {
"filters": {
"type": "array",
"items": {"type": "object", "required": ["filter"], "properties": {"filter": {"type": "string"}}},
"description": "Applied filters"
},
"total": {"type": "integer", "description": "Total number of delivery schedules in the database (on which the filters apply to)"},
"offset": {"type": "integer", "description": "Applied offset"},
"limit": {"oneOf": [{"type": "integer"}, {"type": "null"}], "description": "Applied limit"},
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["year", "dsnr", "date", "branch", "description", "max_weight", "announced_weight", "delivered_weight"],
"properties": {
"year": {"type": "integer"},
"dsnr": {"type": "integer"},
"date": {"type": "string", "format": "date"},
"branch": {"type": "object", "required": ["zwstid", "name"], "properties": {"zwstid": {"type": "string"}, "name": {"type": "string"}}},
"description": {"type": "string"},
"is_cancelled": {"type": "boolean"},
"max_weight": {"oneOf": [{"type": "integer"}, {"type": "null"}]},
"announced_weight": {"oneOf": [{"type": "integer"}, {"type": "null"}]},
"delivered_weight": {"oneOf": [{"type": "integer"}, {"type": "null"}]},
"announcement_from": {"oneOf": [{"type": "string", "format": "date-time"}, {"type": "null"}]},
"announcement_to": {"oneOf": [{"type": "string", "format": "date-time"}, {"type": "null"}]}
}
}
}
}
}
}
}
},
"400": {"description": "Bad Request", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Invalid filter"}}}}}},
"401": {"description": "Unauthorized", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unauthorized"}}}}}},
"500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "required": ["message"], "properties": {"message": {"oneOf": [{"type": "string"}, {"type": "null"}]}}}, "examples": {"simple": {"value": {"message": "Unknown error"}}}}}}
}
}
}
}
}
'''.replace('[VERSION]', VERSION)
if __name__ == '__main__':
main()