Skip to content

Commit 1fa37d0

Browse files
authored
Ported README Generation Script to its Own Repo (#1)
* Created module * Added setup.py * Added requirements.txt * Added logging flag * Slowly refactoring code * Slowly refactoring readme code * Ran a quick cleanup * Removed the requests dependency * Added testing * Added workflows * Added missing dependencies * Removed versions from matrix * Removed further versions from matrix * Added classifiers
1 parent 9497b79 commit 1fa37d0

File tree

7 files changed

+316
-0
lines changed

7 files changed

+316
-0
lines changed

.github/workflows/deploy.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Python Package
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
deploy:
9+
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v2
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v2
17+
with:
18+
python-version: '3.x'
19+
20+
- name: Install dependencies
21+
run: |
22+
python -m pip install --upgrade pip
23+
pip install build
24+
25+
- name: Build package
26+
run: python -m build
27+
28+
- name: Publish package
29+
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
30+
with:
31+
user: __token__
32+
password: ${{ secrets.PYPI_API_TOKEN }}

.github/workflows/test.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: PyTest
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: [3.9]
16+
17+
steps:
18+
- uses: actions/checkout@v2
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v2
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
29+
30+
- name: PyTest
31+
run: python -m pytest --cov=yomu.readme tests/

requirements.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
feedparser==6.0.6
2+
beautifulsoup4==4.9.3
3+
requests==2.25.1
4+
SnakeMD==0.7.0
5+
pytest==6.2.4
6+
pytest-cov==2.12.1

setup.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import setuptools
2+
3+
with open("README.md", "r") as fh:
4+
long_description = fh.read()
5+
6+
setuptools.setup(
7+
name="yomu",
8+
version="0.1.0",
9+
author="The Renegade Coder",
10+
author_email="[email protected]",
11+
description="Generates the README for the 'How to Python Code' repo",
12+
long_description=long_description,
13+
long_description_content_type="text/markdown",
14+
url="https://github.com/TheRenegadeCoder/how-to-python-readme",
15+
packages=setuptools.find_packages(),
16+
install_requires=[
17+
"feedparser>=6",
18+
"beautifulsoup4>=4",
19+
"SnakeMD>=0"
20+
],
21+
entry_points={
22+
"console_scripts": [
23+
'yomu = yomu.readme:main'
24+
],
25+
},
26+
classifiers=(
27+
"Programming Language :: Python :: 3.9",
28+
"Operating System :: OS Independent",
29+
"Development Status :: 5 - Production/Stable",
30+
"License :: OSI Approved :: MIT License"
31+
),
32+
)

tests/test_readme.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import yomu
2+
3+
4+
SERIES = yomu.get_series_posts()
5+
6+
7+
def test_get_series_posts():
8+
assert len(SERIES) > 0
9+
10+
11+
def test_youtube_video():
12+
# https://therenegadecoder.com/code/how-to-invert-a-dictionary-in-python/
13+
assert not yomu.get_youtube_video(SERIES[-1]).is_text()
14+
15+
16+
def test_get_challenge():
17+
# https://therenegadecoder.com/code/how-to-invert-a-dictionary-in-python/
18+
assert yomu.get_challenge(SERIES[-1].title).is_text()
19+
20+
21+
def test_get_notebook():
22+
# https://therenegadecoder.com/code/how-to-invert-a-dictionary-in-python/
23+
assert yomu.get_notebook(SERIES[-1].title).is_text()
24+
25+
26+
def test_get_test():
27+
# https://therenegadecoder.com/code/how-to-invert-a-dictionary-in-python/
28+
assert yomu.get_test(SERIES[-1].title).is_text()

yomu/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .readme import *

yomu/readme.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import argparse
2+
import logging
3+
from typing import Optional
4+
5+
import feedparser
6+
from bs4 import BeautifulSoup
7+
from snakemd import Document, InlineText, Table
8+
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def main() -> None:
14+
"""
15+
The main drop in function for README generation.
16+
17+
:return: nothing
18+
"""
19+
loglevel = _get_log_level()
20+
numeric_level = getattr(logging, loglevel.upper(), None)
21+
if not isinstance(numeric_level, int):
22+
raise ValueError(f'Invalid log level: {loglevel}')
23+
logging.basicConfig(level=numeric_level)
24+
how_to = HowTo()
25+
how_to.page.output_page("")
26+
27+
28+
def _get_log_level() -> str:
29+
"""
30+
A helper function which gets the log level from
31+
the command line. Set as warning from default.
32+
33+
:return: the log level provided by the user
34+
"""
35+
parser = argparse.ArgumentParser()
36+
parser.add_argument(
37+
"-log",
38+
"--log",
39+
default="warning",
40+
help=(
41+
"Provide logging level. "
42+
"Example --log debug', default='warning'"
43+
),
44+
)
45+
options = parser.parse_args()
46+
return options.log
47+
48+
49+
def _get_intro_text() -> str:
50+
return """
51+
Welcome to a collection of Jupyter Notebooks from the How to Python series on The Renegade Coder. For
52+
convenience, you can access all of the articles, videos, challenges, and source code below. Alternatively, I keep
53+
an enormous article up to date with all these snippets as well.
54+
"""
55+
56+
57+
def get_series_posts() -> list:
58+
"""
59+
Collects all posts from the series into a feed.
60+
61+
:return: a list of posts from the How to Python series
62+
"""
63+
index = 1
64+
base = "https://therenegadecoder.com/series/how-to-python/feed/?paged="
65+
feed = []
66+
while (rss := feedparser.parse(f"{base}{index}")).entries:
67+
feed.extend(rss.entries)
68+
index += 1
69+
logger.debug(f"Collected {len(feed)} posts")
70+
return feed
71+
72+
73+
def get_youtube_video(entry) -> InlineText:
74+
"""
75+
Generates an InlineText item corresponding to the YouTube
76+
video link if it exists. Otherwise, it returns an empty
77+
InlineText element.
78+
79+
:param entry: a feedparser entry
80+
:return: the YouTube video as an InlineText element
81+
"""
82+
content = entry.content[0].value
83+
soup = BeautifulSoup(content, "html.parser")
84+
target = soup.find("h2", text="Video Summary")
85+
if target:
86+
url = target.find_next_sibling().find_all("a")[-1]["href"]
87+
return InlineText("Video", url=url)
88+
return InlineText("")
89+
90+
91+
def get_slug(title: str, sep: str) -> str:
92+
return title.split(":")[0][:-10].lower().replace(" ", sep)
93+
94+
95+
def get_challenge(title: str) -> InlineText:
96+
slug = get_slug(title, "-")
97+
base = "https://github.com/TheRenegadeCoder/how-to-python-code/tree/main/challenges/"
98+
challenge = InlineText("Challenge", url=f"{base}{slug}")
99+
if not challenge.verify_url():
100+
return InlineText("")
101+
return challenge
102+
103+
104+
def get_notebook(title: str) -> InlineText:
105+
slug = get_slug(title, "_")
106+
base = "https://github.com/TheRenegadeCoder/how-to-python-code/tree/main/notebooks/"
107+
notebook = InlineText("Notebook", f"{base}{slug}.ipynb")
108+
if not notebook.verify_url():
109+
return InlineText("")
110+
return notebook
111+
112+
113+
def get_test(title: str) -> InlineText:
114+
slug = get_slug(title, "_")
115+
base = "https://github.com/TheRenegadeCoder/how-to-python-code/tree/main/testing/"
116+
test = InlineText("Test", f"{base}{slug}.py")
117+
if not test.verify_url():
118+
return InlineText("")
119+
return test
120+
121+
122+
class HowTo:
123+
def __init__(self):
124+
self.page: Optional[Document] = None
125+
self.feed: Optional[list] = None
126+
self._load_data()
127+
self._build_readme()
128+
129+
def _load_data(self):
130+
self.feed = get_series_posts()
131+
132+
def _build_readme(self):
133+
self.page = Document("README")
134+
135+
# Introduction
136+
self.page.add_header("How to Python - Source Code")
137+
self.page.add_paragraph(_get_intro_text()) \
138+
.insert_link("How to Python", "https://therenegadecoder.com/series/how-to-python/") \
139+
.insert_link(
140+
"an enormous article",
141+
"https://therenegadecoder.com/code/python-code-snippets-for-everyday-problems/"
142+
)
143+
144+
# Table
145+
headers = [
146+
"Index",
147+
"Title",
148+
"Publish Date",
149+
"Article",
150+
"Video",
151+
"Challenge",
152+
"Notebook",
153+
"Testing"
154+
]
155+
table = Table(
156+
[InlineText(header) for header in headers],
157+
self.build_table()
158+
)
159+
self.page.add_element(table)
160+
161+
def build_table(self) -> list[list[InlineText]]:
162+
index = 1
163+
body = []
164+
for entry in self.feed:
165+
if "Code Snippets" not in entry.title:
166+
article = InlineText("Article", url=entry.link)
167+
youtube = get_youtube_video(entry)
168+
challenge = get_challenge(entry.title)
169+
notebook = get_notebook(entry.title)
170+
test = get_test(entry.title)
171+
body.append([
172+
InlineText(str(index)),
173+
InlineText(entry.title),
174+
InlineText(entry.published),
175+
article,
176+
youtube,
177+
challenge,
178+
notebook,
179+
test
180+
])
181+
index += 1
182+
return body
183+
184+
185+
if __name__ == '__main__':
186+
main()

0 commit comments

Comments
 (0)