ballista-cli 0.1.0__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.
File without changes
ballista_cli/cli.py ADDED
@@ -0,0 +1,533 @@
1
+ import os.path
2
+ from enum import StrEnum
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import prettytable
7
+ import typer
8
+ import yaml
9
+ from async_typer import AsyncTyper
10
+ from ballista_sdk.adapters import EnvironmentWithAdapter
11
+ from ballista_sdk.adapters.docker_compose import DockerComposeInfrastructureAdapter
12
+ from ballista_sdk.adapters.kubernetes import KubernetesAPIInfrastructureAdapter
13
+ from ballista_sdk.api.v1 import (
14
+ Bolt,
15
+ DefaultExecutionParameters,
16
+ Environment,
17
+ EnvironmentTier,
18
+ ExecutionParameters,
19
+ ExternalizedServiceParameters,
20
+ VolumeExecutionParameters,
21
+ )
22
+ from ballista_sdk.bolts.factory import BoltFactory
23
+ from ballista_sdk.bolts.v1 import BoltV1Factory
24
+
25
+ DOCKER_COMPOSE_LOCAL = False
26
+
27
+
28
+ # TODO: Stuff to get a ballista-workspace.yaml file, or maybe a .ballista folder in a parent folder?
29
+
30
+
31
+ def get_bolt_factory(api_version: str) -> BoltFactory:
32
+ if api_version == "v1":
33
+ return BoltV1Factory()
34
+
35
+ raise ValueError()
36
+
37
+
38
+ def get_local_bolt() -> Bolt:
39
+ """Get a Bolt from local path."""
40
+
41
+ filename = "./ballista.yaml" if os.path.isfile("./ballista.yaml") else None
42
+ if not filename:
43
+ raise ValueError("NO BALLISTA.YAML")
44
+
45
+ with open(filename, "r") as f:
46
+ ballista_yaml = yaml.safe_load(f)
47
+
48
+ if ballista_yaml is None:
49
+ raise ValueError()
50
+
51
+ api_version = ballista_yaml.get("api_version")
52
+
53
+ factory = get_bolt_factory(api_version=api_version)
54
+ return factory.get_bolt(ballista_yaml)
55
+
56
+
57
+ def get_local_default_bolts() -> list[Bolt]:
58
+ """Loads default Bolts from home folder."""
59
+
60
+ bolts = []
61
+
62
+ home = Path.home()
63
+ ballista_folder = home / ".ballista"
64
+ defaults_yaml = ballista_folder / "defaults.yaml"
65
+ if defaults_yaml.exists():
66
+ with open(defaults_yaml) as f:
67
+ for bolt_yaml in yaml.safe_load_all(f):
68
+ api_version = bolt_yaml.get("api_version")
69
+
70
+ factory = get_bolt_factory(api_version=api_version)
71
+ bolt = factory.get_bolt(bolt_yaml)
72
+ bolts.append(bolt)
73
+
74
+ return bolts
75
+
76
+
77
+ def get_local_environment() -> EnvironmentWithAdapter:
78
+ """Get a local Environment for development purposes."""
79
+
80
+ # TODO: When we have registry support, we won't need this.
81
+ # Load any local Bolts
82
+ local_bolts = get_local_default_bolts()
83
+
84
+ if DOCKER_COMPOSE_LOCAL:
85
+ adapter = DockerComposeInfrastructureAdapter(local_bolts)
86
+ else:
87
+ adapter = KubernetesAPIInfrastructureAdapter()
88
+
89
+ environment = Environment(name="local", tier=EnvironmentTier.DEVELOPMENT)
90
+
91
+ return environment, adapter
92
+
93
+
94
+ # TODO: Improve this when there are more adapters
95
+ async def get_remote_environments() -> list[EnvironmentWithAdapter]:
96
+ # Read in remote Kubernetes environments
97
+ adapter = KubernetesAPIInfrastructureAdapter()
98
+ return [(e, adapter) for e in await adapter.list_environments()]
99
+
100
+
101
+ async def get_available_environments(
102
+ include_remotes: bool = False, environment_names: list[str] | None = None
103
+ ) -> list[EnvironmentWithAdapter]:
104
+ environments = [get_local_environment()]
105
+
106
+ if include_remotes:
107
+ environments.extend(await get_remote_environments())
108
+
109
+ if environment_names:
110
+ return [(environment, _) for environment, _ in environments if environment.name in environment_names]
111
+
112
+ else:
113
+ return environments
114
+
115
+
116
+ def get_local_execution_parameters() -> ExecutionParameters:
117
+ """Get ExecutionParameters for the local development environment with the following settings:
118
+
119
+ - All services are externalized at `localhost`.
120
+ - Volumes are allowed at ???.
121
+
122
+ """
123
+
124
+ return ExecutionParameters(
125
+ initial=DefaultExecutionParameters(
126
+ external_service=ExternalizedServiceParameters(host="localhost"),
127
+ volume=VolumeExecutionParameters(type="standard"),
128
+ )
129
+ )
130
+
131
+
132
+ def get_dev_execution_parameters() -> ExecutionParameters:
133
+ return ExecutionParameters(
134
+ initial=DefaultExecutionParameters(
135
+ external_service=ExternalizedServiceParameters(host="dev.ballista.build", port=8123),
136
+ volume=VolumeExecutionParameters(type="standard"),
137
+ )
138
+ )
139
+
140
+
141
+ cli = AsyncTyper()
142
+
143
+
144
+ @cli.command(short_help="initialize a new ballista powered project")
145
+ def init(project: Annotated[str, typer.Argument(help="Name of new project.")]):
146
+ # TODO: Need to pick an api, so just use v1 for now. We'll probably want a default version with compatibility for old ones up to a certain date.
147
+ if True:
148
+ bolt_factory = BoltV1Factory()
149
+
150
+ # Check if that project (folder) already exists
151
+ if os.path.exists(project):
152
+ raise ValueError(f'Path "{project}" already exists.')
153
+
154
+ os.makedirs(project)
155
+ new_bolt = bolt_factory.create_bolt(project=project, version="0.1.0")
156
+ with open(os.path.join(project, "ballista.yaml"), "w") as f:
157
+ yaml.dump(new_bolt.to_dict(), f)
158
+
159
+
160
+ @cli.command(
161
+ short_help="Build artifacts defined in ballista.yaml",
162
+ help="""Build a path containing a ballista.yaml into a Launch-able Bolt.
163
+
164
+ Defined artifacts contained in a ballista.yaml will be built and ready to be Launched.
165
+ """,
166
+ )
167
+ def build(
168
+ artifacts: Annotated[list[str] | None, typer.Argument(help="List of artifacts to buid.")] = None,
169
+ artifact_types: Annotated[
170
+ list[str] | None,
171
+ typer.Option(help="List of specified Artifact Types to build."),
172
+ ] = None,
173
+ ):
174
+ ballista_bolt = get_local_bolt()
175
+
176
+ for artifact in ballista_bolt.buildable_artifacts:
177
+ build = artifact.build
178
+ image_name = f"{ballista_bolt.project}_{artifact.name}:{ballista_bolt.version}"
179
+
180
+ path = "."
181
+ dockerfile = build.dockerfile or "Dockerfile"
182
+ # Process possible path for the Dockerfile
183
+ dockerfile_pieces = dockerfile.rsplit("/", 1)
184
+ if len(dockerfile_pieces) > 1:
185
+ path, _ = dockerfile_pieces
186
+
187
+ cmd = f"docker build {path} -t {image_name} -f {dockerfile} --target {build.dockerfile_target}"
188
+ os.system(cmd)
189
+
190
+
191
+ @cli.command(short_help="Start local development environment.")
192
+ async def up(artifact_names: Annotated[list[str] | None, typer.Option("--artifact", "-a")] = None):
193
+ bolt = get_local_bolt()
194
+ environment, adapter = get_local_environment()
195
+ execution_parameters = get_local_execution_parameters()
196
+
197
+ executable_artifacts = bolt.executable_artifacts
198
+ if artifact_names and (found_artifacts := [a for a in executable_artifacts if a.name in artifact_names]):
199
+ executable_artifacts = found_artifacts
200
+
201
+ await adapter.deploy(
202
+ bolt=bolt,
203
+ artifacts=executable_artifacts,
204
+ environment=environment,
205
+ execution_parameters=execution_parameters,
206
+ )
207
+
208
+
209
+ @cli.command(short_help="Teardown local development environment.")
210
+ async def down():
211
+ bolt = get_local_bolt()
212
+ environment, adapter = get_local_environment()
213
+ execution_parameters = get_local_execution_parameters()
214
+
215
+ await adapter.teardown(
216
+ bolt=bolt,
217
+ artifacts=bolt.executable_artifacts,
218
+ environment=environment,
219
+ execution_parameters=execution_parameters,
220
+ )
221
+
222
+
223
+ @cli.command(short_help="Deploy to an environment.")
224
+ async def deploy(
225
+ environment_name: str, allow_unknown_environment: bool = False, environment_adapter_name: str | None = None
226
+ ):
227
+ environments = await get_available_environments(include_remotes=True, environment_names=[environment_name])
228
+
229
+ if not environments:
230
+ print(f'Unknown environment "{environment_name}".')
231
+ if allow_unknown_environment:
232
+ environment = Environment(name=environment_name, tier=EnvironmentTier.DEVELOPMENT)
233
+ # TODO: Figure this out
234
+ adapter = KubernetesAPIInfrastructureAdapter()
235
+ else:
236
+ typer.Exit(1)
237
+ return
238
+ else:
239
+ environment, adapter = environments[0]
240
+
241
+ ballista_bolt = get_local_bolt()
242
+
243
+ # TODO: Get real execution parameters somehow!
244
+ execution_parameters = get_dev_execution_parameters()
245
+ await adapter.deploy(
246
+ bolt=ballista_bolt,
247
+ artifacts=ballista_bolt.executable_artifacts,
248
+ environment=environment,
249
+ execution_parameters=execution_parameters,
250
+ )
251
+
252
+
253
+ class GenerationTypes(StrEnum):
254
+ SETTINGS = "settings"
255
+
256
+
257
+ @cli.command(short_help="generate files from ballista dependencies")
258
+ def generate(type: GenerationTypes):
259
+ print("GENERATE")
260
+
261
+
262
+ @cli.command(short_help="Execute a command within an Artifact context.")
263
+ def run(artifact_name: str):
264
+ environment, adapter = get_local_environment()
265
+ bolt = get_local_bolt()
266
+
267
+ artifacts = [a for a in bolt.executable_artifacts if a.name == artifact_name]
268
+ if len(artifacts) == 0:
269
+ raise ValueError("artifact_name not found")
270
+
271
+ artifact = artifacts[0]
272
+ # TODO: Get environment the artifact needs
273
+ #
274
+
275
+
276
+ # @cli.command(short_help="Environment information")
277
+ # def env():
278
+ # pass
279
+
280
+
281
+ list_cli = AsyncTyper(no_args_is_help=True)
282
+ cli.add_typer(list_cli, name="list", short_help="List stuff")
283
+
284
+
285
+ @list_cli.command(name="artifact_types")
286
+ async def list_artifact_types(
287
+ environment_names: Annotated[
288
+ list[str] | None, typer.Option("--environment", "-e", help="Specific environment name.")
289
+ ] = None,
290
+ include_remotes: Annotated[bool, typer.Option(help="Include remote environments.")] = False,
291
+ ):
292
+ table = prettytable.PrettyTable()
293
+ table.align = "l"
294
+ table.field_names = ["Artifact Type", "Configs"]
295
+
296
+ environments = await get_available_environments(
297
+ include_remotes=include_remotes, environment_names=environment_names
298
+ )
299
+ for environment, adapter in environments:
300
+ for artifact_type in await adapter.list_artifact_types([environment]):
301
+ table.add_row(
302
+ [f"{artifact_type.title} ({artifact_type.name})", "NYI"],
303
+ divider=True,
304
+ )
305
+
306
+ print(table.get_string())
307
+
308
+
309
+ @list_cli.command("environments")
310
+ async def list_environments(
311
+ environment_names: Annotated[
312
+ list[str] | None, typer.Option("--environment", "-e", help="Specific environment name.")
313
+ ] = None,
314
+ include_remotes: Annotated[bool, typer.Option(help="Include remote environments.")] = False,
315
+ ):
316
+ table = prettytable.PrettyTable()
317
+ table.align = "l"
318
+ table.field_names = ["Environment", "Tier", "Adapter"]
319
+
320
+ environments = await get_available_environments(
321
+ include_remotes=include_remotes, environment_names=environment_names
322
+ )
323
+ for environment, adapter in environments:
324
+ table.add_row([f"{environment.name} ({environment.name})", environment.tier, adapter.name])
325
+
326
+ print(table.get_string())
327
+
328
+
329
+ @list_cli.command("executable_artifacts")
330
+ async def list_executable_artifacts(
331
+ project_names: Annotated[list[str] | None, typer.Option("--project", "-p", help="Specific project name.")] = None,
332
+ artifact_names: Annotated[
333
+ list[str] | None, typer.Option("--artifact", "-a", help="Specific artifact name.")
334
+ ] = None,
335
+ environment_names: Annotated[
336
+ list[str] | None, typer.Option("--environment", "-e", help="Specific environment name.")
337
+ ] = None,
338
+ include_remotes: Annotated[bool, typer.Option(help="Include remote environments.")] = False,
339
+ ):
340
+ table = prettytable.PrettyTable()
341
+ table.align = "l"
342
+ table.field_names = ["Project", "Artifact", "Version"]
343
+
344
+ environments = await get_available_environments(
345
+ include_remotes=include_remotes, environment_names=environment_names
346
+ )
347
+ for environment, adapter in environments:
348
+ for project_name, artifact_name, artifact_version in await adapter.list_executable_artifacts(
349
+ [environment], project_names=project_names
350
+ ):
351
+ table.add_row([project_name, artifact_name, artifact_version])
352
+
353
+ print(table.get_string())
354
+
355
+
356
+ @list_cli.command("provided_resources")
357
+ async def list_provided_resources(
358
+ project_names: Annotated[list[str] | None, typer.Option("--project", "-p", help="Specific project name.")] = None,
359
+ artifact_names: Annotated[
360
+ list[str] | None, typer.Option("--artifact", "-a", help="Specific artifact name.")
361
+ ] = None,
362
+ environment_names: Annotated[
363
+ list[str] | None, typer.Option("--environment", "-e", help="Specific environment name.")
364
+ ] = None,
365
+ include_remotes: Annotated[bool, typer.Option(help="Include remote environments.")] = False,
366
+ ):
367
+ verbose = False
368
+
369
+ table = prettytable.PrettyTable()
370
+ table.align = "l"
371
+ table.field_names = ["Resource", "Configs", "Secrets", "Requirements"]
372
+
373
+ environments = await get_available_environments(
374
+ include_remotes=include_remotes, environment_names=environment_names
375
+ )
376
+ for environment, adapter in environments:
377
+ for provided_resource, provider_artifact_reference in await adapter.list_provided_resources(
378
+ [environment], project_names=project_names, artifact_names=artifact_names
379
+ ):
380
+ configs = None
381
+ if provided_resource.configs:
382
+ configs = prettytable.PrettyTable(align="l")
383
+ configs.set_style(prettytable.TableStyle.PLAIN_COLUMNS)
384
+ configs.header = False
385
+ if verbose:
386
+ configs.add_rows(
387
+ [[f"{c.title} ({c.name})", c.description, c.shared] for c in provided_resource.configs]
388
+ )
389
+ else:
390
+ configs.add_rows([[f"{c.title} ({c.name})"] for c in provided_resource.configs])
391
+
392
+ secrets = None
393
+ if provided_resource.secrets:
394
+ secrets = prettytable.PrettyTable(align="l")
395
+ secrets.set_style(prettytable.TableStyle.PLAIN_COLUMNS)
396
+ secrets.header = False
397
+ if verbose:
398
+ secrets.add_rows(
399
+ [[f"{s.title} ({s.name})", s.description, s.shared] for s in provided_resource.secrets]
400
+ )
401
+ else:
402
+ secrets.add_rows([[f"{s.title} ({s.name})"] for s in provided_resource.secrets])
403
+
404
+ requirements = None
405
+ if provided_resource.requirements and provided_resource.requirements.properties:
406
+ requirements = prettytable.PrettyTable(align="l")
407
+ requirements.set_style(prettytable.TableStyle.PLAIN_COLUMNS)
408
+ requirements.header = False
409
+ if verbose:
410
+ requirements.add_rows(
411
+ [
412
+ [f"{req_property.title or req_name} ({req_name})", req_property.description]
413
+ for req_name, req_property in provided_resource.requirements.properties.items()
414
+ ]
415
+ )
416
+ else:
417
+ requirements.add_rows(
418
+ [
419
+ [f"{req_property.title or req_name} ({req_name})"]
420
+ for req_name, req_property in provided_resource.requirements.properties.items()
421
+ ]
422
+ )
423
+
424
+ table.add_row(
425
+ [
426
+ f"{provided_resource.title} ({provider_artifact_reference.project_name}:{provided_resource.name})",
427
+ configs,
428
+ secrets,
429
+ requirements,
430
+ ],
431
+ divider=True,
432
+ )
433
+
434
+ print(table.get_string())
435
+
436
+
437
+ @list_cli.command("provided_services", short_help="List all Provided Services.")
438
+ async def list_provided_services(
439
+ project_names: Annotated[list[str] | None, typer.Option("--project", "-p", help="Specific project name.")] = None,
440
+ artifact_names: Annotated[
441
+ list[str] | None, typer.Option("--artifact", "-a", help="Specific artifact name.")
442
+ ] = None,
443
+ environment_names: Annotated[
444
+ list[str] | None, typer.Option("--environment", "-e", help="Specific environment name.")
445
+ ] = None,
446
+ include_remotes: Annotated[bool, typer.Option(help="Include remote environments.")] = False,
447
+ ):
448
+ table = prettytable.PrettyTable()
449
+ table.align = "l"
450
+ table.field_names = ["Project", "Artifact", "Service", "Type", "Port"]
451
+
452
+ environments = await get_available_environments(
453
+ include_remotes=include_remotes, environment_names=environment_names
454
+ )
455
+ for environment, adapter in environments:
456
+ for provided_service, provider_artifact_reference in await adapter.list_provided_services(
457
+ [environment], project_names=project_names, artifact_names=artifact_names
458
+ ):
459
+ if provided_service.grpc:
460
+ service_type = "grpc"
461
+ service_port = provided_service.grpc
462
+ elif provided_service.http:
463
+ service_type = "http"
464
+ service_port = provided_service.http
465
+ elif provided_service.tcp:
466
+ service_type = "tcp"
467
+ service_port = provided_service.tcp
468
+
469
+ table.add_row(
470
+ [
471
+ provider_artifact_reference.project_name,
472
+ provider_artifact_reference.artifact_name,
473
+ provided_service.name,
474
+ service_type,
475
+ service_port,
476
+ ]
477
+ )
478
+
479
+ print(table.get_string())
480
+
481
+
482
+ @list_cli.command("resources", short_help="List Resources and their statuses.")
483
+ async def list_resources(
484
+ project_names: Annotated[list[str] | None, typer.Option("--project", "-p", help="Specific project name.")] = None,
485
+ artifact_names: Annotated[
486
+ list[str] | None, typer.Option("--artifact", "-a", help="Specific artifact name.")
487
+ ] = None,
488
+ environment_names: Annotated[
489
+ list[str] | None, typer.Option("--environment", "-e", help="Specific environment name.")
490
+ ] = None,
491
+ include_remotes: Annotated[bool, typer.Option(help="Include remote environments.")] = False,
492
+ ):
493
+ table = prettytable.PrettyTable()
494
+ table.align = "l"
495
+ table.field_names = ["Project", "Artifact", "Resource", "Status"]
496
+
497
+ environments = await get_available_environments(
498
+ include_remotes=include_remotes, environment_names=environment_names
499
+ )
500
+ for environment, adapter in environments:
501
+ for artifact_reference in await adapter.list_executable_artifacts(
502
+ [environment], project_names=project_names, artifact_names=artifact_names
503
+ ):
504
+ _, resolved_artifact = await adapter.resolve_artifact_reference(environment, artifact_reference)
505
+ if resolved_artifact.execution and resolved_artifact.execution.requires.resources:
506
+ for resource in resolved_artifact.execution.requires.resources:
507
+ table.add_row(
508
+ [
509
+ artifact_reference.project_name,
510
+ artifact_reference.artifact_name,
511
+ resource.resource_name,
512
+ "STATUS",
513
+ ]
514
+ )
515
+
516
+ print(table.get_string())
517
+
518
+
519
+ @list_cli.command("services")
520
+ async def list_services(
521
+ project_names: Annotated[list[str] | None, typer.Option("--project", "-p", help="Specific project name.")] = None,
522
+ artifact_names: Annotated[
523
+ list[str] | None, typer.Option("--artifact", "-a", help="Specific artifact name.")
524
+ ] = None,
525
+ environment_names: Annotated[
526
+ list[str] | None, typer.Option("--environment", "-e", help="Specific environment name.")
527
+ ] = None,
528
+ include_remotes: Annotated[bool, typer.Option(help="Include remote environments.")] = False,
529
+ ):
530
+ table = prettytable.PrettyTable()
531
+ table.align = "l"
532
+
533
+ print(table.get_string())
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: ballista-cli
3
+ Version: 0.1.0
4
+ Summary: Ballista.build CLI
5
+ Author-email: August Bigelow <dev@atbigelow.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Programming Language :: Python :: 3.15
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.12
15
+ Requires-Dist: async-typer>=0.2.1
16
+ Requires-Dist: ballista-sdk<0.2,>=0.1
17
+ Requires-Dist: prettytable
18
+ Requires-Dist: typer<0.26,>=0.25
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Ballista.build CLI
22
+
23
+ CLI for using Ballista.build
24
+
25
+ ## Commands
26
+
27
+ - `init` - Create an initial ballista.yaml
28
+ - `build` - Build the artifacts contained in a ballista.yaml
29
+ - `up` - Execute project in local environment
30
+ - `down` - Teardown project in local environment
31
+ - `run` - Run a command with an Artifact context
32
+ - `deploy` - Deploy to an environment
33
+ - `list` - List various items from environments
34
+
35
+ ## Configuration
36
+
37
+ `~/.ballista/defaults.yaml`
@@ -0,0 +1,7 @@
1
+ ballista_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ballista_cli/cli.py,sha256=Sp8qeFs86L0lNcKtd9BCClWJgbVHLNq-beWrL-RSH2Q,19556
3
+ ballista_cli-0.1.0.dist-info/METADATA,sha256=CYMng0m6LrySxqCUSfjoAKCg9FWOJGHr6P27nAMMRpA,1102
4
+ ballista_cli-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
5
+ ballista_cli-0.1.0.dist-info/entry_points.txt,sha256=FckQJh1fF4FC_3i7MNgUfKqaWYxn3pB-aDzMOFwBSew,50
6
+ ballista_cli-0.1.0.dist-info/licenses/LICENSE,sha256=6nehCbDq6UrbI_AojHmZOQhXgcyaIEZTQECloF-FfGo,1116
7
+ ballista_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ballista = ballista_cli.cli:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 20236 August Trapper Bigelow and the ballista.build contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.