diff --git a/.gitignore b/.gitignore index 781f0be..2549af3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -** -!src/ -!.gitignore -!README.md +.idea +*.sql +*.sqlite3 diff --git a/src/elwig-backend b/src/elwig-backend index 7427e32..cbb5075 100755 --- a/src/elwig-backend +++ b/src/elwig-backend @@ -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 [])] - offset = int(query['offset']) if 'offset' in query else None - limit = int(query['limit']) if 'limit' in query else None + 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':