Skip to content

Commit b16ce8a

Browse files
refactor(app): split monolithic main.py into dedicated modules
Extract functionality from main.py and utils.py into single-responsibility modules: - app.py: MemberApp entry point - file_io.py: file read/write helpers with error handling - git_client.py: pygit2 clone/push/branch operations - github_client.py: PyGitHub API interactions (fork, PR, token auth) - markdown_builder.py: load/build member markdown files - strings.py: i18n helper with pybabel - constants.py: shared platform option tuples and directory constants - styles.tcss: full Textual CSS stylesheet - Remove now-empty utils.py and scripts/translate.py
1 parent 15dcbf9 commit b16ce8a

23 files changed

Lines changed: 2204 additions & 1591 deletions

File tree

TRANSLATIONS.md

Lines changed: 0 additions & 50 deletions
This file was deleted.

scripts/translate.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/edit_python_pe/app.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from textual.app import App
2+
3+
from .screens.language import LanguageScreen
4+
5+
6+
class MemberApp(App):
7+
CSS_PATH = "styles.tcss"
8+
9+
def __init__(self) -> None:
10+
super().__init__()
11+
self.original_repo = None
12+
self.forked_repo = None
13+
self.token = ""
14+
self.repo_path = ""
15+
16+
def on_mount(self) -> None:
17+
self.push_screen(LanguageScreen())

src/edit_python_pe/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@
1010
INSTAGRAM_OPTION = ("Instagram", "instagram")
1111
X_OPTION = ("X", "x")
1212
YOUTUBE_OPTION = ("YouTube", "youtube")
13+
14+
# Repository Paths
15+
BLOG_DIR = "blog"
16+
MEMBERS_DIR = "members"
17+
AUTHORS_FILE = "AUTHORS"

src/edit_python_pe/file_io.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import os
2+
3+
from .constants import AUTHORS_FILE
4+
5+
6+
def _read_file(file_path: str) -> str:
7+
with open(file_path, encoding="utf-8") as fd:
8+
return fd.read()
9+
10+
11+
def _append_file(file_content: str, file_path: str) -> None:
12+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
13+
with open(file_path, "a", encoding="utf-8") as fd:
14+
fd.write(file_content)
15+
16+
17+
def _write_file(file_content: str, file_path: str) -> None:
18+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
19+
with open(file_path, "w", encoding="utf-8") as fd:
20+
fd.write(file_content)
21+
22+
23+
def _write_authors_file(
24+
repo_path: str,
25+
aliases: list[str],
26+
name: str,
27+
email: str,
28+
) -> None:
29+
file_path = os.path.join(repo_path, AUTHORS_FILE)
30+
31+
try:
32+
contents = _read_file(file_path)
33+
except FileNotFoundError:
34+
contents = ""
35+
36+
alias = aliases[0] if aliases else name
37+
file_content = f"\n{name}({alias}) <{email}>"
38+
if file_content.strip() not in contents:
39+
_append_file(file_content, file_path)

