code-battles-cli 1.1.2__tar.gz → 1.2.1__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 (20) hide show
  1. code_battles_cli-1.2.1/.github/workflows/test.yml +21 -0
  2. code_battles_cli-1.2.1/.python-version +1 -0
  3. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/PKG-INFO +2 -2
  4. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/README.md +1 -1
  5. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/pyproject.toml +2 -2
  6. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/requirements-dev.lock +14 -0
  7. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/requirements.lock +2 -0
  8. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/api.py +65 -47
  9. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/commands/run.py +19 -18
  10. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/commands/upload.py +2 -1
  11. code_battles_cli-1.2.1/tests/test_cli.py +2 -0
  12. code_battles_cli-1.1.2/.python-version +0 -1
  13. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/.gitignore +0 -0
  14. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/LICENSE.md +0 -0
  15. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/__init__.py +0 -0
  16. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/__main__.py +0 -0
  17. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/cli.py +0 -0
  18. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/commands/__init__.py +0 -0
  19. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/commands/download.py +0 -0
  20. {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/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
@@ -0,0 +1 @@
1
+ 3.8
@@ -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.1
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.1"
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
@@ -18,6 +18,8 @@ charset-normalizer==3.4.0
18
18
  # via requests
19
19
  click==8.1.7
20
20
  # via code-battles-cli
21
+ exceptiongroup==1.3.0
22
+ # via pytest
21
23
  google-api-core==2.21.0
22
24
  # via google-cloud-core
23
25
  # via google-cloud-firestore
@@ -39,10 +41,16 @@ grpcio-status==1.67.0
39
41
  # via google-api-core
40
42
  idna==3.10
41
43
  # via requests
44
+ iniconfig==2.1.0
45
+ # via pytest
42
46
  markdown-it-py==3.0.0
43
47
  # via rich
44
48
  mdurl==0.1.2
45
49
  # via markdown-it-py
50
+ packaging==25.0
51
+ # via pytest
52
+ pluggy==1.5.0
53
+ # via pytest
46
54
  proto-plus==1.24.0
47
55
  # via google-api-core
48
56
  # via google-cloud-firestore
@@ -59,6 +67,7 @@ pyasn1-modules==0.4.1
59
67
  # via google-auth
60
68
  pygments==2.18.0
61
69
  # via rich
70
+ pytest==8.3.5
62
71
  requests==2.32.3
63
72
  # via code-battles-cli
64
73
  # via google-api-core
@@ -66,5 +75,10 @@ rich==13.9.3
66
75
  # via code-battles-cli
67
76
  rsa==4.9
68
77
  # via google-auth
78
+ tomli==2.2.1
79
+ # via pytest
80
+ typing-extensions==4.13.2
81
+ # via exceptiongroup
82
+ # via rich
69
83
  urllib3==2.2.3
70
84
  # via requests
@@ -66,5 +66,7 @@ rich==13.9.3
66
66
  # via code-battles-cli
67
67
  rsa==4.9
68
68
  # via google-auth
69
+ typing-extensions==4.13.2
70
+ # via rich
69
71
  urllib3==2.2.3
70
72
  # via requests
@@ -6,27 +6,52 @@ 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
+ def normalize(url: str):
32
+ if url.startswith("https"):
33
+ url = url[5:]
34
+
35
+ try:
36
+ url = url[: url.index(".web.app")]
37
+ except ValueError:
38
+ pass
39
+
40
+ return url
41
+
42
+
43
+ class SimulationException(Exception):
44
+ def __init__(self, stderr: bytes, exit_code: int):
45
+ self.stderr = stderr
46
+ self.exit_code = exit_code
47
+
48
+ def __str__(self):
49
+ return (
50
+ f"Simulation failed with exit code {self.exit_code}. Output:\n"
51
+ + self.stderr.decode()
52
+ )
53
+
54
+
30
55
  @dataclass
31
56
  class LogEntry:
32
57
  step: int
@@ -45,12 +70,13 @@ class SimulationResults:
45
70
 
46
71
  @dataclass
47
72
  class Simulation:
48
- map: str
73
+ parameters: Dict[str, str]
49
74
  player_names: str
50
75
  game: str
51
76
  version: str
52
77
  timestamp: datetime.datetime
53
78
  logs: list
79
+ alerts: list
54
80
  decisions: List[bytes]
55
81
  seed: int
56
82
 
@@ -59,12 +85,13 @@ class Simulation:
59
85
  gzip.compress(
60
86
  json.dumps(
61
87
  {
62
- "map": self.map,
88
+ "parameters": self.parameters,
63
89
  "playerNames": self.player_names,
64
90
  "game": self.game,
65
91
  "version": self.version,
66
92
  "timestamp": self.timestamp.isoformat(),
67
93
  "logs": self.logs,
94
+ "alerts": self.alerts,
68
95
  "decisions": [
69
96
  base64.b64encode(decision).decode()
70
97
  for decision in self.decisions
@@ -79,12 +106,15 @@ class Simulation:
79
106
  def load(file: str):
80
107
  contents: Dict[str, Any] = json.loads(gzip.decompress(base64.b64decode(file)))
81
108
  return Simulation(
82
- contents["map"],
109
+ contents["parameters"]
110
+ if "parameters" in contents
111
+ else {"map": contents["map"]},
83
112
  contents["playerNames"],
84
113
  contents["game"],
85
114
  contents["version"],
86
115
  datetime.datetime.fromisoformat(contents["timestamp"]),
87
116
  contents["logs"],
117
+ contents["alerts"],
88
118
  [base64.b64decode(decision) for decision in contents["decisions"]],
89
119
  contents["seed"],
90
120
  )
@@ -162,9 +192,6 @@ class Client:
162
192
  configuration = requests.get(self.url + "/firebase-configuration.json").json()
163
193
  self.firebase_api_key: str = configuration["apiKey"]
164
194
  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
195
 
169
196
  def _sign_in(self, email_domain="gmail.com"):
170
197
  try:
@@ -198,11 +225,7 @@ class Client:
198
225
 
199
226
  def _possibly_download(self, force_download=False):
200
227
  directory_name = "".join(
201
- [
202
- c
203
- for c in self.url.removeprefix("https").removesuffix(".web.app")
204
- if c.isalnum()
205
- ]
228
+ [c for c in normalize(self.url) if c.isalnum() or c == "-"]
206
229
  )
207
230
  code_directory = os.path.expanduser(f"~/.cache/code-battles/{directory_name}")
208
231
 
@@ -212,31 +235,11 @@ class Client:
212
235
  if not os.path.exists(code_directory):
213
236
  os.makedirs(code_directory)
214
237
 
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.")
238
+ with console.status("[blue]Fetching packed Python file..."):
239
+ packed_file = requests.get(self.url + "/scripts/packed.py").text
240
+ local_path = Path(code_directory) / "packed.py"
241
+ local_path.write_text(packed_file)
242
+ logging.info("Fetched packed Python file.")
240
243
 
241
244
  return code_directory
242
245
 
@@ -247,6 +250,9 @@ class Client:
247
250
  on_step: Optional[Callable[[], None]] = None,
248
251
  ) -> Union[SimulationResults, str]:
249
252
  while True:
253
+ if p.poll() is not None:
254
+ raise SimulationException(p.stderr.read(), p.returncode)
255
+
250
256
  line: bytes = p.stdout.readline()
251
257
  line = line.strip()
252
258
  if line == SIMULATION_FINISHED_MARK:
@@ -262,7 +268,7 @@ class Client:
262
268
  return output.decode()
263
269
 
264
270
  output = json.loads(output)
265
- return SimulationResults(
271
+ result = SimulationResults(
266
272
  output["winner_index"],
267
273
  output["winner"],
268
274
  output["steps"],
@@ -274,9 +280,16 @@ class Client:
274
280
  ],
275
281
  )
276
282
 
283
+ error: bytes = p.stderr.read()
284
+ exit_code = p.wait()
285
+ if exit_code != 0:
286
+ raise SimulationException(error, exit_code)
287
+
288
+ return result
289
+
277
290
  def run_simulation(
278
291
  self,
279
- map: str,
292
+ parameters: Dict[str, str],
280
293
  bot_filenames: List[str],
281
294
  bot_names: Optional[List[str]] = None,
282
295
  seed: Optional[int] = None,
@@ -305,16 +318,21 @@ class Client:
305
318
  p = subprocess.Popen(
306
319
  [
307
320
  sys.executable,
308
- os.path.join(code_directory, "main.py"),
321
+ os.path.join(code_directory, "packed.py"),
309
322
  "simulate",
310
323
  str(seed),
311
- str(output_file),
312
- map,
324
+ str(
325
+ os.path.abspath(output_file)
326
+ if output_file is not None
327
+ else output_file
328
+ ),
329
+ json.dumps(parameters),
313
330
  "-".join(bot_names),
314
331
  ]
315
332
  + [os.path.abspath(f) for f in bot_filenames],
316
333
  env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
317
334
  stdout=subprocess.PIPE,
335
+ stderr=subprocess.PIPE,
318
336
  )
319
337
 
320
338
  return self._get_simulation_output(p, json_output, on_step)
@@ -339,7 +357,7 @@ class Client:
339
357
  p = subprocess.Popen(
340
358
  [
341
359
  sys.executable,
342
- os.path.join(code_directory, "main.py"),
360
+ os.path.join(code_directory, "packed.py"),
343
361
  "simulate-from-file",
344
362
  simulation_file,
345
363
  ],
@@ -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.")
@@ -45,4 +45,5 @@ def upload(
45
45
 
46
46
  client = Client(url, username, password, dump_credentials)
47
47
  client.set_bots({bot_name: bot_code})
48
- log.info(f"{bot_code.count('\n') + 1} lines of code were uploaded as '{bot_name}'.")
48
+ line_count = bot_code.count("\n") + 1
49
+ log.info(f"{line_count} lines of code were uploaded as '{bot_name}'.")
@@ -0,0 +1,2 @@
1
+ def test_client():
2
+ from code_battles_cli.api import Client
@@ -1 +0,0 @@
1
- 3.12.2