gitpr-cli 0.0.19__tar.gz → 0.0.20__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.
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/PKG-INFO +1 -1
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/PKG-INFO +1 -1
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/SOURCES.txt +1 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/pyproject.toml +1 -1
- gitpr_cli-0.0.20/src/ai_providers.py +77 -0
- gitpr_cli-0.0.20/src/spinner.py +177 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/updater.py +1 -1
- gitpr_cli-0.0.19/src/ai_providers.py +0 -65
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/LICENSE +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/README.md +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/dependency_links.txt +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/entry_points.txt +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/requires.txt +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/top_level.txt +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/setup.cfg +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/__init__.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/blame_engine.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/cache.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/config.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/core.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/issue_engine.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/linter_engine.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/main.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/security.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/tui_issue.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/ui/__init__.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/ui/help_screen.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/ui/issue_app.py +0 -0
- {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/tests/test_core.py +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import click
|
|
4
|
+
from google import genai
|
|
5
|
+
from openai import OpenAI
|
|
6
|
+
from src.spinner import Spinner
|
|
7
|
+
|
|
8
|
+
def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quiet=False):
|
|
9
|
+
"""
|
|
10
|
+
Motor unificado para chamadas de IA.
|
|
11
|
+
Suporta 'gemini' e 'deepseek'.
|
|
12
|
+
"""
|
|
13
|
+
max_retries = 3
|
|
14
|
+
retry_delay = 2
|
|
15
|
+
spinner = Spinner(quiet=quiet)
|
|
16
|
+
spinner.start()
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
for attempt in range(1, max_retries + 1):
|
|
20
|
+
try:
|
|
21
|
+
if provider == "gemini":
|
|
22
|
+
client = genai.Client(api_key=api_key)
|
|
23
|
+
response = client.models.generate_content(
|
|
24
|
+
model=api_model,
|
|
25
|
+
contents=prompt,
|
|
26
|
+
config={
|
|
27
|
+
"system_instruction": system_instruction,
|
|
28
|
+
"response_mime_type": "application/json",
|
|
29
|
+
"temperature": 0.0,
|
|
30
|
+
"top_p": 0.1,
|
|
31
|
+
"top_k": 1
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
result_text = response.text
|
|
35
|
+
|
|
36
|
+
elif provider == "deepseek":
|
|
37
|
+
# O DeepSeek e 100% compativel com a biblioteca da OpenAI
|
|
38
|
+
client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com")
|
|
39
|
+
response = client.chat.completions.create(
|
|
40
|
+
model=api_model, # Ex: "deepseek-chat"
|
|
41
|
+
messages=[
|
|
42
|
+
{"role": "system", "content": system_instruction},
|
|
43
|
+
{"role": "user", "content": prompt}
|
|
44
|
+
],
|
|
45
|
+
response_format={"type": "json_object"},
|
|
46
|
+
temperature=0.0
|
|
47
|
+
)
|
|
48
|
+
result_text = response.choices[0].message.content
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
spinner.stop()
|
|
52
|
+
click.secho(f"❌ Provedor de IA desconhecido: {provider}", fg="red")
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Tenta converter a resposta de texto para um dicionario JSON do Python
|
|
56
|
+
result_json = json.loads(result_text)
|
|
57
|
+
|
|
58
|
+
# 🛡️ ESCUDO: Se a IA retornar uma lista [ { ... } ] por engano
|
|
59
|
+
if isinstance(result_json, list):
|
|
60
|
+
result_json = result_json[0] if result_json else {}
|
|
61
|
+
|
|
62
|
+
spinner.stop()
|
|
63
|
+
return result_json
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
if attempt < max_retries:
|
|
67
|
+
spinner.stop()
|
|
68
|
+
click.secho(f"\r⚠️ Instabilidade na API ({provider.capitalize()}). A tentar novamente ({attempt}/{max_retries})...", fg="yellow", dim=True)
|
|
69
|
+
time.sleep(retry_delay)
|
|
70
|
+
spinner = Spinner(quiet=quiet)
|
|
71
|
+
spinner.start()
|
|
72
|
+
else:
|
|
73
|
+
spinner.stop()
|
|
74
|
+
click.secho(f"\r❌ Erro critico ao contactar a API do {provider.capitalize()} apos {max_retries} tentativas: {str(e)}", fg="red", bold=True)
|
|
75
|
+
return None
|
|
76
|
+
finally:
|
|
77
|
+
spinner.stop()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spinner animado com caracteres braille e palavras de pensamento.
|
|
3
|
+
Efeito visual: caracter braille girando (magenta) + palavra sendo "descoberta"
|
|
4
|
+
letra a letra com cores aleatorias.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import random
|
|
10
|
+
import string
|
|
11
|
+
import threading
|
|
12
|
+
import urllib.request
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from dotenv import load_dotenv, set_key
|
|
15
|
+
|
|
16
|
+
# ANSI color codes
|
|
17
|
+
MAGENTA = '\033[35m'
|
|
18
|
+
RESET = '\033[0m'
|
|
19
|
+
|
|
20
|
+
# Paleta de cores para as palavras
|
|
21
|
+
WORD_COLORS = [
|
|
22
|
+
'\033[36m', # cyan
|
|
23
|
+
'\033[33m', # yellow
|
|
24
|
+
'\033[32m', # green
|
|
25
|
+
'\033[34m', # blue
|
|
26
|
+
'\033[95m', # bright magenta
|
|
27
|
+
'\033[96m', # bright cyan
|
|
28
|
+
'\033[93m', # bright yellow
|
|
29
|
+
'\033[92m', # bright green
|
|
30
|
+
'\033[94m', # bright blue
|
|
31
|
+
'\033[91m', # red
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Caracteres braille unicode que simulam um giro
|
|
35
|
+
BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
36
|
+
|
|
37
|
+
# URL do template remoto com a lista de palavras
|
|
38
|
+
THINKING_WORDS_URL = (
|
|
39
|
+
"https://raw.githubusercontent.com/natanfiuza/gitpr/"
|
|
40
|
+
"refs/heads/main/templates/gitpr.thinking-words.md"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Fallback interno caso o download falhe
|
|
44
|
+
_FALLBACK_WORDS = [
|
|
45
|
+
"Fabuloso", "Pensando", "Analisando", "Raciocinando",
|
|
46
|
+
"Elaborando", "Processando", "Decifrando", "Calculando",
|
|
47
|
+
"Refletindo", "Maquinando",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def _load_thinking_words():
|
|
51
|
+
"""Carrega a lista de palavras do .env ou faz download do template remoto."""
|
|
52
|
+
env_file = str(Path.home() / ".gitpr" / ".env")
|
|
53
|
+
|
|
54
|
+
# Garante que o .env foi carregado
|
|
55
|
+
load_dotenv(env_file)
|
|
56
|
+
|
|
57
|
+
raw = os.getenv("SPINNER_THINKING_WORDS", "").strip()
|
|
58
|
+
|
|
59
|
+
if raw:
|
|
60
|
+
# .env ja tem palavras: suporta separador | ou ,
|
|
61
|
+
sep = "|" if "|" in raw else ","
|
|
62
|
+
return [w.strip() for w in raw.split(sep) if w.strip()]
|
|
63
|
+
|
|
64
|
+
# .env nao tem palavras: baixa do GitHub
|
|
65
|
+
try:
|
|
66
|
+
with urllib.request.urlopen(THINKING_WORDS_URL, timeout=10) as resp:
|
|
67
|
+
content = resp.read().decode("utf-8")
|
|
68
|
+
|
|
69
|
+
# Parse: suporta palavras separadas por virgula OU uma por linha
|
|
70
|
+
words = []
|
|
71
|
+
for line in content.splitlines():
|
|
72
|
+
line = line.strip()
|
|
73
|
+
if not line or line.startswith("#"):
|
|
74
|
+
continue
|
|
75
|
+
# Cada linha pode ter varias palavras separadas por virgula
|
|
76
|
+
for word in line.split(","):
|
|
77
|
+
word = word.strip()
|
|
78
|
+
if word:
|
|
79
|
+
words.append(word)
|
|
80
|
+
|
|
81
|
+
if words:
|
|
82
|
+
# Salva no .env como pipe-separated
|
|
83
|
+
set_key(env_file, "SPINNER_THINKING_WORDS", "|".join(words))
|
|
84
|
+
return words
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# Fallback: usa lista interna
|
|
89
|
+
return list(_FALLBACK_WORDS)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Palavras que representam "pensamento" da IA (carregadas do .env ou template remoto)
|
|
93
|
+
THINKING_WORDS = _load_thinking_words()
|
|
94
|
+
|
|
95
|
+
class Spinner:
|
|
96
|
+
"""Spinner animado que roda em background enquanto a IA processa."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, quiet=False):
|
|
99
|
+
self._thread = None
|
|
100
|
+
self._running = False
|
|
101
|
+
self._quiet = quiet
|
|
102
|
+
|
|
103
|
+
def _spin(self):
|
|
104
|
+
"""Loop principal da animacao, executado em thread separada."""
|
|
105
|
+
braille_idx = 0
|
|
106
|
+
word_idx = random.randrange(len(THINKING_WORDS))
|
|
107
|
+
word = THINKING_WORDS[word_idx]
|
|
108
|
+
word_color = random.choice(WORD_COLORS)
|
|
109
|
+
discovered = "" # Letras ja "descobertas" da palavra
|
|
110
|
+
dots_cycle = 0 # 0 = ".", 1 = "..", 2 = "..."
|
|
111
|
+
char_step = 0 # Contador de frames para revelar letras
|
|
112
|
+
chars_per_letter = 4 # Frames com caracteres aleatorios antes de revelar uma letra
|
|
113
|
+
|
|
114
|
+
while self._running:
|
|
115
|
+
braille_char = BRAILLE_FRAMES[braille_idx]
|
|
116
|
+
braille_idx = (braille_idx + 1) % len(BRAILLE_FRAMES)
|
|
117
|
+
|
|
118
|
+
# Animacao da palavra sendo "descoberta"
|
|
119
|
+
if len(discovered) < len(word):
|
|
120
|
+
char_step += 1
|
|
121
|
+
if char_step >= chars_per_letter:
|
|
122
|
+
# Revela mais uma letra da palavra
|
|
123
|
+
discovered = word[:len(discovered) + 1]
|
|
124
|
+
char_step = 0
|
|
125
|
+
else:
|
|
126
|
+
# Mostra um caractere aleatorio no lugar da proxima letra
|
|
127
|
+
fake_char = random.choice(string.ascii_uppercase + "0123456789!@#$")
|
|
128
|
+
discovered = word[:len(discovered)] + fake_char
|
|
129
|
+
|
|
130
|
+
display_word = discovered
|
|
131
|
+
else:
|
|
132
|
+
# Palavra completa: ciclo dos pontinhos
|
|
133
|
+
dots_cycle = (dots_cycle + 1) % 12
|
|
134
|
+
if dots_cycle < 4:
|
|
135
|
+
dots = "."
|
|
136
|
+
elif dots_cycle < 8:
|
|
137
|
+
dots = ".."
|
|
138
|
+
else:
|
|
139
|
+
dots = "..."
|
|
140
|
+
|
|
141
|
+
display_word = word + dots
|
|
142
|
+
|
|
143
|
+
# Troca de palavra e cor apos alguns ciclos de pontinhos
|
|
144
|
+
if dots_cycle == 0 and braille_idx == 0:
|
|
145
|
+
word_idx = (word_idx + 1) % len(THINKING_WORDS)
|
|
146
|
+
word = THINKING_WORDS[word_idx]
|
|
147
|
+
word_color = random.choice(WORD_COLORS)
|
|
148
|
+
discovered = ""
|
|
149
|
+
char_step = 0
|
|
150
|
+
|
|
151
|
+
# Monta e exibe a linha: braille magenta + palavra colorida
|
|
152
|
+
line = f"\r {MAGENTA}{braille_char}{RESET} {word_color}{display_word}{RESET}"
|
|
153
|
+
# Limpa o resto da linha com espacos
|
|
154
|
+
line = line.ljust(70 + len(MAGENTA) + len(RESET) * 2 + len(word_color))
|
|
155
|
+
|
|
156
|
+
if not self._quiet:
|
|
157
|
+
sys.stdout.write(line)
|
|
158
|
+
sys.stdout.flush()
|
|
159
|
+
|
|
160
|
+
time.sleep(0.08) # ~12 fps, suave
|
|
161
|
+
|
|
162
|
+
def start(self):
|
|
163
|
+
"""Inicia a animacao em background."""
|
|
164
|
+
if self._quiet:
|
|
165
|
+
return
|
|
166
|
+
self._running = True
|
|
167
|
+
self._thread = threading.Thread(target=self._spin, daemon=True)
|
|
168
|
+
self._thread.start()
|
|
169
|
+
|
|
170
|
+
def stop(self):
|
|
171
|
+
"""Para a animacao e limpa a linha."""
|
|
172
|
+
self._running = False
|
|
173
|
+
if self._thread:
|
|
174
|
+
self._thread.join(timeout=0.5)
|
|
175
|
+
if not self._quiet:
|
|
176
|
+
sys.stdout.write("\r" + " " * 70 + "\r")
|
|
177
|
+
sys.stdout.flush()
|
|
@@ -7,7 +7,7 @@ import click
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
|
|
9
9
|
# Versão atual do seu executável local (Atualize isso a cada novo build!)
|
|
10
|
-
__version__ = "0.0.
|
|
10
|
+
__version__ = "0.0.20"
|
|
11
11
|
GITHUB_API_URL = "https://api.github.com/repos/natanfiuza/gitpr/releases/latest"
|
|
12
12
|
PYPI_API_URL = "https://pypi.org/pypi/gitpr-cli/json"
|
|
13
13
|
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import time
|
|
3
|
-
import click
|
|
4
|
-
from google import genai
|
|
5
|
-
from openai import OpenAI
|
|
6
|
-
|
|
7
|
-
def call_ai_model(provider, api_key, api_model, prompt, system_instruction):
|
|
8
|
-
"""
|
|
9
|
-
Motor unificado para chamadas de IA.
|
|
10
|
-
Suporta 'gemini' e 'deepseek'.
|
|
11
|
-
"""
|
|
12
|
-
max_retries = 3
|
|
13
|
-
retry_delay = 2
|
|
14
|
-
|
|
15
|
-
for attempt in range(1, max_retries + 1):
|
|
16
|
-
try:
|
|
17
|
-
if provider == "gemini":
|
|
18
|
-
client = genai.Client(api_key=api_key)
|
|
19
|
-
response = client.models.generate_content(
|
|
20
|
-
model=api_model,
|
|
21
|
-
contents=prompt,
|
|
22
|
-
config={
|
|
23
|
-
"system_instruction": system_instruction,
|
|
24
|
-
"response_mime_type": "application/json",
|
|
25
|
-
"temperature": 0.0,
|
|
26
|
-
"top_p": 0.1,
|
|
27
|
-
"top_k": 1
|
|
28
|
-
}
|
|
29
|
-
)
|
|
30
|
-
result_text = response.text
|
|
31
|
-
|
|
32
|
-
elif provider == "deepseek":
|
|
33
|
-
# O DeepSeek é 100% compatível com a biblioteca da OpenAI
|
|
34
|
-
client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com")
|
|
35
|
-
response = client.chat.completions.create(
|
|
36
|
-
model=api_model, # Ex: "deepseek-chat"
|
|
37
|
-
messages=[
|
|
38
|
-
{"role": "system", "content": system_instruction},
|
|
39
|
-
{"role": "user", "content": prompt}
|
|
40
|
-
],
|
|
41
|
-
response_format={"type": "json_object"},
|
|
42
|
-
temperature=0.0
|
|
43
|
-
)
|
|
44
|
-
result_text = response.choices[0].message.content
|
|
45
|
-
|
|
46
|
-
else:
|
|
47
|
-
click.secho(f"❌ Provedor de IA desconhecido: {provider}", fg="red")
|
|
48
|
-
return None
|
|
49
|
-
|
|
50
|
-
# Tenta converter a resposta de texto para um dicionário JSON do Python
|
|
51
|
-
result_json = json.loads(result_text)
|
|
52
|
-
|
|
53
|
-
# 🛡️ ESCUDO: Se a IA retornar uma lista [ { ... } ] por engano
|
|
54
|
-
if isinstance(result_json, list):
|
|
55
|
-
result_json = result_json[0] if result_json else {}
|
|
56
|
-
|
|
57
|
-
return result_json
|
|
58
|
-
|
|
59
|
-
except Exception as e:
|
|
60
|
-
if attempt < max_retries:
|
|
61
|
-
click.secho(f"⚠️ Instabilidade na API ({provider.capitalize()}). A tentar novamente ({attempt}/{max_retries})...", fg="yellow", dim=True)
|
|
62
|
-
time.sleep(retry_delay)
|
|
63
|
-
else:
|
|
64
|
-
click.secho(f"❌ Erro crítico ao contactar a API do {provider.capitalize()} após {max_retries} tentativas: {str(e)}", fg="red", bold=True)
|
|
65
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|