ghot 0.1.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.
Files changed (33) hide show
  1. ghot-0.1.0/.gitignore +42 -0
  2. ghot-0.1.0/PKG-INFO +70 -0
  3. ghot-0.1.0/README.md +51 -0
  4. ghot-0.1.0/ghot/auth.py +52 -0
  5. ghot-0.1.0/ghot/config.py +74 -0
  6. ghot-0.1.0/ghot/csv_loader.py +54 -0
  7. ghot-0.1.0/ghot/ghot.py +223 -0
  8. ghot-0.1.0/ghot/org_manager.py +472 -0
  9. ghot-0.1.0/ghot/pattern_formatter.py +102 -0
  10. ghot-0.1.0/ghot/user.py +20 -0
  11. ghot-0.1.0/pyproject.toml +114 -0
  12. ghot-0.1.0/tests/argparse/test_argparse_auth.py +35 -0
  13. ghot-0.1.0/tests/argparse/test_argparse_config.py +38 -0
  14. ghot-0.1.0/tests/argparse/test_argparse_issue_create.py +34 -0
  15. ghot-0.1.0/tests/argparse/test_argparse_repo_clone.py +50 -0
  16. ghot-0.1.0/tests/argparse/test_argparse_repo_create.py +49 -0
  17. ghot-0.1.0/tests/argparse/test_argparse_repo_delete.py +40 -0
  18. ghot-0.1.0/tests/argparse/test_argparse_repo_invite.py +35 -0
  19. ghot-0.1.0/tests/argparse/test_argparse_repo_pull.py +46 -0
  20. ghot-0.1.0/tests/argparse/test_argparse_user_invite.py +34 -0
  21. ghot-0.1.0/tests/argparse/test_argparse_user_remove.py +39 -0
  22. ghot-0.1.0/tests/conftest.py +58 -0
  23. ghot-0.1.0/tests/org_manager/test_issue_create.py +95 -0
  24. ghot-0.1.0/tests/org_manager/test_repo_clone.py +138 -0
  25. ghot-0.1.0/tests/org_manager/test_repo_create.py +66 -0
  26. ghot-0.1.0/tests/org_manager/test_repo_delete.py +81 -0
  27. ghot-0.1.0/tests/org_manager/test_repo_invite.py +102 -0
  28. ghot-0.1.0/tests/org_manager/test_repo_pull.py +113 -0
  29. ghot-0.1.0/tests/org_manager/test_user_invite.py +83 -0
  30. ghot-0.1.0/tests/org_manager/test_user_remove.py +107 -0
  31. ghot-0.1.0/tests/pattern_formatter/test_apply_pattern.py +116 -0
  32. ghot-0.1.0/tests/pattern_formatter/test_resolve_field.py +67 -0
  33. ghot-0.1.0/tests/test_csv_loader.py +32 -0
