File handling for API.
This commit is contained in:
parent
3c544c559b
commit
46558672e8
@ -97,7 +97,12 @@ def neighbors(
|
|||||||
|
|
||||||
|
|
||||||
class Solver(BaseSolver):
|
class Solver(BaseSolver):
|
||||||
def print_path(self, path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None:
|
def print_path(
|
||||||
|
self, name: str, path: list[tuple[int, int]], n_rows: int, n_cols: int
|
||||||
|
) -> None:
|
||||||
|
if not self.files:
|
||||||
|
return
|
||||||
|
|
||||||
end = path[-1]
|
end = path[-1]
|
||||||
|
|
||||||
graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
|
graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
|
||||||
@ -118,8 +123,11 @@ class Solver(BaseSolver):
|
|||||||
else:
|
else:
|
||||||
assert False, "{} -> {} infeasible".format(path[i], path[i + 1])
|
assert False, "{} -> {} infeasible".format(path[i], path[i + 1])
|
||||||
|
|
||||||
for row in graph:
|
self.files.create(
|
||||||
self.logger.info("".join(row))
|
f"graph_{name}.txt",
|
||||||
|
"\n".join("".join(row) for row in graph).encode(),
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
def solve(self, input: str) -> Iterator[Any]:
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
lines = input.splitlines()
|
lines = input.splitlines()
|
||||||
@ -157,7 +165,7 @@ class Solver(BaseSolver):
|
|||||||
path_1 = make_path(parents_1, start, end)
|
path_1 = make_path(parents_1, start, end)
|
||||||
assert path_1 is not None
|
assert path_1 is not None
|
||||||
|
|
||||||
self.print_path(path_1, n_rows=len(grid), n_cols=len(grid[0]))
|
self.print_path("answer1", path_1, n_rows=len(grid), n_cols=len(grid[0]))
|
||||||
yield lengths_1[end] - 1
|
yield lengths_1[end] - 1
|
||||||
|
|
||||||
lengths_2, _ = dijkstra(
|
lengths_2, _ = dijkstra(
|
||||||
|
@ -67,13 +67,16 @@ def flow(
|
|||||||
|
|
||||||
|
|
||||||
class Solver(BaseSolver):
|
class Solver(BaseSolver):
|
||||||
def print_blocks(self, blocks: dict[tuple[int, int], Cell]):
|
def print_blocks(self, name: str, blocks: dict[tuple[int, int], Cell]):
|
||||||
"""
|
"""
|
||||||
Print the given set of blocks on a grid.
|
Print the given set of blocks on a grid.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
blocks: Set of blocks to print.
|
blocks: Set of blocks to print.
|
||||||
"""
|
"""
|
||||||
|
if not self.files:
|
||||||
|
return
|
||||||
|
|
||||||
x_min, y_min, x_max, y_max = (
|
x_min, y_min, x_max, y_max = (
|
||||||
min(x for x, _ in blocks),
|
min(x for x, _ in blocks),
|
||||||
0,
|
0,
|
||||||
@ -81,11 +84,15 @@ class Solver(BaseSolver):
|
|||||||
max(y for _, y in blocks),
|
max(y for _, y in blocks),
|
||||||
)
|
)
|
||||||
|
|
||||||
for y in range(y_min, y_max + 1):
|
self.files.create(
|
||||||
self.logger.info(
|
f"blocks_{name}.txt",
|
||||||
|
"\n".join(
|
||||||
"".join(
|
"".join(
|
||||||
str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1)
|
str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1)
|
||||||
)
|
)
|
||||||
|
for y in range(y_min, y_max + 1)
|
||||||
|
).encode(),
|
||||||
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def solve(self, input: str) -> Iterator[Any]:
|
def solve(self, input: str) -> Iterator[Any]:
|
||||||
@ -115,7 +122,7 @@ class Solver(BaseSolver):
|
|||||||
for y in range(y_start, y_end):
|
for y in range(y_start, y_end):
|
||||||
blocks[x, y] = Cell.ROCK
|
blocks[x, y] = Cell.ROCK
|
||||||
|
|
||||||
self.print_blocks(blocks)
|
self.print_blocks("start", blocks)
|
||||||
|
|
||||||
y_max = max(y for _, y in blocks)
|
y_max = max(y for _, y in blocks)
|
||||||
|
|
||||||
@ -124,7 +131,7 @@ class Solver(BaseSolver):
|
|||||||
blocks_1 = flow(
|
blocks_1 = flow(
|
||||||
blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR
|
blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR
|
||||||
)
|
)
|
||||||
self.print_blocks(blocks_1)
|
self.print_blocks("part1", blocks_1)
|
||||||
yield sum(v == Cell.SAND for v in blocks_1.values())
|
yield sum(v == Cell.SAND for v in blocks_1.values())
|
||||||
|
|
||||||
# === part 2 ===
|
# === part 2 ===
|
||||||
@ -135,5 +142,5 @@ class Solver(BaseSolver):
|
|||||||
fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK,
|
fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK,
|
||||||
)
|
)
|
||||||
blocks_2[500, 0] = Cell.SAND
|
blocks_2[500, 0] = Cell.SAND
|
||||||
self.print_blocks(blocks_2)
|
self.print_blocks("part2", blocks_2)
|
||||||
yield sum(v == Cell.SAND for v in blocks_2.values())
|
yield sum(v == Cell.SAND for v in blocks_2.values())
|
||||||
|
@ -83,18 +83,17 @@ class Solver(BaseSolver):
|
|||||||
if (i, j) in loop_s and lines[i][j] in "|LJ":
|
if (i, j) in loop_s and lines[i][j] in "|LJ":
|
||||||
cnt += 1
|
cnt += 1
|
||||||
|
|
||||||
if self.verbose:
|
if self.files:
|
||||||
for i in range(len(lines)):
|
rows = [["." for _j in range(len(lines[0]))] for _i in range(len(lines))]
|
||||||
s = ""
|
rows[si][sj] = "\033[91mS\033[0m"
|
||||||
for j in range(len(lines[0])):
|
|
||||||
if (i, j) == (si, sj):
|
for i, j in loop:
|
||||||
s += "\033[91mS\033[0m"
|
rows[i][j] = lines[i][j]
|
||||||
elif (i, j) in loop:
|
for i, j in inside:
|
||||||
s += lines[i][j]
|
rows[i][j] = "\033[92mI\033[0m"
|
||||||
elif (i, j) in inside:
|
|
||||||
s += "\033[92mI\033[0m"
|
self.files.create(
|
||||||
else:
|
"output.txt", "\n".join("".join(row) for row in rows).encode(), True
|
||||||
s += "."
|
)
|
||||||
self.logger.info(s)
|
|
||||||
|
|
||||||
yield len(inside)
|
yield len(inside)
|
||||||
|
@ -84,9 +84,14 @@ class Solver(BaseSolver):
|
|||||||
|
|
||||||
beams = propagate(layout, (0, 0), "R")
|
beams = propagate(layout, (0, 0), "R")
|
||||||
|
|
||||||
if self.verbose:
|
if self.files:
|
||||||
for row in beams:
|
self.files.create(
|
||||||
self.logger.info("".join("#" if col else "." for col in row))
|
"beams.txt",
|
||||||
|
"\n".join(
|
||||||
|
"".join("#" if col else "." for col in row) for row in beams
|
||||||
|
).encode(),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
# part 1
|
# part 1
|
||||||
yield sum(sum(map(bool, row)) for row in beams)
|
yield sum(sum(map(bool, row)) for row in beams)
|
||||||
|
@ -33,10 +33,14 @@ MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
|
|||||||
class Solver(BaseSolver):
|
class Solver(BaseSolver):
|
||||||
def print_shortest_path(
|
def print_shortest_path(
|
||||||
self,
|
self,
|
||||||
|
name: str,
|
||||||
grid: list[list[int]],
|
grid: list[list[int]],
|
||||||
target: tuple[int, int],
|
target: tuple[int, int],
|
||||||
per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
|
per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
|
||||||
):
|
):
|
||||||
|
if not self.files:
|
||||||
|
return
|
||||||
|
|
||||||
assert len(per_cell[target]) == 1
|
assert len(per_cell[target]) == 1
|
||||||
label = per_cell[target][0][0]
|
label = per_cell[target][0][0]
|
||||||
|
|
||||||
@ -74,8 +78,9 @@ class Solver(BaseSolver):
|
|||||||
|
|
||||||
p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m"
|
p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m"
|
||||||
|
|
||||||
for row in p_grid:
|
self.files.create(
|
||||||
self.logger.info("".join(row))
|
name, "\n".join("".join(row) for row in p_grid).encode(), True
|
||||||
|
)
|
||||||
|
|
||||||
def shortest_many_paths(self, grid: list[list[int]]) -> dict[tuple[int, int], int]:
|
def shortest_many_paths(self, grid: list[list[int]]) -> dict[tuple[int, int], int]:
|
||||||
n_rows, n_cols = len(grid), len(grid[0])
|
n_rows, n_cols = len(grid), len(grid[0])
|
||||||
@ -129,6 +134,7 @@ class Solver(BaseSolver):
|
|||||||
|
|
||||||
def shortest_path(
|
def shortest_path(
|
||||||
self,
|
self,
|
||||||
|
name: str,
|
||||||
grid: list[list[int]],
|
grid: list[list[int]],
|
||||||
min_straight: int,
|
min_straight: int,
|
||||||
max_straight: int,
|
max_straight: int,
|
||||||
@ -217,8 +223,7 @@ class Solver(BaseSolver):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.verbose:
|
self.print_shortest_path(f"shortest-path_{name}.txt", grid, target, per_cell)
|
||||||
self.print_shortest_path(grid, target, per_cell)
|
|
||||||
|
|
||||||
return per_cell[target][0][1]
|
return per_cell[target][0][1]
|
||||||
|
|
||||||
@ -227,7 +232,7 @@ class Solver(BaseSolver):
|
|||||||
estimates = self.shortest_many_paths(data)
|
estimates = self.shortest_many_paths(data)
|
||||||
|
|
||||||
# part 1
|
# part 1
|
||||||
yield self.shortest_path(data, 1, 3, lower_bounds=estimates)
|
yield self.shortest_path("answer_1", data, 1, 3, lower_bounds=estimates)
|
||||||
|
|
||||||
# part 2
|
# part 2
|
||||||
yield self.shortest_path(data, 4, 10, lower_bounds=estimates)
|
yield self.shortest_path("answer_2", data, 4, 10, lower_bounds=estimates)
|
||||||
|
@ -80,10 +80,9 @@ class Solver(BaseSolver):
|
|||||||
outputs,
|
outputs,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.outputs:
|
if self.files:
|
||||||
with open("./day20.dot", "w") as fp:
|
contents = "digraph G {\n"
|
||||||
fp.write("digraph G {\n")
|
contents += "rx [shape=circle, color=red, style=filled];\n"
|
||||||
fp.write("rx [shape=circle, color=red, style=filled];\n")
|
|
||||||
for name, (type, outputs) in self._modules.items():
|
for name, (type, outputs) in self._modules.items():
|
||||||
if type == "conjunction":
|
if type == "conjunction":
|
||||||
shape = "diamond"
|
shape = "diamond"
|
||||||
@ -91,11 +90,13 @@ class Solver(BaseSolver):
|
|||||||
shape = "box"
|
shape = "box"
|
||||||
else:
|
else:
|
||||||
shape = "circle"
|
shape = "circle"
|
||||||
fp.write(f"{name} [shape={shape}];\n")
|
contents += f"{name} [shape={shape}];\n"
|
||||||
for name, (type, outputs) in self._modules.items():
|
for name, (type, outputs) in self._modules.items():
|
||||||
for output in outputs:
|
for output in outputs:
|
||||||
fp.write(f"{name} -> {output};\n")
|
contents += f"{name} -> {output};\n"
|
||||||
fp.write("}\n")
|
contents += "}\n"
|
||||||
|
|
||||||
|
self.files.create("day20.dot", contents.encode(), False)
|
||||||
|
|
||||||
# part 1
|
# part 1
|
||||||
flip_flop_states: dict[str, Literal["on", "off"]] = {
|
flip_flop_states: dict[str, Literal["on", "off"]] = {
|
||||||
|
@ -50,7 +50,7 @@ class Solver(BaseSolver):
|
|||||||
values.append(len(tiles := reachable(map, tiles, cycle)))
|
values.append(len(tiles := reachable(map, tiles, cycle)))
|
||||||
values.append(len(tiles := reachable(map, tiles, cycle)))
|
values.append(len(tiles := reachable(map, tiles, cycle)))
|
||||||
|
|
||||||
if self.verbose:
|
if self.files:
|
||||||
n_rows, n_cols = len(map), len(map[0])
|
n_rows, n_cols = len(map), len(map[0])
|
||||||
|
|
||||||
rows = [
|
rows = [
|
||||||
@ -66,8 +66,9 @@ class Solver(BaseSolver):
|
|||||||
if (i // cycle) % 2 == (j // cycle) % 2:
|
if (i // cycle) % 2 == (j // cycle) % 2:
|
||||||
rows[i][j] = f"\033[91m{rows[i][j]}\033[0m"
|
rows[i][j] = f"\033[91m{rows[i][j]}\033[0m"
|
||||||
|
|
||||||
for row in rows:
|
self.files.create(
|
||||||
self.logger.info("".join(row))
|
"cycle.txt", "\n".join("".join(row) for row in rows).encode(), True
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"values to fit: {values}")
|
self.logger.info(f"values to fit: {values}")
|
||||||
|
|
||||||
|
@ -1,107 +1,15 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable, Iterator, Literal, Sequence, TextIO, TypeVar
|
|
||||||
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from .base import BaseSolver
|
from .base import BaseSolver
|
||||||
|
from .utils.api import FileHandlerAPI, LoggerAPIHandler, ProgressAPI, dump_answer
|
||||||
_T = TypeVar("_T")
|
from .utils.files import SimpleFileHandler
|
||||||
|
from .utils.progress import ProgressNone, ProgressTQDM
|
||||||
|
|
||||||
def dump_api_message(
|
|
||||||
type: Literal["log", "answer", "progress-start", "progress-step", "progress-end"],
|
|
||||||
content: Any,
|
|
||||||
file: TextIO = sys.stdout,
|
|
||||||
):
|
|
||||||
print(
|
|
||||||
json.dumps(
|
|
||||||
{"type": type, "time": datetime.now().isoformat(), "content": content}
|
|
||||||
),
|
|
||||||
flush=True,
|
|
||||||
file=file,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LoggerAPIHandler(logging.Handler):
|
|
||||||
def __init__(self, output: TextIO = sys.stdout):
|
|
||||||
super().__init__()
|
|
||||||
self.output = output
|
|
||||||
|
|
||||||
def emit(self, record: logging.LogRecord):
|
|
||||||
dump_api_message(
|
|
||||||
"log", {"level": record.levelname, "message": record.getMessage()}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProgressAPI:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
min_step: int = 1,
|
|
||||||
min_time: timedelta = timedelta(milliseconds=100),
|
|
||||||
output: TextIO = sys.stdout,
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.counter = 0
|
|
||||||
self.output = output
|
|
||||||
self.min_step = min_step
|
|
||||||
self.min_time = min_time
|
|
||||||
|
|
||||||
def wrap(
|
|
||||||
self, values: Sequence[_T] | Iterable[_T], total: int | None = None
|
|
||||||
) -> Iterator[_T]:
|
|
||||||
total = total or len(values) # type: ignore
|
|
||||||
|
|
||||||
current = self.counter
|
|
||||||
self.counter += 1
|
|
||||||
|
|
||||||
dump_api_message("progress-start", {"counter": current, "total": total})
|
|
||||||
|
|
||||||
try:
|
|
||||||
percent = 0
|
|
||||||
time = datetime.now()
|
|
||||||
|
|
||||||
for i_value, value in enumerate(values):
|
|
||||||
yield value
|
|
||||||
|
|
||||||
if datetime.now() - time < self.min_time:
|
|
||||||
continue
|
|
||||||
|
|
||||||
time = datetime.now()
|
|
||||||
|
|
||||||
c_percent = round(i_value / total * 100)
|
|
||||||
|
|
||||||
if c_percent >= percent + self.min_step:
|
|
||||||
dump_api_message(
|
|
||||||
"progress-step", {"counter": current, "percent": c_percent}
|
|
||||||
)
|
|
||||||
percent = c_percent
|
|
||||||
finally:
|
|
||||||
dump_api_message(
|
|
||||||
"progress-end",
|
|
||||||
{"counter": current},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProgressTQDM:
|
|
||||||
def wrap(
|
|
||||||
self, values: Sequence[_T] | Iterable[_T], total: int | None = None
|
|
||||||
) -> Iterator[_T]:
|
|
||||||
return iter(tqdm(values, total=total))
|
|
||||||
|
|
||||||
|
|
||||||
class ProgressNone:
|
|
||||||
def wrap(
|
|
||||||
self, values: Sequence[_T] | Iterable[_T], total: int | None = None
|
|
||||||
) -> Iterator[_T]:
|
|
||||||
return iter(values)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -109,6 +17,13 @@ def main():
|
|||||||
parser.add_argument("-v", "--verbose", action="store_true", help="verbose mode")
|
parser.add_argument("-v", "--verbose", action="store_true", help="verbose mode")
|
||||||
parser.add_argument("-t", "--test", action="store_true", help="test mode")
|
parser.add_argument("-t", "--test", action="store_true", help="test mode")
|
||||||
parser.add_argument("-a", "--api", action="store_true", help="API mode")
|
parser.add_argument("-a", "--api", action="store_true", help="API mode")
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
type=Path,
|
||||||
|
default=Path("files"),
|
||||||
|
help="output folder for created files",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-u", "--user", type=str, default="holt59", help="user input to use"
|
"-u", "--user", type=str, default="holt59", help="user input to use"
|
||||||
)
|
)
|
||||||
@ -135,12 +50,12 @@ def main():
|
|||||||
test: bool = args.test
|
test: bool = args.test
|
||||||
stdin: bool = args.stdin
|
stdin: bool = args.stdin
|
||||||
user: str = args.user
|
user: str = args.user
|
||||||
|
files_output: Path = args.output
|
||||||
input_path: Path | None = args.input
|
input_path: Path | None = args.input
|
||||||
|
|
||||||
year: int = args.year
|
year: int = args.year
|
||||||
day: int = args.day
|
day: int = args.day
|
||||||
|
|
||||||
# TODO: change this
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO if verbose or api else logging.WARNING,
|
level=logging.INFO if verbose or api else logging.WARNING,
|
||||||
handlers=[LoggerAPIHandler()] if api else None,
|
handlers=[LoggerAPIHandler()] if api else None,
|
||||||
@ -166,7 +81,11 @@ def main():
|
|||||||
else ProgressTQDM()
|
else ProgressTQDM()
|
||||||
if verbose
|
if verbose
|
||||||
else ProgressNone(), # type: ignore
|
else ProgressNone(), # type: ignore
|
||||||
outputs=not api,
|
files=FileHandlerAPI(files_output)
|
||||||
|
if api
|
||||||
|
else SimpleFileHandler(logging.getLogger("AOC"), files_output)
|
||||||
|
if verbose
|
||||||
|
else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
data: str
|
data: str
|
||||||
@ -189,14 +108,11 @@ def main():
|
|||||||
current = datetime.now()
|
current = datetime.now()
|
||||||
|
|
||||||
if api:
|
if api:
|
||||||
dump_api_message(
|
dump_answer(
|
||||||
"answer",
|
part=i_answer + 1,
|
||||||
{
|
answer=answer,
|
||||||
"answer": i_answer + 1,
|
answer_time=current - last,
|
||||||
"value": str(answer),
|
total_time=current - start,
|
||||||
"answerTime_s": (current - last).total_seconds(),
|
|
||||||
"totalTime_s": (current - start).total_seconds(),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
|
@ -13,6 +13,10 @@ class ProgressHandler(Protocol):
|
|||||||
def wrap(self, values: Iterable[_T], total: int) -> Iterator[_T]: ...
|
def wrap(self, values: Iterable[_T], total: int) -> Iterator[_T]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class FileHandler(Protocol):
|
||||||
|
def create(self, filename: str, content: bytes, text: bool = False): ...
|
||||||
|
|
||||||
|
|
||||||
class BaseSolver:
|
class BaseSolver:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -21,14 +25,14 @@ class BaseSolver:
|
|||||||
year: int,
|
year: int,
|
||||||
day: int,
|
day: int,
|
||||||
progress: ProgressHandler,
|
progress: ProgressHandler,
|
||||||
outputs: bool = False,
|
files: FileHandler | None = None,
|
||||||
):
|
):
|
||||||
self.logger: Final = logger
|
self.logger: Final = logger
|
||||||
self.verbose: Final = verbose
|
self.verbose: Final = verbose
|
||||||
self.year: Final = year
|
self.year: Final = year
|
||||||
self.day: Final = day
|
self.day: Final = day
|
||||||
self.progress: Final = progress
|
self.progress: Final = progress
|
||||||
self.outputs = outputs
|
self.files: Final = files
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def solve(self, input: str) -> Iterator[Any] | None: ...
|
def solve(self, input: str) -> Iterator[Any] | None: ...
|
||||||
|
0
src/holt59/aoc/utils/__init__.py
Normal file
0
src/holt59/aoc/utils/__init__.py
Normal file
13
src/holt59/aoc/utils/api/__init__.py
Normal file
13
src/holt59/aoc/utils/api/__init__.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from .answer import dump_answer
|
||||||
|
from .base import dump_api_message
|
||||||
|
from .files import FileHandlerAPI
|
||||||
|
from .logger import LoggerAPIHandler
|
||||||
|
from .progress import ProgressAPI
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"dump_answer",
|
||||||
|
"dump_api_message",
|
||||||
|
"FileHandlerAPI",
|
||||||
|
"LoggerAPIHandler",
|
||||||
|
"ProgressAPI",
|
||||||
|
]
|
16
src/holt59/aoc/utils/api/answer.py
Normal file
16
src/holt59/aoc/utils/api/answer.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .base import dump_api_message
|
||||||
|
|
||||||
|
|
||||||
|
def dump_answer(part: int, answer: Any, answer_time: timedelta, total_time: timedelta):
|
||||||
|
dump_api_message(
|
||||||
|
"answer",
|
||||||
|
{
|
||||||
|
"answer": part,
|
||||||
|
"value": str(answer),
|
||||||
|
"answerTime_s": answer_time.total_seconds(),
|
||||||
|
"totalTime_s": total_time.total_seconds(),
|
||||||
|
},
|
||||||
|
)
|
28
src/holt59/aoc/utils/api/base.py
Normal file
28
src/holt59/aoc/utils/api/base.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Literal, TextIO
|
||||||
|
|
||||||
|
|
||||||
|
def _datetime_formatter(value: Any) -> Any:
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.isoformat()
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def dump_api_message(
|
||||||
|
type: Literal[
|
||||||
|
"log", "answer", "file", "progress-start", "progress-step", "progress-end"
|
||||||
|
],
|
||||||
|
content: Any,
|
||||||
|
file: TextIO = sys.stdout,
|
||||||
|
):
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{"type": type, "time": datetime.now(), "content": content},
|
||||||
|
default=_datetime_formatter,
|
||||||
|
),
|
||||||
|
flush=True,
|
||||||
|
file=file,
|
||||||
|
)
|
15
src/holt59/aoc/utils/api/files.py
Normal file
15
src/holt59/aoc/utils/api/files.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from .base import dump_api_message
|
||||||
|
|
||||||
|
|
||||||
|
class FileHandlerAPI:
|
||||||
|
def __init__(self, folder: Path):
|
||||||
|
self.folder: Final = folder
|
||||||
|
|
||||||
|
def create(self, filename: str, content: bytes, text: bool = False):
|
||||||
|
self.folder.mkdir(exist_ok=True)
|
||||||
|
with open(self.folder.joinpath(filename), "wb") as fp:
|
||||||
|
fp.write(content)
|
||||||
|
dump_api_message("file", {"filename": filename, "size": len(content)})
|
16
src/holt59/aoc/utils/api/logger.py
Normal file
16
src/holt59/aoc/utils/api/logger.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import logging.handlers
|
||||||
|
import sys
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
from .base import dump_api_message
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerAPIHandler(logging.Handler):
|
||||||
|
def __init__(self, output: TextIO = sys.stdout):
|
||||||
|
super().__init__()
|
||||||
|
self.output = output
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord):
|
||||||
|
dump_api_message(
|
||||||
|
"log", {"level": record.levelname, "message": record.getMessage()}
|
||||||
|
)
|
57
src/holt59/aoc/utils/api/progress.py
Normal file
57
src/holt59/aoc/utils/api/progress.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Iterable, Iterator, Sequence, TextIO, TypeVar
|
||||||
|
|
||||||
|
from .base import dump_api_message
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressAPI:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
min_step: int = 1,
|
||||||
|
min_time: timedelta = timedelta(milliseconds=100),
|
||||||
|
output: TextIO = sys.stdout,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.counter = 0
|
||||||
|
self.output = output
|
||||||
|
self.min_step = min_step
|
||||||
|
self.min_time = min_time
|
||||||
|
|
||||||
|
def wrap(
|
||||||
|
self, values: Sequence[_T] | Iterable[_T], total: int | None = None
|
||||||
|
) -> Iterator[_T]:
|
||||||
|
total = total or len(values) # type: ignore
|
||||||
|
|
||||||
|
current = self.counter
|
||||||
|
self.counter += 1
|
||||||
|
|
||||||
|
dump_api_message("progress-start", {"counter": current, "total": total})
|
||||||
|
|
||||||
|
try:
|
||||||
|
percent = 0
|
||||||
|
time = datetime.now()
|
||||||
|
|
||||||
|
for i_value, value in enumerate(values):
|
||||||
|
yield value
|
||||||
|
|
||||||
|
if datetime.now() - time < self.min_time:
|
||||||
|
continue
|
||||||
|
|
||||||
|
time = datetime.now()
|
||||||
|
|
||||||
|
c_percent = round(i_value / total * 100)
|
||||||
|
|
||||||
|
if c_percent >= percent + self.min_step:
|
||||||
|
dump_api_message(
|
||||||
|
"progress-step", {"counter": current, "percent": c_percent}
|
||||||
|
)
|
||||||
|
percent = c_percent
|
||||||
|
finally:
|
||||||
|
dump_api_message(
|
||||||
|
"progress-end",
|
||||||
|
{"counter": current},
|
||||||
|
)
|
18
src/holt59/aoc/utils/files.py
Normal file
18
src/holt59/aoc/utils/files.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleFileHandler:
|
||||||
|
def __init__(self, logger: logging.Logger, folder: Path):
|
||||||
|
self.logger: Final = logger
|
||||||
|
self.folder: Final = folder
|
||||||
|
|
||||||
|
def create(self, filename: str, content: bytes, text: bool = False):
|
||||||
|
if text:
|
||||||
|
for line in content.decode("utf-8").splitlines():
|
||||||
|
self.logger.info(line)
|
||||||
|
else:
|
||||||
|
self.folder.mkdir(exist_ok=True)
|
||||||
|
with open(self.folder.joinpath(filename), "wb") as fp:
|
||||||
|
fp.write(content)
|
19
src/holt59/aoc/utils/progress.py
Normal file
19
src/holt59/aoc/utils/progress.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from typing import Iterable, Iterator, Sequence, TypeVar
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressTQDM:
|
||||||
|
def wrap(
|
||||||
|
self, values: Sequence[_T] | Iterable[_T], total: int | None = None
|
||||||
|
) -> Iterator[_T]:
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
return iter(tqdm(values, total=total))
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressNone:
|
||||||
|
def wrap(
|
||||||
|
self, values: Sequence[_T] | Iterable[_T], total: int | None = None
|
||||||
|
) -> Iterator[_T]:
|
||||||
|
return iter(values)
|
Loading…
Reference in New Issue
Block a user