code-battles-cli 0.1.1__tar.gz → 1.0.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.0.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,6 +70,7 @@ 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
@@ -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,6 +58,7 @@ 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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "code-battles-cli"
3
- version = "0.1.1"
3
+ version = "1.0.0"
4
4
  description = "A CLI for competitors in any Code Battles event!"
5
5
  dependencies = [
6
6
  "requests>=2.32.3",
@@ -6,13 +6,33 @@ Firestore client implementation inspired by https://medium.com/@bobthomas295/cli
6
6
 
7
7
  import os
8
8
  import json
9
- from typing import Dict, List, Optional
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ from typing import Dict, List, Optional, Union
13
+ from dataclasses import dataclass
10
14
 
11
15
  import requests
12
16
  from google.oauth2.credentials import Credentials
13
17
  from google.cloud.firestore import Client as FirestoreClient
14
18
  from rich.prompt import Prompt
15
- from code_battles_cli.logging import console, log
19
+ from code_battles_cli.log import console, log
20
+
21
+
22
+ @dataclass
23
+ class LogEntry:
24
+ step: int
25
+ text: str
26
+ color: str
27
+ player_index: Optional[int]
28
+
29
+
30
+ @dataclass
31
+ class SimulationResults:
32
+ winner_index: int
33
+ winner: str
34
+ steps: int
35
+ logs: List[LogEntry]
16
36
 
17
37
 
18
38
  class Client:
@@ -117,6 +137,79 @@ class Client:
117
137
  def set_bots(self, bots: Dict[str, str], merge=True) -> None:
118
138
  """
119
139
  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.
140
+ Doesn't remove any bot unless ``merge`` is ``False``, in which case only bots specified in ``bots`` will remain.
121
141
  """
122
142
  self.document.set(bots, merge)
143
+
144
+ def run_simulation(
145
+ self,
146
+ map: str,
147
+ bot_filenames: List[str],
148
+ bot_names: Optional[List[str]] = None,
149
+ force_download=False,
150
+ json_output=False,
151
+ ) -> Union[SimulationResults, str]:
152
+ """
153
+ Runs the given simulation without UI locally.
154
+ If ``bot_names`` is not specified, they will be the filenames without the extension.
155
+
156
+ If required (or ``force_download``), this method downloads the simulation code from the website.
157
+
158
+ If ``json_output`` is ``True``, returns the JSON string of the results instead.
159
+ """
160
+
161
+ if bot_names is None:
162
+ bot_names = [
163
+ os.path.splitext(os.path.basename(filename))[0]
164
+ for filename in bot_filenames
165
+ ]
166
+
167
+ code_directory = os.path.expanduser(
168
+ f"~/.cache/code-battles/{''.join([c for c in self.url.removeprefix("https").removesuffix(".web.app") if c.isalnum()])}"
169
+ )
170
+
171
+ if force_download and os.path.exists(code_directory):
172
+ shutil.rmtree(code_directory)
173
+
174
+ if not os.path.exists(code_directory):
175
+ os.makedirs(code_directory)
176
+
177
+ pyscript_configuration = requests.get(self.url + "/config.json").json()
178
+
179
+ for url_path, path in pyscript_configuration["files"].items():
180
+ local_path = os.path.join(
181
+ code_directory, *path.replace(".pyi", ".py").split("/")
182
+ )
183
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
184
+ text = requests.get(self.url + url_path).text
185
+ with open(local_path, "w") as f:
186
+ f.write(text)
187
+
188
+ p = subprocess.Popen(
189
+ [
190
+ sys.executable,
191
+ os.path.join(code_directory, "main.py"),
192
+ map,
193
+ "-".join(bot_names),
194
+ ]
195
+ + [os.path.abspath(f) for f in bot_filenames],
196
+ env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
197
+ stdout=subprocess.PIPE,
198
+ )
199
+ p.wait()
200
+ output = p.stdout.read().split(b"--- SIMULATION FINISHED ---")[-1]
201
+ if json_output:
202
+ return output.decode()
203
+
204
+ output = json.loads(output)
205
+ return SimulationResults(
206
+ output["winner_index"],
207
+ output["winner"],
208
+ output["steps"],
209
+ [
210
+ LogEntry(
211
+ entry["step"], entry["text"], entry["color"], entry["player_index"]
212
+ )
213
+ for entry in output["logs"]
214
+ ],
215
+ )
@@ -9,15 +9,17 @@ def cli():
9
9
 
10
10
 
11
11
  def main() -> int:
12
- from code_battles_cli.logging import setup_logging, log
12
+ from code_battles_cli.log import setup_logging, log
13
13
 
14
14
  setup_logging()
15
15
 
16
16
  from code_battles_cli.commands.download import download
17
17
  from code_battles_cli.commands.upload import upload
18
+ from code_battles_cli.commands.run import run
18
19
 
19
20
  cli.add_command(download)
20
21
  cli.add_command(upload)
22
+ cli.add_command(run)
21
23
  cli(standalone_mode=False)
22
24
 
23
25
  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()
@@ -0,0 +1,46 @@
1
+ import time
2
+ import logging
3
+ from typing import List, Optional, Tuple
4
+
5
+ import click
6
+
7
+ from code_battles_cli.api import Client
8
+ from code_battles_cli.log import log
9
+
10
+
11
+ @click.command()
12
+ @click.argument("map")
13
+ @click.argument("bots", nargs=-1)
14
+ @click.option(
15
+ "-U", "--url", help="The competitions URL, for example https://code-battles.web.app"
16
+ )
17
+ @click.option("-u", "--username", help="Your team's username, for example mercedes")
18
+ @click.option("-p", "--password", help="Your team's password")
19
+ @click.option(
20
+ "--dump-credentials",
21
+ default=True,
22
+ show_default=True,
23
+ help="Dump the URL, username and password to a `code-battles.json` file",
24
+ )
25
+ @click.option(
26
+ "--force-download",
27
+ default=False,
28
+ show_default=True,
29
+ help="Re-downloads the simulation code from the website",
30
+ )
31
+ def run(
32
+ url: Optional[str],
33
+ username: Optional[str],
34
+ password: Optional[str],
35
+ dump_credentials: bool,
36
+ map: str,
37
+ bots: Tuple[str],
38
+ force_download: bool,
39
+ ):
40
+ logging.getLogger("rich").setLevel(logging.WARNING)
41
+ client = Client(url, username, password, dump_credentials)
42
+ start = time.time()
43
+ results = client.run_simulation(map, list(bots), force_download=force_download)
44
+ print("The winner is", results.winner, "after", results.steps, "steps.")
45
+ end = time.time()
46
+ 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()