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.
- code_battles_cli-1.2.1/.github/workflows/test.yml +21 -0
- code_battles_cli-1.2.1/.python-version +1 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/PKG-INFO +2 -2
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/README.md +1 -1
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/pyproject.toml +2 -2
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/requirements-dev.lock +14 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/requirements.lock +2 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/api.py +65 -47
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/commands/run.py +19 -18
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/commands/upload.py +2 -1
- code_battles_cli-1.2.1/tests/test_cli.py +2 -0
- code_battles_cli-1.1.2/.python-version +0 -1
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/.gitignore +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/LICENSE.md +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/__init__.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/__main__.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/cli.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/commands/__init__.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.1}/src/code_battles_cli/commands/download.py +0 -0
- {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
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
"
|
|
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["
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, "
|
|
321
|
+
os.path.join(code_directory, "packed.py"),
|
|
309
322
|
"simulate",
|
|
310
323
|
str(seed),
|
|
311
|
-
str(
|
|
312
|
-
|
|
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, "
|
|
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
|
|
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("
|
|
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
|
-
|
|
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
|
|
77
|
-
)
|
|
78
|
-
assert (
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
assert (
|
|
82
|
-
seed is
|
|
83
|
-
)
|
|
84
|
-
assert (
|
|
85
|
-
|
|
86
|
-
)
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
48
|
+
line_count = bot_code.count("\n") + 1
|
|
49
|
+
log.info(f"{line_count} lines of code were uploaded as '{bot_name}'.")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.12.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|