src/edit_python_pe/git_client.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pygit2
2+
3+
4+
def _commit_and_push(
5+
repo_path: str,
6+
token: str,
7+
was_changed: bool,
8+
name_file: str,
9+
name: str,
10+
email: str,
11+
) -> tuple[
12+
str,
13+
pygit2.repository.Repository,
14+
pygit2.remotes.Remote,
15+
pygit2.callbacks.RemoteCallbacks,
16+
]:
17+
repo = pygit2.repository.Repository(repo_path)
18+
repo.index.add_all()
19+
repo.index.write()
20+
author_sig = pygit2.Signature(name or "Unknown", email or "unknown@email")
21+
tree_id = repo.index.write_tree()
22+
parents = [] if repo.head_is_unborn else [repo.head.target]
23+
commit_msg = f"Changed {name_file}" if was_changed else f"Added {name_file}"
24+
repo.create_commit(
25+
"HEAD",
26+
author_sig,
27+
author_sig,
28+
commit_msg,
29+
tree_id,
30+
parents,
31+
)
32+
33+
callbacks = pygit2.callbacks.RemoteCallbacks(
34+
credentials=pygit2.UserPass(token, "x-oauth-basic")
35+
)
36+
remote = repo.remotes["origin"]
37+
remote.push([repo.head.name], callbacks=callbacks)
38+
return commit_msg, repo, remote, callbacks
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import os
2+
import shutil
3+
from time import sleep
4+
5+
import pygit2
6+
from github import Auth, Github
7+
from github.GithubException import BadCredentialsException, GithubException
8+
from github.Repository import Repository
9+
from platformdirs import user_data_dir
10+
11+
from .file_io import _write_authors_file
12+
from .git_client import _commit_and_push
13+
from .markdown_builder import _create_member_file
14+
from .strings import _
15+
16+
17+
def get_repo(token: str) -> tuple[str, Repository]:
18+
auth = Auth.Token(token)
19+
g = Github(auth=auth)
20+
21+
try:
22+
return token, g.get_repo("pythonpe/python.pe")
23+
except BadCredentialsException as err:
24+
raise ValueError(
25+
_("Unauthorized access. Please check your access token.")
26+
) from err
27+
except GithubException as err:
28+
raise ValueError(
29+
_("Repository not found. Please check your access token.")
30+
) from err
31+
32+
33+
def fork_repo(token: str, original_repo: Repository) -> tuple[str, Repository]:
34+
forked_repo = original_repo.create_fork()
35+
forked_repo_url = forked_repo.clone_url
36+
repo_path = user_data_dir(appname="edit-python-pe", appauthor="python.pe")
37+
38+
if os.path.exists(repo_path):
39+
shutil.rmtree(repo_path, ignore_errors=True)
40+
41+
callbacks = pygit2.callbacks.RemoteCallbacks(
42+
credentials=pygit2.UserPass(token, "x-oauth-basic")
43+
)
44+
sleep(3)
45+
pygit2.clone_repository(forked_repo_url, repo_path, callbacks=callbacks)
46+
return repo_path, forked_repo
47+
48+
49+
def create_pr(
50+
file_content: str,
51+
current_file: str | None,
52+
repo_path: str,
53+
original_repo: Repository,
54+
forked_repo: Repository,
55+
token: str,
56+
aliases: list[str],
57+
name: str,
58+
email: str,
59+
) -> tuple[str, str | None]:
60+
name_file, file_path = _create_member_file(
61+
file_content,
62+
current_file,
63+
repo_path,
64+
aliases,
65+
name,
66+
email,
67+
)
68+
_write_authors_file(
69+
repo_path,
70+
aliases,
71+
name,
72+
email,
73+
)
74+
75+
# commit & push
76+
commit_msg, repo, remote, callbacks = _commit_and_push(
77+
repo_path,
78+
token,
79+
current_file is not None,
80+
name_file,
81+
name,
82+
email,
83+
)
84+
85+
# PR logic
86+
pr_title = commit_msg
87+
first_alias = aliases[0] if aliases else ""
88+
pr_body = (
89+
f"Changing an entry to `blog/members` for {name} (alias: {first_alias})."
90+
if current_file
91+
else (
92+
f"Creating a new entry to `blog/members` for {name} (alias: {first_alias})."
93+
)
94+
)
95+
fork_owner = forked_repo.owner.login
96+
head_branch = f"{fork_owner}:main"
97+
base_branch = "main"
98+
99+
pr_url = None
100+
101+
# If editing, retrieve PR by title and push to its branch
102+
if current_file:
103+
# Try to find an open PR with matching title
104+
prs = original_repo.get_pulls(
105+
state="open", sort="created", base=base_branch, head=head_branch
106+
)
107+
pr_found = None
108+
for pr in prs:
109+
if pr.title.endswith(current_file):
110+
pr_found = pr
111+
break
112+
if pr_found:
113+
# Push to the PR branch
114+
remote.push([repo.head.name], callbacks=callbacks)
115+
pr_url = pr_found.html_url
116+
return (
117+
_(
118+
"Woohoo! Changes to {name_file} were successfully sent to your "
119+
"existing PR! 🎉"
120+
).format(name_file=name_file),
121+
pr_url,
122+
)
123+
else:
124+
pr = original_repo.create_pull(
125+
title=pr_title,
126+
body=pr_body,
127+
head=head_branch,
128+
base=base_branch,
129+
)
130+
pr_url = pr.html_url
131+
return (
132+
_(
133+
"Woohoo! {name_file} was saved successfully and "
134+
"your new PR is ready! 🎉"
135+
).format(name_file=name_file),
136+
pr_url,
137+
)
138+
else:
139+
pr = original_repo.create_pull(
140+
title=pr_title,
141+
body=pr_body,
142+
head=head_branch,
143+
base=base_branch,
144+
)
145+
pr_url = pr.html_url
146+
return (
147+
_(
148+
"Woohoo! {name_file} was saved successfully and "
149+
"your new PR is ready! 🎉"
150+
).format(name_file=name_file),
151+
pr_url,
152+
)
1.38 KB
Binary file not shown.

0 commit comments

Comments
 (0)