code-battles-cli 0.1.1__tar.gz → 1.1.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: code-battles-cli
3
- Version: 0.1.1
3
+ Version: 1.1.0
4
4
  Summary: A CLI for competitors in any Code Battles event!
5
5
  License-File: LICENSE.md
6
6
  Requires-Python: >=3.8
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
16
16
  <h1 align="center">
17
17
  Code Battles CLI
18
18
  <br />
19
+ <img src="https://img.shields.io/pypi/v/code-battles-cli">
19
20
  <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
20
21
  <img src="https://img.shields.io/badge/license-MIT-blue.svg">
21
22
  </h1>
@@ -34,7 +35,7 @@ Description-Content-Type: text/markdown
34
35
  # Getting Started
35
36
 
36
37
  - Install the library by running `pip install code-battles-cli`.
37
- - Make sure you have the Python scripts folder in your path by running `code-battles --help`.
38
+ - Make sure you have the Python scripts folder in your `PATH` by running `code-battles --help`.
38
39
  - If that fails, run `py -m code_battles_cli --help`.
39
40
 
40
41
  The first time you run any command you will be asked for your URL, username and password, and they'll be saved in a `code-battles.json` file for successive calls.
@@ -51,7 +52,13 @@ You can update a bot in the website by running `code-battles upload bots/example
51
52
 
52
53
  If you want to choose a custom bot name, you can run `code-battles upload bots/example.py -n myamazingbot`.
53
54
 
54
- ## Usage in Scripts
55
+ ## Running a simulation
56
+
57
+ You can run a simulation by running `code-battles run NYC bots/example.py bots/another_example.py`, where `NYC` is an example of a map.
58
+
59
+ You will probably find the scripting API nicer to work with if you want to run simulations locally.
60
+
61
+ ## Usage in scripts
55
62
 
56
63
  You can import the `code_battles_cli.api` module and utilize its `Client` class to hack Code Battles for your needs!
57
64
 
@@ -63,11 +70,12 @@ from code_battles_cli.api import Client
63
70
  client = Client()
64
71
 
65
72
  print(client.get_bots())
73
+ print(client.run_simulation("NYC", ["bots/example.py", "bots/another_example.py"]))
66
74
  ```
67
75
 
68
76
  # Roadmap
69
77
 
70
78
  - [x] Download bots.
71
79
  - [x] Upload bots.
72
- - [ ] Run no-UI simulations locally.
80
+ - [x] Run no-UI simulations locally.
73
81
  - [x] Be usable in scripts.
@@ -4,6 +4,7 @@
4
4
  <h1 align="center">
5
5
  Code Battles CLI
6
6
  <br />
7
+ <img src="https://img.shields.io/pypi/v/code-battles-cli">
7
8
  <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
8
9
  <img src="https://img.shields.io/badge/license-MIT-blue.svg">
9
10
  </h1>
@@ -22,7 +23,7 @@
22
23
  # Getting Started
23
24
 
24
25
  - Install the library by running `pip install code-battles-cli`.
25
- - Make sure you have the Python scripts folder in your path by running `code-battles --help`.
26
+ - Make sure you have the Python scripts folder in your `PATH` by running `code-battles --help`.
26
27
  - If that fails, run `py -m code_battles_cli --help`.
27
28
 
28
29
  The first time you run any command you will be asked for your URL, username and password, and they'll be saved in a `code-battles.json` file for successive calls.
@@ -39,7 +40,13 @@ You can update a bot in the website by running `code-battles upload bots/example
39
40
 
40
41
  If you want to choose a custom bot name, you can run `code-battles upload bots/example.py -n myamazingbot`.
41
42
 
42
- ## Usage in Scripts
43
+ ## Running a simulation
44
+
45
+ You can run a simulation by running `code-battles run NYC bots/example.py bots/another_example.py`, where `NYC` is an example of a map.
46
+
47
+ You will probably find the scripting API nicer to work with if you want to run simulations locally.
48
+
49
+ ## Usage in scripts
43
50
 
44
51
  You can import the `code_battles_cli.api` module and utilize its `Client` class to hack Code Battles for your needs!
45
52
 
@@ -51,11 +58,12 @@ from code_battles_cli.api import Client
51
58
  client = Client()
52
59
 
53
60
  print(client.get_bots())
61
+ print(client.run_simulation("NYC", ["bots/example.py", "bots/another_example.py"]))
54
62
  ```
