gpt-pr 0.2.0__py3-none-any.whl → 0.3.0__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.

Potentially problematic release.


This version of gpt-pr might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gpt-pr
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Automate your GitHub workflow with GPT-PR: an OpenAI powered library for streamlined PR generation.
5
5
  Home-page: http://github.com/alissonperez/gpt-pr
6
6
  Author: Alisson R. Perez
@@ -8,19 +8,19 @@ Author-email: alissonperez@outlook.com
8
8
  License: MIT
9
9
  Requires-Python: >=3.7
10
10
  Requires-Dist: cffi ==1.15.1
11
- Requires-Dist: cryptography ==42.0.5
11
+ Requires-Dist: cryptography ==42.0.7
12
12
  Requires-Dist: fire ==0.6.0
13
13
  Requires-Dist: pycparser ==2.21
14
14
  Requires-Dist: wcwidth ==0.2.13
15
15
  Requires-Dist: charset-normalizer ==3.3.2 ; python_full_version >= "3.7.0"
16
16
  Requires-Dist: prompt-toolkit ==3.0.43 ; python_full_version >= "3.7.0"
17
17
  Requires-Dist: openai ==1.14.0 ; python_full_version >= "3.7.1"
18
- Requires-Dist: exceptiongroup ==1.2.0 ; python_version < "3.11"
18
+ Requires-Dist: exceptiongroup ==1.2.1 ; python_version < "3.11"
19
19
  Requires-Dist: cached-property ==1.5.2 ; python_version < "3.8"
20
20
  Requires-Dist: importlib-metadata ==6.7.0 ; python_version == "3.7"
21
21
  Requires-Dist: six ==1.16.0 ; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2"
22
22
  Requires-Dist: deprecated ==1.2.14 ; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3"
23
- Requires-Dist: idna ==3.6 ; python_version >= "3.5"
23
+ Requires-Dist: idna ==3.7 ; python_version >= "3.5"
24
24
  Requires-Dist: certifi ==2024.2.2 ; python_version >= "3.6"
25
25
  Requires-Dist: distro ==1.9.0 ; python_version >= "3.6"
26
26
  Requires-Dist: pynacl ==1.5.0 ; python_version >= "3.6"
@@ -40,7 +40,7 @@ Requires-Dist: requests ==2.31.0 ; python_version >= "3.7"
40
40
  Requires-Dist: smmap ==5.0.1 ; python_version >= "3.7"
41
41
  Requires-Dist: sniffio ==1.3.1 ; python_version >= "3.7"
42
42
  Requires-Dist: termcolor ==2.3.0 ; python_version >= "3.7"
43
- Requires-Dist: tqdm ==4.66.2 ; python_version >= "3.7"
43
+ Requires-Dist: tqdm ==4.66.4 ; python_version >= "3.7"
44
44
  Requires-Dist: typing-extensions ==4.7.1 ; python_version >= "3.7"
45
45
  Requires-Dist: urllib3 ==2.0.7 ; python_version >= "3.7"
46
46
  Requires-Dist: zipp ==3.15.0 ; python_version >= "3.7"
