Compare commits

..

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

13 changed files with 71 additions and 57 deletions

View file

@ -1,6 +1,8 @@
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:
@ -53,7 +55,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 }}, forgejo.neshweb.net/firq/support-formatter:latest tags: forgejo.neshweb.net/firq/support-formatter:${{ github.ref_name }}
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.2.1" version = "0.1.0-a.2"
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,5 +33,3 @@ 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,9 +1,6 @@
import csv import csv
import pathlib import pathlib
from ..utils import convert_to_bool
class FileTypeInvalidError(ValueError): class FileTypeInvalidError(ValueError):
pass pass
@ -33,7 +30,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": convert_to_bool(entry["bce"]) "bce": bool(entry["bce"])
}) })
else: else:
entry.update({ entry.update({

View file

@ -1,8 +1,6 @@
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

@ -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 # 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, 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 ..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

@ -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 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 FileTypeInvalidError, process_csv from ..logic.csv_processor import process_csv, FileTypeInvalidError
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("/upload", methods=["POST"]) @blp.route("/", 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,28 +39,32 @@ 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 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" 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(missing_ok=True) filepath.unlink()
continue continue
filepaths.append(filepath) filepaths.append(filepath)
if len(filepaths) == 0: if len(filepaths) == 0:
return generate_error(406, message="No files provided") return send_from_directory(APISettings.PAGES_DIRECTORY, "error.html")
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(missing_ok=True) f.unlink()
except FileTypeInvalidError: except FileTypeInvalidError:
for f in filepaths: for f in filepaths:
f.unlink(missing_ok=True) f.unlink()
return generate_error(500, message="Error whilst parsing uploaded file - please contact Firq on Fate/Sacc Order") return send_from_directory(APISettings.PAGES_DIRECTORY, "error.html")
return returndata 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 gevent.pywsgi import WSGIServer
from .app import Application 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 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

@ -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