code-battles-cli 1.2.2__py3-none-any.whl → 1.2.4__py3-none-any.whl

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.
@@ -1 +1 @@
1
- from code_battles_cli.cli import main
1
+ from code_battles_cli.cli import main as main
@@ -1,4 +1,5 @@
1
- from code_battles_cli.cli import main
2
1
  import sys
3
2
 
3
+ from code_battles_cli.cli import main
4
+
4
5
  sys.exit(main())
code_battles_cli/api.py CHANGED
@@ -4,6 +4,8 @@ Code Battles Python Client API
4
4
  Firestore client implementation inspired by https://medium.com/@bobthomas295/client-side-authentication-with-python-firestore-and-firebase-352e484a2634
5
5
  """
6
6
 
7
+ from __future__ import annotations
8
+
7
9
  import base64
8
10
  import datetime
9
11
  import gzip
@@ -15,12 +17,13 @@ import subprocess
15
17
  import sys
16
18
  from dataclasses import dataclass
17
19
  from pathlib import Path
18
- from typing import Any, Callable, Dict, List, Optional, Union
20
+ from typing import Any, Callable, Literal
19
21
 
20
22
  import requests
21
23
  from google.cloud.firestore import Client as FirestoreClient
22
24
  from google.oauth2.credentials import Credentials
23
25
  from rich.prompt import Prompt
26
+ from typing_extensions import overload
24
27
 
25
28
  from code_battles_cli.log import console, log
26
29
 
@@ -28,7 +31,7 @@ SIMULATION_FINISHED_MARK = b"--- SIMULATION FINISHED ---"
28
31
  SIMULATION_STEP_MARK = b"__CODE_BATTLES_ADVANCE_STEP"
29
32
 
30
33
 
31
- def normalize(url: str):
34
+ def normalize(url: str) -> str:
32
35
  if url.startswith("https"):
33
36
  url = url[5:]
34
37
 
@@ -45,7 +48,7 @@ class SimulationException(Exception):
45
48
  self.stderr = stderr
46
49
  self.exit_code = exit_code
47
50
 
48
- def __str__(self):
51
+ def __str__(self) -> str:
49
52
  return (
50
53
  f"Simulation failed with exit code {self.exit_code}. Output:\n"
51
54
  + self.stderr.decode()
@@ -57,7 +60,7 @@ class LogEntry:
57
60
  step: int
58
61
  text: str
59
62
  color: str
60
- player_index: Optional[int]
63
+ player_index: int | None
61
64
 
62
65
 
63
66
  @dataclass
@@ -65,22 +68,23 @@ class SimulationResults:
65
68
  winner_index: int
66
69
  winner: str
67
70
  steps: int
68
- logs: List[LogEntry]
71
+ logs: list[LogEntry]
69
72
 
70
73
 
71
74
  @dataclass
72
75
  class Simulation:
73
- parameters: Dict[str, str]
76
+ parameters: dict[str, str]
77
+ statistics: dict[str, float]
74
78
  player_names: str
75
79
  game: str
76
80
  version: str
77
81
  timestamp: datetime.datetime
78
- logs: list
79
- alerts: list
80
- decisions: List[bytes]
82
+ logs: list[Any]
83
+ alerts: list[Any]
84
+ decisions: list[bytes]
81
85
  seed: int
82
86
 
83
- def dump(self):
87
+ def dump(self) -> str:
84
88
  return base64.b64encode(
85
89
  gzip.compress(
86
90
  json.dumps(
@@ -97,18 +101,20 @@ class Simulation:
97
101
  for decision in self.decisions
98
102
  ],
99
103
  "seed": self.seed,
104
+ "statistics": self.statistics,
100
105
  }
101
106
  ).encode()
102
107
  )
103
108
  ).decode()
104
109
 
105
110
  @staticmethod
106
- def load(file: str):
107
- contents: Dict[str, Any] = json.loads(gzip.decompress(base64.b64decode(file)))
111
+ def load(file: str) -> Simulation:
112
+ contents: dict[str, Any] = json.loads(gzip.decompress(base64.b64decode(file)))
108
113
  return Simulation(
109
114
  contents["parameters"]
110
115
  if "parameters" in contents
111
116
  else {"map": contents["map"]},
117
+ contents.get("statistics", {}),
112
118
  contents["playerNames"],
113
119
  contents["game"],
114
120
  contents["version"],
@@ -123,10 +129,10 @@ class Simulation:
123
129
  class Client:
124
130
  def __init__(
125
131
  self,
126
- url: Optional[str] = None,
127
- username: Optional[str] = None,
128
- password: Optional[str] = None,
129
- dump_credentials=True,
132
+ url: str | None = None,
133
+ username: str | None = None,
134
+ password: str | None = None,
135
+ dump_credentials: bool = True,
130
136
  ):
131
137
  """
