Compare commits

...

4 commits

Author SHA1 Message Date
8d2de1766b
Utils and better handling
Some checks failed
/ backend-pylint (push) Successful in 1m30s
/ build-artifacts (push) Successful in 16s
/ publish-artifacts (push) Successful in 1m7s
/ release (push) Failing after 22m48s
/ build-and-push-container (push) Successful in 3m14s
2024-10-04 15:03:44 +02:00
1eafc1a0e1
Switched API
All checks were successful
/ backend-pylint (push) Successful in 1m5s
/ build-artifacts (push) Successful in 13s
/ publish-artifacts (push) Successful in 43s
/ release (push) Successful in 2m54s
/ build-and-push-container (push) Successful in 2m10s
2024-10-04 00:31:31 +02:00
27fd95ba1e
Rework with UI in mind
Some checks failed
/ backend-pylint (push) Successful in 1m37s
/ publish-artifacts (push) Successful in 1m28s
/ build-artifacts (push) Successful in 16s
/ build-and-push-container (push) Successful in 2m46s
/ release (push) Failing after 34m50s
2024-10-03 21:34:46 +02:00
fbdb6f9f5a
csv delimiter fix
All checks were successful
/ backend-pylint (push) Successful in 17s
/ publish-artifacts (push) Successful in 8s
/ build-and-push-container (push) Successful in 1m13s
/ release (push) Successful in 6s
/ build-artifacts (push) Successful in 7s
2024-09-30 22:53:36 +02:00
13 changed files with 61 additions and 72 deletions

View file

@ -1,8 +1,6 @@
on: on:
push: push:
tags: tags:
- '[0-9]+\.[0-9]+\.[0-9]+-c\.[0-9]+'
- '[0-9]+\.[0-9]+\.[0-9]+-a\.[0-9]+'
- '[0-9]+\.[0-9]+\.[0-9]' - '[0-9]+\.[0-9]+\.[0-9]'
jobs: jobs:
@ -55,7 +53,7 @@ jobs:
build-args: | build-args: |
PACKAGE_VERSION=${{ github.ref_name }} PACKAGE_VERSION=${{ github.ref_name }}
push: true push: true
tags: forgejo.neshweb.net/firq/support-formatter:${{ github.ref_name }} tags: forgejo.neshweb.net/firq/support-formatter:${{ github.ref_name }}, forgejo.neshweb.net/firq/support-formatter:latest
release: release:
needs: [ build-and-push-container, publish-artifacts ] needs: [ build-and-push-container, publish-artifacts ]

View file

@ -1,6 +1,6 @@
[project] [project]
name = "support_formatter" name = "support_formatter"
version = "0.1.0-a.1" version = "0.2.1"
requires-python = ">= 3.10" requires-python = ">= 3.10"
authors = [{name = "Firq", email = "firelp42@gmail.com"}] authors = [{name = "Firq", email = "firelp42@gmail.com"}]
maintainers = [{name = "Firq", email = "firelp42@gmail.com"}] maintainers = [{name = "Firq", email = "firelp42@gmail.com"}]

View file

@ -33,3 +33,5 @@ class APISettings:
ALLOWED_EXTENSIONS = { 'csv', 'txt' } ALLOWED_EXTENSIONS = { 'csv', 'txt' }
FILE_SAVE_DIRECTORY = Path(__file__).parents[1] / ".temp" FILE_SAVE_DIRECTORY = Path(__file__).parents[1] / ".temp"
UI_URL = "https://support-formatter.firq.dev/"

View file

