Skip to content

Commit 525e26d

Browse files
committed
feat: enhance package with improved typing, documentation, and automation
1 parent 3e9edcf commit 525e26d

File tree

10 files changed

+1553
-101
lines changed

10 files changed

+1553
-101
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
name: Code Coverage
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
workflow_dispatch:
9+
10+
jobs:
11+
coverage:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.12"
21+
22+
- name: Install Poetry
23+
run: |
24+
curl -sSL https://install.python-poetry.org | python3 -
25+
echo "$HOME/.local/bin" >> $GITHUB_PATH
26+
27+
- name: Install dependencies
28+
run: |
29+
poetry install
30+
31+
- name: Run tests with coverage
32+
run: |
33+
poetry run pytest --cov=my_python_package --cov-report=xml --cov-report=term
34+
35+
- name: Upload coverage to Codecov
36+
uses: codecov/codecov-action@v4
37+
with:
38+
file: ./coverage.xml
39+
fail_ci_if_error: false
40+
token: ${{ secrets.CODECOV_TOKEN }}
41+
42+
- name: Generate coverage badge
43+
run: |
44+
poetry run python -c "
45+
import xml.etree.ElementTree as ET
46+
import re
47+
48+
# Parse the coverage report
49+
tree = ET.parse('coverage.xml')
50+
root = tree.getroot()
51+
52+
# Get the overall coverage percentage
53+
coverage = float(root.get('line-rate')) * 100
54+
55+
# Create the badge markdown
56+
color = 'red'
57+
if coverage >= 90:
58+
color = 'brightgreen'
59+
elif coverage >= 80:
60+
color = 'green'
61+
elif coverage >= 70:
62+
color = 'yellowgreen'
63+
elif coverage >= 60:
64+
color = 'yellow'
65+
elif coverage >= 50:
66+
color = 'orange'
67+
68+
badge_url = f'https://img.shields.io/badge/coverage-{coverage:.1f}%25-{color}'
69+
70+
# Update README.md
71+
with open('README.md', 'r') as f:
72+
readme = f.read()
73+
74+
# Look for existing coverage badge
75+
coverage_badge_pattern = r'!\[Coverage\]\(https://img\.shields\.io/badge/coverage-[\d\.]+%25-[a-z]+\)'
76+
if re.search(coverage_badge_pattern, readme):
77+
# Replace existing badge
78+
readme = re.sub(coverage_badge_pattern, f'![Coverage]({badge_url})', readme)
79+
else:
80+
# Look for badge section to add to
81+
badge_section = re.search(r'(!\[[^\]]+\]\([^\)]+\)[ \t]*)+', readme)
82+
if badge_section:
83+
# Add to existing badges
84+
end_pos = badge_section.end()
85+
readme = readme[:end_pos] + f' ![Coverage]({badge_url})' + readme[end_pos:]
86+
else:
87+
# Add after first line
88+
first_line_end = readme.find('\n')
89+
if first_line_end != -1:
90+
readme = readme[:first_line_end+1] + f'\n![Coverage]({badge_url})\n' + readme[first_line_end+1:]
91+
else:
92+
readme = readme + f'\n\n![Coverage]({badge_url})\n'
93+
94+
with open('README.md', 'w') as f:
95+
f.write(readme)
96+
"
97+
98+
- name: Commit updated README with coverage badge
99+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
100+
run: |
101+
git config user.name "github-actions[bot]"
102+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
103+
git add README.md
104+
git diff --quiet && git diff --staged --quiet || (
105+
git commit -m "docs: update coverage badge [skip ci]"
106+
git push
107+
)