@@ -0,0 +1,15 @@
1
+ gptpr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ gptpr/config.py,sha256=YVQgTuJYVwqAabqK-E3xSzlc4QUvZb0q4dUW-_pY2cg,2609
3
+ gptpr/consolecolor.py,sha256=_JmBMNjIflWMlgP2VkCWu6uQLR9oHBy52uV3TRJJgF4,800
4
+ gptpr/gh.py,sha256=uSWY_TzbrAM00neOBkyfV5vxDO4FzMtIrs-Zczp-Tck,1127
5
+ gptpr/gitutil.py,sha256=NBD3iRnbFEPRU47w7c5TowwtZieDYkU4zybvv0PoOU0,5783
6
+ gptpr/main.py,sha256=tc1iLkGVWa0O2gGRAPlGP8MTRA3B1xryIX1Hi6MCn9w,2595
7
+ gptpr/prdata.py,sha256=ejx4zLRViJ83OmqvlxGWnw7alo8RPL_YdlwEJFhMQ8g,6183
8
+ gptpr/test_config.py,sha256=_vP-3RJf8WXGGQESr5bCUbmxf8owc1uVJXMSBF_MtH0,2712
9
+ gptpr/test_prdata.py,sha256=rSJ-yqOdw-iYdBWyqnA2SXbdrhT8KgIkRTTf9SY1S1g,474
10
+ gptpr/version.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
11
+ gpt_pr-0.3.0.dist-info/METADATA,sha256=sva8MD89gNrCtK1xWkfHyJvP1A5JOhW15SS1nC-TwwQ,2638
12
+ gpt_pr-0.3.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
13
+ gpt_pr-0.3.0.dist-info/entry_points.txt,sha256=WhcbcQXqo5-IGliYWiYMhop4-Wm7bcH2ljFKLWrmO7c,81
14
+ gpt_pr-0.3.0.dist-info/top_level.txt,sha256=DZcbzlsjh4BD8njGcvhOeCZ83U_oYWgCn0w8qx5--04,6
15
+ gpt_pr-0.3.0.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  gpt-pr = gptpr.main:main
3
+ gpt-pr-config = gptpr.main:run_config
gptpr/config.py ADDED
@@ -0,0 +1,97 @@
1
+ from copy import deepcopy
2
+ import configparser
3
+ import os
4
+
5
+
6
+ def config_command_example(name, value_sample):
7
+ return f'gpt-pr-config set {name} {value_sample}'
8
+
9
+
10
+ CONFIG_README_SECTION = 'https://github.com/alissonperez/gpt-pr?tab=readme-ov-file#authentication--api-keys'
11
+
12
+
13
+ class Config:
14
+
15
+ config_filename = '.gpt-pr.ini'
16
+
17
+ _default_config = {
18
+ # Github
19
+ 'GH_TOKEN': '',
20
+
21
+ # Open AI info
22
+ 'OPENAI_MODEL': 'gpt-4o',
23
+ 'OPENAI_API_KEY': '',
24
+ }
25
+
26
+ def __init__(self, config_dir=None):
27
+ self.default_config = deepcopy(self._default_config)
28
+ self._config_dir = config_dir or os.path.expanduser('~')
29
+ self._config = configparser.ConfigParser()
30
+ self._initialized = False
31
+
32
+ def load(self):
33
+ if self._initialized:
34
+ return
35
+
36
+ config_file_path = self.get_filepath()
37
+
38
+ if os.path.exists(config_file_path):
39
+ self._config.read(config_file_path)
40
+ self._ensure_default_values()
41
+ else:
42
+ self._config['user'] = {}
43
+ self._config['DEFAULT'] = deepcopy(self.default_config)
44
+ self.persist()
45
+
46
+ self._initialized = True
47
+
48
+ def _ensure_default_values(self):
49
+ added = False
50
+ for key, value in self.default_config.items():
51
+ if key not in self._config['DEFAULT']:
52
+ self._config['DEFAULT'][key] = value
53
+ added = True
54
+
55
+ if added:
56
+ self.persist()
57
+
58
+ def persist(self):
59
+ config_file_path = self.get_filepath()
60
+
61
+ with open(config_file_path, 'w') as configfile:
62
+ self._config.write(configfile)
63
+
64
+ def get_filepath(self):
65
+ return os.path.join(self._config_dir, self.config_filename)
66
+
67
+ def set_user_config(self, name, value):
68
+ self.load()
69
+ self._config['user'][name] = value
70
+
71
+ def reset_user_config(self, name):
72
+ self.load()
73
+ self._config['user'][name] = self.default_config[name]
74
+ self.persist()
75
+
76
+ def get_user_config(self, name):
77
+ self.load()
78
+ return self._config['user'][name]
79
+
80
+ def all_values(self):
81
+ self.load()
82
+
83
+ # iterate over all sections and values and return them in a list
84
+ result = []
85
+
86
+ # add default section
87
+ for option in self._config['DEFAULT']:
88
+ result.append(('DEFAULT', option, self._config['DEFAULT'][option]))
89
+
90
+ for section in self._config.sections():
91
+ for option in self._config[section]:
92
+ result.append((section, option, self._config[section][option]))
93
+
94
+ return result
95
+
96
+
97
+ config = Config()
gptpr/gh.py CHANGED
@@ -1,14 +1,24 @@
1
1
  import os
