python-ecd 0.1.9__tar.gz → 0.2.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.
Potentially problematic release.
This version of python-ecd might be problematic. Click here for more details.
- {python_ecd-0.1.9 → python_ecd-0.2.1}/PKG-INFO +19 -5
- {python_ecd-0.1.9 → python_ecd-0.2.1}/README.md +17 -3
- {python_ecd-0.1.9 → python_ecd-0.2.1}/pyproject.toml +5 -2
- {python_ecd-0.1.9 → python_ecd-0.2.1}/src/python_ecd/cli.py +98 -25
- {python_ecd-0.1.9 → python_ecd-0.2.1}/src/python_ecd/templates/README.md.tpl +5 -2
- {python_ecd-0.1.9 → python_ecd-0.2.1}/src/python_ecd/utils.py +89 -12
- {python_ecd-0.1.9 → python_ecd-0.2.1}/src/python_ecd/__init__.py +0 -0
- {python_ecd-0.1.9 → python_ecd-0.2.1}/src/python_ecd/templates/.gitignore.tpl +0 -0
- {python_ecd-0.1.9 → python_ecd-0.2.1}/src/python_ecd/templates/solution.py.tpl +0 -0
- {python_ecd-0.1.9 → python_ecd-0.2.1}/src/python_ecd/version.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-ecd
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Python CLI for Everybody Codes
|
|
5
5
|
Author: Pablo Garcia
|
|
6
6
|
Author-email: Pablo Garcia <pablofueros@gmail.com>
|
|
7
7
|
Classifier: Programming Language :: Python :: 3.13
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
-
Requires-Dist: everybody-codes-data==0.
|
|
9
|
+
Requires-Dist: everybody-codes-data==0.3
|
|
10
10
|
Requires-Dist: typer>=0.20.0
|
|
11
11
|
Requires-Python: >=3.13
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
@@ -84,10 +84,10 @@ Note that is not necessary if you set it during initialization.
|
|
|
84
84
|
|
|
85
85
|
### Download Puzzle Input
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Download the input for a specific puzzle:
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
ecd
|
|
90
|
+
ecd pull <QUEST_NUMBER> [OPTIONS]
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Options:
|
|
@@ -95,6 +95,8 @@ Options:
|
|
|
95
95
|
- `--part`, `-p`: Puzzle part (default: 1)
|
|
96
96
|
- `--force`, `-f`: Overwrite existing files
|
|
97
97
|
|
|
98
|
+
This command will create the necessary dirs and files if they do not exist.
|
|
99
|
+
|
|
98
100
|
### Run Solutions
|
|
99
101
|
|
|
100
102
|
Execute your solution for a specific puzzle:
|
|
@@ -119,6 +121,18 @@ Options:
|
|
|
119
121
|
- `--year`: Event year (default: actual)
|
|
120
122
|
- `--part`: Part number to test (default: 1)
|
|
121
123
|
|
|
124
|
+
### Submit Solutions
|
|
125
|
+
|
|
126
|
+
Submit your solution for a specific puzzle:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
ecd push <QUEST_NUMBER> [OPTIONS]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Options:
|
|
133
|
+
- `--year`: Event year (default: actual)
|
|
134
|
+
- `--part`: Part number to test (default: 1)
|
|
135
|
+
|
|
122
136
|
### Display Version
|
|
123
137
|
|
|
124
138
|
Show the current version of the tool:
|
|
@@ -129,7 +143,7 @@ ecd --version
|
|
|
129
143
|
|
|
130
144
|
## ©️ License
|
|
131
145
|
|
|
132
|
-
|
|
146
|
+
This project is licensed under the terms of the MIT license.
|
|
133
147
|
|
|
134
148
|
## 🤝 Contributing
|
|
135
149
|
|
|
@@ -71,10 +71,10 @@ Note that is not necessary if you set it during initialization.
|
|
|
71
71
|
|
|
72
72
|
### Download Puzzle Input
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
Download the input for a specific puzzle:
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
|
-
ecd
|
|
77
|
+
ecd pull <QUEST_NUMBER> [OPTIONS]
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
Options:
|
|
@@ -82,6 +82,8 @@ Options:
|
|
|
82
82
|
- `--part`, `-p`: Puzzle part (default: 1)
|
|
83
83
|
- `--force`, `-f`: Overwrite existing files
|
|
84
84
|
|
|
85
|
+
This command will create the necessary dirs and files if they do not exist.
|
|
86
|
+
|
|
85
87
|
### Run Solutions
|
|
86
88
|
|
|
87
89
|
Execute your solution for a specific puzzle:
|
|
@@ -106,6 +108,18 @@ Options:
|
|
|
106
108
|
- `--year`: Event year (default: actual)
|
|
107
109
|
- `--part`: Part number to test (default: 1)
|
|
108
110
|
|
|
111
|
+
### Submit Solutions
|
|
112
|
+
|
|
113
|
+
Submit your solution for a specific puzzle:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
ecd push <QUEST_NUMBER> [OPTIONS]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Options:
|
|
120
|
+
- `--year`: Event year (default: actual)
|
|
121
|
+
- `--part`: Part number to test (default: 1)
|
|
122
|
+
|
|
109
123
|
### Display Version
|
|
110
124
|
|
|
111
125
|
Show the current version of the tool:
|
|
@@ -116,7 +130,7 @@ ecd --version
|
|
|
116
130
|
|
|
117
131
|
## ©️ License
|
|
118
132
|
|
|
119
|
-
|
|
133
|
+
This project is licensed under the terms of the MIT license.
|
|
120
134
|
|
|
121
135
|
## 🤝 Contributing
|
|
122
136
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-ecd"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Python CLI for Everybody Codes"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
classifiers = [
|
|
@@ -9,7 +9,10 @@ classifiers = [
|
|
|
9
9
|
]
|
|
10
10
|
authors = [{ name = "Pablo Garcia", email = "pablofueros@gmail.com" }]
|
|
11
11
|
requires-python = ">=3.13"
|
|
12
|
-
dependencies = [
|
|
12
|
+
dependencies = [
|
|
13
|
+
"everybody-codes-data==0.3",
|
|
14
|
+
"typer>=0.20.0",
|
|
15
|
+
]
|
|
13
16
|
|
|
14
17
|
[project.scripts]
|
|
15
18
|
python-ecd = "python_ecd.cli:app"
|
|
@@ -85,14 +85,13 @@ def set_token_cmd(
|
|
|
85
85
|
typer.echo("🔑 Session token saved to /home/.config/ecd/token")
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
@app.command("
|
|
89
|
-
def
|
|
88
|
+
@app.command("pull")
|
|
89
|
+
def pull_cmd(
|
|
90
90
|
quest: int = typer.Argument(..., help="Quest number (e.g. 3)"),
|
|
91
91
|
year: int = typer.Option(datetime.now().year, "--year", "-y", help="Event year"),
|
|
92
|
-
part: int = typer.Option(1, "--part", "-p", help="Puzzle part (1, 2, or 3)"),
|
|
93
92
|
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
94
93
|
) -> None:
|
|
95
|
-
"""Download input data for a given quest and create local structure."""
|
|
94
|
+
"""Download available input data for a given quest and create local structure."""
|
|
96
95
|
|
|
97
96
|
# 0. Locate base directory
|
|
98
97
|
base_dir = utils.find_base()
|
|
@@ -106,10 +105,9 @@ def get_cmd(
|
|
|
106
105
|
|
|
107
106
|
# 2. Download the input for the given part
|
|
108
107
|
try:
|
|
109
|
-
|
|
108
|
+
input_dict = utils.download_input(year, quest)
|
|
110
109
|
except Exception as e:
|
|
111
110
|
typer.echo(f"❌ Failed to fetch input: {e}")
|
|
112
|
-
typer.echo("ℹ️ Maybe ")
|
|
113
111
|
raise typer.Exit(1)
|
|
114
112
|
|
|
115
113
|
# 3. Prepare directory structure
|
|
@@ -118,29 +116,34 @@ def get_cmd(
|
|
|
118
116
|
# 4. Create solution.py if missing
|
|
119
117
|
solution_file = quest_dir / "solution.py"
|
|
120
118
|
if solution_file.exists() and not force:
|
|
121
|
-
typer.echo(
|
|
119
|
+
typer.echo(
|
|
120
|
+
"⚠️ The solution python file already exists (use --force to overwrite)."
|
|
121
|
+
)
|
|
122
122
|
else:
|
|
123
123
|
utils.create_solution(quest_dir, force)
|
|
124
124
|
typer.echo("🧩 Created solution.py")
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
test_file.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
126
|
+
for key, input in input_dict.items():
|
|
127
|
+
# 5. Save available inputs
|
|
128
|
+
input_file = quest_dir / f"input/input_p{key}.txt"
|
|
129
|
+
if input_file.exists() and not force:
|
|
130
|
+
typer.echo(
|
|
131
|
+
f"⚠️ Input file for part {key} already exists (use --force to overwrite)."
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
input_file.write_text(input, encoding="utf-8")
|
|
135
|
+
typer.echo(f"📥 Saved input for quest {quest:02d} part {key}.")
|
|
136
|
+
|
|
137
|
+
# 6. Ensure empty test file exists
|
|
138
|
+
test_file = quest_dir / f"test/test_p{key}.txt"
|
|
139
|
+
if test_file.exists() and not force:
|
|
140
|
+
typer.echo(
|
|
141
|
+
f"⚠️ Test file for part {key} already exists (use --force to overwrite)."
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
test_file.touch(exist_ok=True)
|
|
145
|
+
|
|
146
|
+
typer.echo(f"✅ Quest {quest:02d} ready at {solution_file.relative_to(base_dir)}")
|
|
144
147
|
|
|
145
148
|
|
|
146
149
|
@app.command("run")
|
|
@@ -185,5 +188,75 @@ def test_cmd(
|
|
|
185
188
|
typer.echo(f"🧪 Test result for Quest {quest} Part {part}:\n{result}")
|
|
186
189
|
|
|
187
190
|
|
|
191
|
+
@app.command("push")
|
|
192
|
+
def push_cmd(
|
|
193
|
+
quest: int = typer.Argument(..., help="Quest number (e.g. 3)"),
|
|
194
|
+
year: int = typer.Option(datetime.now().year, "--year", "-y", help="Event year"),
|
|
195
|
+
part: int = typer.Option(1, "--part", "-p", help="Part number to test"),
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Submit the solution for a given quest and part."""
|
|
198
|
+
|
|
199
|
+
# 0. Locate base directory
|
|
200
|
+
base_dir = utils.find_base()
|
|
201
|
+
|
|
202
|
+
# 1. Submit the solution
|
|
203
|
+
try:
|
|
204
|
+
result = utils.push_solution(base_dir, quest, year, part)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
typer.echo(f"❌ Failed to submit solution for Quest {quest} Part {part}: {e}")
|
|
207
|
+
raise typer.Exit(1)
|
|
208
|
+
|
|
209
|
+
if result.get("correct"):
|
|
210
|
+
typer.echo(
|
|
211
|
+
f"✅ Correct answer for Quest {quest} Part {part}!"
|
|
212
|
+
f"\n🏅 - Global place: {result.get('globalPlace', '?')}"
|
|
213
|
+
f"\n🏅 - Global score: {result.get('globalScore', '?')}"
|
|
214
|
+
f"\n⏱️ - Global time: {utils.format_duration(result.get('globalTime'))}"
|
|
215
|
+
f"\n⏱️ - Local time: {utils.format_duration(result.get('localTime'))}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Download the next part (if the quest is not ended)
|
|
219
|
+
if part == 3:
|
|
220
|
+
return
|
|
221
|
+
else:
|
|
222
|
+
typer.echo()
|
|
223
|
+
|
|
224
|
+
# Download the input for the given part
|
|
225
|
+
try:
|
|
226
|
+
input_dict = utils.download_input(year, quest)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
typer.echo(f"❌ Failed to fetch input: {e}")
|
|
229
|
+
raise typer.Exit(1)
|
|
230
|
+
|
|
231
|
+
# Prepare directory structure
|
|
232
|
+
quest_dir = utils.create_quest_dir(base_dir, year, quest)
|
|
233
|
+
|
|
234
|
+
# 5. Save available inputs
|
|
235
|
+
input_file = quest_dir / f"input/input_p{part + 1}.txt"
|
|
236
|
+
if input_file.exists():
|
|
237
|
+
typer.echo(
|
|
238
|
+
f"⚠️ Input file for part {part + 1} already exists (use ecd pull --force to overwrite)."
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
input_file.write_text(input_dict[str(part + 1)], encoding="utf-8")
|
|
242
|
+
typer.echo(f"📥 Saved input for quest {quest:02d} part {part + 1}.")
|
|
243
|
+
|
|
244
|
+
# 6. Ensure empty test file exists
|
|
245
|
+
test_file = quest_dir / f"test/test_p{part + 1}.txt"
|
|
246
|
+
if test_file.exists():
|
|
247
|
+
typer.echo(
|
|
248
|
+
f"⚠️ Test file for part {part + 1} already exists (use ecd pull --force to overwrite)."
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
test_file.touch(exist_ok=True)
|
|
252
|
+
|
|
253
|
+
else:
|
|
254
|
+
typer.echo(
|
|
255
|
+
f"❌ Incorrect answer for Quest {quest} Part {part}:"
|
|
256
|
+
f"\n - lengthCorrect={result.get('lengthCorrect')}, "
|
|
257
|
+
f"\n - firstCorrect={result.get('firstCorrect')}"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
188
261
|
if __name__ == "__main__":
|
|
189
262
|
app()
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
My solutions to the [Everybody Codes](https://everybody.codes/) puzzles — powered by **[`python-ecd`](https://github.com/pablofueros/python-ecd)** ⚙️
|
|
4
4
|
|
|
5
|
-
> A lightweight CLI tool to fetch,
|
|
5
|
+
> A lightweight CLI tool to fetch, test, and submit Everybody Codes challenges with ease.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -35,10 +35,13 @@ Note that **[`python-ecd`](https://github.com/pablofueros/python-ecd)** must be
|
|
|
35
35
|
ecd init
|
|
36
36
|
|
|
37
37
|
# Fetch a puzzle input ()
|
|
38
|
-
ecd
|
|
38
|
+
ecd pull 3 # Quest 3 of the current year
|
|
39
39
|
|
|
40
40
|
# Run your test cases
|
|
41
41
|
ecd test 3 --part 1
|
|
42
42
|
|
|
43
43
|
# Execute your actual input
|
|
44
44
|
ecd run 3 --part 1
|
|
45
|
+
|
|
46
|
+
# Submit your answer
|
|
47
|
+
ecd push 3 --part 1
|
|
@@ -2,9 +2,11 @@ import contextlib
|
|
|
2
2
|
import importlib.util
|
|
3
3
|
import io
|
|
4
4
|
import subprocess
|
|
5
|
+
from datetime import timedelta
|
|
5
6
|
from pathlib import Path
|
|
7
|
+
from typing import Literal, cast
|
|
6
8
|
|
|
7
|
-
from ecd import get_inputs
|
|
9
|
+
from ecd import get_inputs, submit
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def _read_template(template_name: str) -> str:
|
|
@@ -107,20 +109,12 @@ def get_token() -> str | None:
|
|
|
107
109
|
return TOKEN_PATH.read_text().strip()
|
|
108
110
|
|
|
109
111
|
|
|
110
|
-
def download_input(year: int, quest: int
|
|
112
|
+
def download_input(year: int, quest: int) -> dict[str, str]:
|
|
111
113
|
"""
|
|
112
|
-
Fetch the input text for a specific quest
|
|
113
|
-
Raises if part not available.
|
|
114
|
+
Fetch the available input text for a specific quest.
|
|
114
115
|
"""
|
|
115
|
-
|
|
116
116
|
with contextlib.redirect_stderr(io.StringIO()):
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
key = str(part)
|
|
120
|
-
if key not in data:
|
|
121
|
-
raise ValueError(f"Part {part} not unlocked or not available.")
|
|
122
|
-
|
|
123
|
-
return data[key]
|
|
117
|
+
return get_inputs(quest=quest, event=year)
|
|
124
118
|
|
|
125
119
|
|
|
126
120
|
def execute_part(
|
|
@@ -176,3 +170,86 @@ def execute_part(
|
|
|
176
170
|
raise ValueError(f"{func_name} returned None.")
|
|
177
171
|
|
|
178
172
|
return str(result)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def push_solution(
|
|
176
|
+
base_dir: Path,
|
|
177
|
+
quest: int,
|
|
178
|
+
year: int,
|
|
179
|
+
part: int,
|
|
180
|
+
) -> dict:
|
|
181
|
+
"""
|
|
182
|
+
Submit the solution for a given quest and part using input data.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The response message from the submission.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
quest_dir = base_dir / "events" / str(year) / f"quest_{quest:02d}"
|
|
189
|
+
solution_path = quest_dir / "solution.py"
|
|
190
|
+
if not solution_path.exists():
|
|
191
|
+
raise FileNotFoundError(
|
|
192
|
+
f"Missing solution file: {solution_path.relative_to(base_dir)}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Dynamic import
|
|
196
|
+
spec = importlib.util.spec_from_file_location("solution", solution_path)
|
|
197
|
+
if spec is None or spec.loader is None:
|
|
198
|
+
raise ImportError(
|
|
199
|
+
f"Could not load module from {solution_path.relative_to(base_dir)}"
|
|
200
|
+
)
|
|
201
|
+
module = importlib.util.module_from_spec(spec)
|
|
202
|
+
spec.loader.exec_module(module)
|
|
203
|
+
|
|
204
|
+
func_name = f"part_{part}"
|
|
205
|
+
func = getattr(module, func_name, None)
|
|
206
|
+
if not callable(func):
|
|
207
|
+
raise AttributeError(f"No function '{func_name}' defined in solution.py")
|
|
208
|
+
|
|
209
|
+
# Fetch input data
|
|
210
|
+
input_dict = download_input(year=year, quest=quest)
|
|
211
|
+
input_text = input_dict[str(part)]
|
|
212
|
+
|
|
213
|
+
# Run the function
|
|
214
|
+
answer = func(input_text)
|
|
215
|
+
if answer is None:
|
|
216
|
+
raise ValueError(f"{func_name} returned None.")
|
|
217
|
+
|
|
218
|
+
# Submit the answer
|
|
219
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
220
|
+
result = submit(
|
|
221
|
+
quest=quest,
|
|
222
|
+
event=year,
|
|
223
|
+
part=cast(Literal[1, 2, 3], part),
|
|
224
|
+
answer=str(answer),
|
|
225
|
+
quiet=True,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if result.status != 200:
|
|
229
|
+
raise RuntimeError(f"Submission failed with status {result.status}")
|
|
230
|
+
|
|
231
|
+
return result.json()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def format_duration(ms: int | float | None) -> str:
|
|
235
|
+
"""Format duration in milliseconds to a human-readable string using timedelta."""
|
|
236
|
+
if ms is None:
|
|
237
|
+
return "?"
|
|
238
|
+
|
|
239
|
+
td = timedelta(milliseconds=ms)
|
|
240
|
+
|
|
241
|
+
days = td.days
|
|
242
|
+
hours, remainder = divmod(td.seconds, 3600)
|
|
243
|
+
minutes, seconds = divmod(remainder, 60)
|
|
244
|
+
|
|
245
|
+
parts = []
|
|
246
|
+
if days:
|
|
247
|
+
parts.append(f"{days}d")
|
|
248
|
+
if hours:
|
|
249
|
+
parts.append(f"{hours}h")
|
|
250
|
+
if minutes:
|
|
251
|
+
parts.append(f"{minutes}m")
|
|
252
|
+
if seconds or not parts:
|
|
253
|
+
parts.append(f"{seconds}s")
|
|
254
|
+
|
|
255
|
+
return " ".join(parts)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|