Compare commits

..

No commits in common. "main" and "0.1.0-a.1" have entirely different histories.

13 changed files with 72 additions and 61 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,14 @@
<!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

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

View file

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

View file

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

View file

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

View file

@ -1,19 +0,0 @@
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