Repository: SteadBytes/aocpy

Issue:
Add CLI option to set cookie in `~/.config/aocpy/token`
As a user,
I want to set the cookie using cli command
So that I do not have to create the folders and token file myself.


Proposal:
`aocpy set-cookie <cookie>`

- Creates folders if they do not exists
- write `<cookie>` to token file

Comments:

Patch:
diff --git a/README.md b/README.md
index 7ffb74a..ccb3a6a 100644
--- a/README.md
+++ b/README.md
@@ -73,12 +73,12 @@ AoC puzzle inputs differ by user, requiring a browser cookie to determine the cu
   - `$ aocpy begin -c <1234mycookie>`
 - Configuration file:
   - Paste the cookie into a file at `~/.config/aocpy/token`
-  ```
-  # ~/.config/aocpy/token
-  <1234mycookie>
-  ```
+      ```
+      # ~/.config/aocpy/token
+      <1234mycookie>
+      ```
+  - or set it via cli `aocpy set-cookie <1234mycookie>`
 - Environment variable:
-
   - `$ export AOC_SESSION_COOKIE=<1234mycookie>`
 
 ### Finding Your Session Cookie
diff --git a/aocpy/cli.py b/aocpy/cli.py
index a0b4ea8..762b841 100644
--- a/aocpy/cli.py
+++ b/aocpy/cli.py
@@ -14,7 +14,7 @@ from aocpy.puzzle import (
     check_submission_response_text,
     get_puzzle_input,
 )
-from aocpy.utils import current_day, current_year, get_session_cookie
+from aocpy.utils import current_day, current_year, get_session_cookie, get_config_dir, get_token_file
 
 
 def begin_day(session: web.AuthSession, p: Puzzle):
@@ -74,5 +74,13 @@ def submit(answer, level, year, day, session_cookie):
         click.echo(err)
 
 
+@cli.command()
+@click.argument("cookie")
+def set_cookie(cookie):
+    get_config_dir().mkdir(exist_ok=True, parents=True)
+    get_token_file().write_text(cookie)
+    click.echo(f"Saved cookie in {get_token_file()}")
+
+
 if __name__ == "__main__":
     cli()
diff --git a/aocpy/utils.py b/aocpy/utils.py
index df5a897..fca69fe 100644
--- a/aocpy/utils.py
+++ b/aocpy/utils.py
@@ -1,5 +1,6 @@
 import os
 from datetime import datetime
+from pathlib import Path
 
 import pytz
 
@@ -9,6 +10,14 @@ AOC_TZ = pytz.timezone("America/New_York")
 CONFIG_DIRNAME = "~/.config/aocpy"
 
 
+def get_config_dir():
+    return Path(os.path.expanduser(CONFIG_DIRNAME))
+
+
+def get_token_file():
+    return get_config_dir() / 'token'
+
+
 def current_year():
     """ Returns the most recent AOC year available
     """
@@ -36,7 +45,7 @@ def get_session_cookie():
     if cookie is not None:
         return cookie
     try:
-        with open(os.path.join(os.path.expanduser(CONFIG_DIRNAME), "token")) as f:
+        with get_token_file().open() as f:
             cookie = f.read().strip()
     except (OSError, IOError):
         pass


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 06431e8..0fef984 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,16 +1,15 @@
 import contextlib
 from datetime import datetime
+from pathlib import Path
 
 import pytest
 import pytz
 from click.testing import CliRunner
 from freezegun import freeze_time
+from hypothesis import strategies as st, given
 
 from aocpy.cli import cli
 
-from pathlib import Path
-from hypothesis import strategies as st, given
-
 
 class Runner(CliRunner):
     @contextlib.contextmanager
@@ -33,6 +32,13 @@ def home_dir(tmp_path):
 
 @pytest.fixture
 def config_dir(home_dir):
+    d = home_dir / ".config/aocpy"
+    d.mkdir(parents=True)
+    return d
+
+
+@pytest.fixture
+def cache_dir(home_dir):
     d = home_dir / ".config/aocd"
     d.mkdir(parents=True)
     return d
@@ -44,7 +50,7 @@ def runner(home_dir):
 
 
 @freeze_time(datetime(2019, 12, 10, hour=1, tzinfo=pytz.timezone("America/New_York")))
-def test_begin(webbrowser_open, runner, config_dir, responses):
+def test_begin(webbrowser_open, runner, cache_dir, responses):
     puzzle_url = "https://adventofcode.com/2019/day/10"
     puzzle_input = "some text"
     responses.add(responses.GET, puzzle_url + "/input", body=puzzle_input)
@@ -56,13 +62,23 @@ def test_begin(webbrowser_open, runner, config_dir, responses):
         assert (p / "10/input.txt").exists()
         assert (p / "10/solution.py").exists()
         # Input is cached
-        assert (config_dir / cookie / "2019/10.txt").exists()
+        assert (cache_dir / cookie / "2019/10.txt").exists()
         # Browser opened to today's puzzle URL
         webbrowser_open.assert_called_once_with(puzzle_url)
 
 
+def test_set_cookie(runner, config_dir):
+    cookie = "12345"
+    with runner.isolated_filesystem() as p:
+        result = runner.invoke(cli, ["set-cookie", cookie])
+        assert result.exit_code == 0
+        # Token file generated and contains new value
+        assert (config_dir / "token").exists()
+        assert (config_dir / "token").read_text() == cookie
+
+
 @pytest.mark.parametrize("day", range(26, 32))
-def test_begin_uses_day_25_as_max(day, webbrowser_open, runner, config_dir, responses):
+def test_begin_uses_day_25_as_max(day, webbrowser_open, runner, cache_dir, responses):
     puzzle_url = "https://adventofcode.com/2019/day/25"
     puzzle_input = "some text"
     responses.add(responses.GET, puzzle_url + "/input", body=puzzle_input)
