python-ecd 0.1.1__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.

@@ -1,3 +1,16 @@
1
+ Metadata-Version: 2.3
2
+ Name: python-ecd
3
+ Version: 0.2.1
4
+ Summary: Python CLI for Everybody Codes
5
+ Author: Pablo Garcia
6
+ Author-email: Pablo Garcia <pablofueros@gmail.com>
7
+ Classifier: Programming Language :: Python :: 3.13
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Requires-Dist: everybody-codes-data==0.3
10
+ Requires-Dist: typer>=0.20.0
11
+ Requires-Python: >=3.13
12
+ Description-Content-Type: text/markdown
13
+
1
14
  <div align="center">
2
15
 
3
16
  <img src="https://raw.githubusercontent.com/pablofueros/python-ecd/main/assets/banner.png" alt="python-ecd logo" width="600"/>
@@ -6,6 +19,12 @@
6
19
 
7
20
  ### **✨ A Python CLI tool for managing Everybody Codes puzzles ✨**
8
21
 
22
+ [![Code Quality](https://github.com/pablofueros/python-ecd/actions/workflows/code-quality.yaml/badge.svg)](https://github.com/pablofueros/python-ecd/actions/workflows/code-quality.yaml)
23
+ [![Release](https://github.com/pablofueros/python-ecd/actions/workflows/release.yaml/badge.svg)](https://github.com/pablofueros/python-ecd/actions/workflows/release.yaml)
24
+ [![PyPI Latest Release](https://img.shields.io/pypi/v/python-ecd.svg)](https://pypi.org/project/python-ecd/)
25
+ [![PyPI Downloads](https://static.pepy.tech/badge/python-ecd)](https://pepy.tech/projects/python-ecd)
26
+ ![versions](https://img.shields.io/pypi/pyversions/python-ecd.svg)
27
+
9
28
  ---
10
29
 
11
30
  </div>
@@ -65,10 +84,10 @@ Note that is not necessary if you set it during initialization.
65
84
 
66
85
  ### Download Puzzle Input
67
86
 
68
- Get the input for a specific puzzle:
87
+ Download the input for a specific puzzle:
69
88
 
70
89
  ```bash
71
- ecd get <QUEST_NUMBER> [OPTIONS]
90
+ ecd pull <QUEST_NUMBER> [OPTIONS]
72
91
  ```
73
92
 
74
93
  Options:
@@ -76,6 +95,8 @@ Options:
76
95
  - `--part`, `-p`: Puzzle part (default: 1)
77
96
  - `--force`, `-f`: Overwrite existing files
78
97
 
98
+ This command will create the necessary dirs and files if they do not exist.
99
+
79
100
  ### Run Solutions
80
101
 
81
102
  Execute your solution for a specific puzzle:
@@ -100,6 +121,18 @@ Options:
100
121
  - `--year`: Event year (default: actual)
101
122
  - `--part`: Part number to test (default: 1)
102
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
+
103
136
  ### Display Version
104
137
 
105
138
  Show the current version of the tool:
@@ -110,7 +143,7 @@ ecd --version
110
143
 
111
144
  ## ©️ License
112
145
 
113
- [MIT License](LICENSE)
146
+ This project is licensed under the terms of the MIT license.
114
147
 
115
148
  ## 🤝 Contributing
116
149
 
@@ -1,14 +1,3 @@
1
- Metadata-Version: 2.3
2
- Name: python-ecd
3
- Version: 0.1.1
4
- Summary: Add your description here
5
- Author: Pablo Garcia
6
- Author-email: Pablo Garcia <pablofueros@gmail.com>
7
- Requires-Dist: everybody-codes-data==0.2
8
- Requires-Dist: typer>=0.20.0
9
- Requires-Python: >=3.13
10
- Description-Content-Type: text/markdown
11
-
12
1
  <div align="center">
13
2
 
14
3
  <img src="https://raw.githubusercontent.com/pablofueros/python-ecd/main/assets/banner.png" alt="python-ecd logo" width="600"/>
@@ -17,6 +6,12 @@ Description-Content-Type: text/markdown
17
6
 
18
7
  ### **✨ A Python CLI tool for managing Everybody Codes puzzles ✨**
19
8
 
9
+ [![Code Quality](https://github.com/pablofueros/python-ecd/actions/workflows/code-quality.yaml/badge.svg)](https://github.com/pablofueros/python-ecd/actions/workflows/code-quality.yaml)
10
+ [![Release](https://github.com/pablofueros/python-ecd/actions/workflows/release.yaml/badge.svg)](https://github.com/pablofueros/python-ecd/actions/workflows/release.yaml)
11
+ [![PyPI Latest Release](https://img.shields.io/pypi/v/python-ecd.svg)](https://pypi.org/project/python-ecd/)
12
+ [![PyPI Downloads](https://static.pepy.tech/badge/python-ecd)](https://pepy.tech/projects/python-ecd)
13
+ ![versions](https://img.shields.io/pypi/pyversions/python-ecd.svg)
14
+
20
15
  ---
21
16
 
22
17
  </div>
@@ -76,10 +71,10 @@ Note that is not necessary if you set it during initialization.
76
71
 
77
72
  ### Download Puzzle Input
78
73
 
79
- Get the input for a specific puzzle:
74
+ Download the input for a specific puzzle:
80
75
 
81
76
  ```bash
82
- ecd get <QUEST_NUMBER> [OPTIONS]
77
+ ecd pull <QUEST_NUMBER> [OPTIONS]
83
78
  ```
84
79
 
85
80
  Options:
@@ -87,6 +82,8 @@ Options:
87
82
  - `--part`, `-p`: Puzzle part (default: 1)
88
83
  - `--force`, `-f`: Overwrite existing files
89
84
 
85
+ This command will create the necessary dirs and files if they do not exist.
86
+
90
87
  ### Run Solutions
91
88
 
92
89
  Execute your solution for a specific puzzle:
@@ -111,6 +108,18 @@ Options:
111
108
  - `--year`: Event year (default: actual)
112
109
  - `--part`: Part number to test (default: 1)
113
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
+
114
123
  ### Display Version
115
124
 
116
125
  Show the current version of the tool:
@@ -121,7 +130,7 @@ ecd --version
121
130
 
122
131
  ## ©️ License
123
132
 
124
- [MIT License](LICENSE)
133
+ This project is licensed under the terms of the MIT license.
125
134
 
126
135
  ## 🤝 Contributing
127
136
 
@@ -1,11 +1,18 @@
1
1
  [project]
2
2
  name = "python-ecd"
3
- version = "0.1.1"
4
- description = "Add your description here"
3
+ version = "0.2.1"
4
+ description = "Python CLI for Everybody Codes"
5
5
  readme = "README.md"
6
+ classifiers = [
7
+ "Programming Language :: Python :: 3.13",
8
+ "License :: OSI Approved :: MIT License",
9
+ ]
6
10
  authors = [{ name = "Pablo Garcia", email = "pablofueros@gmail.com" }]
7
11
  requires-python = ">=3.13"
8
- dependencies = ["everybody-codes-data==0.2", "typer>=0.20.0"]
12
+ dependencies = [
13
+ "everybody-codes-data==0.3",
14
+ "typer>=0.20.0",
15
+ ]
9
16
 
10
17
  [project.scripts]
11
18
  python-ecd = "python_ecd.cli:app"
@@ -16,6 +23,4 @@ requires = ["uv_build>=0.9.7,<0.10.0"]
16
23
  build-backend = "uv_build"
17
24
 
18
25
  [dependency-groups]
19
- dev = [
20
- "ty>=0.0.1a25",
21
- ]
26
+ dev = ["ty>=0.0.1a25"]
@@ -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("get")
89
- def get_cmd(
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
- input_text = utils.download_input(year, quest, part)
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("⚠️ solution.py already exists (use --force to overwrite).")
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
- # 5. Save input
127
- input_file = quest_dir / f"input/input_p{part}.txt"
128
- if input_file.exists() and not force:
129
- typer.echo("⚠️ Input file already exists (use --force to overwrite).")
130
- else:
131
- input_file.write_text(input_text, encoding="utf-8")
132
- typer.echo(f"📥 Saved input for quest {quest:02d} part {part}.")
133
-
134
- # 6. Ensure empty test file exists
135
- test_file = quest_dir / f"test/test_p{part}.txt"
136
- if test_file.exists() and not force:
137
- typer.echo("⚠️ Test file already exists (use --force to overwrite).")
138
- else:
139
- test_file.touch(exist_ok=True)
140
-
141
- typer.echo(
142
- f"✅ Quest {quest:02d} (Part {part}) ready at {solution_file.relative_to(base_dir)}"
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()
@@ -1,6 +1,8 @@
1
1
  # 🧩 Everybody Codes Solutions
2
2
 
3
- My solutions to the [Everybody Codes](https://everybody.codes/) puzzles (managed by the [python-ecd](https://github.com/pablofueros/python-ecd) library).
3
+ My solutions to the [Everybody Codes](https://everybody.codes/) puzzles powered by **[`python-ecd`](https://github.com/pablofueros/python-ecd)** ⚙️
4
+
5
+ > A lightweight CLI tool to fetch, test, and submit Everybody Codes challenges with ease.
4
6
 
5
7
  ---
6
8
 
@@ -26,15 +28,20 @@ Each quest is stored under `events/<year>/quest_<id>/` and contains:
26
28
 
27
29
  ## 🚀 Usage
28
30
 
31
+ Note that **[`python-ecd`](https://github.com/pablofueros/python-ecd)** must be installed.
32
+
29
33
  ```bash
30
34
  # Initialize your workspace
31
35
  ecd init
32
36
 
33
37
  # Fetch a puzzle input ()
34
- ecd get 3 # Quest 3 of the current year
38
+ ecd pull 3 # Quest 3 of the current year
35
39
 
36
40
  # Run your test cases
37
41
  ecd test 3 --part 1
38
42
 
39
43
  # Execute your actual input
40
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, part: int) -> str:
112
+ def download_input(year: int, quest: int) -> dict[str, str]:
111
113
  """
112
- Fetch the input text for a specific quest and part.
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
- data = get_inputs(quest=quest, event=year)
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)