Initial commit.
This commit is contained in:
14
backend/Dockerfile
Normal file
14
backend/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM python:3.12-alpine
|
||||
|
||||
RUN apk add git
|
||||
WORKDIR /code
|
||||
RUN git clone https://gitea.typename.fr/mikael.capelle/advent-of-code .
|
||||
RUN pip install -e .
|
||||
|
||||
RUN pip install poetry
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN poetry install
|
||||
|
||||
CMD ["poetry", "run", "uvicorn", "holt59.aoc.app:app", "--reload", "--proxy-headers", "--host", "0.0.0.0", "--port", "80", "--root-path", "/api/v1"]
|
1166
backend/poetry.lock
generated
Normal file
1166
backend/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
backend/pyproject.toml
Normal file
55
backend/pyproject.toml
Normal file
@@ -0,0 +1,55 @@
|
||||
[tool.poetry]
|
||||
name = "holt59-aoc-api"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Mikaël Capelle <mikael.capelle@gmail.com>"]
|
||||
packages = [{ include = "holt59", from = "src" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10.0,<4.0"
|
||||
fastapi = {extras = ["standard"], version = "^0.115.6"}
|
||||
uvicorn = "^0.32.1"
|
||||
sse-starlette = "^2.1.3"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.8.2"
|
||||
pytest = "^8.3.4"
|
||||
pyright = "^1.1.390"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poe.tasks]
|
||||
format-imports = "ruff check --select I src tests --fix"
|
||||
format-ruff = "ruff format src tests"
|
||||
format.sequence = ["format-imports", "format-ruff"]
|
||||
lint-ruff = "ruff check src tests"
|
||||
lint-ruff-format = "ruff format --check src tests"
|
||||
lint-pyright = "pyright src tests"
|
||||
lint.sequence = ["lint-ruff", "lint-ruff-format", "lint-pyright"]
|
||||
lint.ignore_fail = "return_non_zero"
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = ["B", "Q", "I"]
|
||||
|
||||
[tool.ruff.lint.flake8-bugbear]
|
||||
extend-immutable-calls = ["fastapi.Depends"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
detect-same-package = true
|
||||
section-order = [
|
||||
"future",
|
||||
"standard-library",
|
||||
"third-party",
|
||||
"first-party",
|
||||
"zik_insat_backend",
|
||||
"local-folder",
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "strict"
|
||||
reportMissingTypeStubs = true
|
0
backend/src/holt59/aoc/__init__.py
Normal file
0
backend/src/holt59/aoc/__init__.py
Normal file
195
backend/src/holt59/aoc/app.py
Normal file
195
backend/src/holt59/aoc/app.py
Normal file
@@ -0,0 +1,195 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal, TypeAlias
|
||||
|
||||
from fastapi import FastAPI, Form
|
||||
from sse_starlette import EventSourceResponse
|
||||
|
||||
DataMode: TypeAlias = Literal["holt59", "tests"]
|
||||
|
||||
LOGGER = logging.getLogger("uvicorn.info")
|
||||
|
||||
GIT_AOC_PATH = Path("/code")
|
||||
|
||||
|
||||
def update_repository():
|
||||
subprocess.run(["git", "fetch"], cwd=GIT_AOC_PATH)
|
||||
subprocess.run(["git", "reset", "--hard", "origin/master"], cwd=GIT_AOC_PATH)
|
||||
|
||||
|
||||
def read_head_commit():
|
||||
# read git commit
|
||||
process = subprocess.run(
|
||||
["git", "log", "-1", '--format="%H"'], cwd=GIT_AOC_PATH, stdout=subprocess.PIPE
|
||||
)
|
||||
return process.stdout.decode("utf-8").strip()[1:-1]
|
||||
|
||||
|
||||
def read_test_file(year: int, day: int, mode: DataMode) -> str:
|
||||
path = GIT_AOC_PATH.joinpath(f"src/holt59/aoc/inputs/{mode}/{year}/day{day}.txt")
|
||||
if not path.exists():
|
||||
return ""
|
||||
|
||||
with open(path, "r") as fp:
|
||||
return fp.read()
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/update")
|
||||
def update():
|
||||
update_repository()
|
||||
head_commit = read_head_commit()
|
||||
LOGGER.info(f"repository now at {head_commit}")
|
||||
return {"HEAD": head_commit}
|
||||
|
||||
|
||||
@app.get("/data")
|
||||
async def data(year: int, day: int, mode: DataMode = "tests"):
|
||||
return {"content": read_test_file(year, day, mode)}
|
||||
|
||||
|
||||
@app.post("/submit-sse")
|
||||
async def submit_sse(year: int = Form(), day: int = Form(), input: str = Form()):
|
||||
data = input.rstrip().replace("\r\n", "\n")
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"holt59-aoc",
|
||||
"--stdin",
|
||||
"--api",
|
||||
"--year",
|
||||
str(year),
|
||||
str(day),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
assert process.stdin is not None
|
||||
assert process.stdout is not None
|
||||
assert process.stderr is not None
|
||||
|
||||
message_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
||||
|
||||
async def watch_stream(
|
||||
fp: asyncio.StreamReader, stream: Literal["stdout", "stderr"]
|
||||
):
|
||||
while True:
|
||||
output = await fp.readline()
|
||||
if output == b"":
|
||||
break
|
||||
await message_queue.put(
|
||||
{
|
||||
"type": "stream",
|
||||
"stream": stream,
|
||||
"data": json.loads(output)
|
||||
if stream == "stdout"
|
||||
else {"type": "error", "message": output.decode("utf-8")},
|
||||
}
|
||||
)
|
||||
|
||||
async def run():
|
||||
assert process.stdin is not None
|
||||
assert process.stdout is not None
|
||||
assert process.stderr is not None
|
||||
|
||||
await message_queue.put(
|
||||
{
|
||||
"type": "start",
|
||||
"data": {
|
||||
"time": datetime.datetime.now().isoformat(),
|
||||
"commit": read_head_commit(),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
process.stdin.write(data.encode("utf-8"))
|
||||
await process.stdin.drain()
|
||||
process.stdin.close()
|
||||
|
||||
await asyncio.gather(
|
||||
# process.stdin.drain(),
|
||||
watch_stream(process.stdout, "stdout"),
|
||||
watch_stream(process.stderr, "stderr"),
|
||||
)
|
||||
|
||||
returncode: int | None = None
|
||||
try:
|
||||
returncode = await asyncio.wait_for(process.wait(), timeout=300)
|
||||
except asyncio.TimeoutError:
|
||||
...
|
||||
|
||||
await message_queue.put(
|
||||
{
|
||||
"type": "complete",
|
||||
"data": {
|
||||
"status": "success" if returncode is not None else "error",
|
||||
"returncode": -1 if returncode is None else -1,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
await message_queue.join()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(run())
|
||||
|
||||
async def stream_queue():
|
||||
while True:
|
||||
message = await message_queue.get()
|
||||
|
||||
yield json.dumps(
|
||||
{
|
||||
"event": message["type"],
|
||||
"id": 0,
|
||||
"retry": 0,
|
||||
"data": message["data"],
|
||||
}
|
||||
)
|
||||
|
||||
message_queue.task_done()
|
||||
|
||||
if message["type"] == "complete":
|
||||
break
|
||||
|
||||
return EventSourceResponse(stream_queue())
|
||||
|
||||
|
||||
@app.post("/submit")
|
||||
async def submit(
|
||||
year: int = Form(), day: int = Form(), verbose: bool = Form(), input: str = Form()
|
||||
) -> dict[str, Any]:
|
||||
data = input.strip().replace("\r\n", "\n")
|
||||
|
||||
args: list[str] = ["holt59-aoc", "--stdin"]
|
||||
if verbose:
|
||||
args += ["-v"]
|
||||
|
||||
start = datetime.datetime.now()
|
||||
process = subprocess.run(
|
||||
args + ["--year", str(year), str(day)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
input=data.encode("utf-8"),
|
||||
timeout=300,
|
||||
)
|
||||
time = datetime.datetime.now() - start
|
||||
|
||||
return {
|
||||
"commit": read_head_commit(),
|
||||
"input": data,
|
||||
"error": process.returncode != 0,
|
||||
"stdout": process.stdout.decode("utf-8"),
|
||||
"stderr": process.stderr.decode("utf-8"),
|
||||
"time": time.total_seconds(),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Hello World"}
|
Reference in New Issue
Block a user