code-battles-cli 1.1.1__tar.gz → 1.2.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.
Files changed (19) hide show
  1. code_battles_cli-1.2.0/.github/workflows/test.yml +21 -0
  2. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/PKG-INFO +2 -2
  3. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/README.md +1 -1
  4. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/pyproject.toml +2 -2
  5. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/requirements-dev.lock +7 -0
  6. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/api.py +52 -44
  7. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/run.py +19 -18
  8. code_battles_cli-1.2.0/tests/test_cli.py +2 -0
  9. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/.gitignore +0 -0
  10. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/.python-version +0 -0
  11. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/LICENSE.md +0 -0
  12. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/requirements.lock +0 -0
  13. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/__init__.py +0 -0
  14. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/__main__.py +0 -0
  15. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/cli.py +0 -0
  16. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/__init__.py +0 -0
  17. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/download.py +0 -0
  18. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/upload.py +0 -0
  19. {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/log.py +0 -0
@@ -0,0 +1,21 @@
1
+ name: Test
2
+ on: [push]
3
+ jobs:
4
+ test:
5
+ name: Test
6
+ strategy:
7
+ matrix:
8
+ version: ["3.8", "3.12"]
9
+ os: [ubuntu-latest, windows-latest, macos-latest]
10
+ runs-on: ${{ matrix.os }}
11
+ steps:
12
+ - name: Clone the repository
13
+ uses: actions/checkout@v4
14
+ - name: Install the latest version of rye
15
+ uses: eifinger/setup-rye@v4
16
+ - name: Pin Python version
17
+ run: rye pin ${{ matrix.version }}
18
+ - name: Sync rye
19
+ run: rye sync
20
+ - name: Run tests
21
+ run: rye test
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: code-battles-cli
3
- Version: 1.1.1
3
+ Version: 1.2.0
4
4
  Summary: A CLI for competitors in any Code Battles event!
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: click>=8.1.7
@@ -53,7 +53,7 @@ If you want to choose a custom bot name, you can run `code-battles upload bots/e
53
53
 
54
54
  ## Running a simulation
55
55
 
56
- 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.
56
+ You can run a simulation by running `code-battles run '{"map": "NYC"}' bots/example.py bots/another_example.py`, where `NYC` is an example of a map.
57
57
 
58
58
  You will probably find the scripting API nicer to work with if you want to run simulations locally.
59
59
 
@@ -42,7 +42,7 @@ If you want to choose a custom bot name, you can run `code-battles upload bots/e
42
42
 
43
43
  ## Running a simulation
44
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.
45
+ You can run a simulation by running `code-battles run '{"map": "NYC"}' bots/example.py bots/another_example.py`, where `NYC` is an example of a map.
46
46
 
47
47
  You will probably find the scripting API nicer to work with if you want to run simulations locally.
48
48
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "code-battles-cli"
3
- version = "1.1.1"
3
+ version = "1.2.0"
4
4
  description = "A CLI for competitors in any Code Battles event!"
5
5
  dependencies = [
6
6
  "requests>=2.32.3",
@@ -20,7 +20,7 @@ build-backend = "hatchling.build"
20
20
 
21
21
  [tool.rye]
22
22
  managed = true
23
- dev-dependencies = []
23
+ dev-dependencies = ["pytest>=8.3.5"]
24
24
 
25
25
  [tool.hatch.metadata]
26
26
  allow-direct-references = true
@@ -39,10 +39,16 @@ grpcio-status==1.67.0
39
39
  # via google-api-core
40
40
  idna==3.10
41
41
  # via requests
42
+ iniconfig==2.1.0
43
+ # via pytest
42
44
  markdown-it-py==3.0.0
43
45
  # via rich
44
46
  mdurl==0.1.2
45
47
  # via markdown-it-py
48
+ packaging==25.0
49
+ # via pytest
50
+ pluggy==1.5.0
51
+ # via pytest
46
52
  proto-plus==1.24.0
47
53
  # via google-api-core
48
54
  # via google-cloud-firestore
@@ -59,6 +65,7 @@ pyasn1-modules==0.4.1
59
65
  # via google-auth
60
66
  pygments==2.18.0
61
67
  # via rich
68
+ pytest==8.3.5
62
69
  requests==2.32.3
63
70
  # via code-battles-cli
64
71
  # via google-api-core
@@ -6,27 +6,40 @@ Firestore client implementation inspired by https://medium.com/@bobthomas295/cli
6
6
 
7
7
  import base64
8
8
  import datetime
9
- import logging
10
9
  import gzip
11
- import os
12
10
  import json
11
+ import logging
12
+ import os
13
13
  import shutil
14
14
  import subprocess
15
15
  import sys
16
- from typing import Any, Callable, Dict, List, Optional, Union
17
16
  from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Any, Callable, Dict, List, Optional, Union
18
19
 
19
20
  import requests
20
- from google.oauth2.credentials import Credentials
21
21
  from google.cloud.firestore import Client as FirestoreClient
22
+ from google.oauth2.credentials import Credentials
22
23
  from rich.prompt import Prompt
23
- from code_battles_cli.log import console, log, progress
24
24
 
25
+ from code_battles_cli.log import console, log
25
26
 
26
27
  SIMULATION_FINISHED_MARK = b"--- SIMULATION FINISHED ---"
27
28
  SIMULATION_STEP_MARK = b"__CODE_BATTLES_ADVANCE_STEP"
28
29
 
29
30
 
31
+ class SimulationException(Exception):
32
+ def __init__(self, stderr: bytes, exit_code: int):
33
+ self.stderr = stderr
34
+ self.exit_code = exit_code
35
+
36
+ def __str__(self):
37
+ return (
38
+ f"Simulation failed with exit code {self.exit_code}. Output:\n"
39
+ + self.stderr.decode()
40
+ )
41
+
42
+
30
43
  @dataclass
31
44
  class LogEntry:
32
45
  step: int
@@ -45,12 +58,13 @@ class SimulationResults:
45
58
 
46
59
  @dataclass
47
60
  class Simulation:
48
- map: str
61
+ parameters: Dict[str, str]
49
62
  player_names: str
50
63
  game: str
51
64
  version: str
52
65
  timestamp: datetime.datetime
53
66
  logs: list
67
+ alerts: list
54
68
  decisions: List[bytes]
55
69
  seed: int
56
70
 
@@ -59,12 +73,13 @@ class Simulation:
59
73
  gzip.compress(
60
74
  json.dumps(
61
75
  {
62
- "map": self.map,
76
+ "parameters": self.parameters,
63
77
  "playerNames": self.player_names,
64
78
  "game": self.game,
65
79
  "version": self.version,
66
80
  "timestamp": self.timestamp.isoformat(),
67
81
  "logs": self.logs,
82
+ "alerts": self.alerts,
68
83
  "decisions": [
69
84
  base64.b64encode(decision).decode()
70
85
  for decision in self.decisions
@@ -79,12 +94,15 @@ class Simulation:
79
94
  def load(file: str):
80
95
  contents: Dict[str, Any] = json.loads(gzip.decompress(base64.b64decode(file)))
81
96
  return Simulation(
82
- contents["map"],
97
+ contents["parameters"]
98
+ if "parameters" in contents
99
+ else {"map": contents["map"]},
83
100
  contents["playerNames"],
84
101
  contents["game"],
85
102
  contents["version"],
86
103
  datetime.datetime.fromisoformat(contents["timestamp"]),
87
104
  contents["logs"],
105
+ contents["alerts"],
88
106
  [base64.b64decode(decision) for decision in contents["decisions"]],
89
107
  contents["seed"],
90
108
  )
@@ -162,9 +180,6 @@ class Client:
162
180
  configuration = requests.get(self.url + "/firebase-configuration.json").json()
163
181
  self.firebase_api_key: str = configuration["apiKey"]
164
182
  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
183
 
169
184
  def _sign_in(self, email_domain="gmail.com"):
170
185
  try:
@@ -197,9 +212,14 @@ class Client:
197
212
  self.document.set(bots, merge)
198
213
 
199
214
  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()])}"
215
+ directory_name = "".join(
216
+ [
217
+ c
218
+ for c in self.url.removeprefix("https").removesuffix(".web.app")
219
+ if c.isalnum() or c == "-"
220
+ ]
202
221
  )
222
+ code_directory = os.path.expanduser(f"~/.cache/code-battles/{directory_name}")
203
223
 
204
224
  if force_download and os.path.exists(code_directory):
205
225
  shutil.rmtree(code_directory)
@@ -207,31 +227,11 @@ class Client:
207
227
  if not os.path.exists(code_directory):
208
228
  os.makedirs(code_directory)
209
229
 
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.")
230
+ with console.status("[blue]Fetching packed Python file..."):
231
+ packed_file = requests.get(self.url + "/scripts/packed.py").text
232
+ local_path = Path(code_directory) / "packed.py"
233
+ local_path.write_text(packed_file)
234
+ logging.info("Fetched packed Python file.")
235
235
 
236
236
  return code_directory
237
237
 
@@ -257,7 +257,7 @@ class Client:
257
257
  return output.decode()
258
258
 
259
259
  output = json.loads(output)
260
- return SimulationResults(
260
+ result = SimulationResults(
261
261
  output["winner_index"],
262
262
  output["winner"],
263
263
  output["steps"],
@@ -269,9 +269,16 @@ class Client:
269
269
  ],
270
270
  )
271
271
 
272
+ error: bytes = p.stderr.read()
273
+ exit_code = p.wait()
274
+ if exit_code != 0:
275
+ raise SimulationException(error, exit_code)
276
+
277
+ return result
278
+
272
279
  def run_simulation(
273
280
  self,
274
- map: str,
281
+ parameters: Dict[str, str],
275
282
  bot_filenames: List[str],
276
283
  bot_names: Optional[List[str]] = None,
277
284
  seed: Optional[int] = None,
@@ -300,16 +307,17 @@ class Client:
300
307
  p = subprocess.Popen(
301
308
  [
302
309
  sys.executable,
303
- os.path.join(code_directory, "main.py"),
310
+ os.path.join(code_directory, "packed.py"),
304
311
  "simulate",
305
312
  str(seed),
306
- str(output_file),
307
- map,
313
+ str(os.path.abspath(output_file)),
314
+ json.dumps(parameters),
308
315
  "-".join(bot_names),
309
316
  ]
310
317
  + [os.path.abspath(f) for f in bot_filenames],
311
318
  env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
312
319
  stdout=subprocess.PIPE,
320
+ stderr=subprocess.PIPE,
313
321
  )
314
322
 
315
323
  return self._get_simulation_output(p, json_output, on_step)
@@ -334,7 +342,7 @@ class Client:
334
342
  p = subprocess.Popen(
335
343
  [
336
344
  sys.executable,
337
- os.path.join(code_directory, "main.py"),
345
+ os.path.join(code_directory, "packed.py"),
338
346
  "simulate-from-file",
339
347
  simulation_file,
340
348
  ],
@@ -1,6 +1,7 @@
1
- import time
1
+ import json
2
2
  import logging
3
3
  import sys
4
+ import time
4
5
  from typing import List, Optional, Tuple
5
6
 
6
7
  import click
@@ -10,7 +11,7 @@ from code_battles_cli.log import log, progress
10
11
 
11
12
 
12
13
  @click.command()
13
- @click.argument("map", required=False)
14
+ @click.argument("parameters", required=False)
14
15
  @click.argument("bots", nargs=-1)
15
16
  @click.option(
16
17
  "-U",
@@ -53,7 +54,7 @@ def run(
53
54
  username: Optional[str],
54
55
  password: Optional[str],
55
56
  dump_credentials: bool,
56
- map: str,
57
+ parameters: Optional[str],
57
58
  bots: Tuple[str],
58
59
  force_download: bool,
59
60
  seed: Optional[int],
@@ -72,18 +73,18 @@ def run(
72
73
  progress.update(progress_id, advance=1)
73
74
 
74
75
  if simulation_file is not None:
75
- assert (
76
- map is None
77
- ), "The simulation's map is given from the simulation file!"
78
- assert (
79
- len(bots) == 0
80
- ), "The simulation's bots are given from the simulation file!"
81
- assert (
82
- seed is None
83
- ), "The simulation's seed is given from the simulation file!"
84
- assert (
85
- output_file is None
86
- ), "Shouldn't dump simualtion file from existing simulation file!"
76
+ assert parameters is None, (
77
+ "The simulation's map is given from the simulation file!"
78
+ )
79
+ assert len(bots) == 0, (
80
+ "The simulation's bots are given from the simulation file!"
81
+ )
82
+ assert seed is None, (
83
+ "The simulation's seed is given from the simulation file!"
84
+ )
85
+ assert output_file is None, (
86
+ "Shouldn't dump simualtion file from existing simulation file!"
87
+ )
87
88
 
88
89
  with open(simulation_file, "r") as f:
89
90
  simulation_text = f.read()
@@ -92,7 +93,7 @@ def run(
92
93
  f"Detected simulation file of {simulation.game} {simulation.version}"
93
94
  )
94
95
  logging.info(
95
- f"Simulation happened in {simulation.map} between {', '.join(simulation.player_names)} with seed {simulation.seed}"
96
+ f"Simulation happened in {simulation.parameters.get('map')} between {', '.join(simulation.player_names)} with seed {simulation.seed}"
96
97
  )
97
98
  logging.info(f"Simulation contains {len(simulation.decisions)} decisions")
98
99
  progress.update(progress_id, total=len(simulation.decisions) - 1)
@@ -104,7 +105,7 @@ def run(
104
105
  assert len(bots) > 0, "Bots are required for simulation!"
105
106
 
106
107
  results = client.run_simulation(
107
- map,
108
+ json.loads(parameters),
108
109
  list(bots),
109
110
  seed=seed,
110
111
  force_download=False,
@@ -113,4 +114,4 @@ def run(
113
114
  )
114
115
  print("The winner is", results.winner, "after", results.steps, "steps.")
115
116
  end = time.time()
116
- log.info(f"Simulation took {(end - start) :.2f}s.")
117
+ log.info(f"Simulation took {(end - start):.2f}s.")
@@ -0,0 +1,2 @@
1
+ def test_client():
2
+ from code_battles_cli.api import Client