132
138
  Creates a client for getting and setting the bots for a Code Battles hosted at `url`
@@ -146,16 +152,13 @@ class Client:
146
152
  if dump_credentials:
147
153
  self._dump_credentials()
148
154
 
149
- def _get_credentials(self):
155
+ def _get_credentials(self) -> None:
150
156
  if os.path.exists("code-battles.json"):
151
- try:
152
- with open("code-battles.json", "r") as f:
153
- configuration = json.load(f)
154
- self.url: str = configuration["url"]
155
- self.username: str = configuration["username"]
156
- self.password: str = configuration["password"]
157
- except Exception:
158
- pass
157
+ with open("code-battles.json", "r") as f:
158
+ configuration = json.load(f)
159
+ self.url = configuration["url"]
160
+ self.username = configuration["username"]
161
+ self.password = configuration["password"]
159
162
 
160
163
  if (
161
164
  not hasattr(self, "url")
@@ -171,14 +174,14 @@ class Client:
171
174
 
172
175
  self.username = Prompt.ask("Enter your team's username", console=console)
173
176
 
174
- if not self.username == self.username.lower():
177
+ if self.username != self.username.lower():
175
178
  log.warning("Your username should most likely be lowercased.")
176
179
 
177
180
  self.password = Prompt.ask(
178
181
  "Enter your team's password", console=console, password=True
179
182
  )
180
183
 
181
- def _dump_credentials(self):
184
+ def _dump_credentials(self) -> None:
182
185
  with open("code-battles.json", "w") as f:
183
186
  json.dump(
184
187
  {"url": self.url, "username": self.username, "password": self.password},
@@ -188,12 +191,12 @@ class Client:
188
191
  "Credentials were dumped to `code-battles.json`. Make sure other teams don't have access to this file!"
189
192
  )
190
193
 
191
- def _get_firebase_data(self):
194
+ def _get_firebase_data(self) -> None:
192
195
  configuration = requests.get(self.url + "/firebase-configuration.json").json()
193
196
  self.firebase_api_key: str = configuration["apiKey"]
194
197
  self.firebase_project_id: str = configuration["projectId"]
195
198
 
196
- def _sign_in(self, email_domain="gmail.com"):
199
+ def _sign_in(self, email_domain: str = "gmail.com") -> None:
197
200
  try:
198
201
  response = requests.post(
199
202
  f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={self.firebase_api_key}",
@@ -203,27 +206,29 @@ class Client:
203
206
  "returnSecureToken": True,
204
207
  },
205
208
  ).json()
206
- except Exception:
207
- raise Exception(
209
+ except Exception as e:
210
+ raise RuntimeError(
208
211
  "Sign in failed! Make sure the username and password are correct."
209
- )
212
+ ) from e
210
213
 
211
214
  self.credentials = Credentials(response["idToken"], response["refreshToken"])
212
215
  self.client = FirestoreClient(self.firebase_project_id, self.credentials)
213
216
  self.document = self.client.document(f"bots/{self.username}")
214
217
 
215
- def get_bots(self) -> Dict[str, str]:
218
+ def get_bots(self) -> dict[str, str]:
216
219
  """Returns a mapping from a bot's name to their Python code."""