2
2
  from github import Github
3
3
  from InquirerPy import inquirer
4
+ from gptpr.config import config, config_command_example, CONFIG_README_SECTION
4
5
 
5
- GH_TOKEN = os.environ.get('GH_TOKEN')
6
6
 
7
- if not GH_TOKEN:
8
- print("Please set GH_TOKEN environment variable")
9
- exit(1)
7
+ def _get_gh_token():
8
+ gh_token = config.get_user_config('GH_TOKEN')
9
+ if not gh_token:
10
+ gh_token = os.environ.get('GH_TOKEN')
10
11
 
11
- gh = Github(GH_TOKEN)
12
+ if not gh_token:
13
+ print('Please set "gh_token" config. Just run:',
14
+ config_command_example('gh_token', '[my gh token]'),
15
+ 'more about at', CONFIG_README_SECTION)
16
+ exit(1)
17
+
18
+ return gh_token
19
+
20
+
21
+ gh = Github(_get_gh_token())
12
22
 
13
23
 
14
24
  def create_pr(pr_data, yield_confirmation):
@@ -21,7 +31,7 @@ def create_pr(pr_data, yield_confirmation):
21
31
 
22
32
  if pr_confirmation:
23
33
  pr = repo.create_pull(title=pr_data.title, body=pr_data.body,
24
- head=pr_data.branch_info.branch, base='main')
34
+ head=pr_data.branch_info.branch, base=pr_data.branch_info.base_branch)
25
35
  print("Pull request created successfully: ", pr.html_url)
26
36
  else:
27
37
  print('cancelling...')
gptpr/gitutil.py CHANGED
@@ -10,6 +10,7 @@ from InquirerPy.base.control import Choice
10
10
  class BranchInfo:
11
11
  owner: str
12
12
  repo: str
13
+ base_branch: str
13
14
  branch: str
14
15
  commits: list
15
16
  highlight_commits: list
@@ -70,10 +71,11 @@ def get_branch_info(base_branch, yield_confirmation):
70
71
  return BranchInfo(
71
72
  owner=owner,
72
73
  repo=repo_name,
74
+ base_branch=base_branch,
73
75
  branch=current_branch.name,
74
76
  commits=commits,
75
77
  highlight_commits=highlight_commits,
76
- diff=_get_diff_changes(repo, current_branch.name, yield_confirmation)
78
+ diff=_get_diff_changes(repo, base_branch, current_branch.name, yield_confirmation)
77
79
  )
78
80
 
79
81
 
@@ -85,7 +87,7 @@ def _branch_exists(repo, branch_name):
85
87
 
86
88
 
87
89
  def _get_diff_messages_against_base_branch(repo, branch, base_branch):
88
- # Get commit messages that are in the current branch but not in the main branch
90
+ # Get commit messages that are in the current branch but not in the base branch
89
91
  commits_diff = list(repo.iter_commits(f'{base_branch}..{branch}'))
90
92
 
91
93
  return [commit.message.strip('\n') for commit in commits_diff]
@@ -147,33 +149,33 @@ def _extract_owner_and_repo(repo_url):
147
149
  return owner, '.'.join(repo_info.split('.')[:-1])
148
150
 
149
151
 
150
- def _get_diff_changes(repo, branch, yield_confirmation):
152
+ def _get_diff_changes(repo, base_branch, branch, yield_confirmation):
151
153
  diff_changes = []
152
154
 
153
- stats = _get_stats(repo, branch)
155
+ stats = _get_stats(repo, base_branch, branch)
154
156
  files_to_ignore = _get_files_to_ignore(stats, yield_confirmation)
