gptcomet 0.0.2__py3-none-any.whl
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.
- gptcomet/__init__.py +6 -0
- gptcomet/_types.py +24 -0
- gptcomet/cli.py +300 -0
- gptcomet/cmd.py +0 -0
- gptcomet/config_manager.py +296 -0
- gptcomet/const.py +37 -0
- gptcomet/exceptions.py +61 -0
- gptcomet/gptcomet.toml +113 -0
- gptcomet/hook.py +59 -0
- gptcomet/llm_client.py +130 -0
- gptcomet/message_generator.py +167 -0
- gptcomet/prepare-commit-msg +13 -0
- gptcomet/support_keys.py +17 -0
- gptcomet/utils.py +68 -0
- gptcomet-0.0.2.dist-info/METADATA +146 -0
- gptcomet-0.0.2.dist-info/RECORD +19 -0
- gptcomet-0.0.2.dist-info/WHEEL +4 -0
- gptcomet-0.0.2.dist-info/entry_points.txt +3 -0
- gptcomet-0.0.2.dist-info/licenses/LICENSE +21 -0
gptcomet/__init__.py
ADDED
gptcomet/_types.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
import tomlkit
|
|
4
|
+
|
|
5
|
+
# Config class types
|
|
6
|
+
CacheKeys = t.Literal["default_config", "config"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CacheType(t.TypedDict):
|
|
10
|
+
default_config: t.Optional[tomlkit.TOMLDocument]
|
|
11
|
+
config: t.Optional[tomlkit.TOMLDocument]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CompleteParams(t.TypedDict):
|
|
15
|
+
api_base: str
|
|
16
|
+
api_key: str
|
|
17
|
+
model: str
|
|
18
|
+
retries: int
|
|
19
|
+
messages: t.Union[list[dict], None, list]
|
|
20
|
+
max_tokens: t.Optional[int]
|
|
21
|
+
temperature: t.Optional[float]
|
|
22
|
+
top_p: t.Optional[float]
|
|
23
|
+
frequency_penalty: t.Optional[float]
|
|
24
|
+
extra_headers: t.Optional[dict]
|
gptcomet/cli.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from click import Context
|
|
6
|
+
from git import InvalidGitRepositoryError, NoSuchPathError
|
|
7
|
+
|
|
8
|
+
import gptcomet
|
|
9
|
+
from gptcomet.config_manager import ConfigManager
|
|
10
|
+
from gptcomet.exceptions import ConfigKeyError, GitNoStagedChanges, KeyNotFound
|
|
11
|
+
from gptcomet.hook import AICommitHook
|
|
12
|
+
from gptcomet.message_generator import MessageGenerator
|
|
13
|
+
from gptcomet.utils import common_options
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
format="%(asctime)s [%(levelname)s] %(filename)s::%(funcName)s: %(message)s",
|
|
18
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
19
|
+
level=logging.WARNING,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group(
|
|
24
|
+
name="gptcomet",
|
|
25
|
+
help="AI-Powered Git Commit Message Generator",
|
|
26
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
27
|
+
)
|
|
28
|
+
@click.pass_context
|
|
29
|
+
@common_options
|
|
30
|
+
@click.version_option(
|
|
31
|
+
gptcomet.__version__,
|
|
32
|
+
"--version",
|
|
33
|
+
"-v",
|
|
34
|
+
prog_name="gptcomet",
|
|
35
|
+
message="%(prog)s: %(version)s",
|
|
36
|
+
)
|
|
37
|
+
def cli(ctx: Context, debug, local):
|
|
38
|
+
"""GPTComet CLI"""
|
|
39
|
+
if debug:
|
|
40
|
+
logger.setLevel(logging.DEBUG)
|
|
41
|
+
else:
|
|
42
|
+
logger.setLevel(logging.INFO)
|
|
43
|
+
logger.debug(f"Running with debug={debug}, local={local}")
|
|
44
|
+
|
|
45
|
+
ctx.obj["debug"] = debug
|
|
46
|
+
ctx.obj["local"] = local
|
|
47
|
+
ctx.obj["config_manager"] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
config: click.Group # type hints for IDE
|
|
51
|
+
|
|
52
|
+
@cli.group(name="config")
|
|
53
|
+
@click.pass_context
|
|
54
|
+
@common_options
|
|
55
|
+
def config(ctx, debug, local):
|
|
56
|
+
"""Manage gptcomet configuration."""
|
|
57
|
+
logger.debug(f"Config manage, local={local}")
|
|
58
|
+
if not ctx.obj["config_manager"]:
|
|
59
|
+
ctx.obj["config_manager"] = ConfigManager(ctx.obj["local"])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@config.command("set", help="Set a configuration value.")
|
|
63
|
+
@click.argument("key")
|
|
64
|
+
@click.argument("value")
|
|
65
|
+
@click.pass_context
|
|
66
|
+
@common_options
|
|
67
|
+
def config_set(ctx: Context, key, value, debug, local):
|
|
68
|
+
"""Set a configuration value."""
|
|
69
|
+
try:
|
|
70
|
+
ctx.obj["config_manager"].set(key, value)
|
|
71
|
+
click.echo(
|
|
72
|
+
f"[GPTComet] Set {click.style(key, fg='green')} to {click.style(value, fg='green')}."
|
|
73
|
+
)
|
|
74
|
+
except ConfigKeyError as e:
|
|
75
|
+
click.echo(f"[GPTComet] Error: {e!s}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@config.command("get")
|
|
79
|
+
@click.argument("key")
|
|
80
|
+
@click.pass_context
|
|
81
|
+
@common_options
|
|
82
|
+
def config_get(ctx, key, debug, local):
|
|
83
|
+
"""Get a configuration value."""
|
|
84
|
+
try:
|
|
85
|
+
value = ctx.obj["config_manager"].get(key)
|
|
86
|
+
click.echo(f"{key}: {value}")
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
click.echo(f"Error: {e!s}")
|
|
89
|
+
except ConfigKeyError as e:
|
|
90
|
+
click.echo(str(e))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@config.command("list")
|
|
94
|
+
@click.pass_context
|
|
95
|
+
@common_options
|
|
96
|
+
def config_list(ctx: Context, debug, local):
|
|
97
|
+
"""List all configuration values."""
|
|
98
|
+
click.echo(
|
|
99
|
+
click.style("Current configuration:\n", fg="green") + ctx.obj["config_manager"].list()
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@config.command("reset")
|
|
104
|
+
@click.pass_context
|
|
105
|
+
@common_options
|
|
106
|
+
def reset(ctx: Context, debug, local):
|
|
107
|
+
"""Reset configuration to default values."""
|
|
108
|
+
ctx.obj["config_manager"].reset()
|
|
109
|
+
click.echo("Configuration reset to default values")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@config.command(name="keys")
|
|
113
|
+
@click.pass_context
|
|
114
|
+
@common_options
|
|
115
|
+
def config_keys(ctx, debug, local):
|
|
116
|
+
cfg_manager: ConfigManager = ctx.obj["config_manager"]
|
|
117
|
+
keys = cfg_manager.list_keys()
|
|
118
|
+
click.echo(click.style("Supported keys:\n", fg="green") + keys)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@config.command("path")
|
|
122
|
+
@click.pass_context
|
|
123
|
+
@common_options
|
|
124
|
+
def config_path(ctx: Context, debug, local):
|
|
125
|
+
"""Get the path to the configuration file."""
|
|
126
|
+
click.echo(
|
|
127
|
+
click.style("Current configuration path:\n", fg="green")
|
|
128
|
+
+ ctx.obj["config_manager"].current_config_path.as_posix()
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
hook: click.Group # type hints for IDE
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@cli.group("hook", help="Manage GPTComet prepare-commit-msg hook.")
|
|
136
|
+
@common_options
|
|
137
|
+
@click.pass_context
|
|
138
|
+
def hook(ctx: click.Context, debug, local):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@hook.command("install", help="Install GPTComet prepare-commit-msg hook to current repository.")
|
|
143
|
+
@click.option(
|
|
144
|
+
"--force/--no-force",
|
|
145
|
+
"-f/-nf",
|
|
146
|
+
default=False,
|
|
147
|
+
help="Force installation or not.",
|
|
148
|
+
show_default=True,
|
|
149
|
+
)
|
|
150
|
+
@common_options
|
|
151
|
+
@click.pass_context
|
|
152
|
+
def install_hook(ctx, debug, local, force=False):
|
|
153
|
+
"""Install GPTComet prepare-commit-msg hook."""
|
|
154
|
+
try:
|
|
155
|
+
comet_hook = AICommitHook()
|
|
156
|
+
if comet_hook.is_hook_installed() and not force:
|
|
157
|
+
click.echo(
|
|
158
|
+
"GPTComet prepare-commit-msg hook is already installed, use --force to force installation."
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
comet_hook.install_hook()
|
|
162
|
+
click.echo("GPTComet prepare-commit-msg hook has been installed successfully.")
|
|
163
|
+
except InvalidGitRepositoryError as e:
|
|
164
|
+
click.echo(f"Error: {e!s}")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
click.echo(f"An error occurred while installing the hook: {e!s}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@hook.command("uninstall", help="Uninstall GPTComet prepare-commit-msg hook from current repository.")
|
|
170
|
+
@common_options
|
|
171
|
+
@click.pass_context
|
|
172
|
+
def uninstall_hook(ctx: click.Context, debug, local, **kwargs):
|
|
173
|
+
"""Uninstall GPTComet prepare-commit-msg hook."""
|
|
174
|
+
try:
|
|
175
|
+
comet_hook = AICommitHook()
|
|
176
|
+
comet_hook.uninstall_hook()
|
|
177
|
+
except (InvalidGitRepositoryError, NoSuchPathError) as e:
|
|
178
|
+
click.echo(f"Error: {e!s}")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
click.echo(f"An error occurred while uninstalling the hook: {e!s}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@hook.command("status")
|
|
184
|
+
def hook_status():
|
|
185
|
+
"""Check if GPTComet prepare-commit-msg hook is installed."""
|
|
186
|
+
try:
|
|
187
|
+
comet_hook = AICommitHook()
|
|
188
|
+
if comet_hook.is_hook_installed():
|
|
189
|
+
click.echo("GPTComet prepare-commit-msg hook is installed.")
|
|
190
|
+
else:
|
|
191
|
+
click.echo(
|
|
192
|
+
f"GPTComet prepare-commit-msg hook is {click.style('not', fg='yellow')} installed."
|
|
193
|
+
)
|
|
194
|
+
except InvalidGitRepositoryError as e:
|
|
195
|
+
click.echo(f"Error: {e!s}")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
click.echo(f"An error occurred while checking the hook status: {e!s}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
generate: click.Group # type hints for IDE
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@cli.group("generate", help="Generate a commit message based on `git diff --staged`.")
|
|
204
|
+
@click.pass_context
|
|
205
|
+
@common_options
|
|
206
|
+
def generate(ctx: click.Context, debug, local):
|
|
207
|
+
ctx.obj["config_manager"] = ConfigManager(ctx.obj["local"])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@generate.command("commit")
|
|
211
|
+
# @click.option("--rich", is_flag=True, default=False, help="Generate rich commit message")
|
|
212
|
+
@common_options
|
|
213
|
+
@click.pass_context
|
|
214
|
+
def generate_commit(ctx: click.Context, debug, local, rich=False, **kwargs):
|
|
215
|
+
"""Generate a commit message based on git diff"""
|
|
216
|
+
click.echo(
|
|
217
|
+
click.style(
|
|
218
|
+
"🤖 Hang tight! I'm having a chat with the AI to craft your commit message...",
|
|
219
|
+
fg="green",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
config_manager = ctx.obj["config_manager"]
|
|
223
|
+
message_generator = MessageGenerator(config_manager)
|
|
224
|
+
retry = True
|
|
225
|
+
commit_msg = None
|
|
226
|
+
while retry:
|
|
227
|
+
try:
|
|
228
|
+
commit_msg = message_generator.generate_commit_message(rich)
|
|
229
|
+
except KeyNotFound as e:
|
|
230
|
+
click.echo(f"Error: {e!s}, please check your configuration.")
|
|
231
|
+
raise click.Abort() from None
|
|
232
|
+
except GitNoStagedChanges as e:
|
|
233
|
+
click.echo(str(e))
|
|
234
|
+
raise click.Abort() from None
|
|
235
|
+
|
|
236
|
+
if commit_msg is None:
|
|
237
|
+
click.echo(click.style("No commit message generated.", fg="magenta"))
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
click.echo(click.style("Generated commit message:", fg="green"))
|
|
241
|
+
click.echo(commit_msg)
|
|
242
|
+
|
|
243
|
+
if sys.stdin.isatty():
|
|
244
|
+
# Interactive mode will ask for confirmation
|
|
245
|
+
char = click.prompt(
|
|
246
|
+
click.style(
|
|
247
|
+
"Do you want to use this commit message? y: yes, n: no, r: retry.", fg="green"
|
|
248
|
+
),
|
|
249
|
+
default="y",
|
|
250
|
+
type=click.Choice(["y", "n", "r"]),
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
click.echo(
|
|
254
|
+
click.style(
|
|
255
|
+
"Non-interactive mode detected, using the generated commit message directly.",
|
|
256
|
+
fg="yellow",
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
char = "y"
|
|
260
|
+
|
|
261
|
+
if char == "n":
|
|
262
|
+
click.echo(click.style("Commit message discarded.", fg="yellow"))
|
|
263
|
+
return
|
|
264
|
+
elif char == "y":
|
|
265
|
+
retry = False
|
|
266
|
+
logger.debug(f"Input: {char}")
|
|
267
|
+
if char == "n":
|
|
268
|
+
click.echo(click.style("Commit message discarded.", fg="yellow"))
|
|
269
|
+
return
|
|
270
|
+
elif char == "y":
|
|
271
|
+
retry = False
|
|
272
|
+
|
|
273
|
+
message_generator.repo.index.commit(commit_msg)
|
|
274
|
+
click.echo(click.style("Commit message saved.", fg="green"))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@click.command()
|
|
278
|
+
@click.pass_context
|
|
279
|
+
def generate_prmsg(ctx: click.Context, debug, local):
|
|
280
|
+
"""Generate a pull request message based on changes compared to master"""
|
|
281
|
+
config_manager = ctx.obj["config_manager"]
|
|
282
|
+
message_generator = MessageGenerator(config_manager)
|
|
283
|
+
|
|
284
|
+
pr_msg = message_generator.generate_pr_message()
|
|
285
|
+
|
|
286
|
+
click.echo("Generated pull request message:")
|
|
287
|
+
click.echo(pr_msg)
|
|
288
|
+
|
|
289
|
+
if click.confirm("Do you want to use this pull request message?"):
|
|
290
|
+
# Here you could integrate with your Git hosting platform's API
|
|
291
|
+
# to create a pull request with this message
|
|
292
|
+
click.echo(
|
|
293
|
+
"Pull request message saved. You can now use it to create a PR on your Git hosting platform."
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
click.echo("Pull request message discarded.")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
cli()
|
gptcomet/cmd.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import tomlkit as toml
|
|
7
|
+
from tomlkit import TOMLDocument, boolean, float_, integer, string
|
|
8
|
+
from tomlkit.items import Item
|
|
9
|
+
|
|
10
|
+
from gptcomet._types import CacheType
|
|
11
|
+
from gptcomet.exceptions import KeyNotFound
|
|
12
|
+
from gptcomet.support_keys import SUPPORT_KEYS
|
|
13
|
+
from gptcomet.utils import is_float
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigManager:
|
|
19
|
+
__slots__ = (
|
|
20
|
+
"local",
|
|
21
|
+
"global_config_file",
|
|
22
|
+
"default_config_file",
|
|
23
|
+
"current_config_path",
|
|
24
|
+
"_cache",
|
|
25
|
+
"_valid_keys"
|
|
26
|
+
)
|
|
27
|
+
local: bool
|
|
28
|
+
global_config_file: Path
|
|
29
|
+
default_config_file: Path
|
|
30
|
+
config: TOMLDocument
|
|
31
|
+
current_config_path: Path
|
|
32
|
+
_cache: CacheType
|
|
33
|
+
_valid_keys: set[str]
|
|
34
|
+
|
|
35
|
+
def __init__(self, local: bool = False):
|
|
36
|
+
self.local: bool = local
|
|
37
|
+
self.global_config_file: Path = Path.home() / ".local" / "gptcomet" / "gptcomet.toml"
|
|
38
|
+
self.default_config_file: Path = Path(__file__).parent / "gptcomet.toml"
|
|
39
|
+
# runtime config file
|
|
40
|
+
self.current_config_path = self.get_config_file()
|
|
41
|
+
|
|
42
|
+
self._cache: CacheType = {"config": None, "default_config": None}
|
|
43
|
+
self._valid_keys: set[str] = set(SUPPORT_KEYS.splitlines())
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def config(self):
|
|
47
|
+
"""
|
|
48
|
+
Returns the configuration stored in the cache.
|
|
49
|
+
|
|
50
|
+
If the configuration is not already loaded, it is loaded by calling the
|
|
51
|
+
`load_config` method and stored in the cache.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The configuration stored in the cache.
|
|
55
|
+
"""
|
|
56
|
+
if "config" not in self._cache or self._cache["config"] is None:
|
|
57
|
+
self._cache["config"] = self.load_config()
|
|
58
|
+
return self._cache["config"]
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def default_config(self) -> toml.TOMLDocument:
|
|
62
|
+
"""
|
|
63
|
+
Returns the default configuration stored in the cache.
|
|
64
|
+
|
|
65
|
+
If the default configuration is not already loaded, it is loaded by calling the
|
|
66
|
+
`load_default_config` method and stored in the cache.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The default configuration stored in the cache.
|
|
70
|
+
"""
|
|
71
|
+
if "default_config" not in self._cache or self._cache["default_config"] is None:
|
|
72
|
+
self._cache["default_config"] = self.load_default_config()
|
|
73
|
+
return self._cache["default_config"]
|
|
74
|
+
|
|
75
|
+
def get_config_file(self) -> Path:
|
|
76
|
+
"""
|
|
77
|
+
Retrieves the path to the configuration file.
|
|
78
|
+
|
|
79
|
+
If local is True, checks if the current directory is a git repository
|
|
80
|
+
and returns the path to .git/gptcomet.toml if it is, otherwise returns
|
|
81
|
+
the global configuration file path.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Path: The path to the configuration file.
|
|
85
|
+
"""
|
|
86
|
+
config_path = self.global_config_file
|
|
87
|
+
if self.local:
|
|
88
|
+
cwd = Path.cwd()
|
|
89
|
+
if not (cwd / ".git").exists():
|
|
90
|
+
click.echo(f"[{click.style('GPTComet', fg='green')}] Not a git repository. Using global config.")
|
|
91
|
+
else:
|
|
92
|
+
config_path = cwd / ".git" / "gptcomet.toml"
|
|
93
|
+
# click.echo(f"[GPTComet] Using config file: {config_path}")
|
|
94
|
+
return config_path
|
|
95
|
+
|
|
96
|
+
def ensure_config_file(self):
|
|
97
|
+
"""
|
|
98
|
+
Ensures that the configuration file exists by creating it with default values if it does not already exist.
|
|
99
|
+
"""
|
|
100
|
+
config_file = self.current_config_path
|
|
101
|
+
if not config_file.exists():
|
|
102
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
with open(self.default_config_file) as default, open(config_file, "w") as target:
|
|
104
|
+
target.write(default.read())
|
|
105
|
+
click.echo(f"[{click.style('GPTComet', fg='green')}] Created default config file at {config_file}")
|
|
106
|
+
|
|
107
|
+
def load_config(self) -> toml.TOMLDocument:
|
|
108
|
+
"""
|
|
109
|
+
Load the configuration from the current configuration file.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
toml.TOMLDocument: The configuration loaded from the current configuration file.
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
self.ensure_config_file()
|
|
116
|
+
with open(self.current_config_path) as f:
|
|
117
|
+
return toml.load(f)
|
|
118
|
+
|
|
119
|
+
def load_default_config(self) -> toml.TOMLDocument:
|
|
120
|
+
"""
|
|
121
|
+
Load the default configuration from the default configuration file.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
toml.TOMLDocument: The default configuration loaded from the file.
|
|
125
|
+
"""
|
|
126
|
+
with open(self.default_config_file) as f:
|
|
127
|
+
return toml.load(f)
|
|
128
|
+
|
|
129
|
+
def save_config(self):
|
|
130
|
+
"""
|
|
131
|
+
Saves the current configuration to a file.
|
|
132
|
+
Returns:
|
|
133
|
+
None
|
|
134
|
+
"""
|
|
135
|
+
with self.current_config_path.open("w") as f:
|
|
136
|
+
f.write(self.config.as_string())
|
|
137
|
+
|
|
138
|
+
def is_valid_key(self, key: str) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Check if the given key is a valid key in the current configuration.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
key (str): The key to be checked.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
bool: True if the key is valid, False otherwise.
|
|
147
|
+
"""
|
|
148
|
+
return key in self._valid_keys
|
|
149
|
+
|
|
150
|
+
def get_nested_value(self, doc: Union[dict, toml.TOMLDocument], keys: Union[str, list[str]]) -> Any:
|
|
151
|
+
"""
|
|
152
|
+
Get the nested value from a dictionary or TOML document using a list of keys.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
doc (Union[dict, toml.TOMLDocument]): The dictionary or TOML document to retrieve the nested value from.
|
|
156
|
+
keys (Union[str, List[str]]): The list of keys to navigate through the document.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Any: The nested value if found, None otherwise.
|
|
160
|
+
"""
|
|
161
|
+
if isinstance(keys, str):
|
|
162
|
+
keys = keys.split(".")
|
|
163
|
+
for key in keys:
|
|
164
|
+
if isinstance(doc, dict):
|
|
165
|
+
doc = doc.get(key)
|
|
166
|
+
else:
|
|
167
|
+
raise KeyNotFound(key)
|
|
168
|
+
return doc
|
|
169
|
+
|
|
170
|
+
def set_nested_value(self, doc: TOMLDocument, keys: Union[str, list[str]], value: Any):
|
|
171
|
+
"""
|
|
172
|
+
Set a nested value in a dictionary using a list of keys.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
doc (TOMLDocument): The dictionary to modify.
|
|
176
|
+
keys (Union[str, List[str]]): The list of keys to navigate through the dictionary.
|
|
177
|
+
value (Any): The value to set at the end of the nested keys.
|
|
178
|
+
|
|
179
|
+
It navigates through the dictionary using the keys and sets the value at the end of the nested keys.
|
|
180
|
+
If any key in the path does not exist, it creates a new dictionary at that key.
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
>>> config = ConfigManager()
|
|
184
|
+
>>> d = {'a': {'b': {'c': 1}}}
|
|
185
|
+
>>> config.set_nested_value(doc, ['a', 'b', 'c'], 2)
|
|
186
|
+
>>> doc
|
|
187
|
+
{'a': {'b': {'c': 2}}}
|
|
188
|
+
|
|
189
|
+
Note:
|
|
190
|
+
This function modifies the input dictionary `d` in-place.
|
|
191
|
+
"""
|
|
192
|
+
if isinstance(keys, str):
|
|
193
|
+
keys = keys.split(".")
|
|
194
|
+
for key in keys[:-1]:
|
|
195
|
+
doc = doc.setdefault(key, {})
|
|
196
|
+
doc[keys[-1]] = value
|
|
197
|
+
|
|
198
|
+
def set(self, key: str, value: str):
|
|
199
|
+
"""
|
|
200
|
+
Set the value of a configuration key.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
key (str): The key to set the value for.
|
|
204
|
+
value (str): The value to set.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
ConfigKeyError: If the key is not supported.
|
|
208
|
+
|
|
209
|
+
This method sets the value of a configuration key in the `config` attribute of the `ConfigManager` object.
|
|
210
|
+
It first checks if the key is valid using the `is_valid_key` method. If the key is not valid, it raises a
|
|
211
|
+
`ConfigKeyError` with the invalid key.
|
|
212
|
+
|
|
213
|
+
Note:
|
|
214
|
+
This method modifies the toml `config` attribute in-place.
|
|
215
|
+
"""
|
|
216
|
+
toml_value: Item = self.convert2toml_value(value)
|
|
217
|
+
self.set_nested_value(self.config, key, toml_value)
|
|
218
|
+
self.save_config()
|
|
219
|
+
|
|
220
|
+
def get(self, key: str, default: Optional[Any] = None) -> Any:
|
|
221
|
+
"""
|
|
222
|
+
Retrieves the value associated with the given key from the configuration.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
key (str): The key to retrieve the value for.
|
|
226
|
+
default (Optional[str]): The default value to return if the key is not found.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Any: The value associated with the given key.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
KeyNotSupportError: If the key is not supported.
|
|
233
|
+
ConfigKeyError: If the key does not exist in the configuration.
|
|
234
|
+
|
|
235
|
+
Note:
|
|
236
|
+
This method assumes that the `config` attribute of the `ConfigManager` object is a nested dictionary
|
|
237
|
+
structure with keys separated by dots.
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
return self.get_nested_value(self.config, key)
|
|
241
|
+
except KeyNotFound:
|
|
242
|
+
return default
|
|
243
|
+
|
|
244
|
+
def list(self) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Reads the contents of the current configuration file and returns them as a string.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
str: The contents of the current configuration file.
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
FileNotFoundError: If the current configuration file does not exist.
|
|
253
|
+
Note:
|
|
254
|
+
This method assumes that `self.current_config_path` is a valid file path.
|
|
255
|
+
|
|
256
|
+
"""
|
|
257
|
+
with self.current_config_path.open() as f:
|
|
258
|
+
return f.read()
|
|
259
|
+
|
|
260
|
+
def reset(self):
|
|
261
|
+
"""
|
|
262
|
+
Resets the current configuration to its default state.
|
|
263
|
+
"""
|
|
264
|
+
with open(self.default_config_file) as default, open(self.current_config_path, "w") as target:
|
|
265
|
+
content = default.read()
|
|
266
|
+
target.write(content)
|
|
267
|
+
self._cache["config"] = toml.loads(content)
|
|
268
|
+
|
|
269
|
+
def list_keys(self) -> str:
|
|
270
|
+
"""
|
|
271
|
+
Returns a list of supported keys.
|
|
272
|
+
Returns:
|
|
273
|
+
str: A list of supported keys.
|
|
274
|
+
"""
|
|
275
|
+
return SUPPORT_KEYS
|
|
276
|
+
|
|
277
|
+
def convert2toml_value(self, value: str) -> Item:
|
|
278
|
+
"""
|
|
279
|
+
Converts a string value to a TOML-compatible value.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
value (str): The string value to be converted.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Item: The converted TOML value or the original string value.
|
|
286
|
+
"""
|
|
287
|
+
if value.lower() in ("true", "false"):
|
|
288
|
+
return boolean(value.lower())
|
|
289
|
+
elif value.isnumeric():
|
|
290
|
+
return integer(value)
|
|
291
|
+
elif value.lower() in ("none", "null"):
|
|
292
|
+
return string(value, literal=True)
|
|
293
|
+
elif is_float(value):
|
|
294
|
+
return float_(value)
|
|
295
|
+
else:
|
|
296
|
+
return string(value)
|
gptcomet/const.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
DEFAULT_API_BASE = "https://api.openai.com/v1"
|
|
2
|
+
DEFAULT_MODEL = "gpt-3.5-turbo"
|
|
3
|
+
DEFAULT_RETRIES = 2
|
|
4
|
+
|
|
5
|
+
SUPPORTED_LANG = ["en", "zh"]
|
|
6
|
+
|
|
7
|
+
SHORT_PREPARE_COMMIT_MSG = """\
|
|
8
|
+
#!/bin/sh
|
|
9
|
+
# GPTComet pre-commit hook
|
|
10
|
+
|
|
11
|
+
# Run GPTComet commit
|
|
12
|
+
gptcomet generate commit
|
|
13
|
+
|
|
14
|
+
# If GPTComet commit was successful, exit with 0
|
|
15
|
+
if [ $? -eq 0 ]; then
|
|
16
|
+
exit 0
|
|
17
|
+
else
|
|
18
|
+
echo "GPTComet commit failed. Please review your changes and try again."
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
RICH_PREPARE_COMMIT_MSG = """\
|
|
24
|
+
#!/bin/sh
|
|
25
|
+
# GPTComet pre-commit hook
|
|
26
|
+
|
|
27
|
+
# Run GPTComet commit
|
|
28
|
+
gptcomet generate commit --rich
|
|
29
|
+
|
|
30
|
+
# If GPTComet commit was successful, exit with 0
|
|
31
|
+
if [ $? -eq 0 ]; then
|
|
32
|
+
exit 0
|
|
33
|
+
else
|
|
34
|
+
echo "GPTComet commit failed. Please review your changes and try again."
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
"""
|