insper-mlops 0.1.0__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.
@@ -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,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,35 @@
1
+ # MLOps Template
2
+
3
+ 🚀 Ferramenta de linha de comando para padronizar e automatizar projetos de MLOps no Insper CDIA.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ pip install insper-mlops
9
+ ```
10
+
11
+ ## Uso
12
+
13
+ ```bash
14
+ mlops start
15
+ ```
16
+
17
+ Siga as instruções no terminal para criar seu repositório de MLOps.
18
+
19
+ ## Funcionalidades
20
+
21
+ - ✅ Criação automática de repositórios GitHub
22
+ - ✅ Configuração de branches (dev/prod)
23
+ - ✅ Aplicação de regras de proteção de branch
24
+ - ✅ Configuração de permissões de equipe
25
+ - ✅ Templates pré-configurados para diferentes perfis
26
+
27
+ ## Requisitos
28
+
29
+ - Python 3.8+
30
+ - GitHub CLI (`gh`) instalado
31
+ - Conta GitHub com acesso às organizações Insper
32
+
33
+ ## Licença
34
+
35
+ MIT License - veja o arquivo [LICENSE](LICENSE) para detalhes.
@@ -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,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ insper_mlops.egg-info/PKG-INFO
5
+ insper_mlops.egg-info/SOURCES.txt
6
+ insper_mlops.egg-info/dependency_links.txt
7
+ insper_mlops.egg-info/entry_points.txt
8
+ insper_mlops.egg-info/requires.txt
9
+ insper_mlops.egg-info/top_level.txt
10
+ mlopstemplate/__init__.py
11
+ mlopstemplate/cli.py
12
+ mlopstemplate/config.py
13
+ mlopstemplate/core.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mlops = mlopstemplate.cli:app
@@ -0,0 +1,4 @@
1
+ GitPython
2
+ PyGithub>=2.0.0
3
+ typer[all]
4
+ requests
@@ -0,0 +1 @@
1
+ mlopstemplate
@@ -0,0 +1,5 @@
1
+ """MLOps Template - Ferramenta para padronizar projetos de MLOps no Insper CDIA."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Insper CDIA"
5
+ __email__ = "cdia@insper.edu.br"
@@ -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)
@@ -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
@@ -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!")
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "insper-mlops"
7
+ version = "0.1.0"
8
+ description = "A command-line tool to standardize the creation of MLOps projects at Insper CDIA."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ { name = "Insper CDIA", email = "cdia@insper.edu.br" }
13
+ ]
14
+ requires-python = ">=3.8"
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "Topic :: Software Development :: Build Tools",
21
+ ]
22
+ dependencies = [
23
+ "GitPython",
24
+ "PyGithub>=2.0.0",
25
+ "typer[all]",
26
+ "requests",
27
+ ]
28
+
29
+ [project.scripts]
30
+ mlops = "mlopstemplate.cli:app"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/centro-dados-ia/insper-mlops"
34
+ "Bug Tracker" = "https://github.com/centro-dados-ia/insper-mlops/issues"
35
+
36
+ [tool.setuptools.packages.find]
37
+ include = ["mlopstemplate"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+