.github/workflows/release.yml

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
name: Release to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
7+
8+
jobs:
9+
build-and-publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write # For creating GitHub release
13+
id-token: write # For PyPI trusted publishing
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0 # Fetch all history for proper changelog generation
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.12"
24+
25+
- name: Install Poetry
26+
run: |
27+
curl -sSL https://install.python-poetry.org | python3 -
28+
echo "$HOME/.local/bin" >> $GITHUB_PATH
29+
30+
- name: Configure Poetry
31+
run: |
32+
poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}
33+
34+
- name: Install dependencies
35+
run: |
36+
poetry install
37+
38+
- name: Run tests
39+
run: |
40+
poetry run pytest
41+
42+
- name: Build package
43+
run: |
44+
poetry build
45+
46+
- name: Extract version from tag
47+
id: extract_version
48+
run: |
49+
VERSION=${GITHUB_REF#refs/tags/v}
50+
echo "version=$VERSION" >> $GITHUB_OUTPUT
51+
echo "Version: $VERSION"
52+
53+
- name: Check if version matches pyproject.toml
54+
run: |
55+
PYPROJECT_VERSION=$(poetry version -s)
56+
TAG_VERSION=${{ steps.extract_version.outputs.version }}
57+
if [[ "$PYPROJECT_VERSION" != "$TAG_VERSION" ]]; then
58+
echo "Error: Tag version v$TAG_VERSION does not match pyproject.toml version $PYPROJECT_VERSION"
59+
exit 1
60+
fi
61+
62+
- name: Generate release notes
63+
id: release_notes
64+
run: |
65+
# Get the previous tag if any
66+
PREV_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || echo "")
67+
68+
if [[ -z "$PREV_TAG" ]]; then
69+
# If no previous tag, use the full history
70+
echo "### Changes in version ${{ steps.extract_version.outputs.version }}" > release_notes.md
71+
echo "" >> release_notes.md
72+
echo "Initial release" >> release_notes.md
73+
else
74+
# Generate changelog between tags
75+
echo "### Changes since $PREV_TAG" > release_notes.md
76+
echo "" >> release_notes.md
77+
git log --pretty=format:"* %s (%an)" $PREV_TAG..${{ github.ref_name }} >> release_notes.md
78+
fi
79+
80+
# Format and organize the changelog better
81+
python -c '
82+
import re
83+
84+
with open("release_notes.md", "r") as f:
85+
notes = f.read()
86+
87+
# Group by change type
88+
changes = {"feat": [], "fix": [], "docs": [], "test": [], "ci": [], "refactor": [], "style": [], "chore": [], "other": []}
89+
90+
for line in notes.split("\n"):
91+
if not line.startswith("*"):
92+
continue
93+
94+
match = re.match(r"\* (\w+)(\([\w\-\.]+\))?:", line)
95+
if match:
96+
change_type = match.group(1)
97+
if change_type in changes:
98+
changes[change_type].append(line)
99+
else:
100+
changes["other"].append(line)
101+
else:
102+
changes["other"].append(line)
103+
104+
# Create formatted notes
105+
formatted = []
106+
header_lines = []
107+
108+
for line in notes.split("\n"):
109+
if not line.startswith("*"):
110+
header_lines.append(line)
111+
112+
formatted.extend(header_lines)
113+
114+
# Features
115+
if changes["feat"]:
116+
formatted.append("\n#### 🚀 Features\n")
117+
formatted.extend(changes["feat"])
118+
119+
# Fixes
120+
if changes["fix"]:
121+
formatted.append("\n#### 🐛 Bug Fixes\n")
122+
formatted.extend(changes["fix"])
123+
124+
# Documentation
125+
if changes["docs"]:
126+
formatted.append("\n#### 📝 Documentation\n")
127+
formatted.extend(changes["docs"])
128+
129+
# Tests
130+
if changes["test"]:
131+
formatted.append("\n#### 🧪 Tests\n")
132+
formatted.extend(changes["test"])
133+
134+
# CI
135+
if changes["ci"]:
136+
formatted.append("\n#### 🔄 CI/CD\n")
137+
formatted.extend(changes["ci"])
138+
139+
# Refactoring
140+
if changes["refactor"]:
141+
formatted.append("\n#### ♻️ Refactoring\n")
142+
formatted.extend(changes["refactor"])
143+
144+
# Style
145+
if changes["style"]:
146+
formatted.append("\n#### 🎨 Style\n")
147+
formatted.extend(changes["style"])
148+
149+
# Other changes
150+
other_changes = changes["other"] + changes["chore"]
151+
if other_changes:
152+
formatted.append("\n#### 🔧 Other Changes\n")
153+
formatted.extend(other_changes)
154+
155+
with open("release_notes.md", "w") as f:
156+
f.write("\n".join(formatted))
157+
'
158+
159+
# Save release notes to output
160+
RELEASE_NOTES=$(cat release_notes.md)
161+
# Escape newlines for GitHub Actions output
162+
RELEASE_NOTES="${RELEASE_NOTES//'%'/'%25'}"
163+
RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}"
164+
RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}"
165+
echo "notes=$RELEASE_NOTES" >> $GITHUB_OUTPUT
166+
167+
- name: Create GitHub Release
168+
uses: softprops/action-gh-release@v1
169+
with:
170+
body_path: release_notes.md
171+
files: |
172+
dist/*.whl
173+
dist/*.tar.gz
174+
draft: false
175+
prerelease: ${{ contains(steps.extract_version.outputs.version, 'rc') || contains(steps.extract_version.outputs.version, 'alpha') || contains(steps.extract_version.outputs.version, 'beta') }}
176+
env:
177+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
178+
179+
- name: Publish to TestPyPI first
180+
uses: pypa/gh-action-pypi-publish@release/v1
181+
with:
182+
repository-url: https://test.pypi.org/legacy/
183+
skip-existing: true
184+
verbose: true
185+
186+
- name: Publish to PyPI
187+
uses: pypa/gh-action-pypi-publish@release/v1
188+
with:
189+
verbose: true
190+
191+
- name: Update CHANGELOG.md
192+
run: |
193+
# Only update if on main branch
194+
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
195+
# Get current date
196+
DATE=$(date +%Y-%m-%d)
197+
198+
# Update CHANGELOG.md
199+
if grep -q "\[Unreleased\]" CHANGELOG.md; then
200+
# Update the Unreleased section to the new version
201+
sed -i "s/\[Unreleased\]/\[${{ steps.extract_version.outputs.version }}\] - $DATE/" CHANGELOG.md
202+
203+
# Add a new Unreleased section at the top
204+
sed -i "0,/^## \[${{ steps.extract_version.outputs.version }}\]/s//## \[Unreleased\]\n\n### Added\n\n### Changed\n\n### Fixed\n\n## \[${{ steps.extract_version.outputs.version }}\]/" CHANGELOG.md
205+
206+
# Update the links at the bottom
207+
if grep -q "\[unreleased\]:" CHANGELOG.md; then
208+
sed -i "s|\[unreleased\]:.*|[unreleased]: https://github.com/DiogoRibeiro7/my_python_package/compare/v${{ steps.extract_version.outputs.version }}...HEAD|" CHANGELOG.md
209+
sed -i "/\[unreleased\]:/a [{{ steps.extract_version.outputs.version }}]: https://github.com/DiogoRibeiro7/my_python_package/compare/v$(git describe --tags --abbrev=0 ${{ github.ref_name }}^)...v${{ steps.extract_version.outputs.version }}" CHANGELOG.md
210+
fi
211+
fi
212+
fi
213+
214+
- name: Notify on success
215+
if: success()
216+
run: |
217+
echo "✅ Package v${{ steps.extract_version.outputs.version }} has been released to PyPI!"
218+
echo "📦 https://pypi.org/project/my-python-package/${{ steps.extract_version.outputs.version }}/"

CONTRIBUTERS.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Contributors
2+
3+
This file lists the contributors to the `my_python_package` project.
4+
5+
## Core Contributors
6+
7+
- **Diogo Ribeiro** - _Initial work and maintenance_ - [DiogoRibeiro7](https://github.com/DiogoRibeiro7)
8+
9+
## Contributors
10+
11+
<!-- Add new contributors at the end of this list -->
12+
13+
<!--
14+
Template for new contributors:
15+
- **Name** - _Role/Contributions_ - [GitHub Username](https://github.com/username)
16+
-->
17+
18+
## How to Contribute
19+
20+
Thank you for considering contributing to `my_python_package`! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for details on how to contribute to this project.
21+
22+
## Contributor License Agreement
23+
24+
By contributing to this repository, you agree that your contributions will be licensed under the project's [MIT License](LICENSE).
25+
26+
## Acknowledgments
27+
28+
- Thanks to all the open-source projects that made this project possible, especially:
29+
- Poetry
30+
- pytest
31+
- mypy
32+
- ruff
33+
- tomlkit
34+
- packaging

0 commit comments

Comments
 (0)