217
- return self.document.get().to_dict()
220
+ result = self.document.get().to_dict()
221
+ assert result is not None
222
+ return result
218
223
 
219
- def set_bots(self, bots: Dict[str, str], merge=True) -> None:
224
+ def set_bots(self, bots: dict[str, str], merge: bool = True) -> None:
220
225
  """
221
226
  Sets the bots in the website to the specified bots.
222
227
  Doesn't remove any bot unless ``merge`` is ``False``, in which case only bots specified in ``bots`` will remain.
223
228
  """
224
229
  self.document.set(bots, merge)
225
230
 
226
- def _possibly_download(self, force_download=False):
231
+ def _possibly_download(self, force_download: bool = False) -> str:
227
232
  directory_name = "".join(
228
233
  [c for c in normalize(self.url) if c.isalnum() or c == "-"]
229
234
  )
@@ -245,14 +250,16 @@ class Client:
245
250
 
246
251
  def _get_simulation_output(
247
252
  self,
248
- p: subprocess.Popen,
249
- json_output=False,
250
- on_step: Optional[Callable[[], None]] = None,
251
- ) -> Union[SimulationResults, str]:
253
+ p: subprocess.Popen[bytes],
254
+ json_output: bool = False,
255
+ on_step: Callable[[], None] | None = None,
256
+ ) -> SimulationResults | str:
252
257
  while True:
253
- if p.poll() is not None and p.returncode != 0:
258
+ if p.poll() is not None:
259
+ assert p.stderr is not None
254
260
  raise SimulationException(p.stderr.read(), p.returncode)
255
261
 
262
+ assert p.stdout is not None
256
263
  line: bytes = p.stdout.readline()
257
264
  line = line.strip()
258
265
  if line == SIMULATION_FINISHED_MARK:
@@ -267,19 +274,20 @@ class Client:
267
274
  if json_output:
268
275
  return output.decode()
269
276
 
270
- output = json.loads(output)
277
+ output_json = json.loads(output)
271
278
  result = SimulationResults(
272
- output["winner_index"],
273
- output["winner"],
274
- output["steps"],
279
+ output_json["winner_index"],
280
+ output_json["winner"],
281
+ output_json["steps"],
275
282
  [
276
283
  LogEntry(
277
284
  entry["step"], entry["text"], entry["color"], entry["player_index"]
278
285
  )
279
- for entry in output["logs"]
286
+ for entry in output_json["logs"]
280
287
  ],
281
288
  )
282
289
 
290
+ assert p.stderr is not None
283
291
  error: bytes = p.stderr.read()
284
292
  exit_code = p.wait()
285
293
  if exit_code != 0:
@@ -287,17 +295,43 @@ class Client:
287
295
 
288
296
  return result
289
297
 
298
+ @overload
299
+ def run_simulation(
300
+ self,
301
+ parameters: dict[str, str],
302
+ bot_filenames: list[str],
303
+ bot_names: list[str] | None = None,
304
+ seed: int | None = None,
305
+ force_download: bool = False,
306
+ json_output: Literal[False] = False,
307
+ on_step: Callable[[], None] | None = None,
308
+ output_file: str | None = None,
309
+ ) -> SimulationResults: ...
310
+
311
+ @overload
312
+ def run_simulation(
313
+ self,
314
+ parameters: dict[str, str],
315
+ bot_filenames: list[str],
316
+ bot_names: list[str] | None = None,
317
+ seed: int | None = None,
318
+ force_download: bool = False,
319
+ json_output: Literal[True] = True,
320
+ on_step: Callable[[], None] | None = None,
321
+ output_file: str | None = None,
322
+ ) -> str: ...
323
+
290
324
  def run_simulation(
291
325
  self,
292
- parameters: Dict[str, str],
293
- bot_filenames: List[str],
294
- bot_names: Optional[List[str]] = None,
295
- seed: Optional[int] = None,
296
- force_download=False,
297
- json_output=False,
298
- on_step: Optional[Callable[[], None]] = None,
299
- output_file: Optional[str] = None,
300
- ) -> Union[SimulationResults, str]:
326
+ parameters: dict[str, str],
327
+ bot_filenames: list[str],
328
+ bot_names: list[str] | None = None,
329
+ seed: int | None = None,
330
+ force_download: bool = False,
331
+ json_output: bool = False,
332
+ on_step: Callable[[], None] | None = None,
333
+ output_file: str | None = None,
334
+ ) -> SimulationResults | str:
301
335
  """
302
336
  Runs the given simulation without UI locally.
303
337
  If ``bot_names`` is not specified, they will be the filenames without the extension.
@@ -340,10 +374,10 @@ class Client:
340
374
  def run_simulation_from_file(
341
375
  self,
342
376
  simulation_file: str,
343
- force_download=False,
344
- json_output=False,
345
- on_step: Optional[Callable[[], None]] = None,
346
- ) -> Union[SimulationResults, str]:
377
+ force_download: bool = False,
378
+ json_output: bool = False,
379
+ on_step: Callable[[], None] | None = None,
380
+ ) -> SimulationResults | str:
347
381
  """
