elwig-backend: Add /openapi.json

This commit is contained in:
2024-12-29 19:12:52 +01:00
parent 88cafb1228
commit f74898706b
2 changed files with 416 additions and 8 deletions

7
.gitignore vendored
View File

@ -1,4 +1,3 @@
**
!src/
!.gitignore
!README.md
.idea
*.sql
*.sqlite3

View File

@ -19,6 +19,399 @@ CNX: sqlite3.Cursor
USER_FILE: str
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": "0.0.1"
},
"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"}}}}}}
}
}
}
}
}
'''
class BadRequestError(Exception):
pass
@ -97,13 +490,15 @@ def get_delivery_schedule_filter_clauses(filters: list[Filter]) -> list[str]:
class ElwigApi(BaseHTTPRequestHandler):
def send(self, data: str, status_code: int = 200):
def send(self, data: str, status_code: int = 200, url: str = 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 status_code == 401:
if 300 <= status_code < 400 and status_code != 304:
self.send_header('Location', url)
elif status_code == 401:
self.send_header('WWW-Authenticate', 'Basic realm=Elwig')
if self.headers.get('Accept-Encoding') and len(data) > 64:
accept_encoding = [e.strip() for e in self.headers.get('Accept-Encoding').split(',')]
@ -119,6 +514,9 @@ class ElwigApi(BaseHTTPRequestHandler):
def error(self, status_code: int, message: str = None):
self.send(f'{{"message":{jdmp(message)}}}\n', status_code=status_code)
def see_other(self, url: str):
self.send(f'{{"url": {jdmp(url)}}}\n', status_code=303, url=url)
def authorize(self) -> (str or None, str or None):
try:
auth = self.headers.get('Authorization')
@ -268,6 +666,14 @@ class ElwigApi(BaseHTTPRequestHandler):
def do_GET(self):
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()
if not username or not role:
return
@ -279,8 +685,11 @@ class ElwigApi(BaseHTTPRequestHandler):
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':