python-ecd 0.1.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.

@@ -0,0 +1,128 @@
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
+ <div align="center">
13
+
14
+ <img src="https://raw.githubusercontent.com/pablofueros/python-ecd/main/assets/banner.png" alt="python-ecd logo" width="600"/>
15
+
16
+ ---
17
+
18
+ ### **✨ A Python CLI tool for managing Everybody Codes puzzles ✨**
19
+
20
+ ---
21
+
22
+ </div>
23
+
24
+ ## 📋 Features
25
+
26
+ - Initialize workspace for [Everybody Codes](https://everybody.codes) puzzles
27
+ - Download puzzle inputs automatically
28
+ - Manage session tokens securely
29
+ - Run solutions with both test and real input data
30
+ - Automatic project structure creation
31
+
32
+ ## 📦 Installation
33
+
34
+ The package can be installed using [uv](https://docs.astral.sh/uv/#installation), which is recommended for better dependency management and faster installations:
35
+
36
+ ```bash
37
+ # Install it as a system tool
38
+ uv tool install python-ecd
39
+
40
+ # Otherwise use:
41
+ uvx python-ecd
42
+ ```
43
+
44
+ The tool will be available as both 'python-ecd' and 'ecd'
45
+
46
+
47
+ ## 💻 Usage
48
+
49
+ ### Initialize a Workspace
50
+
51
+ Create a new workspace for your puzzles solutions:
52
+
53
+ It can be done in the current directory:
54
+
55
+ ```bash
56
+ ecd init
57
+ ```
58
+
59
+ Or in a specified path:
60
+
61
+ ```bash
62
+ ecd init everybody-codes-solutions
63
+ ```
64
+
65
+ Note that if the directory does not exist, it will be created.
66
+
67
+ ### Set Session Token
68
+
69
+ Configure your session token for accessing puzzle inputs:
70
+
71
+ ```bash
72
+ ecd set-token <TOKEN>
73
+ ```
74
+
75
+ Note that is not necessary if you set it during initialization.
76
+
77
+ ### Download Puzzle Input
78
+
79
+ Get the input for a specific puzzle:
80
+
81
+ ```bash
82
+ ecd get <QUEST_NUMBER> [OPTIONS]
83
+ ```
84
+
85
+ Options:
86
+ - `--year`, `-y`: Event year (default: actual)
87
+ - `--part`, `-p`: Puzzle part (default: 1)
88
+ - `--force`, `-f`: Overwrite existing files
89
+
90
+ ### Run Solutions
91
+
92
+ Execute your solution for a specific puzzle:
93
+
94
+ ```bash
95
+ ecd run <QUEST_NUMBER> [OPTIONS]
96
+ ```
97
+
98
+ Options:
99
+ - `--year`: Event year (default: actual)
100
+ - `--part`: Part number to execute (default: 1)
101
+
102
+ ### Test Solutions
103
+
104
+ Run your solution using test data:
105
+
106
+ ```bash
107
+ ecd test <QUEST_NUMBER> [OPTIONS]
108
+ ```
109
+
110
+ Options:
111
+ - `--year`: Event year (default: actual)
112
+ - `--part`: Part number to test (default: 1)
113
+
114
+ ### Display Version
115
+
116
+ Show the current version of the tool:
117
+
118
+ ```bash
119
+ ecd --version
120
+ ```
121
+
122
+ ## ©️ License
123
+
124
+ [MIT License](LICENSE)
125
+
126
+ ## 🤝 Contributing
127
+
128
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,117 @@
1
+ <div align="center">
2
+
3
+ <img src="https://raw.githubusercontent.com/pablofueros/python-ecd/main/assets/banner.png" alt="python-ecd logo" width="600"/>
4
+
5
+ ---
6
+
7
+ ### **✨ A Python CLI tool for managing Everybody Codes puzzles ✨**
8
+
9
+ ---
10
+
11
+ </div>
12
+
13
+ ## 📋 Features
14
+
15
+ - Initialize workspace for [Everybody Codes](https://everybody.codes) puzzles
16
+ - Download puzzle inputs automatically
17
+ - Manage session tokens securely
18
+ - Run solutions with both test and real input data
19
+ - Automatic project structure creation
20
+
21
+ ## 📦 Installation
22
+
23
+ The package can be installed using [uv](https://docs.astral.sh/uv/#installation), which is recommended for better dependency management and faster installations:
24
+
25
+ ```bash
26
+ # Install it as a system tool
27
+ uv tool install python-ecd
28
+
29
+ # Otherwise use:
30
+ uvx python-ecd
31
+ ```
32
+
33
+ The tool will be available as both 'python-ecd' and 'ecd'
34
+
35
+
36
+ ## 💻 Usage
37
+
38
+ ### Initialize a Workspace
39
+
40
+ Create a new workspace for your puzzles solutions:
41
+
42
+ It can be done in the current directory:
43
+
44
+ ```bash
45
+ ecd init
46
+ ```
47
+
48
+ Or in a specified path:
49
+
50
+ ```bash
51
+ ecd init everybody-codes-solutions
52
+ ```
53
+
54
+ Note that if the directory does not exist, it will be created.
55
+
56
+ ### Set Session Token
57
+
58
+ Configure your session token for accessing puzzle inputs:
59
+
60
+ ```bash
61
+ ecd set-token <TOKEN>
62
+ ```
63
+
64
+ Note that is not necessary if you set it during initialization.
65
+
66
+ ### Download Puzzle Input
67
+
68
+ Get the input for a specific puzzle:
69
+
70
+ ```bash
71
+ ecd get <QUEST_NUMBER> [OPTIONS]
72
+ ```
73
+
74
+ Options:
75
+ - `--year`, `-y`: Event year (default: actual)
76
+ - `--part`, `-p`: Puzzle part (default: 1)
77
+ - `--force`, `-f`: Overwrite existing files
78
+
79
+ ### Run Solutions
80
+
81
+ Execute your solution for a specific puzzle:
82
+
83
+ ```bash
84
+ ecd run <QUEST_NUMBER> [OPTIONS]
85
+ ```
86
+
87
+ Options:
88
+ - `--year`: Event year (default: actual)
89
+ - `--part`: Part number to execute (default: 1)
90
+
91
+ ### Test Solutions
92
+
93
+ Run your solution using test data:
94
+
95
+ ```bash
96
+ ecd test <QUEST_NUMBER> [OPTIONS]
97
+ ```
98
+
99
+ Options:
100
+ - `--year`: Event year (default: actual)
101
+ - `--part`: Part number to test (default: 1)
102
+
103
+ ### Display Version
104
+
105
+ Show the current version of the tool:
106
+
107
+ ```bash
108
+ ecd --version
109
+ ```
110
+
111
+ ## ©️ License
112
+
113
+ [MIT License](LICENSE)
114
+
115
+ ## 🤝 Contributing
116
+
117
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "python-ecd"
3
+ version = "0.1.1"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [{ name = "Pablo Garcia", email = "pablofueros@gmail.com" }]
7
+ requires-python = ">=3.13"
8
+ dependencies = ["everybody-codes-data==0.2", "typer>=0.20.0"]
9
+
10
+ [project.scripts]
11
+ python-ecd = "python_ecd.cli:app"
12
+ ecd = "python_ecd.cli:app"
13
+
14
+ [build-system]
15
+ requires = ["uv_build>=0.9.7,<0.10.0"]
16
+ build-backend = "uv_build"
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "ty>=0.0.1a25",
21
+ ]
File without changes
@@ -0,0 +1,189 @@
1
+ from datetime import datetime
2
+
3
+ import typer
4
+
5
+ from . import utils
6
+ from .version import __version__
7
+
8
+ app = typer.Typer(name="python-ecd", help="python-ecd: CLI tool for Everybody Codes")
9
+
10
+
11
+ def _display_version(value: bool) -> None:
12
+ """Display the version of the application and exit."""
13
+ if value:
14
+ typer.echo(f"python-ecd {__version__}")
15
+ raise typer.Exit()
16
+
17
+
18
+ @app.callback()
19
+ def main(
20
+ version: bool = typer.Option(
21
+ None,
22
+ "--version",
23
+ "-v",
24
+ callback=_display_version,
25
+ help="Display the version and exit.",
26
+ is_eager=True, # Process version before other logic
27
+ ),
28
+ ):
29
+ pass
30
+
31
+
32
+ @app.command("init")
33
+ def init_cmd(
34
+ path: str = typer.Argument(None, help="Path to initialize the workspace at"),
35
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
36
+ ) -> None:
37
+ """Initialize the workspace for Everybody Codes puzzles."""
38
+
39
+ # 0. Parse inputs
40
+ base_dir_name = "." if path is None else path
41
+ token = typer.prompt("Session Token", default="", show_default=False)
42
+
43
+ # 1. Create base directory
44
+ base_dir = utils.create_base(base_dir_name)
45
+ typer.echo(f"📁 Created workspace at: {base_dir}")
46
+
47
+ # 2. Initialize git repository
48
+ git_dir = base_dir / ".git"
49
+ if not git_dir.exists():
50
+ utils.create_git(base_dir)
51
+
52
+ # 3. Create .gitignore
53
+ gitignore_path = base_dir / ".gitignore"
54
+ if gitignore_path.exists() and not force:
55
+ typer.echo("⚠️ .gitignore already exists (use --force to overwrite).")
56
+ else:
57
+ utils.create_gitignore(base_dir)
58
+ typer.echo("🛑 .gitignore created")
59
+
60
+ # 4. Create README.md if requested
61
+ readme_path = base_dir / "README.md"
62
+ if readme_path.exists() and not force:
63
+ typer.echo("⚠️ README.md already exists (use --force to overwrite).")
64
+ else:
65
+ utils.create_readme(base_dir, force)
66
+ typer.echo("📝 README.md created")
67
+
68
+ # 5. Save session token if provided
69
+ token_path = utils.TOKEN_PATH
70
+ if not token:
71
+ typer.echo("⚠️ No session token provided (set it later with 'ecd set-token')")
72
+ elif token_path.exists() and not force:
73
+ typer.echo("⚠️ Session token file already exists (use --force to overwrite).")
74
+ else:
75
+ utils.set_token(token, force)
76
+ typer.echo(f"🔑 Session token saved to {token_path}")
77
+
78
+
79
+ @app.command("set-token")
80
+ def set_token_cmd(
81
+ token: str = typer.Argument(..., help="Session token to access puzzle inputs"),
82
+ ) -> None:
83
+ """Set the session token to operate with Everybody Codes webpage."""
84
+ utils.set_token(token)
85
+ typer.echo("🔑 Session token saved to /home/.config/ecd/token")
86
+
87
+
88
+ @app.command("get")
89
+ def get_cmd(
90
+ quest: int = typer.Argument(..., help="Quest number (e.g. 3)"),
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
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
94
+ ) -> None:
95
+ """Download input data for a given quest and create local structure."""
96
+
97
+ # 0. Locate base directory
98
+ base_dir = utils.find_base()
99
+
100
+ # 1. Ensure session token exists
101
+ if not utils.get_token():
102
+ typer.echo(
103
+ "❌ Error: Session token not found in .env. Use 'ecd set-token' to set it."
104
+ )
105
+ raise typer.Exit(1)
106
+
107
+ # 2. Download the input for the given part
108
+ try:
109
+ input_text = utils.download_input(year, quest, part)
110
+ except Exception as e:
111
+ typer.echo(f"❌ Failed to fetch input: {e}")
112
+ typer.echo("ℹ️ Maybe ")
113
+ raise typer.Exit(1)
114
+
115
+ # 3. Prepare directory structure
116
+ quest_dir = utils.create_quest_dir(base_dir, year, quest)
117
+
118
+ # 4. Create solution.py if missing
119
+ solution_file = quest_dir / "solution.py"
120
+ if solution_file.exists() and not force:
121
+ typer.echo("⚠️ solution.py already exists (use --force to overwrite).")
122
+ else:
123
+ utils.create_solution(quest_dir, force)
124
+ typer.echo("🧩 Created solution.py")
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
+ )
144
+
145
+
146
+ @app.command("run")
147
+ def run_cmd(
148
+ quest: int = typer.Argument(..., help="Quest number (e.g. 3)"),
149
+ year: int = typer.Option(datetime.now().year, "--year", "-y", help="Event year"),
150
+ part: int = typer.Option(1, "--part", "-p", help="Part number to execute"),
151
+ ) -> None:
152
+ """Execute the solution for a given quest and part using input data."""
153
+
154
+ # 0. Locate base directory
155
+ base_dir = utils.find_base()
156
+
157
+ # 1. Execute the solution
158
+ try:
159
+ result = utils.execute_part(base_dir, quest, year, part, mode="input")
160
+ except Exception as e:
161
+ typer.echo(f"❌ Failed to execute Quest {quest} Part {part}: {e}")
162
+ raise typer.Exit(1)
163
+
164
+ typer.echo(f"✅ Result for Quest {quest} Part {part}:\n{result}")
165
+
166
+
167
+ @app.command("test")
168
+ def test_cmd(
169
+ quest: int = typer.Argument(..., help="Quest number (e.g. 3)"),
170
+ year: int = typer.Option(datetime.now().year, "--year", "-y", help="Event year"),
171
+ part: int = typer.Option(1, "--part", "-p", help="Part number to test"),
172
+ ) -> None:
173
+ """Run the solution for a given quest and part using test data."""
174
+
175
+ # 0. Locate base directory
176
+ base_dir = utils.find_base()
177
+
178
+ # 1. Execute the solution in test mode
179
+ try:
180
+ result = utils.execute_part(base_dir, quest, year, part, mode="test")
181
+ except Exception as e:
182
+ typer.echo(f"❌ Failed to run test for Quest {quest} Part {part}: {e}")
183
+ raise typer.Exit(1)
184
+
185
+ typer.echo(f"🧪 Test result for Quest {quest} Part {part}:\n{result}")
186
+
187
+
188
+ if __name__ == "__main__":
189
+ app()
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,40 @@
1
+ # 🧩 Everybody Codes Solutions
2
+
3
+ My solutions to the [Everybody Codes](https://everybody.codes/) puzzles (managed by the [python-ecd](https://github.com/pablofueros/python-ecd) library).
4
+
5
+ ---
6
+
7
+ ## 📂 Project Structure
8
+
9
+ Each quest is stored under `events/<year>/quest_<id>/` and contains:
10
+
11
+ | File / Folder | Description |
12
+ |----------------|-------------|
13
+ | `solution.py` | Your Python solution with `part_1`, `part_2`, and `part_3` functions. |
14
+ | `input/` | Puzzle inputs (`input_p1.txt`, `input_p2.txt`, …) fetched automatically. |
15
+ | `test/` | Optional test files (`test_p1.txt`, …) for local validation. |
16
+
17
+ ---
18
+
19
+ ## ✅ Completed Quests
20
+
21
+ | Year | Quest | Part 1 | Part 2 | Part 3 |
22
+ |------|--------|--------|--------|--------|
23
+ | yyyy | n | ✅ | ⬜ | ⬜ |
24
+
25
+ ---
26
+
27
+ ## 🚀 Usage
28
+
29
+ ```bash
30
+ # Initialize your workspace
31
+ ecd init
32
+
33
+ # Fetch a puzzle input ()
34
+ ecd get 3 # Quest 3 of the current year
35
+
36
+ # Run your test cases
37
+ ecd test 3 --part 1
38
+
39
+ # Execute your actual input
40
+ ecd run 3 --part 1
@@ -0,0 +1,18 @@
1
+ def part_1(data: str) -> str:
2
+ return ""
3
+
4
+
5
+ def part_2(data: str) -> str:
6
+ return ""
7
+
8
+
9
+ def part_3(data: str) -> str:
10
+ return ""
11
+
12
+
13
+ def main() -> None:
14
+ pass # debugging
15
+
16
+
17
+ if __name__ == "__main__":
18
+ main()
@@ -0,0 +1,178 @@
1
+ import contextlib
2
+ import importlib.util
3
+ import io
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ from ecd import get_inputs
8
+
9
+
10
+ def _read_template(template_name: str) -> str:
11
+ """
12
+ Read a template file from the templates directory.
13
+ """
14
+ templates_dir = Path(__file__).parent / "templates"
15
+ template_path = templates_dir / template_name
16
+ text = template_path.read_text(encoding="utf-8")
17
+ return text
18
+
19
+
20
+ def create_base(base_dir_name: str) -> Path:
21
+ """
22
+ Create the base directory for the project, including subfolders.
23
+ """
24
+ base_dir = Path(base_dir_name).resolve()
25
+ (base_dir / "events").mkdir(parents=True, exist_ok=True)
26
+ return base_dir
27
+
28
+
29
+ def find_base() -> Path:
30
+ """
31
+ Locate the base directory by searching upwards from the current directory.
32
+ """
33
+ current_dir = Path.cwd()
34
+ for parent in [current_dir] + list(current_dir.parents):
35
+ if (parent / "events").exists():
36
+ return parent
37
+ raise FileNotFoundError("Base directory not found. Please run 'ecd init'.")
38
+
39
+
40
+ def create_quest_dir(base_dir: Path, year: int, quest: int) -> Path:
41
+ """
42
+ Create directory structure for a given quest/year.
43
+ """
44
+ quest_dir = base_dir / "events" / str(year) / f"quest_{quest:02d}"
45
+ quest_dir.mkdir(parents=True, exist_ok=True)
46
+ (quest_dir / "input").mkdir(exist_ok=True)
47
+ (quest_dir / "test").mkdir(exist_ok=True)
48
+ return quest_dir
49
+
50
+
51
+ def create_readme(base_dir: Path, force: bool) -> None:
52
+ """
53
+ Create a simple README.md in the base directory.
54
+ """
55
+ readme_path = base_dir / "README.md"
56
+ if not readme_path.exists() or force:
57
+ content = _read_template("README.md.tpl")
58
+ readme_path.write_text(content, encoding="utf-8")
59
+
60
+
61
+ def create_git(base_dir: Path) -> None:
62
+ """
63
+ Initialize a git repository in the base directory.
64
+ """
65
+ git_dir = base_dir / ".git"
66
+ if git_dir.exists():
67
+ return # Git already initialized
68
+ subprocess.run(
69
+ ["git", "init"],
70
+ cwd=str(base_dir),
71
+ check=True,
72
+ stdout=subprocess.DEVNULL,
73
+ )
74
+
75
+
76
+ def create_gitignore(base_dir: Path) -> None:
77
+ gitignore_path = base_dir / ".gitignore"
78
+ if not gitignore_path.exists():
79
+ content = _read_template(".gitignore.tpl")
80
+ gitignore_path.write_text(content, encoding="utf-8")
81
+
82
+
83
+ def create_solution(quest_dir: Path, force: bool) -> None:
84
+ """
85
+ Create a solution.py file in the quest directory.
86
+ """
87
+ solution_path = quest_dir / "solution.py"
88
+ if not solution_path.exists() or force:
89
+ content = _read_template("solution.py.tpl")
90
+ solution_path.write_text(content, encoding="utf-8")
91
+
92
+
93
+ TOKEN_PATH = Path.home() / ".config" / "ecd" / "token"
94
+
95
+
96
+ def set_token(token: str, force: bool = False) -> None:
97
+ """Create ~/.config/ecd/token with the given token."""
98
+ config_dir = TOKEN_PATH.parent
99
+ config_dir.mkdir(parents=True, exist_ok=True)
100
+ TOKEN_PATH.write_text(token.strip())
101
+
102
+
103
+ def get_token() -> str | None:
104
+ """Read the token if it exists, else return None."""
105
+ if not TOKEN_PATH.exists():
106
+ return None
107
+ return TOKEN_PATH.read_text().strip()
108
+
109
+
110
+ def download_input(year: int, quest: int, part: int) -> str:
111
+ """
112
+ Fetch the input text for a specific quest and part.
113
+ Raises if part not available.
114
+ """
115
+
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]
124
+
125
+
126
+ def execute_part(
127
+ base_dir: Path,
128
+ quest: int,
129
+ year: int,
130
+ part: int,
131
+ mode: str = "input",
132
+ ) -> str:
133
+ """
134
+ Execute a given part of a quest solution with either input or test data.
135
+
136
+ Args:
137
+ base_dir: Base directory
138
+ part: Part number (1, 2, or 3)
139
+ mode: Either "input" or "test"
140
+
141
+ Returns:
142
+ The string result produced by part_{part}(data).
143
+ """
144
+
145
+ quest_dir = base_dir / "events" / str(year) / f"quest_{quest:02d}"
146
+ solution_path = quest_dir / "solution.py"
147
+ data_path = quest_dir / mode / f"{mode}_p{part}.txt"
148
+
149
+ if not solution_path.exists():
150
+ raise FileNotFoundError(
151
+ f"Missing solution file: {solution_path.relative_to(base_dir)}"
152
+ )
153
+ if not data_path.exists():
154
+ raise FileNotFoundError(f"Missing data file: {data_path.relative_to(base_dir)}")
155
+
156
+ # Load data
157
+ data = data_path.read_text(encoding="utf-8")
158
+
159
+ # Dynamic import
160
+ spec = importlib.util.spec_from_file_location("solution", solution_path)
161
+ if spec is None or spec.loader is None:
162
+ raise ImportError(
163
+ f"Could not load module from {solution_path.relative_to(base_dir)}"
164
+ )
165
+ module = importlib.util.module_from_spec(spec)
166
+ spec.loader.exec_module(module)
167
+
168
+ func_name = f"part_{part}"
169
+ func = getattr(module, func_name, None)
170
+ if not callable(func):
171
+ raise AttributeError(f"No function '{func_name}' defined in solution.py")
172
+
173
+ # Run the function
174
+ result = func(data)
175
+ if result is None:
176
+ raise ValueError(f"{func_name} returned None.")
177
+
178
+ return str(result)
@@ -0,0 +1,3 @@
1
+ import importlib.metadata as metadata
2
+
3
+ __version__ = metadata.version("python-ecd")