348
382
  Runs the given simulation without UI locally from the given simulation file.
349
383
 
code_battles_cli/cli.py CHANGED
@@ -1,22 +1,20 @@
1
- import datetime
2
-
3
1
  import click
4
2
 
5
3
 
6
4
  @click.group()
7
5
  @click.version_option(message="%(prog)s %(version)s")
8
- def cli():
6
+ def cli() -> None:
9
7
  pass
10
8
 
11
9
 
12
10
  def main() -> int:
13
- from code_battles_cli.log import setup_logging, log
11
+ from code_battles_cli.log import setup_logging
14
12
 
15
13
  setup_logging()
16
14
 
17
15
  from code_battles_cli.commands.download import download
18
- from code_battles_cli.commands.upload import upload
19
16
  from code_battles_cli.commands.run import run
17
+ from code_battles_cli.commands.upload import upload
20
18
 
21
19
  cli.add_command(download)
22
20
  cli.add_command(upload)
@@ -1,5 +1,6 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
- from typing import Optional
3
4
 
4
5
  import click
5
6
 
@@ -35,18 +36,18 @@ from code_battles_cli.log import log
35
36
  help="The directory to download the bots into",
36
37
  )
37
38
  def download(
38
- url: Optional[str],
39
- username: Optional[str],
40
- password: Optional[str],
39
+ url: str | None,
40
+ username: str | None,
41
+ password: str | None,
41
42
  dump_credentials: bool,
42
- bot: Optional[str],
43
+ bot: str | None,
43
44
  d: str,
44
- ):
45
+ ) -> None:
45
46
  client = Client(url, username, password, dump_credentials)
46
47
  bots = client.get_bots()
47
48
  if bot is not None:
48
49
  if bot not in bots:
49
- raise Exception(f"The specified bot '{bot}' does not exist!")
50
+ raise KeyError(bot)
50
51
  bots = {bot: bots[bot]}
51
52
 
52
53
  if not os.path.exists(d):
@@ -1,8 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  import logging
3
- import sys
4
5
  import time
5
- from typing import List, Optional, Tuple
6
6
 
7
7
  import click
8
8
 
@@ -50,17 +50,17 @@ from code_battles_cli.log import log, progress
50
50
  help="Output file to dump the simulation into",
51
51
  )
52
52
  def run(
53
- url: Optional[str],
54
- username: Optional[str],
55
- password: Optional[str],
53
+ url: str | None,
54
+ username: str | None,
55
+ password: str | None,
56
56
  dump_credentials: bool,
57
- parameters: Optional[str],
58
- bots: Tuple[str],
57
+ parameters: str | None,
58
+ bots: tuple[str],
59
59
  force_download: bool,
60
- seed: Optional[int],
61
- simulation_file: Optional[str],
62
- output_file: Optional[str],
63
- ):
60
+ seed: int | None,
61
+ simulation_file: str | None,
62
+ output_file: str | None,
63
+ ) -> None:
64
64
  logging.getLogger("rich").setLevel(logging.WARNING)