@@ -75,7 +91,7 @@ def test_begin_uses_day_25_as_max(day, webbrowser_open, runner, config_dir, resp
             assert (p / "25/input.txt").exists()
             assert (p / "25/solution.py").exists()
             # Input is cached
-            assert (config_dir / cookie / "2019/25.txt").exists()
+            assert (cache_dir / cookie / "2019/25.txt").exists()
             # Browser opened to today's puzzle URL
             webbrowser_open.assert_called_once_with(puzzle_url)
 
@@ -93,7 +109,7 @@ def test_begin_fails_if_not_december(
 
 @freeze_time(datetime(2019, 12, 25, hour=1, tzinfo=pytz.timezone("America/New_York")))
 @pytest.mark.parametrize("day", range(1, 25))
-def test_begin_specify_day(day, webbrowser_open, runner, config_dir, responses):
+def test_begin_specify_day(day, webbrowser_open, runner, cache_dir, responses):
     puzzle_url = f"https://adventofcode.com/2019/day/{day}"
     puzzle_input = "some text"
     responses.add(responses.GET, puzzle_url + "/input", body=puzzle_input)
@@ -105,7 +121,7 @@ def test_begin_specify_day(day, webbrowser_open, runner, config_dir, responses):
         assert (p / f"{day:02}/input.txt").exists()
         assert (p / f"{day:02}/solution.py").exists()
         # Input is cached
-        assert (config_dir / cookie / f"2019/{day:02}.txt").exists()
+        assert (cache_dir / cookie / f"2019/{day:02}.txt").exists()
         # Browser opened to today's puzzle URL
         webbrowser_open.assert_called_once_with(puzzle_url)
 
@@ -113,7 +129,7 @@ def test_begin_specify_day(day, webbrowser_open, runner, config_dir, responses):
 @freeze_time(datetime(2019, 12, 25, hour=1, tzinfo=pytz.timezone("America/New_York")))
 @given(st.integers().filter(lambda x: not (1 <= x <= 25)))
 def test_begin_specify_fails_if_out_of_range(
-    webbrowser_open, runner, config_dir, day
+        webbrowser_open, runner, config_dir, day
 ):
     cookie = "12345"
     with runner.isolated_filesystem():
@@ -123,7 +139,7 @@ def test_begin_specify_fails_if_out_of_range(
 
 @freeze_time(datetime(2019, 12, 10, hour=1, tzinfo=pytz.timezone("America/New_York")))
 @pytest.mark.parametrize("year", range(2015, 2019))
-def test_begin_specify_year(year, webbrowser_open, runner, config_dir, responses):
+def test_begin_specify_year(year, webbrowser_open, runner, cache_dir, responses):
     day = 10  # matches frozen datetime
     puzzle_url = f"https://adventofcode.com/{year}/day/{day}"
     puzzle_input = "some text"
@@ -136,7 +152,7 @@ def test_begin_specify_year(year, webbrowser_open, runner, config_dir, responses
         assert (p / f"{day:02}/input.txt").exists()
         assert (p / f"{day:02}/solution.py").exists()
         # Input is cached
-        assert (config_dir / cookie / f"{year}/{day:02}.txt").exists()
+        assert (cache_dir / cookie / f"{year}/{day:02}.txt").exists()
         # Browser opened to today's puzzle URL
         webbrowser_open.assert_called_once_with(puzzle_url)
 
@@ -144,7 +160,7 @@ def test_begin_specify_year(year, webbrowser_open, runner, config_dir, responses
 @pytest.mark.parametrize("day", range(1, 25))
 @pytest.mark.parametrize("year", range(2015, 2019))
 def test_begin_specify_day_and_year(
-    year, day, webbrowser_open, runner, config_dir, responses
+        year, day, webbrowser_open, runner, cache_dir, responses
 ):
     puzzle_url = f"https://adventofcode.com/{year}/day/{day}"
     puzzle_input = "some text"
@@ -157,13 +173,13 @@ def test_begin_specify_day_and_year(
         assert (p / f"{day:02}/input.txt").exists()
         assert (p / f"{day:02}/solution.py").exists()
         # Input is cached
-        assert (config_dir / cookie / f"{year}/{day:02}.txt").exists()
+        assert (cache_dir / cookie / f"{year}/{day:02}.txt").exists()
         # Browser opened to today's puzzle URL
         webbrowser_open.assert_called_once_with(puzzle_url)
 
 
 @freeze_time(datetime(2019, 12, 10, hour=1, tzinfo=pytz.timezone("America/New_York")))
-def test_begin_cached_input(webbrowser_open, runner, config_dir, responses):
+def test_begin_cached_input(webbrowser_open, runner, cache_dir, responses):
     """
     Puzzle input should not be fetched from https://adventofcode.com if it has
     been cached locally. Here, the `responses` fixture will raise an exception if
@@ -171,9 +187,9 @@ def test_begin_cached_input(webbrowser_open, runner, config_dir, responses):
     """
     puzzle_url = "https://adventofcode.com/2019/day/10"
     cookie = "12345"
-    cache_dir = config_dir / cookie / "2019"
-    cache_dir.mkdir(parents=True)
-    with (cache_dir / "10.txt").open("w") as f:
+    year_cache_dir = cache_dir / cookie / "2019"
+    year_cache_dir.mkdir(parents=True)
+    with (year_cache_dir / "10.txt").open("w") as f:
         f.write("some text")
     with runner.isolated_filesystem() as p:
         result = runner.invoke(cli, ["begin", "-c", cookie])