code-battles-cli 1.1.2__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.2 → code_battles_cli-1.2.0}/PKG-INFO +2 -2
  3. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/README.md +1 -1
  4. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/pyproject.toml +2 -2
  5. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/requirements-dev.lock +7 -0
  6. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/api.py +46 -43
  7. {code_battles_cli-1.1.2 → 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.2 → code_battles_cli-1.2.0}/.gitignore +0 -0
  10. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/.python-version +0 -0
  11. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/LICENSE.md +0 -0
  12. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/requirements.lock +0 -0
  13. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/__init__.py +0 -0
  14. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/__main__.py +0 -0
  15. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/cli.py +0 -0
  16. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/__init__.py +0 -0
  17. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/download.py +0 -0
  18. {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/upload.py +0 -0
  19. {code_battles_cli-1.1.2 → 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.2
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.2"
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:
@@ -201,7 +216,7 @@ class Client:
201
216
  [
202
217
  c
203
218
  for c in self.url.removeprefix("https").removesuffix(".web.app")
204
- if c.isalnum()
219
+ if c.isalnum() or c == "-"
205
220
  ]
206
221
  )
207
222
  code_directory = os.path.expanduser(f"~/.cache/code-battles/{directory_name}")
@@ -212,31 +227,11 @@ class Client:
212
227
  if not os.path.exists(code_directory):
213
228
  os.makedirs(code_directory)
214
229
 
215
- with console.status("[blue]Fetching configuration from game website..."):
216
- pyscript_configuration = requests.get(self.url + "/config.json").json()
217
- logging.info(
218
- f"Fetched configuration containing {len(pyscript_configuration["files"])} game files."
219
- )
220
-
221
- with progress:
222
- progress_id = progress.add_task(
223
- "[blue]Fetching game files...",
224
- total=len(pyscript_configuration["files"]),
225
- )
226
-
227
- for url_path, path in pyscript_configuration["files"].items():
228
- if not path.endswith(".py"):
229
- continue
230
-
231
- local_path = os.path.join(code_directory, *path.split("/"))
232
- os.makedirs(os.path.dirname(local_path), exist_ok=True)
233
- text = requests.get(self.url + url_path).text
234
- with open(local_path, "w") as f:
235
- f.write(text)
236
- progress.update(progress_id, advance=1)
237
- progress.update(progress_id, visible=False)
238
-
239
- 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.")
240
235
 
241
236
  return code_directory
242
237
 
@@ -262,7 +257,7 @@ class Client:
262
257
  return output.decode()
263
258
 
264
259
  output = json.loads(output)
265
- return SimulationResults(
260
+ result = SimulationResults(
266
261
  output["winner_index"],
267
262
  output["winner"],
268
263
  output["steps"],
@@ -274,9 +269,16 @@ class Client:
274
269
  ],
275
270
  )
276
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
+
277
279
  def run_simulation(
278
280
  self,
279
- map: str,
281
+ parameters: Dict[str, str],
280
282
  bot_filenames: List[str],
281
283
  bot_names: Optional[List[str]] = None,
282
284
  seed: Optional[int] = None,
@@ -305,16 +307,17 @@ class Client:
305
307
  p = subprocess.Popen(
306
308
  [
307
309
  sys.executable,
308
- os.path.join(code_directory, "main.py"),
310
+ os.path.join(code_directory, "packed.py"),
309
311
  "simulate",
310
312
  str(seed),
311
- str(output_file),
312
- map,
313
+ str(os.path.abspath(output_file)),
314
+ json.dumps(parameters),
313
315
  "-".join(bot_names),
314
316
  ]
315
317
  + [os.path.abspath(f) for f in bot_filenames],
316
318
  env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
317
319
  stdout=subprocess.PIPE,
320
+ stderr=subprocess.PIPE,
318
321
  )
319
322
 
320
323
  return self._get_simulation_output(p, json_output, on_step)
@@ -339,7 +342,7 @@ class Client:
339
342
  p = subprocess.Popen(
340
343
  [
341
344
  sys.executable,
342
- os.path.join(code_directory, "main.py"),
345
+ os.path.join(code_directory, "packed.py"),
343
346
  "simulate-from-file",
344
347
  simulation_file,
345
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