155
157
 
156
158
  for file_change in stats:
157
159
  if file_change.file_path in files_to_ignore:
158
160
  continue
159
161
 
160
- file_diff = repo.git.diff('main', branch, '--', file_change.file_path)
162
+ file_diff = repo.git.diff(base_branch, branch, '--', file_change.file_path)
161
163
 
162
164
  diff_changes.append(file_diff)
163
165
 
164
166
  return '\n'.join(diff_changes)
165
167
 
166
168
 
167
- def _get_stats(repo, branch):
169
+ def _get_stats(repo, base_branch, branch):
168
170
  '''
169
- Get the stats of the difference between the current branch and the main branch
171
+ Get the stats of the difference between the current branch and the base branch
170
172
  '''
171
173
 
172
174
  # returns:
173
175
  # 4 0 README.md
174
176
  # 2 0 application/aggregator/aggregator.go
175
177
  # 0 257 go.sum
176
- diff_index = repo.git.diff('main', branch, '--numstat')
178
+ diff_index = repo.git.diff(base_branch, branch, '--numstat')
177
179
 
178
180
  files_changed = []
179
181
  for line in diff_index.split('\n'):
gptpr/main.py CHANGED
@@ -5,6 +5,8 @@ from gptpr.gitutil import get_branch_info
5
5
  from gptpr.gh import create_pr
6
6
  from gptpr.prdata import get_pr_data
7
7
  from gptpr.version import __version__
8
+ from gptpr.config import config, config_command_example, CONFIG_README_SECTION
9
+ from gptpr import consolecolor as cc
8
10
 
9
11
 
10
12
  def run(base_branch='main', yield_confirmation=False, version=False):
@@ -44,9 +46,52 @@ def run(base_branch='main', yield_confirmation=False, version=False):
44
46
  create_pr(pr_data, yield_confirmation)
45
47
 
46
48
 
49
+ def set_config(name, value):
50
+ name = name.upper()
51
+ config.set_user_config(name, value)
52
+ config.persist()
53
+
54
+ print('Config value', cc.bold(name), 'set to', cc.yellow(value))
55
+
56
+
57
+ def get_config(name):
58
+ upper_name = name.upper()
59
+ print('Config value', cc.bold(name), '=', cc.yellow(config.get_user_config(upper_name)))
60
+
61
+
62
+ def reset_config(name):
63
+ upper_name = name.upper()
64
+ config.reset_user_config(upper_name)
65
+ print('Config value', cc.bold(name), '=', cc.yellow(config.get_user_config(upper_name)))
66
+
67
+
68
+ def print_config():
69
+ print('Config values at', cc.yellow(config.get_filepath()))
70
+ print('')
71
+ print('To set values, just run:', cc.yellow(config_command_example('[config name]', '[value]')))
72
+ print('More about at', cc.yellow(CONFIG_README_SECTION))
73
+ print('')
74
+ current_section = None
75
+ for section, option, value in config.all_values():
76
+ if current_section != section:
77
+ print('')
78
+ current_section = section
79
+
80
+ print(f'[{cc.bold(section)}]', option, '=', cc.yellow(value))
81
+
82
+
47
83
  def main():
48
84
  fire.Fire(run)
49
85
 
50
86
 
87
+ def run_config():
88
+ fire.Fire({
89
+ 'set': set_config,
90
+ 'get': get_config,
91
+ 'print': print_config,
92
+ 'reset': reset_config
93
+ })
94
+
95
+
51
96
  if __name__ == '__main__':
52
97
  main()
gptpr/prdata.py CHANGED
@@ -4,6 +4,7 @@ import os
4
4
  from openai import OpenAI
5
5
 
6
6
  from gptpr.gitutil import BranchInfo
7
+ from gptpr.config import config
7
8
  import gptpr.consolecolor as cc
8
9
 
9
10
  TOKENIZER_RATIO = 4
@@ -37,6 +38,20 @@ def _get_pr_template():
37
38
  return pr_template
