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.
- puzzleshelf-0.0.1/PKG-INFO +15 -0
- puzzleshelf-0.0.1/README.md +5 -0
- puzzleshelf-0.0.1/pyproject.toml +31 -0
- puzzleshelf-0.0.1/src/puzzleshelf/__init__.py +0 -0
- puzzleshelf-0.0.1/src/puzzleshelf/connectors/__init__.py +3 -0
- puzzleshelf-0.0.1/src/puzzleshelf/connectors/connector.py +39 -0
- puzzleshelf-0.0.1/src/puzzleshelf/connectors/flipflop_codes.py +36 -0
- puzzleshelf-0.0.1/src/puzzleshelf/connectors/offline.py +15 -0
- puzzleshelf-0.0.1/src/puzzleshelf/puzzleshelf.py +67 -0
- puzzleshelf-0.0.1/src/puzzleshelf/py.typed +0 -0
- puzzleshelf-0.0.1/src/puzzleshelf/types.py +6 -0
|
@@ -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,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,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
|