code-battles-cli 1.1.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.1.1/.gitignore +13 -0
- code_battles_cli-1.1.1/.python-version +1 -0
- code_battles_cli-1.1.1/LICENSE.md +7 -0
- code_battles_cli-1.1.1/PKG-INFO +80 -0
- code_battles_cli-1.1.1/README.md +69 -0
- code_battles_cli-1.1.1/pyproject.toml +29 -0
- code_battles_cli-1.1.1/requirements-dev.lock +70 -0
- code_battles_cli-1.1.1/requirements.lock +70 -0
- code_battles_cli-1.1.1/src/code_battles_cli/__init__.py +1 -0
- code_battles_cli-1.1.1/src/code_battles_cli/__main__.py +4 -0
- code_battles_cli-1.1.1/src/code_battles_cli/api.py +346 -0
- code_battles_cli-1.1.1/src/code_battles_cli/cli.py +26 -0
- code_battles_cli-1.1.1/src/code_battles_cli/commands/__init__.py +0 -0
- code_battles_cli-1.1.1/src/code_battles_cli/commands/download.py +61 -0
- code_battles_cli-1.1.1/src/code_battles_cli/commands/run.py +116 -0
- code_battles_cli-1.1.1/src/code_battles_cli/commands/upload.py +48 -0
- code_battles_cli-1.1.1/src/code_battles_cli/log.py +35 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12.2
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2024 Noam Zaks and Code Battles Contributors
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: code-battles-cli
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: A CLI for competitors in any Code Battles event!
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Requires-Dist: click>=8.1.7
|
|
7
|
+
Requires-Dist: google-cloud-firestore>=2.19.0
|
|
8
|
+
Requires-Dist: requests>=2.32.3
|
|
9
|
+
Requires-Dist: rich>=13.9.3
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://github.com/noamzaks/code-battles/assets/63877260/b330aa14-7003-4204-8907-e77a5c6e8d81" height="140">
|
|
14
|
+
</p>
|
|
15
|
+
<h1 align="center">
|
|
16
|
+
Code Battles CLI
|
|
17
|
+
<br />
|
|
18
|
+
<img src="https://img.shields.io/pypi/v/code-battles-cli">
|
|
19
|
+
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
|
|
20
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg">
|
|
21
|
+
</h1>
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
A CLI for competitors in any Code Battles event!
|
|
25
|
+
</p>
|
|
26
|
+
<p align="center">
|
|
27
|
+
🏃 <a href="#getting-started">Getting Started</a>
|
|
28
|
+
·
|
|
29
|
+
💡 <a href="#features">Features</a>
|
|
30
|
+
·
|
|
31
|
+
🚗 <a href="#roadmap">Roadmap</a>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
# Getting Started
|
|
35
|
+
|
|
36
|
+
- 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
|
+
- If that fails, run `py -m code_battles_cli --help`.
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
42
|
+
# Features
|
|
43
|
+
|
|
44
|
+
## Downloading the bots
|
|
45
|
+
|
|
46
|
+
You can download all of the bots your team has by running `code-battles download`.
|
|
47
|
+
|
|
48
|
+
## Uploading a bot
|
|
49
|
+
|
|
50
|
+
You can update a bot in the website by running `code-battles upload bots/example.py` (in this case the bot's name will be `example`).
|
|
51
|
+
|
|
52
|
+
If you want to choose a custom bot name, you can run `code-battles upload bots/example.py -n myamazingbot`.
|
|
53
|
+
|
|
54
|
+
## Running a simulation
|
|
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.
|
|
57
|
+
|
|
58
|
+
You will probably find the scripting API nicer to work with if you want to run simulations locally.
|
|
59
|
+
|
|
60
|
+
## Usage in scripts
|
|
61
|
+
|
|
62
|
+
You can import the `code_battles_cli.api` module and utilize its `Client` class to hack Code Battles for your needs!
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from code_battles_cli.api import Client
|
|
68
|
+
|
|
69
|
+
client = Client()
|
|
70
|
+
|
|
71
|
+
print(client.get_bots())
|
|
72
|
+
print(client.run_simulation("NYC", ["bots/example.py", "bots/another_example.py"]))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
# Roadmap
|
|
76
|
+
|
|
77
|
+
- [x] Download bots.
|
|
78
|
+
- [x] Upload bots.
|
|
79
|
+
- [x] Run no-UI simulations locally.
|
|
80
|
+
- [x] Be usable in scripts.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://github.com/noamzaks/code-battles/assets/63877260/b330aa14-7003-4204-8907-e77a5c6e8d81" height="140">
|
|
3
|
+
</p>
|
|
4
|
+
<h1 align="center">
|
|
5
|
+
Code Battles CLI
|
|
6
|
+
<br />
|
|
7
|
+
<img src="https://img.shields.io/pypi/v/code-battles-cli">
|
|
8
|
+
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
|
|
9
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg">
|
|
10
|
+
</h1>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
A CLI for competitors in any Code Battles event!
|
|
14
|
+
</p>
|
|
15
|
+
<p align="center">
|
|
16
|
+
🏃 <a href="#getting-started">Getting Started</a>
|
|
17
|
+
·
|
|
18
|
+
💡 <a href="#features">Features</a>
|
|
19
|
+
·
|
|
20
|
+
🚗 <a href="#roadmap">Roadmap</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
# Getting Started
|
|
24
|
+
|
|
25
|
+
- Install the library by running `pip install code-battles-cli`.
|
|
26
|
+
- Make sure you have the Python scripts folder in your `PATH` by running `code-battles --help`.
|
|
27
|
+
- If that fails, run `py -m code_battles_cli --help`.
|
|
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.
|
|
30
|
+
|
|
31
|
+
# Features
|
|
32
|
+
|
|
33
|
+
## Downloading the bots
|
|
34
|
+
|
|
35
|
+
You can download all of the bots your team has by running `code-battles download`.
|
|
36
|
+
|
|
37
|
+
## Uploading a bot
|
|
38
|
+
|
|
39
|
+
You can update a bot in the website by running `code-battles upload bots/example.py` (in this case the bot's name will be `example`).
|
|
40
|
+
|
|
41
|
+
If you want to choose a custom bot name, you can run `code-battles upload bots/example.py -n myamazingbot`.
|
|
42
|
+
|
|
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
|
|
50
|
+
|
|
51
|
+
You can import the `code_battles_cli.api` module and utilize its `Client` class to hack Code Battles for your needs!
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from code_battles_cli.api import Client
|
|
57
|
+
|
|
58
|
+
client = Client()
|
|
59
|
+
|
|
60
|
+
print(client.get_bots())
|
|
61
|
+
print(client.run_simulation("NYC", ["bots/example.py", "bots/another_example.py"]))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
# Roadmap
|
|
65
|
+
|
|
66
|
+
- [x] Download bots.
|
|
67
|
+
- [x] Upload bots.
|
|
68
|
+
- [x] Run no-UI simulations locally.
|
|
69
|
+
- [x] Be usable in scripts.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "code-battles-cli"
|
|
3
|
+
version = "1.1.1"
|
|
4
|
+
description = "A CLI for competitors in any Code Battles event!"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"requests>=2.32.3",
|
|
7
|
+
"google-cloud-firestore>=2.19.0",
|
|
8
|
+
"rich>=13.9.3",
|
|
9
|
+
"click>=8.1.7",
|
|
10
|
+
]
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">= 3.8"
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
"code-battles" = "code_battles_cli:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling==1.26.3"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[tool.rye]
|
|
22
|
+
managed = true
|
|
23
|
+
dev-dependencies = []
|
|
24
|
+
|
|
25
|
+
[tool.hatch.metadata]
|
|
26
|
+
allow-direct-references = true
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/code_battles_cli"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# generated by rye
|
|
2
|
+
# use `rye lock` or `rye sync` to update this lockfile
|
|
3
|
+
#
|
|
4
|
+
# last locked with the following flags:
|
|
5
|
+
# pre: false
|
|
6
|
+
# features: []
|
|
7
|
+
# all-features: false
|
|
8
|
+
# with-sources: false
|
|
9
|
+
# generate-hashes: false
|
|
10
|
+
# universal: false
|
|
11
|
+
|
|
12
|
+
-e file:.
|
|
13
|
+
cachetools==5.5.0
|
|
14
|
+
# via google-auth
|
|
15
|
+
certifi==2024.8.30
|
|
16
|
+
# via requests
|
|
17
|
+
charset-normalizer==3.4.0
|
|
18
|
+
# via requests
|
|
19
|
+
click==8.1.7
|
|
20
|
+
# via code-battles-cli
|
|
21
|
+
google-api-core==2.21.0
|
|
22
|
+
# via google-cloud-core
|
|
23
|
+
# via google-cloud-firestore
|
|
24
|
+
google-auth==2.35.0
|
|
25
|
+
# via google-api-core
|
|
26
|
+
# via google-cloud-core
|
|
27
|
+
# via google-cloud-firestore
|
|
28
|
+
google-cloud-core==2.4.1
|
|
29
|
+
# via google-cloud-firestore
|
|
30
|
+
google-cloud-firestore==2.19.0
|
|
31
|
+
# via code-battles-cli
|
|
32
|
+
googleapis-common-protos==1.65.0
|
|
33
|
+
# via google-api-core
|
|
34
|
+
# via grpcio-status
|
|
35
|
+
grpcio==1.67.0
|
|
36
|
+
# via google-api-core
|
|
37
|
+
# via grpcio-status
|
|
38
|
+
grpcio-status==1.67.0
|
|
39
|
+
# via google-api-core
|
|
40
|
+
idna==3.10
|
|
41
|
+
# via requests
|
|
42
|
+
markdown-it-py==3.0.0
|
|
43
|
+
# via rich
|
|
44
|
+
mdurl==0.1.2
|
|
45
|
+
# via markdown-it-py
|
|
46
|
+
proto-plus==1.24.0
|
|
47
|
+
# via google-api-core
|
|
48
|
+
# via google-cloud-firestore
|
|
49
|
+
protobuf==5.28.2
|
|
50
|
+
# via google-api-core
|
|
51
|
+
# via google-cloud-firestore
|
|
52
|
+
# via googleapis-common-protos
|
|
53
|
+
# via grpcio-status
|
|
54
|
+
# via proto-plus
|
|
55
|
+
pyasn1==0.6.1
|
|
56
|
+
# via pyasn1-modules
|
|
57
|
+
# via rsa
|
|
58
|
+
pyasn1-modules==0.4.1
|
|
59
|
+
# via google-auth
|
|
60
|
+
pygments==2.18.0
|
|
61
|
+
# via rich
|
|
62
|
+
requests==2.32.3
|
|
63
|
+
# via code-battles-cli
|
|
64
|
+
# via google-api-core
|
|
65
|
+
rich==13.9.3
|
|
66
|
+
# via code-battles-cli
|
|
67
|
+
rsa==4.9
|
|
68
|
+
# via google-auth
|
|
69
|
+
urllib3==2.2.3
|
|
70
|
+
# via requests
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# generated by rye
|
|
2
|
+
# use `rye lock` or `rye sync` to update this lockfile
|
|
3
|
+
#
|
|
4
|
+
# last locked with the following flags:
|
|
5
|
+
# pre: false
|
|
6
|
+
# features: []
|
|
7
|
+
# all-features: false
|
|
8
|
+
# with-sources: false
|
|
9
|
+
# generate-hashes: false
|
|
10
|
+
# universal: false
|
|
11
|
+
|
|
12
|
+
-e file:.
|
|
13
|
+
cachetools==5.5.0
|
|
14
|
+
# via google-auth
|
|
15
|
+
certifi==2024.8.30
|
|
16
|
+
# via requests
|
|
17
|
+
charset-normalizer==3.4.0
|
|
18
|
+
# via requests
|
|
19
|
+
click==8.1.7
|
|
20
|
+
# via code-battles-cli
|
|
21
|
+
google-api-core==2.21.0
|
|
22
|
+
# via google-cloud-core
|
|
23
|
+
# via google-cloud-firestore
|
|
24
|
+
google-auth==2.35.0
|
|
25
|
+
# via google-api-core
|
|
26
|
+
# via google-cloud-core
|
|
27
|
+
# via google-cloud-firestore
|
|
28
|
+
google-cloud-core==2.4.1
|
|
29
|
+
# via google-cloud-firestore
|
|
30
|
+
google-cloud-firestore==2.19.0
|
|
31
|
+
# via code-battles-cli
|
|
32
|
+
googleapis-common-protos==1.65.0
|
|
33
|
+
# via google-api-core
|
|
34
|
+
# via grpcio-status
|
|
35
|
+
grpcio==1.67.0
|
|
36
|
+
# via google-api-core
|
|
37
|
+
# via grpcio-status
|
|
38
|
+
grpcio-status==1.67.0
|
|
39
|
+
# via google-api-core
|
|
40
|
+
idna==3.10
|
|
41
|
+
# via requests
|
|
42
|
+
markdown-it-py==3.0.0
|
|
43
|
+
# via rich
|
|
44
|
+
mdurl==0.1.2
|
|
45
|
+
# via markdown-it-py
|
|
46
|
+
proto-plus==1.24.0
|
|
47
|
+
# via google-api-core
|
|
48
|
+
# via google-cloud-firestore
|
|
49
|
+
protobuf==5.28.2
|
|
50
|
+
# via google-api-core
|
|
51
|
+
# via google-cloud-firestore
|
|
52
|
+
# via googleapis-common-protos
|
|
53
|
+
# via grpcio-status
|
|
54
|
+
# via proto-plus
|
|
55
|
+
pyasn1==0.6.1
|
|
56
|
+
# via pyasn1-modules
|
|
57
|
+
# via rsa
|
|
58
|
+
pyasn1-modules==0.4.1
|
|
59
|
+
# via google-auth
|
|
60
|
+
pygments==2.18.0
|
|
61
|
+
# via rich
|
|
62
|
+
requests==2.32.3
|
|
63
|
+
# via code-battles-cli
|
|
64
|
+
# via google-api-core
|
|
65
|
+
rich==13.9.3
|
|
66
|
+
# via code-battles-cli
|
|
67
|
+
rsa==4.9
|
|
68
|
+
# via google-auth
|
|
69
|
+
urllib3==2.2.3
|
|
70
|
+
# via requests
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from code_battles_cli.cli import main
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code Battles Python Client API
|
|
3
|
+
|
|
4
|
+
Firestore client implementation inspired by https://medium.com/@bobthomas295/client-side-authentication-with-python-firestore-and-firebase-352e484a2634
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import datetime
|
|
9
|
+
import logging
|
|
10
|
+
import gzip
|
|
11
|
+
import os
|
|
12
|
+
import json
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
import requests
|
|
20
|
+
from google.oauth2.credentials import Credentials
|
|
21
|
+
from google.cloud.firestore import Client as FirestoreClient
|
|
22
|
+
from rich.prompt import Prompt
|
|
23
|
+
from code_battles_cli.log import console, log, progress
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SIMULATION_FINISHED_MARK = b"--- SIMULATION FINISHED ---"
|
|
27
|
+
SIMULATION_STEP_MARK = b"__CODE_BATTLES_ADVANCE_STEP"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class LogEntry:
|
|
32
|
+
step: int
|
|
33
|
+
text: str
|
|
34
|
+
color: str
|
|
35
|
+
player_index: Optional[int]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class SimulationResults:
|
|
40
|
+
winner_index: int
|
|
41
|
+
winner: str
|
|
42
|
+
steps: int
|
|
43
|
+
logs: List[LogEntry]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Simulation:
|
|
48
|
+
map: str
|
|
49
|
+
player_names: str
|
|
50
|
+
game: str
|
|
51
|
+
version: str
|
|
52
|
+
timestamp: datetime.datetime
|
|
53
|
+
logs: list
|
|
54
|
+
decisions: List[bytes]
|
|
55
|
+
seed: int
|
|
56
|
+
|
|
57
|
+
def dump(self):
|
|
58
|
+
return base64.b64encode(
|
|
59
|
+
gzip.compress(
|
|
60
|
+
json.dumps(
|
|
61
|
+
{
|
|
62
|
+
"map": self.map,
|
|
63
|
+
"playerNames": self.player_names,
|
|
64
|
+
"game": self.game,
|
|
65
|
+
"version": self.version,
|
|
66
|
+
"timestamp": self.timestamp.isoformat(),
|
|
67
|
+
"logs": self.logs,
|
|
68
|
+
"decisions": [
|
|
69
|
+
base64.b64encode(decision).decode()
|
|
70
|
+
for decision in self.decisions
|
|
71
|
+
],
|
|
72
|
+
"seed": self.seed,
|
|
73
|
+
}
|
|
74
|
+
).encode()
|
|
75
|
+
)
|
|
76
|
+
).decode()
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def load(file: str):
|
|
80
|
+
contents: Dict[str, Any] = json.loads(gzip.decompress(base64.b64decode(file)))
|
|
81
|
+
return Simulation(
|
|
82
|
+
contents["map"],
|
|
83
|
+
contents["playerNames"],
|
|
84
|
+
contents["game"],
|
|
85
|
+
contents["version"],
|
|
86
|
+
datetime.datetime.fromisoformat(contents["timestamp"]),
|
|
87
|
+
contents["logs"],
|
|
88
|
+
[base64.b64decode(decision) for decision in contents["decisions"]],
|
|
89
|
+
contents["seed"],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Client:
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
url: Optional[str] = None,
|
|
97
|
+
username: Optional[str] = None,
|
|
98
|
+
password: Optional[str] = None,
|
|
99
|
+
dump_credentials=True,
|
|
100
|
+
):
|
|
101
|
+
"""
|
|
102
|
+
Creates a client for getting and setting the bots for a Code Battles hosted at `url`
|
|
103
|
+
and signing in as `username` with `password`.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if url is None or username is None or password is None:
|
|
107
|
+
self._get_credentials()
|
|
108
|
+
else:
|
|
109
|
+
self.url = url
|
|
110
|
+
self.username = username
|
|
111
|
+
self.password = password
|
|
112
|
+
|
|
113
|
+
self._get_firebase_data()
|
|
114
|
+
self._sign_in()
|
|
115
|
+
|
|
116
|
+
if dump_credentials:
|
|
117
|
+
self._dump_credentials()
|
|
118
|
+
|
|
119
|
+
def _get_credentials(self):
|
|
120
|
+
if os.path.exists("code-battles.json"):
|
|
121
|
+
try:
|
|
122
|
+
with open("code-battles.json", "r") as f:
|
|
123
|
+
configuration = json.load(f)
|
|
124
|
+
self.url: str = configuration["url"]
|
|
125
|
+
self.username: str = configuration["username"]
|
|
126
|
+
self.password: str = configuration["password"]
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
not hasattr(self, "url")
|
|
132
|
+
or not hasattr(self, "username")
|
|
133
|
+
or not hasattr(self, "password")
|
|
134
|
+
):
|
|
135
|
+
self.url = Prompt.ask("Enter your competition's URL", console=console)
|
|
136
|
+
|
|
137
|
+
if not self.url.startswith("https://"):
|
|
138
|
+
log.warning("Your URL should most likely start with 'https://'.")
|
|
139
|
+
if not self.url.endswith(".web.app"):
|
|
140
|
+
log.warning("Your URL should most likely end with '.web.app'.")
|
|
141
|
+
|
|
142
|
+
self.username = Prompt.ask("Enter your team's username", console=console)
|
|
143
|
+
|
|
144
|
+
if not self.username == self.username.lower():
|
|
145
|
+
log.warning("Your username should most likely be lowercased.")
|
|
146
|
+
|
|
147
|
+
self.password = Prompt.ask(
|
|
148
|
+
"Enter your team's password", console=console, password=True
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _dump_credentials(self):
|
|
152
|
+
with open("code-battles.json", "w") as f:
|
|
153
|
+
json.dump(
|
|
154
|
+
{"url": self.url, "username": self.username, "password": self.password},
|
|
155
|
+
f,
|
|
156
|
+
)
|
|
157
|
+
log.info(
|
|
158
|
+
"Credentials were dumped to `code-battles.json`. Make sure other teams don't have access to this file!"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _get_firebase_data(self):
|
|
162
|
+
configuration = requests.get(self.url + "/firebase-configuration.json").json()
|
|
163
|
+
self.firebase_api_key: str = configuration["apiKey"]
|
|
164
|
+
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
|
+
|
|
169
|
+
def _sign_in(self, email_domain="gmail.com"):
|
|
170
|
+
try:
|
|
171
|
+
response = requests.post(
|
|
172
|
+
f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={self.firebase_api_key}",
|
|
173
|
+
json={
|
|
174
|
+
"email": self.username + "@" + email_domain,
|
|
175
|
+
"password": self.password,
|
|
176
|
+
"returnSecureToken": True,
|
|
177
|
+
},
|
|
178
|
+
).json()
|
|
179
|
+
except Exception:
|
|
180
|
+
raise Exception(
|
|
181
|
+
"Sign in failed! Make sure the username and password are correct."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
self.credentials = Credentials(response["idToken"], response["refreshToken"])
|
|
185
|
+
self.client = FirestoreClient(self.firebase_project_id, self.credentials)
|
|
186
|
+
self.document = self.client.document(f"bots/{self.username}")
|
|
187
|
+
|
|
188
|
+
def get_bots(self) -> Dict[str, str]:
|
|
189
|
+
"""Returns a mapping from a bot's name to their Python code."""
|
|
190
|
+
return self.document.get().to_dict()
|
|
191
|
+
|
|
192
|
+
def set_bots(self, bots: Dict[str, str], merge=True) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Sets the bots in the website to the specified bots.
|
|
195
|
+
Doesn't remove any bot unless ``merge`` is ``False``, in which case only bots specified in ``bots`` will remain.
|
|
196
|
+
"""
|
|
197
|
+
self.document.set(bots, merge)
|
|
198
|
+
|
|
199
|
+
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()])}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if force_download and os.path.exists(code_directory):
|
|
205
|
+
shutil.rmtree(code_directory)
|
|
206
|
+
|
|
207
|
+
if not os.path.exists(code_directory):
|
|
208
|
+
os.makedirs(code_directory)
|
|
209
|
+
|
|
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.")
|
|
235
|
+
|
|
236
|
+
return code_directory
|
|
237
|
+
|
|
238
|
+
def _get_simulation_output(
|
|
239
|
+
self,
|
|
240
|
+
p: subprocess.Popen,
|
|
241
|
+
json_output=False,
|
|
242
|
+
on_step: Optional[Callable[[], None]] = None,
|
|
243
|
+
) -> Union[SimulationResults, str]:
|
|
244
|
+
while True:
|
|
245
|
+
line: bytes = p.stdout.readline()
|
|
246
|
+
line = line.strip()
|
|
247
|
+
if line == SIMULATION_FINISHED_MARK:
|
|
248
|
+
break
|
|
249
|
+
elif line == SIMULATION_STEP_MARK:
|
|
250
|
+
if on_step is not None:
|
|
251
|
+
on_step()
|
|
252
|
+
elif len(line) != 0:
|
|
253
|
+
logging.info(line.decode())
|
|
254
|
+
|
|
255
|
+
output: bytes = p.stdout.read()
|
|
256
|
+
if json_output:
|
|
257
|
+
return output.decode()
|
|
258
|
+
|
|
259
|
+
output = json.loads(output)
|
|
260
|
+
return SimulationResults(
|
|
261
|
+
output["winner_index"],
|
|
262
|
+
output["winner"],
|
|
263
|
+
output["steps"],
|
|
264
|
+
[
|
|
265
|
+
LogEntry(
|
|
266
|
+
entry["step"], entry["text"], entry["color"], entry["player_index"]
|
|
267
|
+
)
|
|
268
|
+
for entry in output["logs"]
|
|
269
|
+
],
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def run_simulation(
|
|
273
|
+
self,
|
|
274
|
+
map: str,
|
|
275
|
+
bot_filenames: List[str],
|
|
276
|
+
bot_names: Optional[List[str]] = None,
|
|
277
|
+
seed: Optional[int] = None,
|
|
278
|
+
force_download=False,
|
|
279
|
+
json_output=False,
|
|
280
|
+
on_step: Optional[Callable[[], None]] = None,
|
|
281
|
+
output_file: Optional[str] = None,
|
|
282
|
+
) -> Union[SimulationResults, str]:
|
|
283
|
+
"""
|
|
284
|
+
Runs the given simulation without UI locally.
|
|
285
|
+
If ``bot_names`` is not specified, they will be the filenames without the extension.
|
|
286
|
+
|
|
287
|
+
If required (or ``force_download``), this method downloads the simulation code from the website.
|
|
288
|
+
|
|
289
|
+
If ``json_output`` is ``True``, returns the JSON string of the results instead.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
if bot_names is None:
|
|
293
|
+
bot_names = [
|
|
294
|
+
os.path.splitext(os.path.basename(filename))[0]
|
|
295
|
+
for filename in bot_filenames
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
code_directory = self._possibly_download(force_download=force_download)
|
|
299
|
+
|
|
300
|
+
p = subprocess.Popen(
|
|
301
|
+
[
|
|
302
|
+
sys.executable,
|
|
303
|
+
os.path.join(code_directory, "main.py"),
|
|
304
|
+
"simulate",
|
|
305
|
+
str(seed),
|
|
306
|
+
str(output_file),
|
|
307
|
+
map,
|
|
308
|
+
"-".join(bot_names),
|
|
309
|
+
]
|
|
310
|
+
+ [os.path.abspath(f) for f in bot_filenames],
|
|
311
|
+
env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
|
|
312
|
+
stdout=subprocess.PIPE,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return self._get_simulation_output(p, json_output, on_step)
|
|
316
|
+
|
|
317
|
+
def run_simulation_from_file(
|
|
318
|
+
self,
|
|
319
|
+
simulation_file: str,
|
|
320
|
+
force_download=False,
|
|
321
|
+
json_output=False,
|
|
322
|
+
on_step: Optional[Callable[[], None]] = None,
|
|
323
|
+
) -> Union[SimulationResults, str]:
|
|
324
|
+
"""
|
|
325
|
+
Runs the given simulation without UI locally from the given simulation file.
|
|
326
|
+
|
|
327
|
+
If required (or ``force_download``), this method downloads the simulation code from the website.
|
|
328
|
+
|
|
329
|
+
If ``json_output`` is ``True``, returns the JSON string of the results instead.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
code_directory = self._possibly_download(force_download=force_download)
|
|
333
|
+
|
|
334
|
+
p = subprocess.Popen(
|
|
335
|
+
[
|
|
336
|
+
sys.executable,
|
|
337
|
+
os.path.join(code_directory, "main.py"),
|
|
338
|
+
"simulate-from-file",
|
|
339
|
+
simulation_file,
|
|
340
|
+
],
|
|
341
|
+
env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
|
|
342
|
+
stdout=subprocess.PIPE,
|
|
343
|
+
stderr=subprocess.PIPE,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return self._get_simulation_output(p, json_output, on_step)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.group()
|
|
7
|
+
@click.version_option(message="%(prog)s %(version)s")
|
|
8
|
+
def cli():
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
from code_battles_cli.log import setup_logging, log
|
|
14
|
+
|
|
15
|
+
setup_logging()
|
|
16
|
+
|
|
17
|
+
from code_battles_cli.commands.download import download
|
|
18
|
+
from code_battles_cli.commands.upload import upload
|
|
19
|
+
from code_battles_cli.commands.run import run
|
|
20
|
+
|
|
21
|
+
cli.add_command(download)
|
|
22
|
+
cli.add_command(upload)
|
|
23
|
+
cli.add_command(run)
|
|
24
|
+
cli(standalone_mode=False)
|
|
25
|
+
|
|
26
|
+
return 0
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from code_battles_cli.api import Client
|
|
7
|
+
from code_battles_cli.log import log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.option(
|
|
12
|
+
"-U",
|
|
13
|
+
"--url",
|
|
14
|
+
help="The competition's URL, for example https://code-battles.web.app",
|
|
15
|
+
)
|
|
16
|
+
@click.option("-u", "--username", help="Your team's username, for example mercedes")
|
|
17
|
+
@click.option("-p", "--password", help="Your team's password")
|
|
18
|
+
@click.option(
|
|
19
|
+
"--dump-credentials",
|
|
20
|
+
is_flag=True,
|
|
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
|
+
"-b",
|
|
27
|
+
"--bot",
|
|
28
|
+
help="A specific bot name to download (downloads everything by default)",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"-d",
|
|
32
|
+
type=click.Path(file_okay=False),
|
|
33
|
+
default="bots",
|
|
34
|
+
show_default=True,
|
|
35
|
+
help="The directory to download the bots into",
|
|
36
|
+
)
|
|
37
|
+
def download(
|
|
38
|
+
url: Optional[str],
|
|
39
|
+
username: Optional[str],
|
|
40
|
+
password: Optional[str],
|
|
41
|
+
dump_credentials: bool,
|
|
42
|
+
bot: Optional[str],
|
|
43
|
+
d: str,
|
|
44
|
+
):
|
|
45
|
+
client = Client(url, username, password, dump_credentials)
|
|
46
|
+
bots = client.get_bots()
|
|
47
|
+
if bot is not None:
|
|
48
|
+
if bot not in bots:
|
|
49
|
+
raise Exception(f"The specified bot '{bot}' does not exist!")
|
|
50
|
+
bots = {bot: bots[bot]}
|
|
51
|
+
|
|
52
|
+
if not os.path.exists(d):
|
|
53
|
+
os.makedirs(d)
|
|
54
|
+
|
|
55
|
+
for bot_name, bot_code in bots.items():
|
|
56
|
+
with open(os.path.join(d, bot_name + ".py"), "w") as f:
|
|
57
|
+
f.write(bot_code)
|
|
58
|
+
|
|
59
|
+
log.info(
|
|
60
|
+
f"{len(bots)} {'bots were' if len(bots) != 1 else 'bot was'} downloaded to [blue]{d}[/blue]."
|
|
61
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from code_battles_cli.api import Client, Simulation
|
|
9
|
+
from code_battles_cli.log import log, progress
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.argument("map", required=False)
|
|
14
|
+
@click.argument("bots", nargs=-1)
|
|
15
|
+
@click.option(
|
|
16
|
+
"-U",
|
|
17
|
+
"--url",
|
|
18
|
+
help="The competition's URL, for example https://code-battles.web.app",
|
|
19
|
+
)
|
|
20
|
+
@click.option("-u", "--username", help="Your team's username, for example mercedes")
|
|
21
|
+
@click.option("-p", "--password", help="Your team's password")
|
|
22
|
+
@click.option(
|
|
23
|
+
"--dump-credentials",
|
|
24
|
+
is_flag=True,
|
|
25
|
+
default=True,
|
|
26
|
+
show_default=True,
|
|
27
|
+
help="Dump the URL, username and password to a `code-battles.json` file",
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--force-download",
|
|
31
|
+
is_flag=True,
|
|
32
|
+
default=False,
|
|
33
|
+
show_default=True,
|
|
34
|
+
help="Re-downloads the simulation code from the website",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"-s", "--seed", type=int, help="Sets the given randomness seed for the simulation"
|
|
38
|
+
)
|
|
39
|
+
@click.option(
|
|
40
|
+
"-S",
|
|
41
|
+
"--simulation-file",
|
|
42
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
43
|
+
help="Re-downloads the simulation code from the website",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"-O",
|
|
47
|
+
"output_file",
|
|
48
|
+
type=click.Path(exists=False),
|
|
49
|
+
help="Output file to dump the simulation into",
|
|
50
|
+
)
|
|
51
|
+
def run(
|
|
52
|
+
url: Optional[str],
|
|
53
|
+
username: Optional[str],
|
|
54
|
+
password: Optional[str],
|
|
55
|
+
dump_credentials: bool,
|
|
56
|
+
map: str,
|
|
57
|
+
bots: Tuple[str],
|
|
58
|
+
force_download: bool,
|
|
59
|
+
seed: Optional[int],
|
|
60
|
+
simulation_file: Optional[str],
|
|
61
|
+
output_file: Optional[str],
|
|
62
|
+
):
|
|
63
|
+
logging.getLogger("rich").setLevel(logging.WARNING)
|
|
64
|
+
client = Client(url, username, password, dump_credentials)
|
|
65
|
+
client._possibly_download(force_download)
|
|
66
|
+
start = time.time()
|
|
67
|
+
|
|
68
|
+
with progress:
|
|
69
|
+
progress_id = progress.add_task("[green]Simulating...", total=None)
|
|
70
|
+
|
|
71
|
+
def on_step():
|
|
72
|
+
progress.update(progress_id, advance=1)
|
|
73
|
+
|
|
74
|
+
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!"
|
|
87
|
+
|
|
88
|
+
with open(simulation_file, "r") as f:
|
|
89
|
+
simulation_text = f.read()
|
|
90
|
+
simulation = Simulation.load(simulation_text)
|
|
91
|
+
logging.info(
|
|
92
|
+
f"Detected simulation file of {simulation.game} {simulation.version}"
|
|
93
|
+
)
|
|
94
|
+
logging.info(
|
|
95
|
+
f"Simulation happened in {simulation.map} between {', '.join(simulation.player_names)} with seed {simulation.seed}"
|
|
96
|
+
)
|
|
97
|
+
logging.info(f"Simulation contains {len(simulation.decisions)} decisions")
|
|
98
|
+
progress.update(progress_id, total=len(simulation.decisions) - 1)
|
|
99
|
+
results = client.run_simulation_from_file(
|
|
100
|
+
simulation_file, force_download=False, on_step=on_step
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
assert map is not None, "Map is required for simulation!"
|
|
104
|
+
assert len(bots) > 0, "Bots are required for simulation!"
|
|
105
|
+
|
|
106
|
+
results = client.run_simulation(
|
|
107
|
+
map,
|
|
108
|
+
list(bots),
|
|
109
|
+
seed=seed,
|
|
110
|
+
force_download=False,
|
|
111
|
+
on_step=on_step,
|
|
112
|
+
output_file=output_file,
|
|
113
|
+
)
|
|
114
|
+
print("The winner is", results.winner, "after", results.steps, "steps.")
|
|
115
|
+
end = time.time()
|
|
116
|
+
log.info(f"Simulation took {(end - start) :.2f}s.")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from code_battles_cli.api import Client
|
|
7
|
+
from code_battles_cli.log import log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.option(
|
|
12
|
+
"-U",
|
|
13
|
+
"--url",
|
|
14
|
+
help="The competition's URL, for example https://code-battles.web.app",
|
|
15
|
+
)
|
|
16
|
+
@click.option("-u", "--username", help="Your team's username, for example mercedes")
|
|
17
|
+
@click.option("-p", "--password", help="Your team's password")
|
|
18
|
+
@click.option(
|
|
19
|
+
"--dump-credentials",
|
|
20
|
+
is_flag=True,
|
|
21
|
+
default=True,
|
|
22
|
+
show_default=True,
|
|
23
|
+
help="Dump the URL, username and password to a `code-battles.json` file",
|
|
24
|
+
)
|
|
25
|
+
@click.argument(
|
|
26
|
+
"file",
|
|
27
|
+
type=click.Path(exists=True, readable=True, dir_okay=False),
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"-n",
|
|
31
|
+
"--name",
|
|
32
|
+
help="The name of the bot to upload. By default, the file name without the extension will be used as the bot's name.",
|
|
33
|
+
)
|
|
34
|
+
def upload(
|
|
35
|
+
file: str,
|
|
36
|
+
name: Optional[str],
|
|
37
|
+
url: Optional[str],
|
|
38
|
+
username: Optional[str],
|
|
39
|
+
password: Optional[str],
|
|
40
|
+
dump_credentials: bool,
|
|
41
|
+
):
|
|
42
|
+
bot_name = name if name is not None else os.path.splitext(os.path.basename(file))[0]
|
|
43
|
+
with open(file, "r") as f:
|
|
44
|
+
bot_code = f.read()
|
|
45
|
+
|
|
46
|
+
client = Client(url, username, password, dump_credentials)
|
|
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}'.")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn
|
|
6
|
+
from rich.table import Column
|
|
7
|
+
from rich.traceback import install
|
|
8
|
+
from rich.logging import RichHandler
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger("rich")
|
|
11
|
+
console = Console()
|
|
12
|
+
progress = Progress(
|
|
13
|
+
TextColumn(
|
|
14
|
+
"[progress.description]{task.description}",
|
|
15
|
+
table_column=Column(width=50),
|
|
16
|
+
),
|
|
17
|
+
BarColumn(bar_width=None),
|
|
18
|
+
MofNCompleteColumn(table_column=Column(width=10, justify="right")),
|
|
19
|
+
console=console,
|
|
20
|
+
expand=True,
|
|
21
|
+
transient=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def setup_logging():
|
|
26
|
+
FORMAT = "%(message)s"
|
|
27
|
+
|
|
28
|
+
logging.basicConfig(
|
|
29
|
+
level=os.environ.get("CODE_BATTLES_LOG_LEVEL", "INFO"),
|
|
30
|
+
format=FORMAT,
|
|
31
|
+
datefmt="[%X]",
|
|
32
|
+
handlers=[RichHandler(console=console, markup=True)],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
install()
|