insper-mlops 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.
- insper_mlops-0.1.0.dist-info/METADATA +57 -0
- insper_mlops-0.1.0.dist-info/RECORD +10 -0
- insper_mlops-0.1.0.dist-info/WHEEL +5 -0
- insper_mlops-0.1.0.dist-info/entry_points.txt +2 -0
- insper_mlops-0.1.0.dist-info/licenses/LICENSE +21 -0
- insper_mlops-0.1.0.dist-info/top_level.txt +1 -0
- mlopstemplate/__init__.py +5 -0
- mlopstemplate/cli.py +286 -0
- mlopstemplate/config.py +40 -0
- mlopstemplate/core.py +193 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: insper-mlops
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A command-line tool to standardize the creation of MLOps projects at Insper CDIA.
|
|
5
|
+
Author-email: Insper CDIA <cdia@insper.edu.br>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/centro-dados-ia/insper-mlops
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/centro-dados-ia/insper-mlops/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: GitPython
|
|
18
|
+
Requires-Dist: PyGithub>=2.0.0
|
|
19
|
+
Requires-Dist: typer[all]
|
|
20
|
+
Requires-Dist: requests
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# MLOps Template
|
|
24
|
+
|
|
25
|
+
🚀 Ferramenta de linha de comando para padronizar e automatizar projetos de MLOps no Insper CDIA.
|
|
26
|
+
|
|
27
|
+
## Instalação
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install insper-mlops
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Uso
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
mlops start
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Siga as instruções no terminal para criar seu repositório de MLOps.
|
|
40
|
+
|
|
41
|
+
## Funcionalidades
|
|
42
|
+
|
|
43
|
+
- ✅ Criação automática de repositórios GitHub
|
|
44
|
+
- ✅ Configuração de branches (dev/prod)
|
|
45
|
+
- ✅ Aplicação de regras de proteção de branch
|
|
46
|
+
- ✅ Configuração de permissões de equipe
|
|
47
|
+
- ✅ Templates pré-configurados para diferentes perfis
|
|
48
|
+
|
|
49
|
+
## Requisitos
|
|
50
|
+
|
|
51
|
+
- Python 3.8+
|
|
52
|
+
- GitHub CLI (`gh`) instalado
|
|
53
|
+
- Conta GitHub com acesso às organizações Insper
|
|
54
|
+
|
|
55
|
+
## Licença
|
|
56
|
+
|
|
57
|
+
MIT License - veja o arquivo [LICENSE](LICENSE) para detalhes.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
insper_mlops-0.1.0.dist-info/licenses/LICENSE,sha256=fyMKVo-Zc6ujqbXAfU2DyKDcMJAL9sPsKa7xyNDMSy0,1089
|
|
2
|
+
mlopstemplate/__init__.py,sha256=yg-KaUTCBpIh_lrtwAR3rXc5Vx1uENenJbc6BhmmkUw,170
|
|
3
|
+
mlopstemplate/cli.py,sha256=C2ZmLTSQEMtdezIVM9_tuUxHdgx5PFLlWy1wN8oPXxI,10939
|
|
4
|
+
mlopstemplate/config.py,sha256=D3pliugZp7kxABlbqMlC6YUknvpMmREJVSE-b7IGHBE,1314
|
|
5
|
+
mlopstemplate/core.py,sha256=aZYVuscAiiYqCL1SQoZmMWLZXTblptgyCadhS6lR3o0,8523
|
|
6
|
+
insper_mlops-0.1.0.dist-info/METADATA,sha256=zVWnwITnVG_oz7gIdKI67fZUzpmnptzYGwzqITT19fI,1619
|
|
7
|
+
insper_mlops-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
insper_mlops-0.1.0.dist-info/entry_points.txt,sha256=Hj2mv_faG2PncP8n-Zuyt6-Qr52BWoNQffgFQsQVuy8,48
|
|
9
|
+
insper_mlops-0.1.0.dist-info/top_level.txt,sha256=-uTjn-a-Bxr2Yn41ZsLF57wIYIEJd2kI09ovnkA5quk,14
|
|
10
|
+
insper_mlops-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Insper CDIA
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mlopstemplate
|
mlopstemplate/cli.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Command-line interface for the MLOps Template tool."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from mlopstemplate import config, core
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
help="🚀 Ferramenta de linha de comando para padronizar e automatizar projetos de MLOps no Insper.",
|
|
14
|
+
add_completion=False,
|
|
15
|
+
)
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
def get_token_from_gh_cli() -> str:
|
|
19
|
+
"""
|
|
20
|
+
Retrieves the GitHub authentication token from the GitHub CLI.
|
|
21
|
+
|
|
22
|
+
This function first checks if the user is authenticated. If not, it
|
|
23
|
+
initiates the interactive login process for the user to authenticate.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The GitHub authentication token.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
typer.Exit: If the GitHub CLI is not installed or if the login
|
|
30
|
+
process fails.
|
|
31
|
+
"""
|
|
32
|
+
# 1. Verifica se o GitHub CLI (gh) está instalado
|
|
33
|
+
try:
|
|
34
|
+
subprocess.run(
|
|
35
|
+
["gh", "--version"], capture_output=True, check=True, text=True
|
|
36
|
+
)
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
console.print(
|
|
39
|
+
"[bold red]❌ GitHub CLI (gh) não encontrado. "
|
|
40
|
+
"Instale em https://cli.github.com/ e tente novamente.[/bold red]"
|
|
41
|
+
)
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
|
|
44
|
+
# 2. Tenta obter o token silenciosamente
|
|
45
|
+
result = subprocess.run(["gh", "auth", "token"], capture_output=True, text=True)
|
|
46
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
47
|
+
return result.stdout.strip()
|
|
48
|
+
|
|
49
|
+
# 3. Se não houver token, inicia o processo de login interativo
|
|
50
|
+
console.print(
|
|
51
|
+
Panel(
|
|
52
|
+
"[bold yellow]🔐 GitHub CLI não está autenticado.[/bold yellow]\\n\\n"
|
|
53
|
+
"Iniciando processo de login interativo. "
|
|
54
|
+
"Por favor, siga as instruções no seu terminal e navegador.",
|
|
55
|
+
title="Ação Necessária",
|
|
56
|
+
border_style="yellow",
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
subprocess.run(["gh", "auth", "login"], check=True)
|
|
62
|
+
except subprocess.CalledProcessError:
|
|
63
|
+
console.print(
|
|
64
|
+
"[bold red]❌ O processo de login com o GitHub CLI falhou ou foi cancelado.[/bold red]"
|
|
65
|
+
)
|
|
66
|
+
raise typer.Exit(code=1)
|
|
67
|
+
|
|
68
|
+
# 4. Após o login, tenta obter o token novamente
|
|
69
|
+
console.print("✅ [bold green]Autenticação concluída.[/bold green] Verificando token...")
|
|
70
|
+
result = subprocess.run(["gh", "auth", "token"], capture_output=True, text=True)
|
|
71
|
+
|
|
72
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
73
|
+
return result.stdout.strip()
|
|
74
|
+
else:
|
|
75
|
+
console.print(
|
|
76
|
+
"[bold red]❌ Falha ao obter o token após o login. "
|
|
77
|
+
"Por favor, tente 'mlops login' novamente.[/bold red]"
|
|
78
|
+
)
|
|
79
|
+
raise typer.Exit(code=1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command(hidden=True)
|
|
83
|
+
def login(
|
|
84
|
+
temp: bool = typer.Option(
|
|
85
|
+
False, "--temp-login", help="Login temporário, não salva token em arquivo"
|
|
86
|
+
)
|
|
87
|
+
):
|
|
88
|
+
"""
|
|
89
|
+
Authenticates the user via GitHub CLI and saves the token.
|
|
90
|
+
"""
|
|
91
|
+
console.print("🔐 Verificando autenticação via GitHub CLI...")
|
|
92
|
+
token = get_token_from_gh_cli()
|
|
93
|
+
|
|
94
|
+
if temp:
|
|
95
|
+
config.set_session_token(token)
|
|
96
|
+
console.print("⚠️ [yellow]Login temporário. Token armazenado apenas nesta sessão.[/yellow]")
|
|
97
|
+
else:
|
|
98
|
+
config.save_token(token)
|
|
99
|
+
|
|
100
|
+
console.print("✅ [bold green]Login bem-sucedido![/bold green]")
|
|
101
|
+
|
|
102
|
+
@app.command(hidden=True)
|
|
103
|
+
def make_repo_pesq(nome: str):
|
|
104
|
+
"""
|
|
105
|
+
Clones the base MLOps template for the 'Pesquisa' profile.
|
|
106
|
+
(This command is intended for internal use by the 'start' command).
|
|
107
|
+
"""
|
|
108
|
+
token = config.get_token()
|
|
109
|
+
core.clonar_template_com_nome(
|
|
110
|
+
config.TEMPLATE_REPO_URL_PESQUISA.format(token=token), nome
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@app.command(hidden=True)
|
|
114
|
+
def make_repo(nome: str):
|
|
115
|
+
"""
|
|
116
|
+
Clones the base MLOps template repository into a new directory.
|
|
117
|
+
"""
|
|
118
|
+
token = config.get_token()
|
|
119
|
+
core.clonar_template_com_nome(
|
|
120
|
+
config.TEMPLATE_REPO_URL.format(token=token), nome
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _start_pesquisador_flow():
|
|
124
|
+
"""Handles the workflow for the 'Pesquisador' profile."""
|
|
125
|
+
console.print(Panel(
|
|
126
|
+
"[bold yellow]⚠️ Para criar repositórios na organização 'Insper-CDIA-Pesquisa', você precisa estar autenticado com uma conta que tenha acesso a ela.[/bold yellow]\n\n"
|
|
127
|
+
"Se você já estiver logado com outra conta no GitHub CLI, recomenda-se fazer logout antes de prosseguir.",
|
|
128
|
+
title="Atenção",
|
|
129
|
+
border_style="yellow"
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
resposta = typer.confirm("Deseja fazer logout do GitHub CLI antes de continuar?", default=True)
|
|
133
|
+
if resposta:
|
|
134
|
+
console.print("🔐 Fazendo logout do GitHub CLI...")
|
|
135
|
+
subprocess.run(["gh", "auth", "logout", "--hostname", "github.com"], check=False)
|
|
136
|
+
|
|
137
|
+
console.print("🔑 Iniciando processo de login com a conta certa (use o navegador quando solicitado)...")
|
|
138
|
+
login(temp=True) # Força novo login via GitHub CLI, armazena token somente na sessão
|
|
139
|
+
|
|
140
|
+
repo_nome = typer.prompt("Qual o nome do repositório que você deseja criar?")
|
|
141
|
+
make_repo_pesq(repo_nome)
|
|
142
|
+
console.print(f"✅ Repositório '[bold cyan]{repo_nome}[/bold cyan]' criado com sucesso!")
|
|
143
|
+
|
|
144
|
+
# Automatically upload the repository to GitHub
|
|
145
|
+
console.print("🚀 Enviando repositório para o GitHub...")
|
|
146
|
+
token = config.get_token()
|
|
147
|
+
org_name = "Insper-CDIA-Pesquisa"
|
|
148
|
+
pasta_projeto = os.path.join(".", repo_nome)
|
|
149
|
+
|
|
150
|
+
core.criar_repositorio(token, org_name, pasta_projeto, repo_type="pesquisa")
|
|
151
|
+
console.print(
|
|
152
|
+
Panel(
|
|
153
|
+
"✅ [bold green]Processo concluído com sucesso![/bold green]",
|
|
154
|
+
title="Finalizado",
|
|
155
|
+
border_style="green",
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _start_administrativo_flow():
|
|
160
|
+
"""Handles the workflow for the 'Administrativo' profile."""
|
|
161
|
+
console.print(Panel(
|
|
162
|
+
"[bold yellow]⚠️ Para criar repositórios na organização 'centro-dados-ia', você precisa estar autenticado com uma conta que tenha acesso a ela.[/bold yellow]\n\n"
|
|
163
|
+
"Se você já estiver logado com outra conta no GitHub CLI, recomenda-se fazer logout antes de prosseguir.",
|
|
164
|
+
title="Atenção",
|
|
165
|
+
border_style="yellow"
|
|
166
|
+
))
|
|
167
|
+
|
|
168
|
+
resposta = typer.confirm("Deseja fazer logout do GitHub CLI antes de continuar?", default=True)
|
|
169
|
+
if resposta:
|
|
170
|
+
console.print("🔐 Fazendo logout do GitHub CLI...")
|
|
171
|
+
subprocess.run(["gh", "auth", "logout", "--hostname", "github.com"], check=False)
|
|
172
|
+
|
|
173
|
+
console.print("🔑 Iniciando processo de login com a conta certa (use o navegador quando solicitado)...")
|
|
174
|
+
login(temp=True) # Força novo login via GitHub CLI, armazena token somente na sessão
|
|
175
|
+
|
|
176
|
+
repo_nome = typer.prompt("Qual o nome do repositório que você deseja criar a partir do template?")
|
|
177
|
+
make_repo(repo_nome)
|
|
178
|
+
console.print(f"✅ Repositório '[bold cyan]{repo_nome}[/bold cyan]' criado com sucesso!")
|
|
179
|
+
|
|
180
|
+
console.print("🚀 Enviando repositório para o GitHub com branches dev e prod...")
|
|
181
|
+
token = config.get_token()
|
|
182
|
+
org_name = "centro-dados-ia"
|
|
183
|
+
pasta_projeto = os.path.join(".", repo_nome)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
core.criar_repositorio(token, org_name, pasta_projeto, repo_type="administrativo")
|
|
187
|
+
console.print(
|
|
188
|
+
Panel(
|
|
189
|
+
"✅ [bold green]Processo concluído com sucesso![/bold green]\n\n",
|
|
190
|
+
title="Finalizado",
|
|
191
|
+
border_style="green",
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
console.print(Panel(
|
|
196
|
+
f"[bold red]❌ Erro durante a criação do repositório:[/bold red]\n\n{str(e)}",
|
|
197
|
+
title="Erro",
|
|
198
|
+
border_style="red"
|
|
199
|
+
))
|
|
200
|
+
|
|
201
|
+
def _check_directory_is_project_root():
|
|
202
|
+
"""
|
|
203
|
+
Checks if the current directory is a valid project root.
|
|
204
|
+
|
|
205
|
+
If the directory contains only one subdirectory, it suggests the user
|
|
206
|
+
to change into that directory.
|
|
207
|
+
"""
|
|
208
|
+
items = os.listdir(".")
|
|
209
|
+
dirs = [d for d in items if os.path.isdir(d) and not d.startswith(".")]
|
|
210
|
+
files = [f for f in items if os.path.isfile(f)]
|
|
211
|
+
|
|
212
|
+
if len(dirs) == 1 and not files:
|
|
213
|
+
project_dir = dirs[0]
|
|
214
|
+
console.print(
|
|
215
|
+
Panel(
|
|
216
|
+
f"❌ [bold red]Comando executado no diretório errado.[/bold red]\\n\\n"
|
|
217
|
+
f"Parece que seu projeto está na pasta '[bold cyan]{project_dir}[/bold cyan]'.\\n"
|
|
218
|
+
f"Por favor, entre na pasta do projeto e tente novamente:\\n\\n"
|
|
219
|
+
f"[green]cd {project_dir}[/green]\\n"
|
|
220
|
+
f"[green]mlops upload-repo[/green]",
|
|
221
|
+
title="Erro de Diretório",
|
|
222
|
+
border_style="red",
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
raise typer.Exit(code=1)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@app.command(hidden=True)
|
|
229
|
+
def upload_repo():
|
|
230
|
+
"""
|
|
231
|
+
Creates a GitHub repository from the current directory and uploads the files.
|
|
232
|
+
|
|
233
|
+
This command guides the user to select an organization and then triggers
|
|
234
|
+
the repository creation and configuration process.
|
|
235
|
+
"""
|
|
236
|
+
_check_directory_is_project_root()
|
|
237
|
+
|
|
238
|
+
console.print(
|
|
239
|
+
Panel(
|
|
240
|
+
"[bold]Em qual organização você deseja criar o repositório?[/bold]",
|
|
241
|
+
title="Seleção de Organização",
|
|
242
|
+
border_style="blue",
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
console.print(" [cyan]1[/cyan]: Pesquisa (Insper-CDIA-Pesquisa)")
|
|
246
|
+
console.print(" [cyan]2[/cyan]: Administrativo (centro-dados-ia)")
|
|
247
|
+
choice = typer.prompt("Digite o número da opção")
|
|
248
|
+
|
|
249
|
+
if choice == "1":
|
|
250
|
+
org_name = "Insper-CDIA-Pesquisa"
|
|
251
|
+
repo_type = "pesquisa"
|
|
252
|
+
elif choice == "2":
|
|
253
|
+
org_name = "centro-dados-ia"
|
|
254
|
+
repo_type = "administrativo"
|
|
255
|
+
else:
|
|
256
|
+
console.print("[bold red]❌ Opção inválida. Por favor, digite 1 ou 2.[/bold red]")
|
|
257
|
+
raise typer.Exit(code=1)
|
|
258
|
+
|
|
259
|
+
token = config.get_token()
|
|
260
|
+
pasta_atual = "."
|
|
261
|
+
|
|
262
|
+
core.criar_repositorio(token, org_name, pasta_atual, repo_type=repo_type)
|
|
263
|
+
console.print(
|
|
264
|
+
Panel(
|
|
265
|
+
"✅ [bold green]Processo concluído com sucesso![/bold green]",
|
|
266
|
+
title="Finalizado",
|
|
267
|
+
border_style="green",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@app.command()
|
|
272
|
+
def start():
|
|
273
|
+
"""Starts the interactive setup process for a new MLOps project."""
|
|
274
|
+
console.print(Panel("[bold green]Bem-vindo à biblioteca MLOps Insper![/bold green]", title="👋 Boas-vindas"))
|
|
275
|
+
console.print("Para começar, por favor, selecione o seu perfil:")
|
|
276
|
+
console.print(" [cyan]1[/cyan]: Pesquisador")
|
|
277
|
+
console.print(" [cyan]2[/cyan]: Administrativo")
|
|
278
|
+
choice = typer.prompt("Digite o número da opção")
|
|
279
|
+
|
|
280
|
+
if choice == "1":
|
|
281
|
+
_start_pesquisador_flow()
|
|
282
|
+
elif choice == "2":
|
|
283
|
+
_start_administrativo_flow()
|
|
284
|
+
else:
|
|
285
|
+
console.print("[bold red]❌ Opção inválida. Por favor, digite 1 ou 2.[/bold red]")
|
|
286
|
+
raise typer.Exit(code=1)
|
mlopstemplate/config.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Configurações e utilitários para autenticação e templates do MLOps Template."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
# URLs dos templates para cada perfil
|
|
7
|
+
TEMPLATE_REPO_URL = "https://github.com/centro-dados-ia/cdiaTemplateMlops.git"
|
|
8
|
+
TEMPLATE_REPO_URL_PESQUISA = "https://github.com/Insper-CDIA-Pesquisa/cdiaTemplateMlops.git"
|
|
9
|
+
|
|
10
|
+
TOKEN_ENV_VAR = "GITHUB_TOKEN"
|
|
11
|
+
TOKEN_FILE = os.path.join(os.path.expanduser("~"), ".mlops_token")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def set_session_token(token: str):
|
|
15
|
+
"""Armazena o token do GitHub como uma variável de ambiente para a sessão atual."""
|
|
16
|
+
os.environ[TOKEN_ENV_VAR] = token
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_token(token: str):
|
|
20
|
+
"""Salva o token do GitHub em um arquivo de configuração local."""
|
|
21
|
+
with open(TOKEN_FILE, "w") as f:
|
|
22
|
+
f.write(token)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_token() -> Optional[str]:
|
|
26
|
+
"""
|
|
27
|
+
Obtém o token do GitHub, priorizando a variável de ambiente da sessão.
|
|
28
|
+
Se não encontrar, busca no arquivo de configuração local.
|
|
29
|
+
"""
|
|
30
|
+
# 1. Prioriza o token da sessão (variável de ambiente)
|
|
31
|
+
token = os.environ.get(TOKEN_ENV_VAR)
|
|
32
|
+
if token:
|
|
33
|
+
return token
|
|
34
|
+
|
|
35
|
+
# 2. Se não houver, tenta ler do arquivo
|
|
36
|
+
if os.path.exists(TOKEN_FILE):
|
|
37
|
+
with open(TOKEN_FILE, "r") as f:
|
|
38
|
+
return f.read().strip()
|
|
39
|
+
|
|
40
|
+
return None
|
mlopstemplate/core.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Core logic for handling Git and GitHub operations."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
import git
|
|
9
|
+
from github import Github, GithubException, Repository
|
|
10
|
+
from github.Organization import Organization
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def clonar_template_com_nome(
|
|
17
|
+
template_repo_url: str, nome_projeto: str, destino_base: str = "."
|
|
18
|
+
) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Clones a template repository into a new directory with a given name.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
template_repo_url: The URL of the template repository to clone.
|
|
24
|
+
nome_projeto: The name of the new project and directory.
|
|
25
|
+
destino_base: The base directory where the new project will be created.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The destination path of the cloned repository.
|
|
29
|
+
"""
|
|
30
|
+
destino = os.path.join(destino_base, nome_projeto)
|
|
31
|
+
if os.path.exists(destino):
|
|
32
|
+
console.print(f"[red]❌ A pasta '{destino}' já existe. Por segurança, o processo será interrompido.[/red]")
|
|
33
|
+
console.print("[yellow]💡 Dica: Apague manualmente ou escolha outro nome para o repositório.[/yellow]")
|
|
34
|
+
raise typer.Exit(code=1)
|
|
35
|
+
console.print(f"🌱 Criando seu repositório em {destino}")
|
|
36
|
+
git.Repo.clone_from(template_repo_url, destino)
|
|
37
|
+
return destino
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_or_create_repo(
|
|
41
|
+
org: 'Organization', repo_nome: str, visibilidade: str
|
|
42
|
+
) -> 'Repository':
|
|
43
|
+
"""Gets an existing repository or creates a new one."""
|
|
44
|
+
try:
|
|
45
|
+
return org.get_repo(repo_nome)
|
|
46
|
+
except GithubException as e:
|
|
47
|
+
if e.status == 404:
|
|
48
|
+
return org.create_repo(repo_nome, private=(visibilidade == "private"))
|
|
49
|
+
raise e
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _init_and_configure_git(
|
|
53
|
+
pasta: str, repo_url: str, branches_to_push: List[str], repo_type: str
|
|
54
|
+
) -> 'git.Repo':
|
|
55
|
+
"""Initializes and configures the local Git repository."""
|
|
56
|
+
repo_git = git.Repo.init(pasta)
|
|
57
|
+
|
|
58
|
+
if "origin" in [remote.name for remote in repo_git.remotes]:
|
|
59
|
+
repo_git.remote("origin").set_url(repo_url)
|
|
60
|
+
else:
|
|
61
|
+
repo_git.create_remote("origin", repo_url)
|
|
62
|
+
|
|
63
|
+
# ⚠️ Cria commit inicial diretamente na branch 'dev'
|
|
64
|
+
repo_git.git.checkout("-b", "dev")
|
|
65
|
+
repo_git.git.add(A=True)
|
|
66
|
+
|
|
67
|
+
if repo_git.is_dirty(untracked_files=True):
|
|
68
|
+
repo_git.index.commit("Commit inicial do projeto")
|
|
69
|
+
|
|
70
|
+
# ⚙️ Cria a branch 'prod' a partir da 'dev' se for administrativo
|
|
71
|
+
if repo_type == "administrativo":
|
|
72
|
+
if "prod" not in [b.name for b in repo_git.branches]:
|
|
73
|
+
repo_git.create_head("prod", "dev")
|
|
74
|
+
|
|
75
|
+
# 🚀 Push das branches existentes
|
|
76
|
+
for branch in branches_to_push:
|
|
77
|
+
if branch in [b.name for b in repo_git.branches]:
|
|
78
|
+
repo_git.git.push("--set-upstream", "origin", branch)
|
|
79
|
+
|
|
80
|
+
return repo_git
|
|
81
|
+
|
|
82
|
+
def _apply_branch_protection(repo: 'Repository', branch_name: str):
|
|
83
|
+
"""Applies a comprehensive set of branch protection rules."""
|
|
84
|
+
try:
|
|
85
|
+
console.print(f"🛡️ Aplicando regras de proteção à branch '[bold]{branch_name}[/bold]'...")
|
|
86
|
+
branch = repo.get_branch(branch_name)
|
|
87
|
+
branch.edit_protection(
|
|
88
|
+
# Requer 1 aprovação em PRs
|
|
89
|
+
required_approving_review_count=1,
|
|
90
|
+
# Desabilita revisões obsoletas após novos pushes
|
|
91
|
+
dismiss_stale_reviews=True,
|
|
92
|
+
# Não permite deletar a branch
|
|
93
|
+
allow_deletions=False,
|
|
94
|
+
# Exige que o histórico de commits seja linear (impede merge fast-forward)
|
|
95
|
+
required_linear_history=True,
|
|
96
|
+
# Passando outras regras como kwargs, baseado na API do GitHub
|
|
97
|
+
require_code_owner_reviews=False,
|
|
98
|
+
required_conversation_resolution=False,
|
|
99
|
+
)
|
|
100
|
+
console.print(f"✅ Proteção da branch '[bold]{branch_name}[/bold]' configurada.")
|
|
101
|
+
except GithubException as e:
|
|
102
|
+
if e.status == 403:
|
|
103
|
+
console.print(f"[yellow]⚠️ Aviso: A proteção de branch em '{branch_name}' não foi aplicada (requer plano Pro/Team).[/yellow]")
|
|
104
|
+
else:
|
|
105
|
+
console.print(f"[red]❌ Erro ao proteger a branch '{branch_name}': {e}[/red]")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
console.print(f"[red]❌ Erro inesperado ao proteger a branch '{branch_name}': {e}[/red]")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _set_repository_permissions(org: 'Organization', repo: 'Repository', g: 'Github'):
|
|
111
|
+
"""Sets repository permissions for the admin team and the creator."""
|
|
112
|
+
try:
|
|
113
|
+
team = org.get_team_by_slug("cdia-admin")
|
|
114
|
+
team.set_repo_permission(repo, "admin")
|
|
115
|
+
|
|
116
|
+
creator = g.get_user()
|
|
117
|
+
if creator and creator.login:
|
|
118
|
+
repo.add_to_collaborators(creator.login, permission="push")
|
|
119
|
+
console.print(f"✅ Permissões configuradas: Equipe '[bold]CDIA-Admin[/bold]' é admin, usuário '[bold]{creator.login}[/bold]' é writer.")
|
|
120
|
+
else:
|
|
121
|
+
console.print("✅ Permissões configuradas: Equipe '[bold]CDIA-Admin[/bold]' é admin.")
|
|
122
|
+
console.print("[yellow]⚠️ Aviso: Não foi possível rebaixar o criador do repositório para 'writer' (usuário não identificado).[/yellow]")
|
|
123
|
+
|
|
124
|
+
except GithubException as e:
|
|
125
|
+
if e.status == 404:
|
|
126
|
+
console.print("[yellow]⚠️ Aviso: Equipe 'cdia-admin' não encontrada. Permissões de equipe não ajustadas.[/yellow]")
|
|
127
|
+
else:
|
|
128
|
+
console.print(f"[red]❌ Erro ao configurar permissões: {e}[/red]")
|
|
129
|
+
|
|
130
|
+
def criar_repositorio(
|
|
131
|
+
github_token: str,
|
|
132
|
+
org_name: str,
|
|
133
|
+
pasta: str,
|
|
134
|
+
repo_type: str = "administrativo",
|
|
135
|
+
visibilidade: str = "private",
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
Creates and configures a GitHub repository based on the specified type.
|
|
139
|
+
|
|
140
|
+
This function handles the entire workflow: creating the repo on GitHub,
|
|
141
|
+
configuring the local git repository, pushing branches, and setting up
|
|
142
|
+
permissions and protections.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
github_token: The GitHub personal access token.
|
|
146
|
+
org_name: The name of the GitHub organization.
|
|
147
|
+
pasta: The local path to the project directory.
|
|
148
|
+
repo_type: The type of repository ('administrativo' or 'pesquisa').
|
|
149
|
+
This determines the configuration applied.
|
|
150
|
+
visibilidade: The visibility of the repository ('private' or 'public').
|
|
151
|
+
"""
|
|
152
|
+
with console.status("[bold green]Iniciando processo...") as status:
|
|
153
|
+
repo_nome = os.path.basename(os.path.abspath(pasta))
|
|
154
|
+
console.print(f"📝 Nome do repositório: [bold cyan]{repo_nome}[/bold cyan]")
|
|
155
|
+
|
|
156
|
+
status.update("Conectando ao GitHub e criando repositório...")
|
|
157
|
+
g = Github(github_token)
|
|
158
|
+
try:
|
|
159
|
+
org = g.get_organization(org_name)
|
|
160
|
+
except GithubException as e:
|
|
161
|
+
if e.status == 404:
|
|
162
|
+
console.print(f"[red]❌ Organização '{org_name}' não encontrada ou sem acesso.[/red]")
|
|
163
|
+
console.print("[yellow]💡 Dicas:[/yellow]")
|
|
164
|
+
console.print(" • Verifique se o nome da organização está correto")
|
|
165
|
+
console.print(" • Confirme se você tem acesso à organização")
|
|
166
|
+
console.print(" • Considere usar sua conta pessoal do GitHub")
|
|
167
|
+
raise e
|
|
168
|
+
else:
|
|
169
|
+
raise e
|
|
170
|
+
repo = _get_or_create_repo(org, repo_nome, visibilidade)
|
|
171
|
+
console.print("✅ Repositório criado com sucesso.")
|
|
172
|
+
|
|
173
|
+
status.update("Configurando repositório local e enviando branches...")
|
|
174
|
+
repo_url = repo.clone_url.replace("https://", f"https://{github_token}@")
|
|
175
|
+
branches_to_push = ["dev", "prod"] if repo_type == "administrativo" else ["dev"]
|
|
176
|
+
_init_and_configure_git(pasta, repo_url, branches_to_push, repo_type)
|
|
177
|
+
console.print("✅ Branches enviadas.")
|
|
178
|
+
|
|
179
|
+
status.update("Configurando permissões e proteções...")
|
|
180
|
+
if repo_type == "administrativo":
|
|
181
|
+
# Configura os métodos de merge permitidos para o repositório
|
|
182
|
+
repo.edit(
|
|
183
|
+
allow_merge_commit=True,
|
|
184
|
+
allow_squash_merge=True,
|
|
185
|
+
allow_rebase_merge=True,
|
|
186
|
+
delete_branch_on_merge=True, # Conveniência: apaga a branch após o merge
|
|
187
|
+
)
|
|
188
|
+
console.print("✅ Métodos de merge e configurações do repositório aplicados.")
|
|
189
|
+
_apply_branch_protection(repo, "dev")
|
|
190
|
+
_apply_branch_protection(repo, "prod")
|
|
191
|
+
_set_repository_permissions(org, repo, g)
|
|
192
|
+
|
|
193
|
+
console.print("✅ Processo concluído com sucesso!")
|