65
65
  client = Client(url, username, password, dump_credentials)
66
66
  client._possibly_download(force_download)
@@ -69,7 +69,7 @@ def run(
69
69
  with progress:
70
70
  progress_id = progress.add_task("[green]Simulating...", total=None)
71
71
 
72
- def on_step():
72
+ def on_step() -> None:
73
73
  progress.update(progress_id, advance=1)
74
74
 
75
75
  if simulation_file is not None:
@@ -103,6 +103,7 @@ def run(
103
103
  else:
104
104
  assert map is not None, "Map is required for simulation!"
105
105
  assert len(bots) > 0, "Bots are required for simulation!"
106
+ assert parameters is not None, "Parameters are required for simulation!"
106
107
 
107
108
  results = client.run_simulation(
108
109
  json.loads(parameters),
@@ -1,5 +1,6 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
- from typing import Optional
3
4
 
4
5
  import click
5
6
 
@@ -33,12 +34,12 @@ from code_battles_cli.log import log
33
34
  )
34
35
  def upload(
35
36
  file: str,
36
- name: Optional[str],
37
- url: Optional[str],
38
- username: Optional[str],
39
- password: Optional[str],
37
+ name: str | None,
38
+ url: str | None,
39
+ username: str | None,
40
+ password: str | None,
40
41
  dump_credentials: bool,
41
- ):
42
+ ) -> None:
42
43
  bot_name = name if name is not None else os.path.splitext(os.path.basename(file))[0]
43
44
  with open(file, "r") as f:
44
45
  bot_code = f.read()
code_battles_cli/log.py CHANGED
@@ -1,11 +1,11 @@
1
- import os
2
1
  import logging
2
+ import os
3
3
 
4
4
  from rich.console import Console
5
+ from rich.logging import RichHandler
5
6
  from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn
6
7
  from rich.table import Column
7
8
  from rich.traceback import install
8
- from rich.logging import RichHandler
9
9
 
10
10
  log = logging.getLogger("rich")
11
11
  console = Console()
@@ -22,7 +22,7 @@ progress = Progress(
22
22
  )
23
23
 
24
24
 
25
- def setup_logging():
25
+ def setup_logging() -> None:
26
26
  FORMAT = "%(message)s"
27
27
 
28
28
  logging.basicConfig(
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: code-battles-cli
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
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
5
  Requires-Dist: requests>=2.32.3
6
+ Requires-Dist: google-cloud-firestore>=2.19.0
9
7
  Requires-Dist: rich>=13.9.3
8
+ Requires-Dist: click>=8.1.7
9
+ Requires-Python: >=3.8
10
10
  Description-Content-Type: text/markdown
11
11
 
12
12
  <p align="center">
@@ -27,8 +27,6 @@ Description-Content-Type: text/markdown
27
27
  🏃 <a href="#getting-started">Getting Started</a>
28
28
  &nbsp;&middot&nbsp;
29
29
  💡 <a href="#features">Features</a>
30
- &nbsp;&middot&nbsp;
31
- 🚗 <a href="#roadmap">Roadmap</a>
32
30
  </p>
33
31
 
34
32
  # Getting Started
@@ -72,9 +70,6 @@ print(client.get_bots())
72
70
  print(client.run_simulation("NYC", ["bots/example.py", "bots/another_example.py"]))
73
71
  ```
74
72
 
75
- # Roadmap
73
+ # Development
76
74
 
77
- - [x] Download bots.
78
- - [x] Upload bots.
79
- - [x] Run no-UI simulations locally.
80
- - [x] Be usable in scripts.
75
+ For development, you should set up the pre-commit hooks with `uv run pre-commit install`.
@@ -0,0 +1,13 @@
1
+ code_battles_cli/__init__.py,sha256=e4yZ-ISH6W6yB4H3LfSwWqgLCoDq590AqLc9oDBJm28,46
2
+ code_battles_cli/__main__.py,sha256=GPfXVInXFwXguLMPetLgyZMpurU_JTLZSA-434qa5FQ,68
3
+ code_battles_cli/api.py,sha256=wgJvasD4KRjMvKm7mY439VVbCqTznq2G1KmHuH8ySwE,13242
4
+ code_battles_cli/cli.py,sha256=i_q52rrwhgcvJAMPw22TWjnux8ZJuqTBmtpRmZ8Sij8,501
5
+ code_battles_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ code_battles_cli/commands/download.py,sha256=3t0if_umsLVdLL6Ub16uabnKJ1lYahxl6Vl_q6QcttQ,1562
7
+ code_battles_cli/commands/run.py,sha256=Sn6ae_gKKPCdQeVc0ngs5Sc158kAYEQxB3nkC9_IM5M,3936
8
+ code_battles_cli/commands/upload.py,sha256=yQnDFV32q7Ceca1d7uhBjSKh0q9nEwNUWIFI55Cxs2s,1393
9
+ code_battles_cli/log.py,sha256=cptG9uKvfFt_qQqWiFy3HwTsJOgl-kQ1RGsf7C1MD8E,868
10
+ code_battles_cli-1.2.4.dist-info/WHEEL,sha256=cmC5s21ojypbVslldL7IJq3hjZH-tINy4rziKePFsG0,81
11
+ code_battles_cli-1.2.4.dist-info/entry_points.txt,sha256=kBhS-oNCbSPAyLJql9I0uPxnYqgcnhCIgDM9ZpdLiKs,56
12
+ code_battles_cli-1.2.4.dist-info/METADATA,sha256=3msqI8ydwhsX75du8SEIgZCfOhlrOjwPvJOfgWOmMgI,2439
13
+ code_battles_cli-1.2.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.26.3
2
+ Generator: uv 0.12.23
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  code-battles = code_battles_cli:main
3
+
@@ -1,14 +0,0 @@
1
- code_battles_cli/__init__.py,sha256=BST1iXWs0ner82h-LTYTgVNjLtOpVZnvBMiCEiBkOXE,38
2
- code_battles_cli/__main__.py,sha256=HguVXadDv5QVywZWoB-BETvX1WV4VPYxclfnKhvOrZc,67
3
- code_battles_cli/api.py,sha256=iv7peRbTsjL29lU5XjrjfFo9cZ3QUubA5Q4kziz2SAo,12056
4
- code_battles_cli/cli.py,sha256=yfUAlU85FBykU_pHB6HHQCc7_ZN0KiPbc8ssB4TOgIc,515
5
- code_battles_cli/log.py,sha256=Ume7UPFdKNqu-DI2hpc6cefyMZQeFavc_cWizKooq98,860
6
- code_battles_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- code_battles_cli/commands/download.py,sha256=ZLwbHdcqak6uuXEMb1Mp4fXW7QxkdbIlpKxSUR-WQDA,1600
8
- code_battles_cli/commands/run.py,sha256=J3IFVVoUAJWLa7ZtDvDhrFYt7Yx3aQYbVo9Z2SomeWQ,3872
9
- code_battles_cli/commands/upload.py,sha256=8Y6kKS4ynnmsmfM0IqtljF6gT7PUcrcJz9ilzrKUuTk,1389
10
- code_battles_cli-1.2.2.dist-info/METADATA,sha256=S5v0En-U7awBMja-F0AbF8p7HfWsBjRZNb4NDWRL8vE,2523
11
- code_battles_cli-1.2.2.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
12
- code_battles_cli-1.2.2.dist-info/entry_points.txt,sha256=U_iQUEDK-9L09jXd_sJg0IgKLWeN3rccCyNVUeJuQxw,55
13
- code_battles_cli-1.2.2.dist-info/licenses/LICENSE.md,sha256=eYDByTiNbk3_TPH_i-FU-bHnfNb6VidN57zMO2dfaUM,1087
14
- code_battles_cli-1.2.2.dist-info/RECORD,,
@@ -1,7 +0,0 @@
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.