38
39
 
39
40
 
41
+ def _get_open_ai_key():
42
+ api_key = config.get_user_config('OPENAI_API_KEY')
43
+
44
+ if not api_key:
45
+ api_key = os.environ.get('OPENAI_API_KEY')
46
+
47
+ if not api_key:
48
+ print('Please set "openai_api_key" config, just run:',
49
+ cc.yellow('gpt-pr-config set openai_api_key [open ai key]'))
50
+ exit(1)
51
+
52
+ return api_key
53
+
54
+
40
55
  @dataclass
41
56
  class PrData():
42
57
  branch_info: BranchInfo
@@ -48,7 +63,7 @@ class PrData():
48
63
  f'{cc.bold("Repository")}: {cc.yellow(self.branch_info.owner)}/{cc.yellow(self.branch_info.repo)}',
49
64
  f'{cc.bold("Title")}: {cc.yellow(self.title)}',
50
65
  f'{cc.bold("Branch name")}: {cc.yellow(self.branch_info.branch)}',
51
- f'{cc.bold("Base branch")}: {cc.yellow("main")}',
66
+ f'{cc.bold("Base branch")}: {cc.yellow(self.branch_info.base_branch)}',
52
67
  f'{cc.bold("PR Description")}:\n{self.body}',
53
68
  ])
54
69
 
@@ -108,17 +123,14 @@ def get_pr_data(branch_info):
108
123
  else:
109
124
  messages.append({'role': 'user', 'content': 'Diff changes:\n' + branch_info.diff})
110
125
 
111
- openai_api_key = os.environ.get('OPENAI_API_KEY')
112
-
113
- if not openai_api_key:
114
- print("Please set OPENAI_API_KEY environment variable.")
115
- exit(1)
126
+ client = OpenAI(api_key=_get_open_ai_key())
116
127
 
117
- client = OpenAI(api_key=openai_api_key)
128
+ openai_model = config.get_user_config('OPENAI_MODEL')
129
+ print('Using OpenAI model:', cc.yellow(openai_model))
118
130
 
