puzzleshelf 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.3
2
+ Name: puzzleshelf
3
+ Version: 0.0.1
4
+ Summary: A code puzzle solving framework
5
+ Author: Sean Malloy
6
+ Author-email: Sean Malloy <sfmalloy.dev@gmail.com>
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # PuzzleShelf
12
+
13
+ Docs coming soon.
14
+
15
+ Note on connectors that have the ability to download inputs: none of them are official in any way, and require you to provide your own user-agent.
@@ -0,0 +1,5 @@
1
+ # PuzzleShelf
2
+
3
+ Docs coming soon.
4
+
5
+ Note on connectors that have the ability to download inputs: none of them are official in any way, and require you to provide your own user-agent.
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "puzzleshelf"
3
+ version = "0.0.1"
4
+ description = "A code puzzle solving framework"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Sean Malloy", email = "sfmalloy.dev@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "httpx>=0.28.1",
12
+ ]
13
+
14
+ [tool.ruff]
15
+ line-length = 120
16
+
17
+ [tool.ruff.format]
18
+ quote-style = 'single'
19
+
20
+ [tool.ruff.lint]
21
+ select = ["B", "E", "F", "I"]
22
+ ignore = ["E501"]
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.9.7,<0.12.0"]
26
+ build-backend = "uv_build"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "ruff>=0.15.21",
31
+ ]
File without changes
@@ -0,0 +1,3 @@
1
+ from .connector import Connector as Connector
2
+ from .flipflop_codes import FlipFlopCodesConnector as FlipFlopCodesConnector
3
+ from .offline import OfflineConnector as OfflineConnector
@@ -0,0 +1,39 @@
1
+ from abc import ABCMeta, abstractmethod
2
+ from os import PathLike
3
+ from pathlib import Path
4
+
5
+ from ..types import Id
6
+
7
+
8
+ class Connector(metaclass=ABCMeta):
9
+ input_dir: Path
10
+ name: str
11
+ session_token: str | None
12
+ user_agent: str | None
13
+
14
+ def __init__(
15
+ self,
16
+ *,
17
+ name: str,
18
+ input_dir: str | PathLike,
19
+ session_token: str | None = None,
20
+ user_agent: str | None = None,
21
+ ):
22
+ self.name = name
23
+ self.session_token = session_token
24
+ self.user_agent = user_agent
25
+ self.input_dir = Path(input_dir)
26
+
27
+ @abstractmethod
28
+ def download_input(self, *, event_id: Id, problem_id: Id): ...
29
+
30
+ @abstractmethod
31
+ def read_input(self, *, problem_id: Id, part: int | None = None): ...
32
+
33
+ def _build_filepath(self, event_id: Id, problem_id: Id, part: int) -> Path:
34
+ filepath = self.input_dir / self.name / str(event_id)
35
+ if part:
36
+ filepath = filepath / str(problem_id) / f'{part}.txt'
37
+ else:
38
+ filepath = filepath / f'{problem_id}.txt'
39
+ return filepath
@@ -0,0 +1,36 @@
1
+ import os
2
+ from io import TextIOWrapper
3
+ from os import PathLike
4
+
5
+ import httpx
6
+
7
+ from ..types import Id
8
+ from .connector import Connector
9
+
10
+ BASE_URL = 'https://flipflop.slome.org'
11
+
12
+
13
+ class FlipFlopCodesConnector(Connector):
14
+ def __init__(self, *, year: int, user_agent: str, input_dir: str | PathLike):
15
+ super().__init__(session_token=os.getenv('FLIPFLOP_CODES_SESSION'), user_agent=user_agent, input_dir=input_dir)
16
+ self._year = year
17
+
18
+ def download_input(self, *, problem_id: Id, part: int | None = None):
19
+ """
20
+ Download input file if it does not exist already.
21
+ """
22
+ path = self._build_filepath(self._year, problem_id, part)
23
+ if path.exists():
24
+ return
25
+
26
+ res = httpx.get(
27
+ f'{BASE_URL}/{self._year}/{problem_id}/input',
28
+ headers={'PHPSESSID': self.session_token, 'User-Agent': self.user_agent},
29
+ )
30
+ if res.text == 'You must be logged in to view the input!':
31
+ raise RuntimeError('Unable to download input: double check session token.')
32
+ with open(path, 'w') as f:
33
+ f.write(res.text)
34
+
35
+ def read_input(self, *, problem_id: Id, part: int | None = None) -> TextIOWrapper:
36
+ return open(self._build_filepath(self._year, problem_id, part))
@@ -0,0 +1,15 @@
1
+ from os import PathLike
2
+
3
+ from .connector import Connector
4
+
5
+
6
+ class OfflineConnector(Connector):
7
+ def __init__(self, *, input_dir: str | PathLike):
8
+ super().__init__(name='flipflop-codes', input_dir=input_dir)
9
+
10
+ def download_input(self, *, event_id, problem_id):
11
+ """Does nothing on purpose"""
12
+ return
13
+
14
+ def read_input(self, *, problem_id, part=None):
15
+ return open(self._build_filepath(problem_id, part))
@@ -0,0 +1,67 @@
1
+ from os import PathLike
2
+ from pathlib import Path
3
+
4
+ from .connectors import Connector, OfflineConnector
5
+ from .types import Id, ParserFunction, SolverFunction
6
+
7
+
8
+ class PuzzleShelf:
9
+ _name: str
10
+ _input_dir: Path
11
+ _connector: Connector | None
12
+
13
+ _parsers: dict[Id, ParserFunction]
14
+ _solvers: dict[tuple[Id, Id], SolverFunction]
15
+
16
+ def __init__(
17
+ self,
18
+ *,
19
+ name: str,
20
+ input_dir: str | PathLike = Path('./inputs'),
21
+ connector: Connector | None = None,
22
+ ):
23
+ """
24
+ Object that stores references to solver and parser functions for a given puzzle event.
25
+
26
+ :name: Name of this shelf
27
+ :event_id: Typically the year the event takes place
28
+ :input_dir: Directory to store input files
29
+ """
30
+ self._name = name
31
+ self._input_dir = Path(str(input_dir))
32
+ self._connector = connector if connector else OfflineConnector
33
+ self._parsers = dict()
34
+ self._solvers = dict()
35
+
36
+ def _load_input_file(self, id: Id):
37
+ path = self._input_dir / f'input_{id}.txt'
38
+ if not path.exists():
39
+ raise RuntimeError(f'Input path does not exist: {path}')
40
+ return open(path)
41
+
42
+ def parser(self, id: Id):
43
+ def decorator(fn: ParserFunction):
44
+ self._parsers[id] = fn
45
+
46
+ return decorator
47
+
48
+ def solver(self, id: Id, *, part: Id | None = None):
49
+ def decorator(fn: SolverFunction):
50
+ self._solvers[(id, part)] = fn
51
+
52
+ return decorator
53
+
54
+ def run(self, id: Id, *, part: Id | None = None):
55
+ file = self._connector.read_input(problem_id=id, part=part)
56
+ parse = self._parsers.get(id)
57
+ parsed_input = file if not parse else parse(file)
58
+ if not isinstance(parsed_input, tuple):
59
+ parsed_input = (parsed_input,)
60
+
61
+ solve = self._solvers.get((id, part))
62
+ if not solve:
63
+ raise RuntimeError(f'Solver does not exist: {id}{f" {part}" if part else ""}') from None
64
+
65
+ result = solve(*parsed_input)
66
+ file.close()
67
+ return result
File without changes
@@ -0,0 +1,6 @@
1
+ from io import TextIOWrapper
2
+ from typing import Any, Callable
3
+
4
+ Id = str | int
5
+ ParserFunction = Callable[[TextIOWrapper], Any]
6
+ SolverFunction = Callable[[tuple[Any, ...]], Any]