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.
Files changed (29) hide show
  1. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/PKG-INFO +1 -1
  2. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/PKG-INFO +1 -1
  3. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/SOURCES.txt +1 -0
  4. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/pyproject.toml +1 -1
  5. gitpr_cli-0.0.20/src/ai_providers.py +77 -0
  6. gitpr_cli-0.0.20/src/spinner.py +177 -0
  7. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/updater.py +1 -1
  8. gitpr_cli-0.0.19/src/ai_providers.py +0 -65
  9. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/LICENSE +0 -0
  10. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/README.md +0 -0
  11. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/dependency_links.txt +0 -0
  12. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/entry_points.txt +0 -0
  13. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/requires.txt +0 -0
  14. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/gitpr_cli.egg-info/top_level.txt +0 -0
  15. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/setup.cfg +0 -0
  16. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/__init__.py +0 -0
  17. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/blame_engine.py +0 -0
  18. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/cache.py +0 -0
  19. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/config.py +0 -0
  20. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/core.py +0 -0
  21. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/issue_engine.py +0 -0
  22. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/linter_engine.py +0 -0
  23. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/main.py +0 -0
  24. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/security.py +0 -0
  25. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/tui_issue.py +0 -0
  26. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/ui/__init__.py +0 -0
  27. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/ui/help_screen.py +0 -0
  28. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/src/ui/issue_app.py +0 -0
  29. {gitpr_cli-0.0.19 → gitpr_cli-0.0.20}/tests/test_core.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 0.0.19
3
+ Version: 0.0.20
4
4
  Summary: Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)
5
5
  Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 0.0.19
3
+ Version: 0.0.20
4
4
  Summary: Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)
5
5
  Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
6
6
  Requires-Python: >=3.10
@@ -17,6 +17,7 @@ src/issue_engine.py
17
17
  src/linter_engine.py
18
18
  src/main.py
19
19
  src/security.py
20
+ src/spinner.py
20
21
  src/tui_issue.py
21
22
  src/updater.py
22
23
  src/ui/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitpr-cli"
7
- version = "0.0.19"
7
+ version = "0.0.20"
8
8
  description = "Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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.19"
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