119
131
  chat_completion = client.chat.completions.create(
120
132
  messages=messages,
121
- model='gpt-4-0613',
133
+ model=openai_model,
122
134
  functions=functions,
123
135
  function_call={'name': 'create_pr'},
124
136
  temperature=0,
gptpr/test_config.py ADDED
@@ -0,0 +1,99 @@
1
+ import os
2
+ import configparser
3
+
4
+ from pytest import fixture
5
+
6
+ from gptpr.config import Config
7
+
8
+
9
+ @fixture
10
+ def temp_config(tmpdir):
11
+ temp_dir = tmpdir.mkdir('config_dir')
12
+ config = Config(temp_dir)
13
+ return config, temp_dir
14
+
15
+
16
+ def _check_config(config, temp_dir, config_list):
17
+ # Read the configuration file and verify its contents
18
+ config_to_test = configparser.ConfigParser()
19
+ config_to_test.read(os.path.join(str(temp_dir), config.config_filename))
20
+
21
+ for section, key, value in config_list:
22
+ assert config_to_test[section][key] == value
23
+
24
+
25
+ def test_init_config_file(temp_config):
26
+ config, temp_dir = temp_config
27
+ config.load()
28
+
29
+ # Check if the file exists
30
+ assert os.path.isfile(os.path.join(str(temp_dir), config.config_filename))
31
+
32
+ _check_config(config, temp_dir, [
33
+ ('DEFAULT', 'OPENAI_MODEL', 'gpt-4o'),
34
+ ('DEFAULT', 'OPENAI_API_KEY', ''),
35
+ ])
36
+
37
+
38
+ def test_new_default_value_should_be_added(temp_config):
39
+ config, temp_dir = temp_config
40
+ config.load() # data was written to the file
41
+
42
+ new_config = Config(temp_dir)
43
+
44
+ # Add a new default value
45
+ new_config.default_config['NEW_DEFAULT'] = 'new_default_value'
46
+ new_config.load() # Should update config file...
47
+
48
+ _check_config(new_config, temp_dir, [
49
+ ('DEFAULT', 'NEW_DEFAULT', 'new_default_value'),
50
+ ])
51
+
52
+
53
+ def test_set_user_config(temp_config):
54
+ config, temp_dir = temp_config
55
+
56
+ config.set_user_config('OPENAI_MODEL', 'gpt-3.5')
57
+ config.persist()
58
+
59
+ # Read the configuration file and verify its contents
60
+ config_to_test = configparser.ConfigParser()
61
+ config_to_test.read(os.path.join(str(temp_dir), config.config_filename))
62
+
63
+ _check_config(config, temp_dir, [
64
+ ('user', 'OPENAI_MODEL', 'gpt-3.5'),
65
+ ('user', 'OPENAI_API_KEY', ''),
66
+ ])
67
+
68
+
69
+ def test_all_values(temp_config):
70
+ config, temp_dir = temp_config
71
+
72
+ all_values = config.all_values()
73
+
74
+ assert all_values == [
75
+ ('DEFAULT', 'gh_token', ''),
76
+ ('DEFAULT', 'openai_model', 'gpt-4o'),
77
+ ('DEFAULT', 'openai_api_key', ''),
78
+ ('user', 'gh_token', ''),
79
+ ('user', 'openai_model', 'gpt-4o'),
80
+ ('user', 'openai_api_key', ''),
81
+ ]
82
+
83
+
84
+ def test_reset_user_config(temp_config):
85
+ config, temp_dir = temp_config
86
+
87
+ config.set_user_config('OPENAI_MODEL', 'gpt-3.5')
88
+ config.persist()
89
+
90
+ config.reset_user_config('OPENAI_MODEL')
91
+
92
+ # Read the configuration file and verify its contents
93
+ config_to_test = configparser.ConfigParser()
94
+ config_to_test.read(os.path.join(str(temp_dir), config.config_filename))
95
+
96
+ _check_config(config, temp_dir, [
97
+ ('user', 'OPENAI_MODEL', 'gpt-4o'),
98
+ ('user', 'OPENAI_API_KEY', ''),
99
+ ])
gptpr/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.0"
1
+ __version__ = "0.3.0"
@@ -1,13 +0,0 @@
1
- gptpr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- gptpr/consolecolor.py,sha256=_JmBMNjIflWMlgP2VkCWu6uQLR9oHBy52uV3TRJJgF4,800
3
- gptpr/gh.py,sha256=y8JgWYTzYzjOWitkEK9Lshjmuv1gV7Tt4DHBrISxAJM,761
4
- gptpr/gitutil.py,sha256=uhNwwH3r84p5GK1j_19sZEPMyfftEiGpzuV6QZSW6qE,5667
5
- gptpr/main.py,sha256=rkalqLcc1Nh5WH51w7ayEMIYNoScrRBNVYl3KLuZFdY,1270
6
- gptpr/prdata.py,sha256=RErSbUyIb0LxCjlUS4KKHoNi2WN94NRO3Cw0mCxtHiU,5824
7
- gptpr/test_prdata.py,sha256=rSJ-yqOdw-iYdBWyqnA2SXbdrhT8KgIkRTTf9SY1S1g,474
8
- gptpr/version.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
9
- gpt_pr-0.2.0.dist-info/METADATA,sha256=oLLwZT78ZD4ymXo5rABD0L_vI8zbJlidiKOu4OzkaNg,2638
10
- gpt_pr-0.2.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
11
- gpt_pr-0.2.0.dist-info/entry_points.txt,sha256=aXCkyNdoUHfSJXVhRKHj8m09twDfcDmY7xC66u5N3hE,43
12
- gpt_pr-0.2.0.dist-info/top_level.txt,sha256=DZcbzlsjh4BD8njGcvhOeCZ83U_oYWgCn0w8qx5--04,6
13
- gpt_pr-0.2.0.dist-info/RECORD,,
File without changes