55
63
 
56
64
  # Roadmap
57
65
 
58
66
  - [x] Download bots.
59
67
  - [x] Upload bots.
60
- - [ ] Run no-UI simulations locally.
68
+ - [x] Run no-UI simulations locally.
61
69
  - [x] Be usable in scripts.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "code-battles-cli"
3
- version = "0.1.1"
3
+ version = "1.1.0"
4
4
  description = "A CLI for competitors in any Code Battles event!"
5
5
  dependencies = [
6
6
  "requests>=2.32.3",
@@ -0,0 +1,347 @@
1
+ """
2
+ Code Battles Python Client API
3
+
4
+ Firestore client implementation inspired by https://medium.com/@bobthomas295/client-side-authentication-with-python-firestore-and-firebase-352e484a2634
5
+ """
6
+
7
+ import base64
8
+ import datetime
9
+ import logging
10
+ import gzip
11
+ import os
12
+ import json
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ from typing import Any, Callable, Dict, List, Optional, Union
17
+ from dataclasses import dataclass
18
+
19
+ import requests
20
+ from google.oauth2.credentials import Credentials
21
+ from google.cloud.firestore import Client as FirestoreClient
22
+ from rich.prompt import Prompt
23
+ from code_battles_cli.log import console, log, progress
24
+
25
+
26
+ SIMULATION_FINISHED_MARK = b"--- SIMULATION FINISHED ---"
27
+ SIMULATION_STEP_MARK = b"__CODE_BATTLES_ADVANCE_STEP"
28
+
29
+
30
+ @dataclass
31
+ class LogEntry:
32
+ step: int
33
+ text: str
34
+ color: str
35
+ player_index: Optional[int]
36
+
37
+
38
+ @dataclass
39
+ class SimulationResults:
40
+ winner_index: int
41
+ winner: str
42
+ steps: int
43
+ logs: List[LogEntry]
44
+
45
+
46
+ @dataclass
47
+ class Simulation:
48
+ map: str
49
+ player_names: str
50
+ game: str
51
+ version: str
52
+ timestamp: datetime.datetime
53
+ logs: list
54
+ decisions: List[bytes]
55
+ seed: int
56
+
57
+ def dump(self):
58
+ return base64.b64encode(
59
+ gzip.compress(
60
+ json.dumps(
61
+ {
62
+ "map": self.map,
63
+ "playerNames": self.player_names,
64
+ "game": self.game,
65
+ "version": self.version,
66
+ "timestamp": self.timestamp.isoformat(),
67
+ "logs": self.logs,
68
+ "decisions": [
69
+ base64.b64encode(decision).decode()
70
+ for decision in self.decisions
71
+ ],
72
+ "seed": self.seed,
73
+ }
74
+ ).encode()
75
+ )
76
+ ).decode()
77
+
78
+ @staticmethod
79
+ def load(file: str):
80
+ contents: Dict[str, Any] = json.loads(gzip.decompress(base64.b64decode(file)))
81
+ return Simulation(
82
+ contents["map"],
83
+ contents["playerNames"],
84
+ contents["game"],
85
+ contents["version"],
86
+ datetime.datetime.fromisoformat(contents["timestamp"]),
87
+ contents["logs"],
88
+ [base64.b64decode(decision) for decision in contents["decisions"]],
89
+ contents["seed"],
90
+ )
91
+
92
+
93
+ class Client:
94
+ def __init__(
95
+ self,
96
+ url: Optional[str] = None,
97
+ username: Optional[str] = None,
98
+ password: Optional[str] = None,
99
+ dump_credentials=True,
100
+ ):
101
+ """
102
+ Creates a client for getting and setting the bots for a Code Battles hosted at `url`
103
+ and signing in as `username` with `password`.
104
+ """
105
+
106
+ if url is None or username is None or password is None:
107
+ self._get_credentials()
108
+ else:
109
+ self.url = url
110
+ self.username = username
111
+ self.password = password
112
+
113
+ self._get_firebase_data()
114
+ self._sign_in()
115
+
116
+ if dump_credentials:
117
+ self._dump_credentials()
118
+
119
+ def _get_credentials(self):
120
+ if os.path.exists("code-battles.json"):
121
+ try:
122
+ with open("code-battles.json", "r") as f:
123
+ configuration = json.load(f)
124
+ self.url: str = configuration["url"]
125
+ self.username: str = configuration["username"]
126
+ self.password: str = configuration["password"]
127
+ except Exception:
128
+ pass
129
+
130
+ if (
131
+ not hasattr(self, "url")
132
+ or not hasattr(self, "username")
133
+ or not hasattr(self, "password")
134
+ ):
135
+ self.url = Prompt.ask("Enter your competition's URL", console=console)
136
+
137
+ if not self.url.startswith("https://"):
138
+ log.warning("Your URL should most likely start with 'https://'.")
139
+ if not self.url.endswith(".web.app"):
140
+ log.warning("Your URL should most likely end with '.web.app'.")
141
+
142
+ self.username = Prompt.ask("Enter your team's username", console=console)
143
+
144
+ if not self.username == self.username.lower():
145
+ log.warning("Your username should most likely be lowercased.")
146
+
147
+ self.password = Prompt.ask(
148
+ "Enter your team's password", console=console, password=True
149
+ )
150
+
151
+ def _dump_credentials(self):
152
+ with open("code-battles.json", "w") as f:
153
+ json.dump(
154
+ {"url": self.url, "username": self.username, "password": self.password},
155
+ f,
156
+ )
157
+ log.info(
158
+ "Credentials were dumped to `code-battles.json`. Make sure other teams don't have access to this file!"
159
+ )
160
+
161
+ def _get_firebase_data(self):
162
+ configuration = requests.get(self.url + "/firebase-configuration.json").json()
163
+ self.firebase_api_key: str = configuration["apiKey"]
164
+ self.firebase_project_id: str = configuration["projectId"]
165
+ # print(
166
+ # f"Got API Key: '{self.firebase_api_key}' and Project ID: '{self.firebase_project_id}'"
167
+ # )
168
+
169
+ def _sign_in(self, email_domain="gmail.com"):
170
+ try:
171
+ response = requests.post(
172
+ f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={self.firebase_api_key}",
173
+ json={
174
+ "email": self.username + "@" + email_domain,
175
+ "password": self.password,
176
+ "returnSecureToken": True,
177
+ },
178
+ ).json()
179
+ except Exception:
180
+ raise Exception(
181
+ "Sign in failed! Make sure the username and password are correct."
182
+ )
183
+
184
+ self.credentials = Credentials(response["idToken"], response["refreshToken"])
185
+ self.client = FirestoreClient(self.firebase_project_id, self.credentials)
186
+ self.document = self.client.document(f"bots/{self.username}")
187
+
188
+ def get_bots(self) -> Dict[str, str]:
189
+ """Returns a mapping from a bot's name to their Python code."""
190
+ return self.document.get().to_dict()
191
+
192
+ def set_bots(self, bots: Dict[str, str], merge=True) -> None:
193
+ """
194
+ Sets the bots in the website to the specified bots.
195
+ Doesn't remove any bot unless ``merge`` is ``False``, in which case only bots specified in ``bots`` will remain.
196
+ """
197
+ self.document.set(bots, merge)
198
+
199
+ def _possibly_download(self, force_download=False):
200
+ code_directory = os.path.expanduser(
201
+ f"~/.cache/code-battles/{''.join([c for c in self.url.removeprefix("https").removesuffix(".web.app") if c.isalnum()])}"
202
+ )
203
+
204
+ if force_download and os.path.exists(code_directory):
205
+ shutil.rmtree(code_directory)
206
+
207
+ if not os.path.exists(code_directory):
208
+ os.makedirs(code_directory)
209
+
210
+ with console.status("[blue]Fetching configuration from game website..."):
211
+ pyscript_configuration = requests.get(self.url + "/config.json").json()
212
+ logging.info(
213
+ f"Fetched configuration containing {len(pyscript_configuration["files"])} game files."
214
+ )
215
+
216
+ with progress:
217
+ progress_id = progress.add_task(
218
+ "[blue]Fetching game files...",
219
+ total=len(pyscript_configuration["files"]),
220
+ )
221
+
222
+ for url_path, path in pyscript_configuration["files"].items():
223
+ if not path.endswith(".py"):
224
+ continue
225
+
226
+ local_path = os.path.join(code_directory, *path.split("/"))
227
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
228
+ text = requests.get(self.url + url_path).text
229
+ with open(local_path, "w") as f:
230
+ f.write(text)
231
+ progress.update(progress_id, advance=1)
232
+ progress.update(progress_id, visible=False)
233
+
234
+ logging.info("Finished fetching game files.")
235
+
236
+ return code_directory
237
+
238
+ def _get_simulation_output(
239
+ self,
240
+ p: subprocess.Popen,
241
+ json_output=False,
242
+ on_step: Optional[Callable[[], None]] = None,
243
+ ) -> Union[SimulationResults, str]:
244
+ while True:
245
+ line: bytes = p.stdout.readline()
246
+ line = line.strip()
247
+ if line == SIMULATION_FINISHED_MARK:
248
+ break
249
+ elif line == SIMULATION_STEP_MARK:
250
+ if on_step is not None:
251
+ on_step()
252
+ elif len(line) != 0:
253
+ logging.info(line.decode())
254
+
255
+ output: bytes = p.stdout.read()
256
+ if json_output:
257
+ return output.decode()
258
+
259
+ output = json.loads(output)
260
+ return SimulationResults(
261
+ output["winner_index"],
262
+ output["winner"],
263
+ output["steps"],
264
+ [
265
+ LogEntry(
266
+ entry["step"], entry["text"], entry["color"], entry["player_index"]
267
+ )
268
+ for entry in output["logs"]
269
+ ],
270
+ )
271
+
272
+ def run_simulation(
273
+ self,
274
+ map: str,
275
+ bot_filenames: List[str],
276
+ bot_names: Optional[List[str]] = None,
277
+ seed: Optional[int] = None,
278
+ force_download=False,
279
+ json_output=False,
280
+ on_step: Optional[Callable[[], None]] = None,
281
+ output_file: Optional[str] = None,
282
+ ) -> Union[SimulationResults, str]:
283
+ """
284
+ Runs the given simulation without UI locally.
285
+ If ``bot_names`` is not specified, they will be the filenames without the extension.
286
+
287
+ If required (or ``force_download``), this method downloads the simulation code from the website.
288
+
289
+ If ``json_output`` is ``True``, returns the JSON string of the results instead.
290
+ """
291
+
292
+ if bot_names is None:
293
+ bot_names = [
294
+ os.path.splitext(os.path.basename(filename))[0]
295
+ for filename in bot_filenames
296
+ ]
297
+
298
+ code_directory = self._possibly_download(force_download=force_download)
299
+
300
+ p = subprocess.Popen(
301
+ [
302
+ sys.executable,
303
+ os.path.join(code_directory, "main.py"),
304
+ "simulate",
305
+ str(seed),
306
+ str(output_file),
307
+ map,
308
+ "-".join(bot_names),
309
+ ]
310
+ + [os.path.abspath(f) for f in bot_filenames],
311
+ env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
312
+ stdout=subprocess.PIPE,
313
+ stderr=subprocess.PIPE,
314
+ )
315
+
316
+ return self._get_simulation_output(p, json_output, on_step)
317
+
318
+ def run_simulation_from_file(
319
+ self,
320
+ simulation_file: str,
321
+ force_download=False,
322
+ json_output=False,
323
+ on_step: Optional[Callable[[], None]] = None,
324
+ ) -> Union[SimulationResults, str]:
325
+ """
326
+ Runs the given simulation without UI locally from the given simulation file.
327
+
328
+ If required (or ``force_download``), this method downloads the simulation code from the website.
329
+
330
+ If ``json_output`` is ``True``, returns the JSON string of the results instead.
331
+ """
332
+
333
+ code_directory = self._possibly_download(force_download=force_download)
334
+
335
+ p = subprocess.Popen(
336
+ [
337
+ sys.executable,
338
+ os.path.join(code_directory, "main.py"),
339
+ "simulate-from-file",
340
+ simulation_file,
341
+ ],
342
+ env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
343
+ stdout=subprocess.PIPE,
344
+ stderr=subprocess.PIPE,
345
+ )
346
+
347
+ return self._get_simulation_output(p, json_output, on_step)
@@ -4,20 +4,23 @@ import click
4
4
 
5
5
 
6
6
  @click.group()
7
+ @click.version_option(message="%(prog)s %(version)s")
7
8
  def cli():
8
9
  pass
9
10
 
10
11
 
11
12
  def main() -> int:
12
- from code_battles_cli.logging import setup_logging, log
13
+ from code_battles_cli.log import setup_logging, log
13
14
 
14
15
  setup_logging()
15
16
 
16
17
  from code_battles_cli.commands.download import download
17
18
  from code_battles_cli.commands.upload import upload
19
+ from code_battles_cli.commands.run import run
18
20
 
19
21
  cli.add_command(download)
20
22
  cli.add_command(upload)
23
+ cli.add_command(run)
21
24
  cli(standalone_mode=False)
22
25
 
23
26
  return 0
@@ -4,7 +4,7 @@ from typing import Optional
4
4
  import click
5
5
 
6
6
  from code_battles_cli.api import Client
7
- from code_battles_cli.logging import log
7
+ from code_battles_cli.log import log
8
8
 
9
9
 
10
10
  @click.command()
@@ -15,6 +15,7 @@ from code_battles_cli.logging import log
15
15
  @click.option("-p", "--password", help="Your team's password")
16
16
  @click.option(
17
17
  "--dump-credentials",
18
+ is_flag=True,
18
19
  default=True,
19
20
  show_default=True,
20
21
  help="Dump the URL, username and password to a `code-battles.json` file",
@@ -0,0 +1,114 @@
1
+ import time
2
+ import logging
3
+ import sys
4
+ from typing import List, Optional, Tuple
5
+
6
+ import click
7
+
8
+ from code_battles_cli.api import Client, Simulation
9
+ from code_battles_cli.log import log, progress
10
+
11
+
12
+ @click.command()
13
+ @click.argument("map", required=False)
14
+ @click.argument("bots", nargs=-1)
15
+ @click.option(
16
+ "-U", "--url", help="The competitions URL, for example https://code-battles.web.app"
17
+ )
18
+ @click.option("-u", "--username", help="Your team's username, for example mercedes")
19
+ @click.option("-p", "--password", help="Your team's password")
20
+ @click.option(
21
+ "--dump-credentials",
22
+ is_flag=True,
23
+ default=True,
24
+ show_default=True,
25
+ help="Dump the URL, username and password to a `code-battles.json` file",
26
+ )
27
+ @click.option(
28
+ "--force-download",
29
+ is_flag=True,
30
+ default=False,
31
+ show_default=True,
32
+ help="Re-downloads the simulation code from the website",
33
+ )
34
+ @click.option(
35
+ "-s", "--seed", type=int, help="Sets the given randomness seed for the simulation"
36
+ )
37
+ @click.option(
38
+ "-S",
39
+ "--simulation-file",
40
+ type=click.Path(exists=True, dir_okay=False),
41
+ help="Re-downloads the simulation code from the website",
42
+ )
43
+ @click.option(
44
+ "-O",
45
+ "output_file",
46
+ type=click.Path(exists=False),
47
+ help="Output file to dump the simulation into",
48
+ )
49
+ def run(
50
+ url: Optional[str],
51
+ username: Optional[str],
52
+ password: Optional[str],
53
+ dump_credentials: bool,
54
+ map: str,
55
+ bots: Tuple[str],
56
+ force_download: bool,
57
+ seed: Optional[int],
58
+ simulation_file: Optional[str],
59
+ output_file: Optional[str],
60
+ ):
61
+ logging.getLogger("rich").setLevel(logging.WARNING)
62
+ client = Client(url, username, password, dump_credentials)
63
+ client._possibly_download(force_download)
64
+ start = time.time()
65
+
66
+ with progress:
67
+ progress_id = progress.add_task("[green]Simulating...", total=None)
68
+
69
+ def on_step():
70
+ progress.update(progress_id, advance=1)
71
+
72
+ if simulation_file is not None:
73
+ assert (
74
+ map is None
75
+ ), "The simulation's map is given from the simulation file!"
76
+ assert (
77
+ len(bots) == 0
78
+ ), "The simulation's bots are given from the simulation file!"
79
+ assert (
80
+ seed is None
81
+ ), "The simulation's seed is given from the simulation file!"
82
+ assert (
83
+ output_file is None
84
+ ), "Shouldn't dump simualtion file from existing simulation file!"
85
+
86
+ with open(simulation_file, "r") as f:
87
+ simulation_text = f.read()
88
+ simulation = Simulation.load(simulation_text)
89
+ logging.info(
90
+ f"Detected simulation file of {simulation.game} {simulation.version}"
91
+ )
92
+ logging.info(
93
+ f"Simulation happened in {simulation.map} between {', '.join(simulation.player_names)} with seed {simulation.seed}"
94
+ )
95
+ logging.info(f"Simulation contains {len(simulation.decisions)} decisions")
96
+ progress.update(progress_id, total=len(simulation.decisions) - 1)
97
+ results = client.run_simulation_from_file(
98
+ simulation_file, force_download=False, on_step=on_step
99
+ )
100
+ else:
101
+ assert map is not None, "Map is required for simulation!"
102
+ assert len(bots) > 0, "Bots are required for simulation!"
103
+
104
+ results = client.run_simulation(
105
+ map,
106
+ list(bots),
107
+ seed=seed,
108
+ force_download=False,
109
+ on_step=on_step,
110
+ output_file=output_file,
111
+ )
112
+ print("The winner is", results.winner, "after", results.steps, "steps.")
113
+ end = time.time()
114
+ log.info(f"Simulation took {(end - start) :.2f}s.")
@@ -4,7 +4,7 @@ from typing import Optional
4
4
  import click
5
5
 
6
6
  from code_battles_cli.api import Client
7
- from code_battles_cli.logging import log
7
+ from code_battles_cli.log import log
8
8
 
9
9
 
10
10
  @click.command()
@@ -15,6 +15,7 @@ from code_battles_cli.logging import log
15
15
  @click.option("-p", "--password", help="Your team's password")
16
16
  @click.option(
17
17
  "--dump-credentials",
18
+ is_flag=True,
18
19
  default=True,
19
20
  show_default=True,
20
21
  help="Dump the URL, username and password to a `code-battles.json` file",
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import logging
2
3
 
3
4
  from rich.console import Console
@@ -25,7 +26,7 @@ def setup_logging():
25
26
  FORMAT = "%(message)s"
26
27
 
27
28
  logging.basicConfig(
28
- level=logging.INFO,
29
+ level=os.environ.get("CODE_BATTLES_LOG_LEVEL", "INFO"),
29
30
  format=FORMAT,
30
31
  datefmt="[%X]",
31
32
  handlers=[RichHandler(console=console, markup=True)],
@@ -1,122 +0,0 @@
1
- """
2
- Code Battles Python Client API
3
-
4
- Firestore client implementation inspired by https://medium.com/@bobthomas295/client-side-authentication-with-python-firestore-and-firebase-352e484a2634
5
- """
6
-
7
- import os
8
- import json
9
- from typing import Dict, List, Optional
10
-
11
- import requests
12
- from google.oauth2.credentials import Credentials
13
- from google.cloud.firestore import Client as FirestoreClient
14
- from rich.prompt import Prompt
15
- from code_battles_cli.logging import console, log
16
-
17
-
18
- class Client:
19
- def __init__(
20
- self,
21
- url: Optional[str] = None,
22
- username: Optional[str] = None,
23
- password: Optional[str] = None,
24
- dump_credentials=True,
25
- ):
26
- """
27
- Creates a client for getting and setting the bots for a Code Battles hosted at `url`
28
- and signing in as `username` with `password`.
29
- """
30
-
31
- if url is None or username is None or password is None:
32
- self._get_credentials()
33
- else:
34
- self.url = url
35
- self.username = username
36
- self.password = password
37
-
38
- self._get_firebase_data()
39
- self._sign_in()
40
-
41
- if dump_credentials:
42
- self._dump_credentials()
43
-
44
- def _get_credentials(self):
45
- if os.path.exists("code-battles.json"):
46
- try:
47
- with open("code-battles.json", "r") as f:
48
- configuration = json.load(f)
49
- self.url: str = configuration["url"]
50
- self.username: str = configuration["username"]
51
- self.password: str = configuration["password"]
52
- except Exception:
53
- pass
54
-
55
- if (
56
- not hasattr(self, "url")
57
- or not hasattr(self, "username")
58
- or not hasattr(self, "password")
59
- ):
60
- self.url = Prompt.ask("Enter your competition's URL", console=console)
61
-
62
- if not self.url.startswith("https://"):
63
- log.warning("Your URL should most likely start with 'https://'.")
64
- if not self.url.endswith(".web.app"):
65
- log.warning("Your URL should most likely end with '.web.app'.")
66
-
67
- self.username = Prompt.ask("Enter your team's username", console=console)
68
-
69
- if not self.username == self.username.lower():
70
- log.warning("Your username should most likely be lowercased.")
71
-
72
- self.password = Prompt.ask(
73
- "Enter your team's password", console=console, password=True
74
- )
75
-
76
- def _dump_credentials(self):
77
- with open("code-battles.json", "w") as f:
78
- json.dump(
79
- {"url": self.url, "username": self.username, "password": self.password},
80
- f,
81
- )
82
- log.info(
83
- "Credentials were dumped to `code-battles.json`. Make sure other teams don't have access to this file!"
84
- )
85
-
86
- def _get_firebase_data(self):
87
- configuration = requests.get(self.url + "/firebase-configuration.json").json()
88
- self.firebase_api_key: str = configuration["apiKey"]
89
- self.firebase_project_id: str = configuration["projectId"]
90
- # print(
91
- # f"Got API Key: '{self.firebase_api_key}' and Project ID: '{self.firebase_project_id}'"
92
- # )
93
-
94
- def _sign_in(self, email_domain="gmail.com"):
95
- try:
96
- response = requests.post(
97
- f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={self.firebase_api_key}",
98
- json={
99
- "email": self.username + "@" + email_domain,
100
- "password": self.password,
101
- "returnSecureToken": True,
102
- },
103
- ).json()
104
- except Exception:
105
- raise Exception(
106
- "Sign in failed! Make sure the username and password are correct."
107
- )
108
-
109
- self.credentials = Credentials(response["idToken"], response["refreshToken"])
110
- self.client = FirestoreClient(self.firebase_project_id, self.credentials)
111
- self.document = self.client.document(f"bots/{self.username}")
112
-
113
- def get_bots(self) -> Dict[str, str]:
114
- """Returns a mapping from a bot's name to their Python code."""
115
- return self.document.get().to_dict()
116
-
117
- def set_bots(self, bots: Dict[str, str], merge=True) -> None:
118
- """
119
- Sets the bots in the website to the specified bots.
120
- Doesn't remove any bot unless `merge` is `False`, in which case only bots specified in `bots` will remain.
121
- """
122
- self.document.set(bots, merge)