bumpy-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.
- bumpy/__init__.py +0 -0
- bumpy/commands/__init__.py +4 -0
- bumpy/commands/job.py +49 -0
- bumpy/commands/pipeline.py +49 -0
- bumpy/exceptions/__init__.py +0 -0
- bumpy/exceptions/file_client_exceptions.py +23 -0
- bumpy/file_client.py +90 -0
- bumpy/main.py +15 -0
- bumpy/models.py +10 -0
- bumpy_cli-0.1.0.dist-info/METADATA +141 -0
- bumpy_cli-0.1.0.dist-info/RECORD +14 -0
- bumpy_cli-0.1.0.dist-info/WHEEL +4 -0
- bumpy_cli-0.1.0.dist-info/entry_points.txt +3 -0
- bumpy_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
bumpy/__init__.py
ADDED
|
File without changes
|
bumpy/commands/job.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from bumpy.file_client import FileClient
|
|
5
|
+
from bumpy.models import VersionPart
|
|
6
|
+
|
|
7
|
+
# Criamos um sub-app dedicado a ações de "job"
|
|
8
|
+
job_app = typer.Typer(help='Comandos para manipular um unico job especifico.')
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@job_app.command('up')
|
|
13
|
+
def job_up(
|
|
14
|
+
name: str = typer.Argument(..., help='Nome da pasta do job.'),
|
|
15
|
+
part: VersionPart = typer.Argument(
|
|
16
|
+
VersionPart.patch,
|
|
17
|
+
help='Parte da versão a atualizar (major, minor, patch)',
|
|
18
|
+
),
|
|
19
|
+
uat: bool = typer.Option(
|
|
20
|
+
False, '--uat', help='Aplica no ambiente de UAT.'
|
|
21
|
+
),
|
|
22
|
+
):
|
|
23
|
+
"""Aumenta a versao de um job especifico."""
|
|
24
|
+
console.print(
|
|
25
|
+
f"[bold green]Aumentando versão do job '{name}'...[/bold green]"
|
|
26
|
+
)
|
|
27
|
+
ambient = 'uat' if uat else 'prd-marketplace'
|
|
28
|
+
path_str = f'{ambient}/{name}/version'
|
|
29
|
+
FileClient().increase(segment=part.value, pipeline_path=path_str)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@job_app.command('down')
|
|
33
|
+
def job_down(
|
|
34
|
+
name: str = typer.Argument(..., help='Nome da pasta do job.'),
|
|
35
|
+
part: VersionPart = typer.Argument(
|
|
36
|
+
VersionPart.patch,
|
|
37
|
+
help='Parte da versão a atualizar (major, minor, patch)',
|
|
38
|
+
),
|
|
39
|
+
uat: bool = typer.Option(
|
|
40
|
+
False, '--uat', help='Aplica no ambiente de UAT.'
|
|
41
|
+
),
|
|
42
|
+
):
|
|
43
|
+
"""Diminui a versao de um job especifico."""
|
|
44
|
+
console.print(
|
|
45
|
+
f"[bold yellow]Diminuindo versão do job '{name}'...[/bold yellow]"
|
|
46
|
+
)
|
|
47
|
+
ambient = 'uat' if uat else 'prd-marketplace'
|
|
48
|
+
path_str = f'{ambient}/{name}/version'
|
|
49
|
+
FileClient().decrease(segment=part.value, pipeline_path=path_str)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from bumpy.file_client import FileClient
|
|
5
|
+
from bumpy.models import VersionPart
|
|
6
|
+
|
|
7
|
+
# Criamos um sub-app dedicado a ações do "pipeline" em lote
|
|
8
|
+
pipeline_app = typer.Typer(
|
|
9
|
+
help='Comandos para manipular todos os jobs do pipeline.'
|
|
10
|
+
)
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pipeline_app.command('up')
|
|
15
|
+
def pipeline_up(
|
|
16
|
+
part: VersionPart = typer.Argument(
|
|
17
|
+
VersionPart.patch,
|
|
18
|
+
help='Parte da versão a atualizar (major, minor, patch)',
|
|
19
|
+
),
|
|
20
|
+
uat: bool = typer.Option(
|
|
21
|
+
False, '--uat', help='Aplica no ambiente de UAT.'
|
|
22
|
+
),
|
|
23
|
+
):
|
|
24
|
+
"""Aumenta a versao de TODOS os jobs do pipeline de uma vez."""
|
|
25
|
+
console.print(
|
|
26
|
+
'[bold green]Subindo a versão de todos os jobs no ambiente selecionado...[/bold green]'
|
|
27
|
+
)
|
|
28
|
+
ambient = 'uat' if uat else 'prd-marketplace'
|
|
29
|
+
path_str = f'pipeline/{ambient}'
|
|
30
|
+
FileClient().increase_all(segment=part.value, pipeline_path=path_str)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pipeline_app.command('down')
|
|
34
|
+
def pipeline_down(
|
|
35
|
+
part: VersionPart = typer.Argument(
|
|
36
|
+
VersionPart.patch,
|
|
37
|
+
help='Parte da versão a atualizar (major, minor, patch)',
|
|
38
|
+
),
|
|
39
|
+
uat: bool = typer.Option(
|
|
40
|
+
False, '--uat', help='Aplica no ambiente de UAT.'
|
|
41
|
+
),
|
|
42
|
+
):
|
|
43
|
+
"""Diminui a versao de TODOS os jobs do pipeline de uma vez."""
|
|
44
|
+
console.print(
|
|
45
|
+
'[bold yellow]Diminuindo a versão de todos os jobs no ambiente selecionado...[/bold yellow]'
|
|
46
|
+
)
|
|
47
|
+
ambient = 'uat' if uat else 'prd-marketplace'
|
|
48
|
+
path_str = f'pipeline/{ambient}'
|
|
49
|
+
FileClient().decrease_all(segment=part.value, pipeline_path=path_str)
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class EmptyFileException(Exception):
|
|
2
|
+
def __init__(self, file_name: str):
|
|
3
|
+
message = f'An error occured, the file {file_name} is empty'
|
|
4
|
+
super().__init__(message)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MissingSegmentsException(ValueError):
|
|
8
|
+
def __init__(self, file_name: str):
|
|
9
|
+
message = f'An error occured, the file {file_name} must have 3 segments (major.minor.patch)'
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NotFoundPipelinePathException(FileNotFoundError):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
message = 'An error occured, pipeline path was not found'
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VersionUnderflowError(Exception):
|
|
20
|
+
def __init__(self, file_name: str, version_part: str = 'versão'):
|
|
21
|
+
super().__init__(
|
|
22
|
+
f'An error occured, It is not possible to decrease the {version_part} in {file_name}: the minimum value is 0.'
|
|
23
|
+
)
|
bumpy/file_client.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .exceptions.file_client_exceptions import (
|
|
5
|
+
EmptyFileException,
|
|
6
|
+
MissingSegmentsException,
|
|
7
|
+
NotFoundPipelinePathException,
|
|
8
|
+
VersionUnderflowError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FileClient:
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self.__absolute_path = Path.cwd()
|
|
15
|
+
self.__current_dir = None
|
|
16
|
+
self.__segment = {}
|
|
17
|
+
|
|
18
|
+
def __validate_pipeline_path(self, pipeline_path: str) -> None:
|
|
19
|
+
for f in self.__absolute_path.iterdir():
|
|
20
|
+
list_path = list(f.glob(pipeline_path))
|
|
21
|
+
if list_path:
|
|
22
|
+
self.__current_dir = list_path[0]
|
|
23
|
+
return None
|
|
24
|
+
raise NotFoundPipelinePathException()
|
|
25
|
+
|
|
26
|
+
def __get_current_version(self) -> None:
|
|
27
|
+
with open(self.__current_dir, 'r') as file:
|
|
28
|
+
content = file.read()
|
|
29
|
+
if not content:
|
|
30
|
+
raise EmptyFileException(str(self.__current_dir))
|
|
31
|
+
|
|
32
|
+
version = content.split('.')
|
|
33
|
+
if len(version) < 3:
|
|
34
|
+
raise MissingSegmentsException(str(self.__current_dir))
|
|
35
|
+
|
|
36
|
+
self.__segment['major'] = int(version[0])
|
|
37
|
+
self.__segment['minor'] = int(version[1])
|
|
38
|
+
self.__segment['patch'] = int(version[2])
|
|
39
|
+
|
|
40
|
+
def __update_file_version(self) -> None:
|
|
41
|
+
major = self.__segment['major']
|
|
42
|
+
minor = self.__segment['minor']
|
|
43
|
+
patch = self.__segment['patch']
|
|
44
|
+
|
|
45
|
+
with open(self.__current_dir, 'w') as file:
|
|
46
|
+
new_version = f'{major}.{minor}.{patch}'
|
|
47
|
+
file.write(new_version)
|
|
48
|
+
|
|
49
|
+
def increase(self, segment: str, pipeline_path: str) -> None:
|
|
50
|
+
self.__validate_pipeline_path(pipeline_path)
|
|
51
|
+
self.__get_current_version()
|
|
52
|
+
|
|
53
|
+
self.__segment[segment] += 1
|
|
54
|
+
|
|
55
|
+
self.__update_file_version()
|
|
56
|
+
|
|
57
|
+
def decrease(self, segment: str, pipeline_path: str) -> None:
|
|
58
|
+
self.__validate_pipeline_path(pipeline_path)
|
|
59
|
+
self.__get_current_version()
|
|
60
|
+
|
|
61
|
+
if self.__segment[segment] == 0:
|
|
62
|
+
raise VersionUnderflowError(pipeline_path, segment)
|
|
63
|
+
|
|
64
|
+
self.__segment[segment] -= 1
|
|
65
|
+
|
|
66
|
+
self.__update_file_version()
|
|
67
|
+
|
|
68
|
+
def increase_all(self, segment: str, pipeline_path: str) -> None:
|
|
69
|
+
folder_paths = self.__absolute_path / pipeline_path
|
|
70
|
+
|
|
71
|
+
for folder_name in os.listdir(folder_paths):
|
|
72
|
+
version_path = folder_paths / folder_name / 'version'
|
|
73
|
+
self.__current_dir = version_path
|
|
74
|
+
self.__get_current_version()
|
|
75
|
+
self.__segment[segment] += 1
|
|
76
|
+
self.__update_file_version()
|
|
77
|
+
|
|
78
|
+
def decrease_all(self, segment: str, pipeline_path: str) -> None:
|
|
79
|
+
folder_paths = self.__absolute_path / pipeline_path
|
|
80
|
+
|
|
81
|
+
for folder_name in os.listdir(folder_paths):
|
|
82
|
+
version_path = folder_paths / folder_name / 'version'
|
|
83
|
+
self.__current_dir = version_path
|
|
84
|
+
self.__get_current_version()
|
|
85
|
+
|
|
86
|
+
if self.__segment[segment] == 0:
|
|
87
|
+
raise VersionUnderflowError(folder_name, segment)
|
|
88
|
+
|
|
89
|
+
self.__segment[segment] -= 1
|
|
90
|
+
self.__update_file_version()
|
bumpy/main.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from bumpy.commands import job_app, pipeline_app
|
|
4
|
+
|
|
5
|
+
# App principal (Raiz da Árvore)
|
|
6
|
+
app = typer.Typer(
|
|
7
|
+
help='Bumpy: Gerenciador de versoes para pipelines multi-plataforma.'
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# Registrando os galhos/nós na árvore de comandos
|
|
11
|
+
app.add_typer(job_app, name='job')
|
|
12
|
+
app.add_typer(pipeline_app, name='all')
|
|
13
|
+
|
|
14
|
+
if __name__ == '__main__':
|
|
15
|
+
app()
|
bumpy/models.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bumpy-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI para gerenciar versoes de jobs em pipelines.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: cli,pipeline,versionamento,jobs
|
|
8
|
+
Author: Daniel Felipe Santana Batista
|
|
9
|
+
Author-email: daniel.sbatista@shopee.com
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Natural Language :: Portuguese (Brazilian)
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Dist: typer (>=0.27.2,<0.28.0)
|
|
22
|
+
Project-URL: Documentation, https://bumpy-cli.readthedocs.io/pt-br/latest/
|
|
23
|
+
Project-URL: Homepage, https://github.com/iamDFSB/Bumpy-CLI
|
|
24
|
+
Project-URL: Repository, https://github.com/iamDFSB/Bumpy-CLI
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
# Bumpy CLI
|
|
30
|
+
|
|
31
|
+
[](https://bumpy-cli.readthedocs.io/pt-br/latest/)
|
|
32
|
+
[](https://app.codecov.io/github/iamDFSB/Bumpy-CLI)
|
|
33
|
+
[](https://github.com/iamDFSB/Bumpy-CLI/actions)
|
|
34
|
+
|
|
35
|
+
O Bumpy CLI ajuda a atualizar as versões dos jobs mantidos em arquivos `version`
|
|
36
|
+
de um pipeline. Com ele, você pode incrementar ou decrementar `major`, `minor`
|
|
37
|
+
ou `patch` de um job específico ou de todos os jobs de um ambiente.
|
|
38
|
+
|
|
39
|
+
## Requisitos
|
|
40
|
+
|
|
41
|
+
- Python 3.12 ou superior;
|
|
42
|
+
- um projeto com a estrutura de pastas descrita abaixo.
|
|
43
|
+
|
|
44
|
+
## Instalação
|
|
45
|
+
|
|
46
|
+
Como o Bumpy é uma ferramenta de linha de comando, recomendamos instalá-lo com
|
|
47
|
+
[pipx](https://pipx.pypa.io/), que mantem as dependencias isoladas:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pipx install bumpy-cli
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Também é possível instalar no ambiente Python ativo:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python -m pip install bumpy-cli
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Confirme a instalacao e veja os comandos disponiveis:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bumpy --help
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Prepare o projeto
|
|
66
|
+
|
|
67
|
+
Execute os comandos a partir da raiz do projeto que contem a pasta `pipeline`.
|
|
68
|
+
Cada job precisa ter um arquivo `version` com tres numeros separados por ponto:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
pipeline/
|
|
72
|
+
├── prd-marketplace/
|
|
73
|
+
│ └── meu-cron/
|
|
74
|
+
│ └── version
|
|
75
|
+
└── uat/
|
|
76
|
+
└── meu-cron/
|
|
77
|
+
└── version
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Por exemplo, o arquivo `pipeline/prd-marketplace/meu-cron/version` pode conter:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
1.2.3
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Uso
|
|
87
|
+
|
|
88
|
+
### Atualizar um job
|
|
89
|
+
|
|
90
|
+
Por padrão, os comandos atuam no ambiente `prd-marketplace`:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bumpy job up meu-cron patch
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Esse comando altera `1.2.3` para `1.2.4`. Para atualizar outro segmento:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bumpy job up meu-cron minor
|
|
100
|
+
bumpy job up meu-cron major
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Use `down` para decrementar o segmento. O valor não pode ficar abaixo de zero:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
bumpy job down meu-cron patch
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Para atuar no ambiente `uat`, adicione `--uat`:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bumpy job up meu-cron patch --uat
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Atualizar todos os jobs de um ambiente
|
|
116
|
+
|
|
117
|
+
Os comandos `all` atualizam todos os jobs dentro do ambiente selecionado:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
bumpy all up patch
|
|
121
|
+
bumpy all down minor
|
|
122
|
+
bumpy all up major --uat
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Cada pasta de job do ambiente precisa conter um arquivo `version` válido.
|
|
126
|
+
|
|
127
|
+
## Ajuda
|
|
128
|
+
|
|
129
|
+
Consulte a ajuda geral ou a de um grupo de comandos:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
bumpy --help
|
|
133
|
+
bumpy job --help
|
|
134
|
+
bumpy all --help
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Desenvolvimento
|
|
138
|
+
|
|
139
|
+
O código-fonte, as instruções para contribuir e a documentação do projeto estão
|
|
140
|
+
no [repositorio no GitHub](https://github.com/iamDFSB/Bumpy-CLI).
|
|
141
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
bumpy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
bumpy/commands/__init__.py,sha256=AdOJNCTixiDJmce74rdf9Cm4SywbDU9WgEmLn9CDu_Y,103
|
|
3
|
+
bumpy/commands/job.py,sha256=CJTkbKxa9NlgyxSeamtijnytY7-k_uJBgzVrHdQ3cQQ,1637
|
|
4
|
+
bumpy/commands/pipeline.py,sha256=5UHmHrFj5-qf8M9FtS3S5CzDHau934h8_2b4QdWnv5I,1632
|
|
5
|
+
bumpy/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
bumpy/exceptions/file_client_exceptions.py,sha256=T7Z0BdHl3Sxe_Nqm1vq40sLCWLMPNVTblr_nVScaMuA,878
|
|
7
|
+
bumpy/file_client.py,sha256=-hBSP9jGKzFibpYS5aaSIN2OwU1P8fiUxPTju4CGNgA,3138
|
|
8
|
+
bumpy/main.py,sha256=Ficxa1Hgk_RCgncgPt-6EXb8zUvAQnYCoYFxH-dtqNg,374
|
|
9
|
+
bumpy/models.py,sha256=VEI6Jp_BHoQJY_5569ABQe1O2SC9zQCKPer2soWYiXU,223
|
|
10
|
+
bumpy_cli-0.1.0.dist-info/entry_points.txt,sha256=GbqWLgrELUXzjObHCjziIldzsSkX60r50Lr8XS83o3Q,40
|
|
11
|
+
bumpy_cli-0.1.0.dist-info/licenses/LICENSE,sha256=t7g9QmejlmrMtCUP-x2uh3f1pjo9UIItK5d387MC7H8,1107
|
|
12
|
+
bumpy_cli-0.1.0.dist-info/METADATA,sha256=Zo8qiCwpamz5iATRPO617sEzcAVesueTjX9oOzZ-doU,3746
|
|
13
|
+
bumpy_cli-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
14
|
+
bumpy_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel-Felipe-Santana-Batista
|
|
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.
|