@ -1,6 +1,9 @@
import csv import csv
import pathlib import pathlib
from ..utils import convert_to_bool
class FileTypeInvalidError(ValueError): class FileTypeInvalidError(ValueError):
pass pass
@ -15,7 +18,10 @@ def process_csv(path: pathlib.Path):
data, entries = {}, [] data, entries = {}, []
with open(path, "r") as csv_file: with open(path, "r") as csv_file:
csv_reader = csv.DictReader(csv_file) dialect = csv.Sniffer().sniff(csv_file.read(1024), delimiters=";,")
csv_file.seek(0)
csv_reader = csv.DictReader(csv_file, dialect=dialect)
csv_type = determine_type(csv_reader.fieldnames) csv_type = determine_type(csv_reader.fieldnames)
if csv_type == "unknown": if csv_type == "unknown":
@ -27,7 +33,7 @@ def process_csv(path: pathlib.Path):
entry.update({ entry.update({
"level": int(entry['level']), "level": int(entry['level']),
"np_level": int(entry['np_level']), "np_level": int(entry['np_level']),
"bce": bool(entry["bce"]) "bce": convert_to_bool(entry["bce"])
}) })
else: else:
entry.update({ entry.update({

View file

@ -1,6 +1,8 @@
from enum import Enum from enum import Enum
import marshmallow as ma import marshmallow as ma
class HealthStatus(Enum): class HealthStatus(Enum):
OK = 0 OK = 0
WARNING = 1 WARNING = 1

View file

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Support File Converter - Error</title>
</head>
<body>
<h1>Error</h1>
</body>
</html>

View file

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Support File Converter</title>
</head>
<body>
<h1>Convert to Nerone-compatible data</h1>
<form method=post enctype=multipart/form-data>
<label for="fusername">Username:</label>
<input type="text" id="fusername" name="username" placeholder="Your username">
<br />
<label for="ffriendcode">Friendcode:</label>
<input type="text" id="ffriendcode" name="friendcode" placeholder="000,000,000">
<br />
<br />
<label for="fservantdata">Servant Data</label>
<br />
<input type="file" id="fservantdata" name="servantdata">
<br />
<br />
<label for="fcedata">CE Data (optional)</label>
<br />
<input type="file" id="fcedata" name="cedata">
<br />
<br />
<input type="submit" value="Upload">
</form>
</body>
</html>

View file

@ -1,7 +1,8 @@
# pylint: disable=wrong-import-position,cyclic-import # pylint: disable=wrong-import-position,cyclic-import
from flask_smorest import Blueprint from flask_smorest import Blueprint
redirect_routes = Blueprint("redirect", "recirects", url_prefix="/", description="")
interface_routes = Blueprint("interface", "interface", url_prefix="/", description="") interface_routes = Blueprint("interface", "interface", url_prefix="/", description="")
formatter_routes = Blueprint("formatter", "formatter", url_prefix="/", description="") formatter_routes = Blueprint("formatter", "formatter", url_prefix="/", description="")
from . import interface, upload # avoids circular imports problem from . import interface, upload, redirects # avoids circular imports problem

View file

@ -6,7 +6,6 @@ from flask.views import MethodView
from ..app import Application from ..app import Application
from ..config import APISettings from ..config import APISettings
from ..models.interface import ApiVersionGet, HealthGet, HealthStatus, OpenAPIGet from ..models.interface import ApiVersionGet, HealthGet, HealthStatus, OpenAPIGet
from . import interface_routes as blp from . import interface_routes as blp
APP = Application.get_instance() APP = Application.get_instance()

View file

@ -0,0 +1,12 @@
from flask import redirect
from ..app import Application
from ..config import APISettings
from . import redirect_routes as blp
APP = Application.get_instance()
@blp.route("/", methods=["GET"])
@blp.doc(summary="This route just exists to redirect any visitor to the root domain to the UI")
def redirect_to_ui():
return redirect(APISettings.UI_URL)

View file

@ -1,16 +1,16 @@
import os import os
import pathlib import pathlib
import uuid
from typing import List from typing import List
from flask import send_from_directory
import marshmallow as ma import marshmallow as ma
from flask_smorest.fields import Upload from flask_smorest.fields import Upload
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
import uuid
from ..app import Application from ..app import Application
from ..config import APISettings from ..config import APISettings
from ..logic.csv_processor import process_csv, FileTypeInvalidError from ..logic.csv_processor import FileTypeInvalidError, process_csv
from ..utils import generate_error
from . import formatter_routes as blp from . import formatter_routes as blp
APP = Application.get_instance() APP = Application.get_instance()
@ -26,7 +26,7 @@ class MultipartFileSchema(ma.Schema):
servantdata = Upload() servantdata = Upload()
cedata = Upload() cedata = Upload()
@blp.route("/", methods=["POST"]) @blp.route("/upload", methods=["POST"])
@blp.arguments(MultipartFormSchema, location="form") @blp.arguments(MultipartFormSchema, location="form")
@blp.arguments(MultipartFileSchema, location="files") @blp.arguments(MultipartFileSchema, location="files")
@blp.response(200) @blp.response(200)
@ -39,32 +39,28 @@ def upload_file(form: dict[str, str], files: dict[str, FileStorage]):
for name, file in files.items(): for name, file in files.items():
if name not in ("servantdata", "cedata"): if name not in ("servantdata", "cedata"):
return send_from_directory(APISettings.PAGES_DIRECTORY, "error.html") return generate_error(406, message="Invalid form sent")
filepath = APISettings.FILE_SAVE_DIRECTORY / f"{uuid.uuid4()}.csv" filepath = APISettings.FILE_SAVE_DIRECTORY / f"{uuid.uuid4()}.csv"
file.save(filepath) file.save(filepath)
if os.stat(filepath).st_size < 1 or not allowed_file(file.filename): if os.stat(filepath).st_size < 1 or not allowed_file(file.filename):
filepath.unlink() filepath.unlink(missing_ok=True)
continue continue
filepaths.append(filepath) filepaths.append(filepath)
if len(filepaths) == 0: if len(filepaths) == 0:
return send_from_directory(APISettings.PAGES_DIRECTORY, "error.html") return generate_error(406, message="No files provided")
try: try:
for f in filepaths: for f in filepaths:
result = process_csv(f) result = process_csv(f)
returndata = returndata | result returndata = returndata | result
f.unlink() f.unlink(missing_ok=True)
except FileTypeInvalidError: except FileTypeInvalidError:
for f in filepaths: for f in filepaths:
f.unlink() f.unlink(missing_ok=True)
return send_from_directory(APISettings.PAGES_DIRECTORY, "error.html") return generate_error(500, message="Error whilst parsing uploaded file - please contact Firq on Fate/Sacc Order")
return returndata return returndata
@blp.route("/", methods=["GET"])
def file_dialog():
return send_from_directory(APISettings.PAGES_DIRECTORY, "index.html")

View file

@ -3,13 +3,15 @@ from gevent.monkey import patch_all; patch_all()
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
from .app import Application from .app import Application
from .routes import interface_routes, formatter_routes from .routes import interface_routes, formatter_routes, redirect_routes
from .config import APISettings, ServerSettings from .config import APISettings, ServerSettings
APISettings.FILE_SAVE_DIRECTORY.mkdir(parents=True, exist_ok=True) APISettings.FILE_SAVE_DIRECTORY.mkdir(parents=True, exist_ok=True)
APP = Application.get_instance() APP = Application.get_instance()
app, api = APP.app, APP.api app, api = APP.app, APP.api
api.register_blueprint(redirect_routes)
api.register_blueprint(interface_routes) api.register_blueprint(interface_routes)
api.register_blueprint(formatter_routes) api.register_blueprint(formatter_routes)

View file

@ -0,0 +1,19 @@
from typing import Any, Optional, Tuple, TypedDict
class ErrorReturn(TypedDict):
status: int
message: Optional[str]
details: Optional[dict[str, Any]]
def generate_error(status: int, message: Optional[str]=None, additional_data: Optional[dict[str, Any]]=None) -> Tuple[ErrorReturn, int]:
obj: ErrorReturn = { "status": status }
if message is not None:
obj |= { "message": message }
if additional_data is not None:
obj |= { "details": additional_data }
return obj, status
def convert_to_bool(val: str) -> bool:
return bool(val) if str(val).lower() not in ("null", "none") else False