Compare commits

..
47 Commits
Author SHA1 Message Date
jt 0782cfb041 refactor: migrar comando merge para que utilice questionary 2026-07-14 00:28:20 -03:00
jt 320d40a211 refactor: migrar comando commit para que utilice questionary 2026-07-14 00:23:36 -03:00
jt a5a753f429 refactor: migrar comando new para que utilice metodos usando questionary 2026-07-14 00:22:22 -03:00
gitea-actions 4968667dad chore: bump version and update CHANGELOG.md [skip ci] 2026-07-09 21:15:32 +00:00
jt 79ae88b012 Merge pull request 'bugfix: actualizo versiones de checkout, setup-python, clonar depth completo' (#10) from bugfix/publish-pypi-workflow into main
Reviewed-on: #10
2026-07-09 21:15:00 +00:00
jt 4666503f32 bugfix: actualizo versiones de checkout, setup-python, clonar depth completo 2026-07-09 18:14:38 -03:00
gitea-actions 4c089eb8da chore: bump version and update CHANGELOG.md [skip ci] 2026-07-09 21:05:19 +00:00
jt dc858d026f Merge pull request 'feature: add publish pypi package workflow' (#9) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #9
2026-07-09 21:05:03 +00:00
jt 5989699f08 bugfix: usar https por defecto como esquema si se detecta ssh 2026-07-09 18:04:40 -03:00
jt ecfe058f8d feature: usar esquema de remoto para host de api gitea 2026-07-09 18:00:39 -03:00
gitea-actions ffe813bf06 chore: bump version and update CHANGELOG.md [skip ci] 2026-07-09 01:21:44 +00:00
jt 6cd6771241 Merge pull request 'bugfix: agregar setup previo a ejecucion de git tag' (#8) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #8
2026-07-09 01:21:35 +00:00
jt 1cc2de0b75 bugfix: agregar setup previo a ejecucion de git tag 2026-07-08 22:21:17 -03:00
jt 0e669019f4 Merge pull request 'bugfix: arreglo acceso a secretos' (#7) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #7
2026-07-09 01:11:06 +00:00
jt d456565b2f bugfix: arreglo acceso a secretos 2026-07-08 22:10:35 -03:00
jt 74a78d30a1 Merge pull request 'bugfix: agrego fetch-depth a checkout' (#6) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #6
2026-07-09 01:08:20 +00:00
jt ad8984cb28 bugfix: agrego fetch-depth a checkout 2026-07-08 22:08:02 -03:00
jt 53b02f1f8f Merge pull request 'bugfix: agrego log de git luego del checkout' (#5) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #5
2026-07-09 01:06:41 +00:00
jt bf6a0e8443 bugfix: agrego log de git luego del checkout 2026-07-08 22:06:24 -03:00
jt 12351e64ba Merge pull request 'bugfix: cambio version de action setup-uv' (#4) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #4
2026-07-09 00:59:09 +00:00
jt cb742425c1 bugfix: cambio version de action setup-uv 2026-07-08 21:58:52 -03:00
jt bd47b40aff Merge pull request 'feature: agrego workflow para subir paquete a pypi' (#3) from feature/add-publish-pypi-package-workflow into main
Reviewed-on: #3
2026-07-09 00:55:53 +00:00
jt e94ee2ccc7 feature: agrego workflow para subir paquete a pypi 2026-07-08 21:55:16 -03:00
jt 069c62b697 Merge pull request 'feature: agrego implementacion de github remote api' (#2) from feature/add-github-remote-api into main
Reviewed-on: #2
2026-07-09 00:28:52 +00:00
jt c18ca454c0 feature: agrego implementacion de github remote api 2026-07-08 21:27:42 -03:00
jt 0fcf53bcb5 Merge pull request 'feature: add gitea remote api' (#1) from feature/add-gitea-remote-api into main
Reviewed-on: #1
2026-07-09 00:22:34 +00:00
jt f9dddbc4d6 bugfix: agregar schema a host de gitea api 2026-07-08 21:20:43 -03:00
jt c7888c74f1 bugfix: corregir headers de gitea 2026-07-08 21:19:24 -03:00
jt 87c5ba81c4 feature: agrego soporte para apis gitea 2026-07-08 21:12:06 -03:00
jt a10e9fe4fe Add .gitea/workflows/demo.yaml 2026-07-08 02:56:08 +00:00
bitbucket-pipelines c93833769b chore: bump version and update CHANGELOG.md [skip ci] 2026-06-03 00:40:26 +00:00
JonathanGitFlowandjt c536deb54d Merged in bugfix/allow-commit-messages-non-conventional (pull request #77)
bugfix: allow commits not compliant with conventional commits in changelog generation

Approved-by: Jonathan Teran
2026-06-03 00:39:43 +00:00
jt 3ff9f76a6c bugfix: allow commits not compliant with conventional commits in changelog generation 2026-06-02 21:38:18 -03:00
bitbucket-pipelines 1c319beff4 chore: bump version and update CHANGELOG.md [skip ci] 2026-05-24 05:42:13 +00:00
JonathanGitFlowandjt f885c27e08 Merged in bugfix/pr-title-with-ticket (pull request #76)
bugfix: use rest of branch name instead of ticket for pr title generation

Approved-by: Jonathan Teran
2026-05-24 05:41:05 +00:00
jt 094bc1a698 bugfix: use rest of branch name instead of ticket for pr title generation 2026-05-24 02:40:33 -03:00
bitbucket-pipelines 9b0d7d6166 chore: bump version and update CHANGELOG.md [skip ci] 2026-05-22 19:20:02 +00:00
JonathanGitFlowandjt f52edf03e0 Merged in bugfix/prevent-close-branch-on-environment-merges (pull request #75)
bugfix: dont close branches on merges between environments

Approved-by: Jonathan Teran
2026-05-22 19:19:30 +00:00
Jonathan Teran 8ee01290ef bugfix: dont close branches on merges between environments 2026-05-22 16:18:56 -03:00
bitbucket-pipelines 9c89f4d638 chore: bump version and update CHANGELOG.md [skip ci] 2026-05-22 19:14:59 +00:00
JonathanGitFlowandjt 8d35610c00 Merged in bugfix/customize-pr-title-on-environment-merges (pull request #74)
bugfix: change pr title on merges between environments

Approved-by: Jonathan Teran
2026-05-22 19:14:19 +00:00
Jonathan Teran de124baf1a bugfix: change pr title on merges between environments 2026-05-22 16:13:39 -03:00
bitbucket-pipelines 2676f41def chore: bump version and update CHANGELOG.md [skip ci] 2026-05-20 20:14:50 +00:00
JonathanGitFlowandjt 22fd68c5b6 Merged in bugfix/dont-push-environment-branches-on-merge (pull request #73)
bugfix: dont push environment branches since they are already created on remote

Approved-by: Jonathan Teran
2026-05-20 20:14:08 +00:00
jt 04c9a4b449 bugfix: dont push environment branches since they are already created on remote 2026-05-20 17:13:34 -03:00
JonathanGitFlowandjt 417021b36e Merged in refactor/unify-flow-environments-usage (pull request #72)
refactor: unify flow branches usage

Approved-by: Jonathan Teran
2026-05-20 20:12:26 +00:00
jt be245114d5 refactor: unify flow branches usage 2026-05-20 17:10:58 -03:00
22 changed files with 402 additions and 155 deletions
+2
View File
@@ -2,3 +2,5 @@
remote = origin remote = origin
branches = main branches = main
initialized = true initialized = true
version = 2
remote-type = gitea
+37
View File
@@ -0,0 +1,37 @@
name: Publish to PyPI
on:
push:
branches:
- main
jobs:
Publish-to-PyPI:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Configure git
run: |
git config --global user.name "gitea-actions"
git config --global user.email "actions@git.jonathanteran.dev"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@v8.3.2
- name: Bump version
run: |
uv run git-flow tag --token="${{ secrets.GITEA_TOKEN }}"
- name: Build and publish
run: |
uv build
uv publish --token="${{ secrets.PYPI_TOKEN }}"
+68
View File
@@ -1,3 +1,71 @@
## v1.17.6 - 2026-07-09
- **bugfix**: actualizo versiones de checkout, setup-python, clonar depth completo [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (4666503)
---
## v0.1.0-main - 2026-07-09
- **bugfix**: usar https por defecto como esquema si se detecta ssh [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5989699)
- **feature**: usar esquema de remoto para host de api gitea [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ecfe058)
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
---
## v0.1.0-main - 2026-07-09
- **bugfix**: agregar setup previo a ejecucion de git tag [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (1cc2de0)
- **bugfix**: arreglo acceso a secretos [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (d456565)
- **bugfix**: agrego fetch-depth a checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (ad8984c)
- **bugfix**: agrego log de git luego del checkout [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (bf6a0e8)
- **bugfix**: cambio version de action setup-uv [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (cb74242)
- **feature**: agrego workflow para subir paquete a pypi [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (e94ee2c)
---
## v1.17.5 - 2026-06-03
- **bugfix**: allow commits not compliant with conventional commits in changelog generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (3ff9f76)
---
## v1.17.4 - 2026-05-24
- **bugfix**: use rest of branch name instead of ticket for pr title generation [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (094bc1a)
---
## v1.17.3 - 2026-05-22
- **bugfix**: dont close branches on merges between environments [[jteran](mailto:jteran@renatre.org.ar)] (8ee0129)
---
## v1.17.2 - 2026-05-22
- **bugfix**: change pr title on merges between environments [[jteran](mailto:jteran@renatre.org.ar)] (de124ba)
---
## v1.17.1 - 2026-05-20
- **bugfix**: dont push environment branches since they are already created on remote [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (04c9a4b)
---
## v1.17.0 - 2026-05-20 ## v1.17.0 - 2026-05-20
- **feature**: allow merges between environments [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5806aea) - **feature**: allow merges between environments [[jonathan.nerat](mailto:jonathan.nerat@gmail.com)] (5806aea)
+2 -1
View File
@@ -47,7 +47,8 @@ El comando solicitará 2 opciones:
automáticamente. Por el momento, se soportan los siguientes remotos: automáticamente. Por el momento, se soportan los siguientes remotos:
- [x] Bitbucket - [x] Bitbucket
- [ ] Github: planeado - [x] Github
- [x] Gitea
La configuración del remoto requiere tener un Access Token para poder crear los PRs automáticamente. La configuración del remoto requiere tener un Access Token para poder crear los PRs automáticamente.
El mismo se puede crear desde el repositorio al que se quiere dar acceso: *"Repository settings" > El mismo se puede crear desde el repositorio al que se quiere dar acceso: *"Repository settings" >
+1 -1
View File
@@ -8,7 +8,7 @@ description = "Git workflow automation to follow best practices when working wit
dynamic = ["version"] dynamic = ["version"]
requires-python = ">= 3.14" requires-python = ">= 3.14"
dependencies = [ dependencies = [
"requests", "typer" "requests", "typer", "rich", "questionary"
] ]
authors = [ authors = [
{name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"}, {name = "Alan Facundo Biglieri", email = "abiglieri@renatre.org.ar"},
+2
View File
@@ -51,3 +51,5 @@ SUPPORTED_REMOTE_APIS = [
REPOSITORY_TOKEN_FILENAME = ".repository-token" REPOSITORY_TOKEN_FILENAME = ".repository-token"
FLOWCONFIG_FILENAME = ".flowconfig" FLOWCONFIG_FILENAME = ".flowconfig"
FLOWCONFIG_VERSION = 2
+8 -4
View File
@@ -28,11 +28,15 @@ class Changelog:
message = Git("show", commit, patch=False, format="%s").firstline() message = Git("show", commit, patch=False, format="%s").firstline()
email = Git("show", commit, patch=False, format="%ae").firstline() email = Git("show", commit, patch=False, format="%ae").firstline()
username = email[: email.index("@")] username = email[: email.index("@")]
index = message.index(":") description = message
commit_type = message[:index]
commit_message = message[index + 1 :]
content += f"- **{commit_type}**: {commit_message} [[{username}](mailto:{email})] ({commit})\n" if ":" in message:
index = message.index(":")
commit_type = message[:index]
commit_message = message[index + 1 :]
description = f"**{commit_type}**: {commit_message}"
content += f"- {description} [[{username}](mailto:{email})] ({commit})\n"
return content return content
+62 -17
View File
@@ -2,23 +2,25 @@ from git_flow import (
BRANCH_TYPES, BRANCH_TYPES,
REPOSITORY_TOKEN_FILENAME, REPOSITORY_TOKEN_FILENAME,
FLOWCONFIG_FILENAME, FLOWCONFIG_FILENAME,
FLOWCONFIG_VERSION,
GitFlowError, GitFlowError,
) )
from git_flow.git import Git from git_flow.git import Git
from git_flow.remote.base import RemoteAPI from git_flow.remote.base import RemoteAPI
from git_flow.remote.bitbucket import BitbucketRemoteAPI from git_flow.remote.bitbucket import BitbucketRemoteAPI
from git_flow.remote.github import GithubRemoteAPI from git_flow.remote.github import GithubRemoteAPI
from git_flow.remote.gitea import GiteaRemoteAPI
from git_flow.io import * from git_flow.io import *
from os.path import isfile from os.path import isfile
TYPE_SUCCESS = 0 from typing import Optional
TYPE_WARNING = 1 import rich
TYPE_ERROR = 2 import rich.panel
TYPE_INFO = 3 import questionary
flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {} flowconfig = Git.get_config(FLOWCONFIG_FILENAME) if isfile(FLOWCONFIG_FILENAME) else {}
environments = flowconfig["flow.branches"].split(",") if "flow.branches" in flowconfig else []
def ensure_initialized(): def ensure_initialized():
initialized = flowconfig["flow.initialized"] if flowconfig else None initialized = flowconfig["flow.initialized"] if flowconfig else None
@@ -32,13 +34,21 @@ def ensure_initialized():
f"El valor de `flow.initialized` ({initialized}) es inválido." f"El valor de `flow.initialized` ({initialized}) es inválido."
) )
version = int(flowconfig.get("flow.version", "1"))
if version != FLOWCONFIG_VERSION:
raise GitFlowError(
f"La versión del flowconfig ({version}) no es compatible con esta versión de git-flow ({FLOWCONFIG_VERSION}). "
f"Debe reinicializar el repositorio."
)
def ensure_right_branch(): def ensure_right_branch():
branch = Git.get_current_branch() branch = Git.get_current_branch()
if branch == "HEAD": if branch == "HEAD":
raise GitFlowError("No se encuentra parado sobre una rama.") raise GitFlowError("No se encuentra parado sobre una rama.")
elif confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"): elif io_confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"):
return branch return branch
else: else:
raise GitFlowError("Ejecución cancelada.") raise GitFlowError("Ejecución cancelada.")
@@ -46,7 +56,6 @@ def ensure_right_branch():
def get_branch_env_and_type(branch: str) -> tuple[str, str]: def get_branch_env_and_type(branch: str) -> tuple[str, str]:
components = branch.split("/") components = branch.split("/")
target_branches = flowconfig["flow.branches"].split(",")
if len(components) == 2: if len(components) == 2:
if components[0] not in BRANCH_TYPES: if components[0] not in BRANCH_TYPES:
@@ -54,13 +63,13 @@ def get_branch_env_and_type(branch: str) -> tuple[str, str]:
f"Branch inválida, el tipo '{components[0]}' no es válido." f"Branch inválida, el tipo '{components[0]}' no es válido."
) )
return (target_branches[0], components[0]) return (environments[0], components[0])
elif len(components) == 4: elif len(components) == 4:
target_branches = target_branches[1:] valid_environments = environments[1:]
if ( if (
components[0] != "release" components[0] != "release"
or components[1] not in target_branches or components[1] not in valid_environments
or components[2] not in BRANCH_TYPES or components[2] not in BRANCH_TYPES
): ):
raise GitFlowError( raise GitFlowError(
@@ -93,11 +102,11 @@ def ensure_clean_worktree(has_remote: bool):
if not_empty: if not_empty:
if not has_remote: if not has_remote:
warning("Existen cambios en tu entorno de trabajo sin commitear.") io_warning("Existen cambios en tu entorno de trabajo sin commitear.")
else: else:
raise GitFlowError("No se puede continuar con cambios pendientes.") raise GitFlowError("No se puede continuar con cambios pendientes.")
if not confirm("¿Desea continuar?", False): if not io_confirm("¿Desea continuar?", False):
raise GitFlowError("Ejecución abortada") raise GitFlowError("Ejecución abortada")
@@ -105,14 +114,18 @@ def get_remote_api(token: str) -> RemoteAPI:
if "flow.remote" not in flowconfig: if "flow.remote" not in flowconfig:
raise GitFlowError("El repositorio no tiene configurado un remoto.") raise GitFlowError("El repositorio no tiene configurado un remoto.")
[host, repository] = RemoteAPI.parse(flowconfig["flow.remote"]) remote_type = flowconfig.get("flow.remote-type", "").lower()
[schema, host, repository] = RemoteAPI.parse(flowconfig["flow.remote"])
if host == "bitbucket.org": if remote_type == "gitea":
return GiteaRemoteAPI(("https://" if schema == "ssh://" else schema) + host, repository, token)
elif remote_type == "bitbucket":
return BitbucketRemoteAPI(repository, token) return BitbucketRemoteAPI(repository, token)
elif host == "github.com": elif remote_type == "github":
return GithubRemoteAPI(repository, token) return GithubRemoteAPI(repository, token)
else: else:
raise GitFlowError("El host del repositorio remoto es inválido") raise GitFlowError("El tipo del remoto es inválido")
def is_valid_ticket(ticket: str) -> bool: def is_valid_ticket(ticket: str) -> bool:
"""Check if the specified string is a valid ticket number (ABC-123)""" """Check if the specified string is a valid ticket number (ABC-123)"""
@@ -124,4 +137,36 @@ def is_valid_ticket(ticket: str) -> bool:
ticket_project = components[0] ticket_project = components[0]
ticket_number = components[1] ticket_number = components[1]
return ticket_project.isalpha() and ticket_project.isupper() and ticket_number.isnumeric() return ticket_project.isalpha() and ticket_project.isupper() and ticket_number.isnumeric()
def io_error(message: str, title: Optional[str] = None):
_io_status(message, "❌ Error: ", "red", title)
def io_warning(message: str, title: Optional[str] = None):
_io_status(message, "⚠️ Warning: ", "yellow", title)
def io_info(message: str, title: Optional[str] = None):
_io_status(message, "️ Info: ", "blue", title)
def io_success(message: str, title: Optional[str] = None):
_io_status(message, "✅ Success: ", "green", title)
def _io_status(message: str, prefix: str, style: str, title: Optional[str] = None):
if title:
rich.print(rich.panel.Panel(message, title=prefix + title, style=style, expand=False, title_align="left"))
else:
rich.print(f"[{style}]{prefix}{message}[/{style}]")
def io_confirm(question: str, default: bool = True) -> bool:
return questionary.confirm(question, default=default, auto_enter=False).ask()
def io_choice(prompt: str, options: list[str]) -> str:
return questionary.select(prompt, options, instruction="Usar flechas").ask()
def io_prompt(message: str, default: str = "", persistent: bool = False, strip: bool = False, validator = None) -> str:
if persistent and not validator:
validator = lambda s: len(s.strip()) > 0
value = questionary.text(message, default, validate=validator).ask()
return value.strip() if strip else value
-1
View File
@@ -41,7 +41,6 @@ def branch(
ensure_initialized() ensure_initialized()
current_branch = Git.get_current_branch() current_branch = Git.get_current_branch()
environments = flowconfig["flow.branches"].split(",")
if current_branch in environments: if current_branch in environments:
current_environment = current_branch current_environment = current_branch
+7 -7
View File
@@ -2,7 +2,7 @@ import typer
from datetime import datetime from datetime import datetime
from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
from git_flow.git import Git from git_flow.git import Git
from git_flow.command.base import * import git_flow.command.base as base
app = typer.Typer() app = typer.Typer()
@@ -10,18 +10,18 @@ app = typer.Typer()
@app.command() @app.command()
def commit(): def commit():
"""Crea un commit siguiendo el formato de Conventional Commits""" """Crea un commit siguiendo el formato de Conventional Commits"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
if not _has_files_staged(): if not _has_files_staged():
warning("No hay cambios en el indice para commitear.") base.io_warning("No hay cambios en el indice para commitear.")
if confirm("¿Desea agregar la carpeta actual?"): if base.io_confirm("¿Desea agregar la carpeta actual?"):
Git("add", ".").exec(print="Agregando cambios") Git("add", ".").exec(print="Agregando cambios")
else: else:
raise GitFlowError("Debe agregar algún cambio al indice para continuar.") raise GitFlowError("Debe agregar algún cambio al indice para continuar.")
commit_type = choice("Tipo de commit", COMMIT_TYPES) commit_type = base.io_choice("Tipo de commit", COMMIT_TYPES)
commit_message = prompt( commit_message = base.io_prompt(
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True "Mensaje (máximo recomendado: 100 caracteres)", persistent=True
) )
message = commit_type + ": " + commit_message message = commit_type + ": " + commit_message
+9 -4
View File
@@ -1,6 +1,6 @@
import typer import typer
from git_flow import FLOWCONFIG_FILENAME, GitFlowError from git_flow import FLOWCONFIG_FILENAME, FLOWCONFIG_VERSION, GitFlowError
from git_flow.git import Git from git_flow.git import Git
from git_flow.command.base import * from git_flow.command.base import *
@@ -15,15 +15,17 @@ def init():
_ensure_not_already_initialized() _ensure_not_already_initialized()
branches = _setup_flow_branches() branches = _setup_flow_branches()
remote = _setup_flow_remote() (remote, remote_type) = _setup_flow_remote()
flowconfig = { flowconfig = {
"flow.version": str(FLOWCONFIG_VERSION),
"flow.initialized": "true", "flow.initialized": "true",
"flow.branches": ",".join(branches), "flow.branches": ",".join(branches),
} }
if remote: if remote and remote_type:
flowconfig["flow.remote"] = remote flowconfig["flow.remote"] = remote
flowconfig["flow.remote-type"] = remote_type
Git.set_config(flowconfig, FLOWCONFIG_FILENAME) Git.set_config(flowconfig, FLOWCONFIG_FILENAME)
Git("add", FLOWCONFIG_FILENAME).exec() Git("add", FLOWCONFIG_FILENAME).exec()
@@ -74,6 +76,7 @@ def _setup_flow_remote():
) )
remote = None remote = None
remote_type = None
if confirm("¿Configurar repositorio remoto?"): if confirm("¿Configurar repositorio remoto?"):
remotes = Git("remote").lines(print="Listando remotos disponibles") remotes = Git("remote").lines(print="Listando remotos disponibles")
@@ -93,7 +96,9 @@ def _setup_flow_remote():
info("Tiene más de un remoto, seleccione el que va a utilizar.") info("Tiene más de un remoto, seleccione el que va a utilizar.")
remote = choice("Remoto", remotes) remote = choice("Remoto", remotes)
return remote remote_type = choice("Tipo de remoto:", ["bitbucket", "github", "gitea"])
return (remote, remote_type)
def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None): def _ensure_all_flow_branches_exist(branches: list[str], remote: str | None):
+38 -35
View File
@@ -1,7 +1,7 @@
import typer import typer
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.changelog import Changelog from git_flow.changelog import Changelog
from git_flow.command.base import * import git_flow.command.base as base
from git_flow.git import Git from git_flow.git import Git
app = typer.Typer() app = typer.Typer()
@@ -10,37 +10,36 @@ app = typer.Typer()
@app.command() @app.command()
def merge(): def merge():
"""Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado""" """Realiza el merge de la rama, o crea el PR para hacerlo si tiene un remoto configurado"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
environments = flowconfig["flow.branches"].split(",")
if branch in environments: if branch in base.environments:
if branch == environments[-1]: if branch == base.environments[-1]:
raise GitFlowError("No se puede hacer un merge del último entorno.") raise GitFlowError("No se puede hacer un merge del último entorno.")
panel( base.io_info(
"Merge de entornos",
"""Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""", """Está por hacer un merge de 2 entornos que puede implicar muchos cambios en el proyecto.""",
"Merge de entornos",
) )
if not confirm("¿Desea continuar?"): if not base.io_confirm("¿Desea continuar?"):
raise GitFlowError("Ejecución abortada") raise GitFlowError("Ejecución abortada")
target = environments[environments.index(branch) + 1] target = base.environments[base.environments.index(branch) + 1]
else: else:
target, _ = get_branch_env_and_type(branch) target, _ = base.get_branch_env_and_type(branch)
if "flow.remote" in flowconfig: if "flow.remote" in base.flowconfig:
remote = flowconfig["flow.remote"] remote = base.flowconfig["flow.remote"]
token = ensure_repository_token() token = base.ensure_repository_token()
run_remote(remote, token, branch, target) run_remote(remote, token, branch, target)
else: else:
run_local(branch, target) run_local(branch, target)
def run_remote(remote: str, token: str, branch: str, target: str): def run_remote(remote: str, token: str, branch: str, target: str):
ensure_clean_worktree(True) base.ensure_clean_worktree(True)
Git("switch", target).exec(print="Cambiando a rama destino") Git("switch", target).exec(print="Cambiando a rama destino")
Git("pull").exec(print="Obteniendo ultimos cambios del remoto") Git("pull").exec(print="Obteniendo ultimos cambios del remoto")
@@ -49,20 +48,21 @@ def run_remote(remote: str, token: str, branch: str, target: str):
check_merge_conflicts(target) check_merge_conflicts(target)
show_commits_to_merge(target) show_commits_to_merge(target)
if confirm(f"¿Crear PR de '{branch}' a '{target}'?"): if base.io_confirm(f"¿Crear PR de '{branch}' a '{target}'?"):
Git("push", remote, branch, set_upstream=True).exec( if branch not in base.environments:
print="Subiendo rama al remoto para crear PR" Git("push", remote, branch, set_upstream=True).exec(
) print="Subiendo rama al remoto para crear PR"
)
create_pull_request(token, branch, target) create_pull_request(token, branch, target)
def run_local(branch: str, target: str): def run_local(branch: str, target: str):
ensure_clean_worktree(False) base.ensure_clean_worktree(False)
check_merge_conflicts(target) check_merge_conflicts(target)
show_commits_to_merge(target) show_commits_to_merge(target)
if confirm(f"¿Mergear rama '{branch}' a '{target}'?"): if base.io_confirm(f"¿Mergear rama '{branch}' a '{target}'?"):
Git("switch", target).exec(print="Cambiando a rama destino") Git("switch", target).exec(print="Cambiando a rama destino")
Git("merge", branch, ff=False).exec(print="Mergeando") Git("merge", branch, ff=False).exec(print="Mergeando")
@@ -87,13 +87,11 @@ def check_merge_conflicts(target: str):
Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False) Git("merge", abort=True).exec(print="Abortando merge de prueba", check=False)
if merge_conflicts: if merge_conflicts:
error(f"La rama actual tiene conflictos con {target}") base.io_error(f"La rama actual tiene conflictos con {target}.\n"
info( f"Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando", "Conflictos de merge")
f"""Se recomienda mergear {target} a la rama actual, resolver los conflictos localmente, y ejecutar nuevamente este comando"""
)
raise GitFlowError("Ejecución abortada.") raise GitFlowError("Ejecución abortada.")
else: else:
success("No se detectaron merge conflicts.") base.io_success("No se detectaron merge conflicts.")
def create_pull_request(token: str, branch: str, target: str): def create_pull_request(token: str, branch: str, target: str):
@@ -101,25 +99,30 @@ def create_pull_request(token: str, branch: str, target: str):
commits = Git("log", target + "..", format="%s").lines() commits = Git("log", target + "..", format="%s").lines()
title = commits[0] if len(commits) == 1 else _get_pr_title_from_branch(branch) title = commits[0] if len(commits) == 1 else _get_pr_title_from_branch(branch)
panel("Título del PR", title) base.panel("Título del PR", title)
if confirm("¿Desea cambiar el título del PR?", False): if base.io_confirm("¿Desea cambiar el título del PR?", False):
title = prompt("Título del PR") title = base.io_prompt("Título del PR", persistent=True)
message = get_remote_api(token).create_pull_request( message = base.get_remote_api(token).create_pull_request(
branch, branch,
target, target,
title, title,
changelog.generate_content(target, branch), changelog.generate_content(target, branch),
branch not in base.environments
) )
success(message) base.io_success(message, "Rama creada")
if confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"): if base.io_confirm("¿Desea cambiar a la rama objetivo y bajar los cambios?"):
Git("switch", target).exec(print="Cambiando a rama objetivo") Git("switch", target).exec(print="Cambiando a rama objetivo")
Git("pull").exec(print="Obteniendo cambios") Git("pull").exec(print="Obteniendo cambios")
def _get_pr_title_from_branch(branch: str) -> str: def _get_pr_title_from_branch(branch: str) -> str:
if branch in base.environments:
target = base.environments[base.environments.index(branch) + 1]
return f"Sincronización de entorno {branch} a {target}"
components = branch.split("/") # <type>/<desc> or release/<env>/<type>/<desc> components = branch.split("/") # <type>/<desc> or release/<env>/<type>/<desc>
branch_type = components[-2] branch_type = components[-2]
branch_desc = components[-1] branch_desc = components[-1]
@@ -131,13 +134,13 @@ def _get_pr_title_from_branch(branch: str) -> str:
if first_dash >= 0 and second_dash >= 0: if first_dash >= 0 and second_dash >= 0:
maybe_ticket = branch_desc[:second_dash] maybe_ticket = branch_desc[:second_dash]
if is_valid_ticket(maybe_ticket): if base.is_valid_ticket(maybe_ticket):
branch_ticket = maybe_ticket branch_ticket = maybe_ticket
branch_desc = branch_desc[:second_dash+1] branch_desc = branch_desc[second_dash+1:]
return " ".join(filter(None, [ return " ".join(filter(None, [
f"[Pasaje a {components[1]}]" if len(components) == 4 else None, f"[Pasaje a {components[1]}]" if len(components) == 4 else None,
branch_type + ":", branch_type + ":",
branch_ticket, branch_ticket,
branch_desc.replace("-", " ") branch_desc.replace("-", " ")
])) ]))
+18 -26
View File
@@ -1,6 +1,6 @@
import typer import typer
from git_flow import BRANCH_TYPES from git_flow import BRANCH_TYPES
from git_flow.command.base import * import git_flow.command.base as base
from git_flow.git import Git from git_flow.git import Git
from typing import Optional from typing import Optional
@@ -11,53 +11,45 @@ app = typer.Typer()
def new(): def new():
"""Crea una nueva rama siguiendo Conventional Branches""" """Crea una nueva rama siguiendo Conventional Branches"""
ensure_initialized() base.ensure_initialized()
branch = ensure_right_branch() branch = base.ensure_right_branch()
envs = flowconfig["flow.branches"].split(",")
if branch not in envs: if branch not in base.environments:
warning("La rama actual no corresponde a un entorno configurado.") base.io_warning("La rama actual no corresponde a un entorno configurado.")
info("Se creará una nueva rama de trabajo sobre la rama actual.") base.io_info("Se creará una nueva rama de trabajo sobre la rama actual.\n"
info("Debe seleccionar el tipo de cambio a realizar.") "Debe seleccionar el tipo de cambio a realizar.", title="Tipo de cambio")
branch_type = choice("Tipos de cambio", BRANCH_TYPES) branch_type = base.io_choice("Tipos de cambio", BRANCH_TYPES)
ticket = _get_optional_ticket() ticket = _get_optional_ticket()
new_branch = _get_unique_branch_name(branch_type, ticket) new_branch = _get_unique_branch_name(branch_type, ticket)
if "flow.remote" in flowconfig and Git.get_tracking_branch(branch): if "flow.remote" in base.flowconfig and Git.get_tracking_branch(branch):
Git("pull").exec(print="Actualizando rama actual") Git("pull").exec(print="Actualizando rama actual")
if confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"): if base.io_confirm(f"¿Crear rama '{new_branch}' sobre la rama actual?"):
Git("switch", new_branch, create=True).exec(print="Creando nueva rama") Git("switch", new_branch, create=True).exec(print="Creando nueva rama")
def _get_optional_ticket() -> Optional[str]: def _get_optional_ticket() -> Optional[str]:
ticket = prompt("[Opcional] Ticket que respalda el cambio (en formato ABC-123)") return base.io_prompt(
"Ticket que respalda el cambio (en formato ABC-123)",
if not ticket: validator=lambda x: len(x.strip()) == 0 or base.is_valid_ticket(x)
return None )
if not is_valid_ticket(ticket):
warning("Formato de ticket inválido, ignorando.")
return None
return ticket
def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str: def _get_unique_branch_name(branch_type: str, ticket: Optional[str]) -> str:
info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.") base.io_info(f"Ingrese las palabras clave que describan el cambio de tipo '{branch_type}'.\n"
"Las mismas se utilizaran para generar el nombre de la rama.", title="Nombre de rama")
branch = None branch = None
branches = Git.get_branches() branches = Git.get_branches()
while branch is None: while branch is None:
keywords = prompt( keywords = base.io_prompt("Palabras clave", persistent=True)
"Palabras clave del cambio (por ejemplo: 'create worker form')",
persistent=True,
)
keywords = filter(lambda k: len(k) > 0, keywords.split(" ")) keywords = filter(lambda k: len(k) > 0, keywords.split(" "))
branch = branch_type + "/" + (ticket + "-" if ticket else "") + "-".join(keywords) branch = branch_type + "/" + (ticket + "-" if ticket else "") + "-".join(keywords)
if branch in branches: if branch in branches:
error( base.io_error(
f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra" f"La rama '{branch}' ya existe, cambie las palabras claves para generar otra"
) )
branch = None branch = None
+2 -3
View File
@@ -13,10 +13,9 @@ def release(group: Optional[str] = None):
ensure_initialized() ensure_initialized()
branch = ensure_right_branch() branch = ensure_right_branch()
envs = flowconfig["flow.branches"].split(",")
env, _ = get_branch_env_and_type(branch) env, _ = get_branch_env_and_type(branch)
if env == envs[-1]: if env == environments[-1]:
raise GitFlowError( raise GitFlowError(
"No se puede hacer release de una rama en el ultimo entorno." "No se puede hacer release de una rama en el ultimo entorno."
) )
@@ -25,7 +24,7 @@ def release(group: Optional[str] = None):
ensure_clean_worktree(has_remote) ensure_clean_worktree(has_remote)
next_env = envs[envs.index(env) + 1] next_env = environments[environments.index(env) + 1]
next_branch = ( next_branch = (
branch.replace(f"/{env}/", f"/{next_env}/") branch.replace(f"/{env}/", f"/{next_env}/")
if branch.startswith("release/") if branch.startswith("release/")
+1 -2
View File
@@ -20,10 +20,9 @@ def tag(token: Optional[str] = None):
"""Crea un nuevo tag para el último merge.""" """Crea un nuevo tag para el último merge."""
ensure_initialized() ensure_initialized()
envs = flowconfig["flow.branches"].split(",")
target = Git.get_current_branch() target = Git.get_current_branch()
if target not in envs: if target not in environments:
raise GitFlowError("Solo se pueden taggear commits en ramas principales.") raise GitFlowError("Solo se pueden taggear commits en ramas principales.")
branch = Git( branch = Git(
+4 -14
View File
@@ -140,17 +140,7 @@ class Git:
return process return process
def _run(self, print: str | None = None, check: bool = True): def _run(self, print: str | None = None, check: bool = True):
process = subprocess.run( return self._get(print, check)
self.command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
if print is not None:
self._print_process("", "", title=print)
if check:
process.check_returncode()
return process
def _print_process(self, stdout: str, stderr: str, title: str): def _print_process(self, stdout: str, stderr: str, title: str):
command = [] command = []
@@ -164,12 +154,12 @@ class Git:
command.append(arg) command.append(arg)
text = f"[yellow]$ {" ".join(command)}[/yellow]" text = f"[green]$ {" ".join(command)}[/green]"
for line in stdout.splitlines(): for line in stdout.splitlines():
text += "\n\\[out] " + line text += "\n" + line
for line in stderr.splitlines(): for line in stderr.splitlines():
text += "\n[red]\\[err][/red] " + line text += "\n[red]" + line + "[/red]"
panel(title or "Ejecutando", text) panel(title or "Ejecutando", text)
+7 -7
View File
@@ -3,7 +3,7 @@
import typer import typer
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.command.base import * import git_flow.command.base as base
from git_flow.command import init, new, commit, merge, tag, release, branch from git_flow.command import init, new, commit, merge, tag, release, branch
REPOSITORY_TOKEN_PATH = ".repository-token" REPOSITORY_TOKEN_PATH = ".repository-token"
@@ -23,10 +23,10 @@ app.add_typer(branch.app)
def main(): def main():
try: try:
app() app()
except GitFlowError as e:
error(str(e))
except Exception as e:
error("Ocurrió un error inesperado: " + str(e))
except KeyboardInterrupt as e:
print() print()
error("Ejecución abortada") except GitFlowError as e:
base.io_error(str(e), title="Git Flow")
except Exception as e:
base.io_error(str(e), title="Inesperado")
except KeyboardInterrupt as e:
base.io_error("Ejecución abortada")
+11 -28
View File
@@ -12,42 +12,24 @@ class RemoteAPI(ABC):
@staticmethod @staticmethod
def parse(remote: str): def parse(remote: str):
url = Git("remote", "get-url", remote).firstline() url = Git("remote", "get-url", remote).firstline()
invalid_url_error = GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
url_type = None
if url.startswith("git@"): if url.startswith("git@"):
url_type = "ssh"
elif url.startswith("http://") or url.startswith("https://"):
url_type = "https"
else:
raise invalid_url_error
if url_type == "ssh":
# git@host:user/repo[.git]
at_pos = url.index("@") at_pos = url.index("@")
colon_pos = url.index(":") colon_pos = url.index(":")
schema = "ssh://"
host = url[at_pos+1:colon_pos] host = url[at_pos+1:colon_pos]
repository = url[colon_pos+1:] repository = url[colon_pos+1:]
else: elif url.startswith("http://") or url.startswith("https://"):
# https://host/user/repo[.git] start_pos = url.index("://") + 3
host_start = url.index("://") + 3 uri_start = url.index("/", start_pos)
uri_start = url.index("/", host_start) schema = url[:start_pos]
host = url[host_start:uri_start] host = url[start_pos:uri_start]
repository = url[uri_start+1:] repository = url[uri_start+1:]
else:
raise GitFlowError(f"La URL del remoto {remote} es inválida: {url}")
# Esto permite configurar hosts "imaginarios" para permitir el uso de multiples claves ssh repository = repository.removesuffix(".git")
if host.endswith(".bitbucket.org"): return [schema, host, repository]
host = "bitbucket.org"
elif host.endswith(".github.com"):
host = "github.com"
if host not in SUPPORTED_REMOTE_APIS:
raise GitFlowError("El host del repositorio remoto es inválido")
repository.removesuffix(".git")
return [host, repository]
@abstractmethod @abstractmethod
def create_pull_request( def create_pull_request(
@@ -56,6 +38,7 @@ class RemoteAPI(ABC):
destination: str, destination: str,
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
close_source_branch: bool = True
) -> str: ) -> str:
pass pass
+2 -1
View File
@@ -12,6 +12,7 @@ class BitbucketRemoteAPI(RemoteAPI):
destination: str, destination: str,
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
close_source_branch: bool = True
): ):
request = requests.post( request = requests.post(
self.get_endpoint("/pullrequests"), self.get_endpoint("/pullrequests"),
@@ -21,7 +22,7 @@ class BitbucketRemoteAPI(RemoteAPI):
"description": description, "description": description,
"source": {"branch": {"name": source}}, "source": {"branch": {"name": source}},
"destination": {"branch": {"name": destination}}, "destination": {"branch": {"name": destination}},
"close_source_branch": True, "close_source_branch": close_source_branch,
}, },
) )
+55
View File
@@ -0,0 +1,55 @@
import requests
from git_flow import GitFlowError
from git_flow.remote.base import RemoteAPI
class GiteaRemoteAPI(RemoteAPI):
def __init__(self, base_url: str, repository: str, token: str) -> None:
super().__init__(repository, token)
self.base_url = base_url
def create_pull_request(
self,
source: str,
destination: str,
title: str | None = None,
description: str | None = None,
close_source_branch: bool = True
) -> str:
response = requests.post(
self.get_endpoint("/pulls"),
headers=self.get_headers(),
json={
"title": title,
"body": description,
"head": source,
"base": destination,
},
)
if response.ok:
return "PR creado exitosamente: " + response.json()["html_url"]
else:
raise GitFlowError("Ocurrió un error al crear el PR: " + response.text)
def create_tag(self, tag: str, commit: str) -> str:
response = requests.post(
self.get_endpoint("/tags"),
headers=self.get_headers(),
json={"tag_name": tag, "target": commit},
)
if response.ok:
return "Tag creado exitosamente: " + tag
else:
raise GitFlowError("Ocurrió un error al crear el tag: " + response.text)
def get_endpoint(self, resource: str) -> str:
return f"{self.base_url}/api/v1/repos/{self.repository}{resource}"
def get_headers(self):
return {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "token " + self.token,
}
+29 -4
View File
@@ -1,9 +1,10 @@
import requests
from git_flow import GitFlowError from git_flow import GitFlowError
from git_flow.remote.base import RemoteAPI from git_flow.remote.base import RemoteAPI
class GithubRemoteAPI(RemoteAPI): class GithubRemoteAPI(RemoteAPI):
API_ENDPOINT = "https://github.com" API_ENDPOINT = "https://api.github.com/repos"
def create_pull_request( def create_pull_request(
self, self,
@@ -11,11 +12,35 @@ class GithubRemoteAPI(RemoteAPI):
destination: str, destination: str,
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
close_source_branch: bool = True
) -> str: ) -> str:
raise GitFlowError("not implemented yet!") response = requests.post(
self.get_endpoint("/pulls"),
headers=self.get_headers(),
json={
"title": title,
"body": description,
"head": source,
"base": destination,
},
)
if response.ok:
return "PR creado exitosamente: " + response.json()["html_url"]
else:
raise GitFlowError("Ocurrió un error al crear el PR: " + response.text)
def create_tag(self, tag: str, commit: str) -> str: def create_tag(self, tag: str, commit: str) -> str:
raise GitFlowError("not implemented yet!") response = requests.post(
self.get_endpoint("/git/refs"),
headers=self.get_headers(),
json={"ref": f"refs/tags/{tag}", "sha": commit},
)
if response.ok:
return "Tag creado exitosamente: " + tag
else:
raise GitFlowError("Ocurrió un error al crear el tag: " + response.text)
def get_endpoint(self, resource: str) -> str: def get_endpoint(self, resource: str) -> str:
return GithubRemoteAPI.API_ENDPOINT return f"{GithubRemoteAPI.API_ENDPOINT}/{self.repository}{resource}"
Generated
+37
View File
@@ -70,13 +70,17 @@ wheels = [
name = "git-flow-envs" name = "git-flow-envs"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "questionary" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "questionary" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" },
{ name = "typer" }, { name = "typer" },
] ]
@@ -110,6 +114,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
] ]
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.20.0" version = "2.20.0"
@@ -119,6 +135,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
] ]
[[package]]
name = "questionary"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "prompt-toolkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.5" version = "2.32.5"
@@ -179,3 +207,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
] ]
[[package]]
name = "wcwidth"
version = "0.8.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
]