devgen-cli 0.2.9__tar.gz → 0.3.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.
- {devgen_cli-0.2.9/devgen_cli.egg-info → devgen_cli-0.3.0}/PKG-INFO +7 -7
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/modules/commit_generator.py +15 -7
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/modules/diff_builder.py +11 -10
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/modules/git_ops.py +4 -3
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/providers/base.py +43 -20
- devgen_cli-0.3.0/devgen/providers/gemini.py +25 -0
- devgen_cli-0.3.0/devgen/templates/commit/commit_message.tpl +29 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/utils.py +5 -2
- {devgen_cli-0.2.9 → devgen_cli-0.3.0/devgen_cli.egg-info}/PKG-INFO +7 -7
- devgen_cli-0.3.0/devgen_cli.egg-info/requires.txt +9 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/pyproject.toml +9 -9
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/setup.py +1 -1
- devgen_cli-0.2.9/devgen/providers/gemini.py +0 -63
- devgen_cli-0.2.9/devgen/templates/commit/commit_message.tpl +0 -14
- devgen_cli-0.2.9/devgen_cli.egg-info/requires.txt +0 -9
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/LICENSE +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/MANIFEST.in +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/README.md +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/__init__.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/ai.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/cli/__init__.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/cli/changelog.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/cli/commit.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/cli/config.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/cli/gitignore.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/cli/license.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/cli/main.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/cli/release.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/cli/setup.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/modules/__init__.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/modules/changelog_generator.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/modules/changelog_sections.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/modules/gitignore_generator.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/modules/license_generator.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/modules/release_note_generator.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/providers/__init__.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/providers/anthropic.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/providers/huggingface.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/providers/ollama.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/providers/openai.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/providers/openrouter.py +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/agpl-3.0.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/apache-2.0.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/bsd-2-clause.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/bsd-3-clause.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/bsl-1.0.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/cc0-1.0.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/epl-2.0.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/gpl-2.0.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/gpl-3.0.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/lgpl-2.1.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/mit.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/mpl-2.0.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen/templates/licenses/unlicense.json +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen_cli.egg-info/SOURCES.txt +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen_cli.egg-info/dependency_links.txt +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen_cli.egg-info/entry_points.txt +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/devgen_cli.egg-info/top_level.txt +0 -0
- {devgen_cli-0.2.9 → devgen_cli-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devgen-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A collection of developer tools
|
|
5
5
|
Home-page: https://github.com/S4NKALP/devgen
|
|
6
6
|
Author: Sankalp Tharu
|
|
@@ -20,15 +20,15 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: anthropic>=0.
|
|
24
|
-
Requires-Dist: google-genai>=
|
|
25
|
-
Requires-Dist: openai>=2.
|
|
23
|
+
Requires-Dist: anthropic>=0.116.0
|
|
24
|
+
Requires-Dist: google-genai>=2.10.0
|
|
25
|
+
Requires-Dist: openai>=2.44.0
|
|
26
26
|
Requires-Dist: pyyaml>=6.0.3
|
|
27
27
|
Requires-Dist: questionary>=2.1.1
|
|
28
|
-
Requires-Dist: requests>=2.
|
|
29
|
-
Requires-Dist: rich>=
|
|
28
|
+
Requires-Dist: requests>=2.34.2
|
|
29
|
+
Requires-Dist: rich>=15.0.0
|
|
30
30
|
Requires-Dist: toml>=0.10.2
|
|
31
|
-
Requires-Dist: typer>=0.
|
|
31
|
+
Requires-Dist: typer>=0.26.8
|
|
32
32
|
Dynamic: author
|
|
33
33
|
Dynamic: home-page
|
|
34
34
|
Dynamic: license-file
|
|
@@ -33,6 +33,8 @@ from devgen.utils import (
|
|
|
33
33
|
extract_commit_messages,
|
|
34
34
|
get_commit_dry_run_path,
|
|
35
35
|
is_file_recent,
|
|
36
|
+
is_token_limit_error,
|
|
37
|
+
format_token_limit_error,
|
|
36
38
|
load_template,
|
|
37
39
|
render_template,
|
|
38
40
|
sanitize_ai_commit_message,
|
|
@@ -241,9 +243,10 @@ class CommitEngine:
|
|
|
241
243
|
if not self._process_group(group, group_files, cache_map):
|
|
242
244
|
failed.append(group)
|
|
243
245
|
if len(groups) > 1 and i < len(groups) - 1:
|
|
244
|
-
|
|
246
|
+
if self.provider != "ollama":
|
|
247
|
+
import time
|
|
245
248
|
|
|
246
|
-
|
|
249
|
+
time.sleep(15) # Conservative 4 RPM guard for free-tier
|
|
247
250
|
return failed
|
|
248
251
|
|
|
249
252
|
def _process_group(
|
|
@@ -265,7 +268,7 @@ class CommitEngine:
|
|
|
265
268
|
return self._apply_message(group, cache[group], files)
|
|
266
269
|
|
|
267
270
|
try:
|
|
268
|
-
manifest_context = ManifestInspector.summary()
|
|
271
|
+
manifest_context = ManifestInspector.summary(cwd=self.git.cwd)
|
|
269
272
|
if manifest_context:
|
|
270
273
|
self.logger.info("Including manifest context in prompt")
|
|
271
274
|
with self.console.status(
|
|
@@ -279,10 +282,15 @@ class CommitEngine:
|
|
|
279
282
|
)
|
|
280
283
|
message = sanitize_ai_commit_message(raw)
|
|
281
284
|
except Exception as e:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
285
|
+
if is_token_limit_error(e):
|
|
286
|
+
msg = format_token_limit_error(self.provider, e, group=group)
|
|
287
|
+
self.console.print(f"[bold red]{msg}[/bold red]")
|
|
288
|
+
self.logger.error(msg)
|
|
289
|
+
else:
|
|
290
|
+
self.logger.error(
|
|
291
|
+
f"Failed to generate message for group {group!r}: {e}",
|
|
292
|
+
exc_info=True,
|
|
293
|
+
)
|
|
286
294
|
self.git.reset(files)
|
|
287
295
|
return False
|
|
288
296
|
|
|
@@ -143,13 +143,14 @@ class ManifestInspector:
|
|
|
143
143
|
"""
|
|
144
144
|
|
|
145
145
|
@staticmethod
|
|
146
|
-
def summary() -> str:
|
|
146
|
+
def summary(cwd: Optional[Path] = None) -> str:
|
|
147
|
+
cwd = cwd or get_git_root() or Path(".")
|
|
147
148
|
lines = [
|
|
148
149
|
s
|
|
149
150
|
for s in (
|
|
150
|
-
ManifestInspector._pyproject(),
|
|
151
|
-
ManifestInspector._package_json(),
|
|
152
|
-
ManifestInspector._requirements(),
|
|
151
|
+
ManifestInspector._pyproject(cwd),
|
|
152
|
+
ManifestInspector._package_json(cwd),
|
|
153
|
+
ManifestInspector._requirements(cwd),
|
|
153
154
|
)
|
|
154
155
|
if s
|
|
155
156
|
]
|
|
@@ -164,8 +165,8 @@ class ManifestInspector:
|
|
|
164
165
|
return re.split(r"[><=!~\[]", spec, 1)[0].strip()
|
|
165
166
|
|
|
166
167
|
@staticmethod
|
|
167
|
-
def _pyproject() -> Optional[str]:
|
|
168
|
-
path =
|
|
168
|
+
def _pyproject(cwd: Path) -> Optional[str]:
|
|
169
|
+
path = cwd / "pyproject.toml"
|
|
169
170
|
if not path.exists():
|
|
170
171
|
return None
|
|
171
172
|
try:
|
|
@@ -186,8 +187,8 @@ class ManifestInspector:
|
|
|
186
187
|
return f"[pyproject.toml] {' '.join(parts)}" if parts else None
|
|
187
188
|
|
|
188
189
|
@staticmethod
|
|
189
|
-
def _package_json() -> Optional[str]:
|
|
190
|
-
path =
|
|
190
|
+
def _package_json(cwd: Path) -> Optional[str]:
|
|
191
|
+
path = cwd / "package.json"
|
|
191
192
|
if not path.exists():
|
|
192
193
|
return None
|
|
193
194
|
try:
|
|
@@ -203,8 +204,8 @@ class ManifestInspector:
|
|
|
203
204
|
return f"[package.json] {' '.join(parts)}" if parts else None
|
|
204
205
|
|
|
205
206
|
@staticmethod
|
|
206
|
-
def _requirements() -> Optional[str]:
|
|
207
|
-
path =
|
|
207
|
+
def _requirements(cwd: Path) -> Optional[str]:
|
|
208
|
+
path = cwd / "requirements.txt"
|
|
208
209
|
if not path.exists():
|
|
209
210
|
return None
|
|
210
211
|
try:
|
|
@@ -91,15 +91,16 @@ class GitOperator:
|
|
|
91
91
|
[
|
|
92
92
|
"git",
|
|
93
93
|
"ls-files",
|
|
94
|
+
"-z",
|
|
94
95
|
"--deleted",
|
|
95
96
|
"--modified",
|
|
96
97
|
"--others",
|
|
97
98
|
"--exclude-standard",
|
|
98
99
|
]
|
|
99
100
|
)
|
|
100
|
-
files = {f
|
|
101
|
-
staged_out = self._run(["git", "diff", "--name-only", "--cached"])
|
|
102
|
-
files.update(f
|
|
101
|
+
files = {f for f in out.split("\0") if f}
|
|
102
|
+
staged_out = self._run(["git", "diff", "-z", "--name-only", "--cached"])
|
|
103
|
+
files.update(f for f in staged_out.split("\0") if f)
|
|
103
104
|
return sorted(files)
|
|
104
105
|
|
|
105
106
|
# ------------------------------------------------------------------ actions
|
|
@@ -30,33 +30,56 @@ class BaseProvider(ABC):
|
|
|
30
30
|
model: str | None = None,
|
|
31
31
|
**kwargs: Any,
|
|
32
32
|
) -> str:
|
|
33
|
-
"""Public entry point. Validates input, dispatches, and wraps errors.
|
|
33
|
+
"""Public entry point. Validates input, dispatches, and wraps errors."""
|
|
34
|
+
import time
|
|
34
35
|
|
|
35
|
-
Args:
|
|
36
|
-
prompt: The prompt to send to the model.
|
|
37
|
-
api_key: Provider API key. May be ``None`` for local providers
|
|
38
|
-
(see :attr:`REQUIRES_API_KEY`).
|
|
39
|
-
model: Model id. Falls back to :attr:`DEFAULT_MODEL` when ``None``.
|
|
40
|
-
**kwargs: Forwarded to :meth:`_generate`.
|
|
41
|
-
|
|
42
|
-
Returns:
|
|
43
|
-
The generated text, stripped.
|
|
44
|
-
|
|
45
|
-
Raises:
|
|
46
|
-
ValueError: If the provider requires an API key and none was given.
|
|
47
|
-
RuntimeError: For any underlying failure, with a friendly message.
|
|
48
|
-
"""
|
|
49
36
|
if self.REQUIRES_API_KEY and not api_key:
|
|
50
37
|
raise ValueError(
|
|
51
38
|
f"{self.DISPLAY_NAME} API key is missing. "
|
|
52
39
|
"Set it via `devgen setup config` or pass --api-key."
|
|
53
40
|
)
|
|
54
41
|
chosen_model = model or self.DEFAULT_MODEL
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
|
|
43
|
+
max_retries = kwargs.get("max_retries", 5)
|
|
44
|
+
base_delay = kwargs.get("retry_delay", 10)
|
|
45
|
+
|
|
46
|
+
for attempt in range(max_retries + 1):
|
|
47
|
+
try:
|
|
48
|
+
text = self._generate(prompt, api_key, chosen_model, **kwargs)
|
|
49
|
+
return (text or "").strip()
|
|
50
|
+
except Exception as e:
|
|
51
|
+
if is_token_limit_error(e):
|
|
52
|
+
self._handle_error(e)
|
|
53
|
+
if self._is_quota_error(e) and attempt < max_retries:
|
|
54
|
+
delay = base_delay * (2**attempt)
|
|
55
|
+
print(
|
|
56
|
+
f"\n[{self.DISPLAY_NAME}] Quota/Rate limit hit (429). "
|
|
57
|
+
f"Waiting {delay}s and retrying... "
|
|
58
|
+
f"(Attempt {attempt + 1}/{max_retries})"
|
|
59
|
+
)
|
|
60
|
+
time.sleep(delay)
|
|
61
|
+
continue
|
|
62
|
+
if self._is_quota_error(e) and attempt >= max_retries:
|
|
63
|
+
raise RuntimeError(
|
|
64
|
+
f"{self.DISPLAY_NAME} API quota exhausted (429) after multiple retries."
|
|
65
|
+
) from e
|
|
66
|
+
self._handle_error(e)
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _is_quota_error(error: Exception) -> bool:
|
|
71
|
+
text = str(error).upper()
|
|
72
|
+
return any(
|
|
73
|
+
marker in text
|
|
74
|
+
for marker in (
|
|
75
|
+
"429",
|
|
76
|
+
"RESOURCE_EXHAUSTED",
|
|
77
|
+
"QUOTA_EXCEEDED",
|
|
78
|
+
"THROTTLED",
|
|
79
|
+
"RATE_LIMIT_EXCEEDED",
|
|
80
|
+
"TOO_MANY_REQUESTS",
|
|
81
|
+
)
|
|
82
|
+
)
|
|
60
83
|
|
|
61
84
|
def _handle_error(self, error: Exception) -> None:
|
|
62
85
|
"""Translate an internal exception into a user-facing RuntimeError.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from google import genai
|
|
2
|
+
from google.genai import types
|
|
3
|
+
|
|
4
|
+
from devgen.providers.base import BaseProvider
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GeminiProvider(BaseProvider):
|
|
8
|
+
"""Generates content using Google's Gemini models."""
|
|
9
|
+
|
|
10
|
+
DISPLAY_NAME = "Gemini"
|
|
11
|
+
DEFAULT_MODEL = "gemini-2.5-flash"
|
|
12
|
+
|
|
13
|
+
def _generate(self, prompt, api_key, model, **kwargs):
|
|
14
|
+
client = genai.Client(api_key=api_key)
|
|
15
|
+
response = client.models.generate_content(
|
|
16
|
+
model=model,
|
|
17
|
+
contents=prompt,
|
|
18
|
+
config=types.GenerateContentConfig(
|
|
19
|
+
temperature=kwargs.get("temperature", 0.7),
|
|
20
|
+
top_p=kwargs.get("top_p", 0.95),
|
|
21
|
+
top_k=kwargs.get("top_k", 40),
|
|
22
|
+
max_output_tokens=kwargs.get("max_output_tokens", 2048),
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
return response.text or ""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
You write Conventional Commits. Output ONLY the message — no preamble, no labels, no quotes, no markdown.
|
|
2
|
+
|
|
3
|
+
Format:
|
|
4
|
+
<type>[optional scope][optional !]: <description>
|
|
5
|
+
[optional blank line]
|
|
6
|
+
[optional body]
|
|
7
|
+
[optional blank line]
|
|
8
|
+
[optional footer(s)]
|
|
9
|
+
|
|
10
|
+
types: feat fix refactor perf docs test build ci chore style
|
|
11
|
+
scope: {% if group_name and group_name != 'root' %}({{ group_name }}){% else %}optional{% endif %}{% if use_emoji %} (prefix description with one matching emoji){% endif %}
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
feat: add email notifications on new direct messages
|
|
15
|
+
feat(shopping cart): add the amazing button
|
|
16
|
+
feat!: remove ticket list endpoint
|
|
17
|
+
refers to JIRA-1337
|
|
18
|
+
BREAKING CHANGE: ticket endpoints no longer supports list all entities.
|
|
19
|
+
fix(shopping-cart): prevent order an empty shopping cart
|
|
20
|
+
fix(api): fix wrong calculation of request body checksum
|
|
21
|
+
fix: add missing parameter to service call
|
|
22
|
+
The error occurred due to <reasons>.
|
|
23
|
+
perf: decrease memory footprint for determine unique visitors by using HyperLogLog
|
|
24
|
+
build: update dependencies
|
|
25
|
+
build(release): bump version to 1.0.0
|
|
26
|
+
refactor: implement fibonacci number calculation as recursion
|
|
27
|
+
style: remove empty line
|
|
28
|
+
|
|
29
|
+
{{ diff_text }}{{ context }}
|
|
@@ -46,7 +46,7 @@ def sanitize_ai_commit_message(raw_text: str) -> str:
|
|
|
46
46
|
# - **fix: bug fix**
|
|
47
47
|
# - chore(deps)!: breaking change
|
|
48
48
|
header_pattern = re.compile(
|
|
49
|
-
r"^(\*\*)?(feat|fix|chore|refactor|docs|style|test|build|ci)(\(.*\))?!?: .*",
|
|
49
|
+
r"^(\*\*)?(feat|fix|chore|refactor|docs|style|test|build|ci|perf|revert|deps|wip)(\(.*\))?!?: .*",
|
|
50
50
|
re.IGNORECASE,
|
|
51
51
|
)
|
|
52
52
|
|
|
@@ -75,6 +75,9 @@ def sanitize_ai_commit_message(raw_text: str) -> str:
|
|
|
75
75
|
# If we hit another header or a known separator, we stop
|
|
76
76
|
if "**Sponsor**" in line:
|
|
77
77
|
break
|
|
78
|
+
# Ignore trailing markdown code block markers
|
|
79
|
+
if line.strip() in ("```", "```md", "```markdown"):
|
|
80
|
+
continue
|
|
78
81
|
cleaned_lines.append(line)
|
|
79
82
|
|
|
80
83
|
if cleaned_lines:
|
|
@@ -196,7 +199,7 @@ def get_git_staged_files() -> list[str]:
|
|
|
196
199
|
try:
|
|
197
200
|
output = run_git_command(["git", "diff", "--name-only", "--cached"])
|
|
198
201
|
return [f for f in output.splitlines() if f.strip()]
|
|
199
|
-
except subprocess.CalledProcessError:
|
|
202
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
200
203
|
return []
|
|
201
204
|
|
|
202
205
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devgen-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A collection of developer tools
|
|
5
5
|
Home-page: https://github.com/S4NKALP/devgen
|
|
6
6
|
Author: Sankalp Tharu
|
|
@@ -20,15 +20,15 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: anthropic>=0.
|
|
24
|
-
Requires-Dist: google-genai>=
|
|
25
|
-
Requires-Dist: openai>=2.
|
|
23
|
+
Requires-Dist: anthropic>=0.116.0
|
|
24
|
+
Requires-Dist: google-genai>=2.10.0
|
|
25
|
+
Requires-Dist: openai>=2.44.0
|
|
26
26
|
Requires-Dist: pyyaml>=6.0.3
|
|
27
27
|
Requires-Dist: questionary>=2.1.1
|
|
28
|
-
Requires-Dist: requests>=2.
|
|
29
|
-
Requires-Dist: rich>=
|
|
28
|
+
Requires-Dist: requests>=2.34.2
|
|
29
|
+
Requires-Dist: rich>=15.0.0
|
|
30
30
|
Requires-Dist: toml>=0.10.2
|
|
31
|
-
Requires-Dist: typer>=0.
|
|
31
|
+
Requires-Dist: typer>=0.26.8
|
|
32
32
|
Dynamic: author
|
|
33
33
|
Dynamic: home-page
|
|
34
34
|
Dynamic: license-file
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devgen-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "A collection of developer tools"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -25,15 +25,15 @@ classifiers = [
|
|
|
25
25
|
"Programming Language :: Python :: 3.10",
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
|
-
"anthropic>=0.
|
|
29
|
-
"google-genai>=
|
|
30
|
-
"openai>=2.
|
|
28
|
+
"anthropic>=0.116.0",
|
|
29
|
+
"google-genai>=2.10.0",
|
|
30
|
+
"openai>=2.44.0",
|
|
31
31
|
"pyyaml>=6.0.3",
|
|
32
32
|
"questionary>=2.1.1",
|
|
33
|
-
"requests>=2.
|
|
34
|
-
"rich>=
|
|
33
|
+
"requests>=2.34.2",
|
|
34
|
+
"rich>=15.0.0",
|
|
35
35
|
"toml>=0.10.2",
|
|
36
|
-
"typer>=0.
|
|
36
|
+
"typer>=0.26.8",
|
|
37
37
|
]
|
|
38
38
|
[project.urls]
|
|
39
39
|
Homepage = "https://github.com/S4NKALP/devgen"
|
|
@@ -49,6 +49,6 @@ include = ["devgen*"]
|
|
|
49
49
|
|
|
50
50
|
[dependency-groups]
|
|
51
51
|
dev = [
|
|
52
|
-
"pre-commit>=4.
|
|
53
|
-
"ruff>=0.
|
|
52
|
+
"pre-commit>=4.6.0",
|
|
53
|
+
"ruff>=0.15.20",
|
|
54
54
|
]
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
|
|
3
|
-
from google import genai
|
|
4
|
-
from google.genai import types
|
|
5
|
-
|
|
6
|
-
from devgen.providers.base import BaseProvider
|
|
7
|
-
from devgen.utils import is_token_limit_error
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class GeminiProvider(BaseProvider):
|
|
11
|
-
"""Generates content using Google's Gemini models."""
|
|
12
|
-
|
|
13
|
-
DISPLAY_NAME = "Gemini"
|
|
14
|
-
DEFAULT_MODEL = "gemini-2.5-flash"
|
|
15
|
-
|
|
16
|
-
def _generate(self, prompt, api_key, model, **kwargs):
|
|
17
|
-
max_retries = kwargs.get("max_retries", 5)
|
|
18
|
-
base_delay = kwargs.get("retry_delay", 10) # 10s base for 429 backoff
|
|
19
|
-
client = genai.Client(api_key=api_key)
|
|
20
|
-
|
|
21
|
-
for attempt in range(max_retries + 1):
|
|
22
|
-
try:
|
|
23
|
-
response = client.models.generate_content(
|
|
24
|
-
model=model,
|
|
25
|
-
contents=prompt,
|
|
26
|
-
config=types.GenerateContentConfig(
|
|
27
|
-
temperature=kwargs.get("temperature", 0.7),
|
|
28
|
-
top_p=kwargs.get("top_p", 0.95),
|
|
29
|
-
top_k=kwargs.get("top_k", 40),
|
|
30
|
-
max_output_tokens=kwargs.get("max_output_tokens", 2048),
|
|
31
|
-
),
|
|
32
|
-
)
|
|
33
|
-
return response.text or ""
|
|
34
|
-
except Exception as e:
|
|
35
|
-
if is_token_limit_error(e):
|
|
36
|
-
raise # base class handles with friendly message
|
|
37
|
-
if self._is_quota_error(e) and attempt < max_retries:
|
|
38
|
-
delay = base_delay * (2**attempt)
|
|
39
|
-
print(
|
|
40
|
-
f"\n[Gemini] Quota limit hit (429). This is often a temporary "
|
|
41
|
-
f"rate limit (RPM). Waiting {delay}s and retrying... "
|
|
42
|
-
f"(Attempt {attempt + 1}/{max_retries})"
|
|
43
|
-
)
|
|
44
|
-
time.sleep(delay)
|
|
45
|
-
continue
|
|
46
|
-
if self._is_quota_error(e):
|
|
47
|
-
raise RuntimeError(
|
|
48
|
-
"Gemini API quota exhausted (429) after multiple retries.\n\n"
|
|
49
|
-
"POSSIBLE CAUSES:\n"
|
|
50
|
-
"1. Rate Limit (RPM/RPD): You are sending requests too fast for the Free Tier.\n"
|
|
51
|
-
"2. Daily Limit: You have reached the maximum free requests allowed for today (typically 1,500).\n"
|
|
52
|
-
"3. Credit/Billing: If on a paid plan, your credit may be exhausted.\n\n"
|
|
53
|
-
"Please check your usage metrics at: https://aistudio.google.com/app/usage"
|
|
54
|
-
) from e
|
|
55
|
-
raise
|
|
56
|
-
|
|
57
|
-
@staticmethod
|
|
58
|
-
def _is_quota_error(error: Exception) -> bool:
|
|
59
|
-
text = str(error).upper()
|
|
60
|
-
return any(
|
|
61
|
-
marker in text
|
|
62
|
-
for marker in ("429", "RESOURCE_EXHAUSTED", "QUOTA_EXCEEDED", "THROTTLED")
|
|
63
|
-
)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
You write Conventional Commits. Output ONLY the message — no preamble, no labels, no quotes, no markdown.
|
|
2
|
-
|
|
3
|
-
Format:
|
|
4
|
-
<type>(<scope>): <summary>
|
|
5
|
-
[optional 1-2 line body on WHY]
|
|
6
|
-
|
|
7
|
-
types: feat fix refactor perf docs test build ci chore style
|
|
8
|
-
scope: {{ group_name }}{% if use_emoji %} (prefix summary with one matching emoji){% endif %}
|
|
9
|
-
|
|
10
|
-
Example:
|
|
11
|
-
feat(auth): add login rate limit
|
|
12
|
-
Prevents brute-force attacks.
|
|
13
|
-
|
|
14
|
-
{{ diff_text }}{{ context }}
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|