ghot-0.1.0/.gitignore ADDED
@@ -0,0 +1,42 @@
1
+ venv/
2
+ .venv/
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+
9
+ # C extensions
10
+ *.so
11
+
12
+ # Distribution / packaging
13
+ .Python
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ .coverage
39
+
40
+ # Mkdocs
41
+ .cache
42
+ site/
ghot-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghot
3
+ Version: 0.1.0
4
+ Summary: GitHub Organization Tools
5
+ Project-URL: Documentation, https://joapuiib.github.io/github-organization-tools
6
+ Project-URL: Download, https://github.com/joapuiib/github-organization-tools/releases
7
+ Project-URL: Homepage, https://joapuiib.github.io/github-organization-tools
8
+ Project-URL: Source, https://github.com/joapuiib/github-organization-tools
9
+ Project-URL: Tracker, https://github.com/joapuiib/github-organization-tools/issues
10
+ Author-email: Joan Puigcerver <joapuiib@gmail.com>
11
+ License-Expression: MIT
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Requires-Python: >=3.8
14
+ Requires-Dist: colorama
15
+ Requires-Dist: gitpython
16
+ Requires-Dist: keyring
17
+ Requires-Dist: pygithub
18
+ Description-Content-Type: text/markdown
19
+
20
+ # GitHub Organization Tools
21
+
22
+ __GitHub Organization Tools (`ghot`)__ is a CLI tool designed to simplify the management of users and repositories
23
+ within a GitHub organization.
24
+
25
+ __Features__:
26
+
27
+ - Invite and remove users from your organization.
28
+ - Create, clone, pull or delete repositories.
29
+ - Create issues to multiple repositories.
30
+
31
+ ## Installation
32
+ This tool can be installed via `pip`:
33
+
34
+ ```bash
35
+ pip install ghot
36
+ ```
37
+
38
+ ## Quick Start Example
39
+ - Create a new [organization][org] in GitHub.
40
+
41
+ [org]: https://docs.github.com/articles/creating-a-new-organization-from-scratch
42
+
43
+ - Define a CSV file with the users and repositories.
44
+ ```csv
45
+ id,username,repo
46
+ id1,user1,user1-repo
47
+ id2,user2,user2-repo
48
+ ```
49
+
50
+ > - `id` is a custom identifier for the user.
51
+ > - `username` is the GitHub username.
52
+ > - `repo` is the repository name in the organization.
53
+ >
54
+ > Check the [documentation](https://joapuiib.github.io/github-organization-tools/) for more details!
55
+
56
+ - Invite users to the organization:
57
+ ```bash
58
+ ghot user invite my-org users.csv
59
+ ```
60
+
61
+ - Let users accept the invitation and create their repositories — Or do it for them!
62
+ ```bash
63
+ ghot repo create my-org users.csv
64
+ ghot repo invite my-org users.csv
65
+ ```
66
+
67
+ - And clone the repositories!
68
+ ```bash
69
+ ghot repo clone my-org users.csv
70
+ ```
ghot-0.1.0/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # GitHub Organization Tools
2
+
3
+ __GitHub Organization Tools (`ghot`)__ is a CLI tool designed to simplify the management of users and repositories
4
+ within a GitHub organization.
5
+
6
+ __Features__:
7
+
8
+ - Invite and remove users from your organization.
9
+ - Create, clone, pull or delete repositories.
10
+ - Create issues to multiple repositories.
11
+
12
+ ## Installation
13
+ This tool can be installed via `pip`:
14
+
15
+ ```bash
16
+ pip install ghot
17
+ ```
18
+
19
+ ## Quick Start Example
20
+ - Create a new [organization][org] in GitHub.
21
+
22
+ [org]: https://docs.github.com/articles/creating-a-new-organization-from-scratch
23
+
24
+ - Define a CSV file with the users and repositories.
25
+ ```csv
26
+ id,username,repo
27
+ id1,user1,user1-repo
28
+ id2,user2,user2-repo
29
+ ```
30
+
31
+ > - `id` is a custom identifier for the user.
32
+ > - `username` is the GitHub username.
33
+ > - `repo` is the repository name in the organization.
34
+ >
35
+ > Check the [documentation](https://joapuiib.github.io/github-organization-tools/) for more details!
36
+
37
+ - Invite users to the organization:
38
+ ```bash
39
+ ghot user invite my-org users.csv
40
+ ```
41
+
42
+ - Let users accept the invitation and create their repositories — Or do it for them!
43
+ ```bash
44
+ ghot repo create my-org users.csv
45
+ ghot repo invite my-org users.csv
46
+ ```
47
+
48
+ - And clone the repositories!
49
+ ```bash
50
+ ghot repo clone my-org users.csv
51
+ ```
@@ -0,0 +1,52 @@
1
+ import getpass
2
+ import keyring
3
+ from github import Github, Auth
4
+
5
+ SERVICE_NAME = "github_pat"
6
+
7
+ class AuthManager:
8
+ def __init__(self):
9
+ self.system_user = getpass.getuser()
10
+ self._load_token()
11
+
12
+
13
+ def _load_token(self):
14
+ self.token = keyring.get_password(SERVICE_NAME, self.system_user)
15
+
16
+
17
+ def init(self):
18
+ if not self.token:
19
+ self.token = getpass.getpass("Enter your GitHub Personal Access Token: ").strip()
20
+ if input("Save this token for future use? (y/n): ").strip().lower() == 'y':
21
+ keyring.set_password(SERVICE_NAME, self.system_user, self.token)
22
+
23
+
24
+ def has_token(self):
25
+ return self.token is not None
26
+
27
+
28
+ def client(self):
29
+ if not self.token:
30
+ return None
31
+
32
+ auth = Auth.Token(self.token)
33
+ return Github(auth=auth)
34
+
35
+
36
+ def print_token(self):
37
+ if not self.token:
38
+ print("No token found.")
39
+ return
40
+
41
+ print(f"Token for user '{self.system_user}': {self.token}")
42
+
43
+
44
+ def remove_token(self):
45
+ if not self.has_token():
46
+ print("No token found.")
47
+ return
48
+
49
+ response = input("Are you sure you want to remove the stored key? (y/N): ")
50
+ if response.lower() == "y":
51
+ keyring.delete_password(SERVICE_NAME, self.system_user)
52
+ print(f"Removed token for user '{self.system_user}'.")
@@ -0,0 +1,74 @@
1
+ import configparser
2
+ import os
3
+
4
+ def load_config():
5
+ paths = [os.path.expanduser('~/.ghot')]
6
+ if os.path.exists('.ghot'):
7
+ paths.append(os.path.abspath('.ghot'))
8
+
9
+ config = configparser.ConfigParser(interpolation=None)
10
+ config.read(paths)
11
+ return config
12
+
13
+
14
+ def apply_config_defaults(parser, config):
15
+ def set_str(default, section, key):
16
+ if config.has_option(section, key):
17
+ parser.set_defaults(**{default: config.get(section, key)})
18
+
19
+ def set_bool(default, section, key):
20
+ if config.has_option(section, key):
21
+ parser.set_defaults(**{default: config.getboolean(section, key)})
22
+
23
+ set_str('pattern_id', 'csv', 'pattern.id')
24
+ set_str('pattern_username', 'csv', 'pattern.username')
25
+ set_str('pattern_repo', 'csv', 'pattern.repo')
26
+ set_str('pattern_description', 'csv', 'pattern.description')
27
+ set_bool('lower_id', 'csv', 'lower.id')
28
+ set_bool('remove_accents', 'csv', 'remove.accents')
29
+
30
+
31
+ def write_config(key, value, global_scope=False):
32
+ config_path = os.path.expanduser("~/.ghot") if global_scope else ".ghot"
33
+ config = configparser.ConfigParser(interpolation=None)
34
+ config.read(config_path)
35
+
36
+ section, key = key.split(".", 1)
37
+
38
+ if not config.has_section(section):
39
+ config.add_section(section)
40
+
41
+ config.set(section, key, value)
42
+
43
+ with open(config_path, "w") as configfile:
44
+ config.write(configfile)
45
+
46
+
47
+ def show_config(key=None):
48
+ config = load_config()
49
+
50
+ if key is None:
51
+ for section in config.sections():
52
+ print(f"[{section}]")
53
+ for key, value in config.items(section):
54
+ print(f"{key} = {value}")
55
+ print()
56
+ return
57
+
58
+ else:
59
+ section = ""
60
+ if "." in key:
61
+ section, key = key.split(".", 1)
62
+
63
+ if not config.has_option(section, key):
64
+ if section:
65
+ print(f"Config '{section}.{key}' not found.")
66
+ else:
67
+ print(f"Config '{key}' not found.")
68
+ return
69
+
70
+ value = config.get(section, key)
71
+ if section:
72
+ print(f"{section}.{key} = {value}")
73
+ else:
74
+ print(f"{key} = {value}")
@@ -0,0 +1,54 @@
1
+ import csv
2
+
3
+ from .user import User
4
+ from .pattern_formatter import PatternFormatter
5
+
6
+
7
+ class CSVUserLoader:
8
+ def __init__(
9
+ self,
10
+ pattern_id="",
11
+ pattern_username="",
12
+ pattern_repo="",
13
+ pattern_description="",
14
+ ):
15
+ self.pattern_id = pattern_id
16
+ self.pattern_username = pattern_username
17
+ self.pattern_repo = pattern_repo
18
+ self.pattern_description = pattern_description
19
+
20
+ self.schema = {}
21
+ self.formatter = PatternFormatter()
22
+
23
+
24
+ def __repr__(self):
25
+ return f"CSVUserLoader(pattern_id={self.pattern_id}, pattern_username={self.pattern_username}, pattern_repo={self.pattern_repo}, pattern_description={self.pattern_description}, lower_id={self.lower_id}, remove_accents={self.remove_accents})"
26
+
27
+
28
+ def load(self, path):
29
+ try:
30
+ with open(path, newline='') as f:
31
+ reader = csv.reader(f, delimiter=',')
32
+ header = next(reader)
33
+ self.load_schema(header)
34
+ return [ self.map(row) for row in reader ]
35
+ except FileNotFoundError:
36
+ print(f"Could not find file: {path}")
37
+ exit(1)
38
+
39
+
40
+ def load_schema(self, header):
41
+ self.schema = {name: idx for idx, name in enumerate(header)}
42
+ self.formatter.schema = self.schema
43
+
44
+
45
+ def map(self, row):
46
+ """
47
+ Maps a CSV line to a User object.
48
+ """
49
+ return User(**{
50
+ "id": self.formatter.format(self.pattern_id, row),
51
+ "username": self.formatter.format(self.pattern_username, row),
52
+ "repo": self.formatter.format(self.pattern_repo, row),
53
+ "description": self.formatter.format(self.pattern_description, row),
54
+ })
@@ -0,0 +1,223 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from .auth import AuthManager
5
+ from .config import load_config, apply_config_defaults, write_config, show_config
6
+ from .csv_loader import CSVUserLoader
7
+ from .org_manager import OrgManager
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ def build_parser():
12
+ config = load_config()
13
+ def add_csv_options(parser):
14
+ parser.add_argument('csv', help='CSV file')
15
+ parser.add_argument('--pattern-id', default='{f0}', help='Pattern for user ID')
16
+ parser.add_argument('--pattern-username', default='{f1}', help='Pattern for username')
17
+ parser.add_argument('--pattern-repo', default='{f2}', help='Pattern for repository name')
18
+ parser.add_argument('--pattern-description', default='', help='Pattern for repository description')
19
+ apply_config_defaults(parser, config)
20
+
21
+ def add_dry_option(parser):
22
+ parser.add_argument('--dry', action='store_true', help='Dry run mode')
23
+
24
+ parser = argparse.ArgumentParser(description='Git tools.')
25
+ commands = parser.add_subparsers(title="commands", dest="commands")
26
+
27
+ # ghot auth
28
+ auth_p = commands.add_parser('auth')
29
+ ## ghot auth check|print|remove
30
+ auth_p.add_argument('auth_commands', choices=['check', 'remove', 'print'], help='Authentication command')
31
+
32
+ # ghot config
33
+ config_p = commands.add_parser('config')
34
+ config_commands = config_p.add_subparsers(title="config_commands", dest="config_commands", required=True)
35
+ ## ghot config [set] [--global] <key> <value>
36
+ config_set_p = config_commands.add_parser('set')
37
+ config_set_p.add_argument('--global', dest="_global", action='store_true', help='Set the config globally')
38
+ config_set_p.add_argument('key', help='Config key to set')
39
+ config_set_p.add_argument('value', help='Config value to set')
40
+ ## ghot config show <key>
41
+ config_show_p = config_commands.add_parser('show')
42
+ config_show_p.add_argument('key', help='Config key to show', nargs='?', default=None)
43
+
44
+ # ghot user
45
+ user_p = commands.add_parser('user')
46
+ user_commands = user_p.add_subparsers(title="user_commands", dest="user_commands", required=True)
47
+ ## ghot user invite <org> <csv>
48
+ user_invite_p = user_commands.add_parser('invite')
49
+ user_invite_p.add_argument("org", help='Organization name')
50
+ add_csv_options(user_invite_p)
51
+ add_dry_option(user_invite_p)
52
+ ## ghot user remove <org> <csv> [-f|--force]
53
+ user_remove_p = user_commands.add_parser('remove')
54
+ user_remove_p.add_argument("org", help='Organization name')
55
+ add_csv_options(user_remove_p)
56
+ add_dry_option(user_remove_p)
57
+ user_remove_p.add_argument("-f", "--force", action="store_true", default=False, help='Force removal')
58
+
59
+ # ghot repo
60
+ repo_p = commands.add_parser('repo')
61
+ repo_commands = repo_p.add_subparsers(title="repo_commands", dest="repo_commands", required=True)
62
+ ## ghot repo create [--public] [--private] <org> <csv>
63
+ repo_create_p = repo_commands.add_parser('create')
64
+ repo_create_p.add_argument("org", help='Organization name')
65
+ add_csv_options(repo_create_p)
66
+ add_dry_option(repo_create_p)
67
+ repo_create_p.add_argument('--public', action='store_true', help='Create public repositories')
68
+ repo_create_p.add_argument('--private', action='store_true', help='Create private repositories')
69
+ ## ghot repo clone [-d|--destination <path>] <csv>
70
+ repo_clone_p = repo_commands.add_parser('clone')
71
+ repo_clone_p.add_argument("org", help='Organization name')
72
+ add_csv_options(repo_clone_p)
73
+ add_dry_option(repo_clone_p)
74
+ repo_clone_p.add_argument('-d', '--destination', help='Destination directory where the repositoiry will be cloned')
75
+ repo_clone_p.add_argument('--ssh', action='store_true', help='Use SSH for cloning')
76
+ ## ghot repo pull [-d|--destination <path>] <csv>
77
+ repo_pull_p = repo_commands.add_parser('pull')
78
+ add_csv_options(repo_pull_p)
79
+ add_dry_option(repo_pull_p)
80
+ repo_pull_p.add_argument('-d', '--destination', help='Destination directory where the repositoiry will be pulled')
81
+ ## ghot repo delete [-f|--force] <org> <csv>
82
+ repo_delete_p = repo_commands.add_parser('delete')
83
+ repo_delete_p.add_argument('org', help='Organization name')
84
+ add_csv_options(repo_delete_p)
85
+ add_dry_option(repo_delete_p)
86
+ repo_delete_p.add_argument('-f', '--force', action='store_true', default=False, help='Force deletion')
87
+ ## ghot repo invite <org> <csv>
88
+ repo_invite_p = repo_commands.add_parser('invite')
89
+ repo_invite_p.add_argument('org', help='Organization name')
90
+ add_csv_options(repo_invite_p)
91
+ add_dry_option(repo_invite_p)
92
+
93
+ # ghot issue
94
+ issue_p = commands.add_parser('issue')
95
+ issue_commands = issue_p.add_subparsers(title="issue_commands", dest="issue_commands", required=True)
96
+ ## ghot issue create <org> <csv> <title> <body>
97
+ issue_create_p = issue_commands.add_parser('create')
98
+ issue_create_p.add_argument('org', help='Organization name')
99
+ add_csv_options(issue_create_p)
100
+ issue_create_p.add_argument('title', help='Issue title')
101
+ issue_create_p.add_argument('body', help='Issue body')
102
+ add_dry_option(issue_create_p)
103
+
104
+ return parser
105
+
106
+
107
+ def preprocess_args(argv):
108
+ if len(argv) > 2:
109
+ if argv[1] == "config" and argv[2] not in ["set", "show"]:
110
+ argv.insert(2, "set")
111
+
112
+ return argv
113
+
114
+
115
+ def handle_config(args):
116
+ # Default config action
117
+ if args.config_commands is None:
118
+ args.config_commands = "set"
119
+
120
+ match args.config_commands:
121
+ case "set":
122
+ write_config(args.key, args.value, global_scope=args._global)
123
+ case "show":
124
+ show_config(args.key)
125
+
126
+
127
+ def handle_auth(args):
128
+ auth = AuthManager()
129
+ match args.auth_commands:
130
+ case "check":
131
+ auth.init()
132
+ print(f"Authenticated as {auth.client().get_user().login}")
133
+ case "print":
134
+ auth.print_token()
135
+ case "remove":
136
+ auth.remove_token()
137
+
138
+
139
+ def handle_user(args):
140
+ org_manager = init_org_manager(args)
141
+ users = load_users(args)
142
+
143
+ match args.user_commands:
144
+ case "invite":
145
+ org_manager.user_invite(args.org, users, dry=args.dry)
146
+ case "remove":
147
+ org_manager.user_remove(args.org, users, dry=args.dry, force=args.force)
148
+
149
+
150
+ def handle_repo(args):
151
+ org_manager = init_org_manager(args)
152
+ users = load_users(args)
153
+
154
+ match args.repo_commands:
155
+ case "create":
156
+ private = True
157
+ if args.public and not args.private:
158
+ private = False
159
+
160
+ org_manager.repo_create(args.org, users, private=private, dry=args.dry)
161
+
162
+ case "clone":
163
+ org_manager.repo_clone(args.org, users, destination=args.destination, dry=args.dry, ssh=args.ssh)
164
+ case "pull":
165
+ org_manager.repo_pull(users, destination=args.destination, dry=args.dry)
166
+ case "delete":
167
+ org_manager.repo_delete(args.org, users, dry=args.dry, force=args.force)
168
+ case "invite":
169
+ org_manager.repo_invite(args.org, users, dry=args.dry)
170
+
171
+
172
+ def handle_issue(args):
173
+ org_manager = init_org_manager(args)
174
+ users = load_users(args)
175
+
176
+ match args.issue_commands:
177
+ case "create":
178
+ org_manager.issue_create(args.org, users, args.title, args.body, dry=args.dry)
179
+
180
+
181
+ def init_org_manager(args):
182
+ auth = AuthManager()
183
+ auth.init()
184
+ org_manager = OrgManager(auth.client())
185
+ return org_manager
186
+
187
+
188
+ def load_users(args):
189
+ csv_loader = CSVUserLoader(
190
+ pattern_id=args.pattern_id,
191
+ pattern_username=args.pattern_username,
192
+ pattern_repo=args.pattern_repo,
193
+ pattern_description=args.pattern_description,
194
+ )
195
+ users = csv_loader.load(args.csv)
196
+ return users
197
+
198
+
199
+ def main():
200
+ sys.argv = preprocess_args(sys.argv)
201
+ parser = build_parser()
202
+ args = parser.parse_args()
203
+
204
+ args = parser.parse_args()
205
+
206
+ try:
207
+ match args.commands:
208
+ case "auth":
209
+ handle_auth(args)
210
+ case "config":
211
+ handle_config(args)
212
+ case "user":
213
+ handle_user(args)
214
+ case "repo":
215
+ handle_repo(args)
216
+ case "issue":
217
+ handle_issue(args)
218
+
219
+ except KeyboardInterrupt:
220
+ print("\nCancelled by user.")
221
+ except ValueError as e:
222
+ # print on stderr
223
+ print(e, file=sys.stderr)