Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Python CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt

- name: Lint
run: |
ruff check \
_exclude \
"lectures/Week 02 - Processing files, Making Web Requests" \
"lectures/Week 04 - Data Processing and Visualization Part 1/Manifold_Approximation_and_Projection.py" \
"lectures/Week 06 - Web Scraping/scrapers" \
"lectures/Week 01 - Language basics, Generating Data, Storing Data/test_class.py" \
"lectures/Week 01 - Language basics, Generating Data, Storing Data/test_sample.py" \
"lectures/Week 01 - Language basics, Generating Data, Storing Data/test_tmpdir.py" \
"lectures/Week 01 - Language basics, Generating Data, Storing Data/test_tmppath.py" \
"lectures/Week 01 - Language basics, Generating Data, Storing Data/test_sysexit.py"

- name: Run tests
run: pytest
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@ development life-cycle: planning, development, testing, implementation and maint

*Note: The course schedule and assignments are subject to change. Please see your Enterprise Learning Management System (e.g. Canvas, Blackboard, Desire2Learn) for the official schedule.*

## Working with the code samples

The notebooks, scripts, and exercises in this repository now target **Python 3.12**, the latest stable CPython release. To install the required libraries, create and activate a virtual environment and then run:

```
python -m pip install --upgrade pip
pip install -r requirements.txt
```

For contributors, the developer tooling (pytest and Ruff) can be installed with:

```
pip install -r requirements-dev.txt
```

Always run the test suite and the linter before opening a pull request:

```
ruff check .
pytest
```

Both commands are also executed automatically in CI.

## Lectures

Lectures will contain a mixture of content form this site and others.
Expand Down
93 changes: 48 additions & 45 deletions _exclude/build-README.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,59 @@
import glob, os
import sys
import urllib
import shutil
"""Utility helpers for regenerating the README from lecture fragments."""

from pathlib import Path

def build_lectures_md(lectures_md_file_name):
weekly_lectures = []
lectures_path = '../lectures'
weekly_lectures = [lecture for lecture in os.listdir(lectures_path) \
if lecture.startswith('Week') and not lecture.endswith('.md')]
weekly_lectures.sort()

lectures_md = []
lectures_md.append("## Lectures\n")
lectures_md.append("\n")
lectures_md.append("Lectures will contain a mixture of content form this site and others.\n")
lectures_md.append("\n")
for lecture in weekly_lectures:
print('***** ', lecture)
(week, content) = tuple(lecture.split(' - '))
lectures_md.append(f'1. [{week}](lectures/lectures.md) - {content}\n')
with open(lectures_md_file_name, 'w+') as f:
f.write(''.join(lectures_md))
REPO_ROOT = Path(__file__).resolve().parent
LECTURES_PATH = REPO_ROOT.parent / "lectures"


if __name__ == "__main__":
os.chdir(os.path.dirname(sys.argv[0]))
def build_lectures_md(target: Path) -> None:
"""Write the lecture overview page."""

md = []
weekly_lectures = sorted(
lecture
for lecture in LECTURES_PATH.iterdir()
if lecture.name.startswith("Week") and lecture.is_dir()
)

lines = [
"## Lectures\n",
"\n",
"Lectures will contain a mixture of content form this site and others.\n",
"\n",
]
for lecture in weekly_lectures:
week, content = lecture.name.split(" - ", maxsplit=1)
lines.append(f"1. [{week}](lectures/lectures.md) - {content}\n")
target.write_text("".join(lines), encoding="utf-8")

title = "Data Focused Python"
md.append("---\n")
md.append("layout: default\n")
md.append(f"title: {title}\n")
md.append("nav_order: 1\n")
md.append("permalink: /\n")
md.append("---\n")
md.append("\n")

lectures_md_file_name = '02-lectures.md'
build_lectures_md(lectures_md_file_name)
def main() -> None:
lectures_md_path = REPO_ROOT / "02-lectures.md"
build_lectures_md(lectures_md_path)

title = "Data Focused Python"
md = [
"---\n",
"layout: default\n",
f"title: {title}\n",
"nav_order: 1\n",
"permalink: /\n",
"---\n",
"\n",
]

files = [
'01-data-focused-python.md',
lectures_md_file_name,
'03-quizzes.md',
'04-assignments.md'
REPO_ROOT / "01-data-focused-python.md",
lectures_md_path,
REPO_ROOT / "03-quizzes.md",
REPO_ROOT / "04-assignments.md",
]
for file in files:
with open(file, 'r') as f:
md.extend(f.readlines())
md.append("\n")

with open('../README.md', 'w+') as f:
f.write(''.join(md))
md.append(file.read_text(encoding="utf-8"))
md.append("\n")

(REPO_ROOT.parent / "README.md").write_text("".join(md), encoding="utf-8")


if __name__ == "__main__":
main()
152 changes: 82 additions & 70 deletions _exclude/build-lecture-toc.py
Original file line number Diff line number Diff line change
@@ -1,80 +1,92 @@
import glob, os
import sys
import urllib
"""Build lecture navigation markdown files.

The original version of this helper relied on implicit working-directory changes and
produced several syntax warnings on Python 3.12 due to invalid escape sequences. This
rewrite leans on ``pathlib`` for clarity and keeps string handling explicit.
"""

from pathlib import Path
from urllib.parse import quote

REPO_ROOT = Path(__file__).resolve().parent.parent
LECTURE_ROOT = REPO_ROOT / "lectures"
IPYNB_ROOT = "https://github.com/BrianKolowitz/data-focused-python/blob/master/lectures"


def _write_week_index(week_dir: Path, nav_order: int, lecture_toc_title: str) -> None:
week_header = [
"---",
"layout: default",
f"title: {week_dir.name}",
f"parent: {lecture_toc_title}",
"has_children: true",
f"nav_order: {nav_order}",
"---",
"",
]
week_index_path = week_dir.with_suffix(".md")
week_index_path.write_text("\n".join(week_header), encoding="utf-8")


def _prepend_front_matter(target: Path, header: list[str]) -> None:
content = target.read_text(encoding="utf-8")
if content.startswith("---\n"):
return
body = content.splitlines()
new_text = "\n".join(["\n".join(header)] + body)
target.write_text(new_text, encoding="utf-8")

if __name__ == "__main__":
os.chdir(os.path.dirname(sys.argv[0]))

lecture_toc_md = []
lecture_root = "../lectures"
weeks = [week for week in os.listdir(lecture_root) if week.lower().startswith('week') and not week.lower().endswith('.md')]
weeks.sort()
def main() -> None:
lecture_toc_title = "Lectures"
lecture_toc_md.append("---")
lecture_toc_md.append("layout: default")
lecture_toc_md.append(f"title: {lecture_toc_title}")
lecture_toc_md.append("nav_order: 3")
lecture_toc_md.append("has_children: true")
lecture_toc_md.append("has_toc: false")
lecture_toc_md.append("permalink: /lectures")
lecture_toc_md.append("---")
# lecture_toc_md.append("")

lecture_toc_md = [
"---",
"layout: default",
f"title: {lecture_toc_title}",
"nav_order: 3",
"has_children: true",
"has_toc: false",
"permalink: /lectures",
"---",
]

week_nav_order = 1
# todo : delete all md files
for week_title in weeks:
week_path = os.path.join(lecture_root, week_title)
for week_dir in sorted(
path
for path in LECTURE_ROOT.iterdir()
if path.is_dir() and path.name.lower().startswith("week")
):
lecture_toc_md.append("")
lecture_toc_md.append(f"## {week_title}")
lecture_toc_md.append(f"## {week_dir.name}")
lecture_toc_md.append("")
with open(week_path + '.md', 'w') as week_file:
week_md = [
f"---",
f"layout: default",
f"title: {week_title}",
f"parent: {lecture_toc_title}",
f"has_children: true",
f"nav_order: {week_nav_order}",
f"---",
f"",
]
week_file.write('\n'.join(week_md))
week_nav_order += 1

files = os.listdir(week_path)
files = [file for file in files if file.endswith('.md')]
files.sort()
_write_week_index(week_dir, week_nav_order, lecture_toc_title)
week_nav_order += 1

file_nav_order = 1
for file in files:
lecture_md_path = os.path.join(week_title, file)

# todo : figure out why this broke
ipynb_root = "https://github.com/BrianKolowitz/data-focused-python/blob/master/lectures"
ipynb_route = os.path.join(week_title, file[:-3] + ".ipynb")
ipynb_route = urllib.parse.quote(ipynb_route)
lecture_ipynb_path = os.path.join(ipynb_root, ipynb_route)
# lecture_ipynb_path = os.path.join(week_path, file[:-3] + ".ipynb")
# lecture_md_path = urllib.parse.quote(md_path)
# lecture_ipynb_path = urllib.parse.quote(lecture_ipynb_path)
lecture_toc_md.append(f"* [{Path(file).resolve().stem.title()}]({lecture_md_path}) \([ipynb]({lecture_ipynb_path})\)")
for lecture_md in sorted(week_dir.glob("*.md")):
relative_md = lecture_md.relative_to(LECTURE_ROOT)
ipynb_route = quote(str(relative_md.with_suffix(".ipynb")))
lecture_ipynb_path = f"{IPYNB_ROOT}/{ipynb_route}"
lecture_toc_md.append(
f"* [{lecture_md.stem.title()}]({relative_md.as_posix()}) "
f"([ipynb]({lecture_ipynb_path}))"
)

with open(os.path.join(lecture_root, lecture_md_path), 'r+') as lecture_md_file:
lines = lecture_md_file.readlines()
header = [
f"---",
f"layout: default",
f"title: {file[:-3]}",
f"parent: {week_title}",
f"grand_parent: {lecture_toc_title}",
f"nav_order: {file_nav_order}",
f"---",
f""
]
file_nav_order += 1
lines.insert(0, '\n'.join(header))
lecture_md_file.seek(0)
lecture_md_file.writelines(lines)
header = [
"---",
"layout: default",
f"title: {lecture_md.stem}",
f"parent: {week_dir.name}",
f"grand_parent: {lecture_toc_title}",
f"nav_order: {file_nav_order}",
"---",
"",
]
_prepend_front_matter(lecture_md, header)
file_nav_order += 1

with open(os.path.join(lecture_root, 'lectures.md'), 'w') as f:
f.write('\n'.join(lecture_toc_md))
(LECTURE_ROOT / "lectures.md").write_text("\n".join(lecture_toc_md), encoding="utf-8")


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# content of test_class.py
class TestClass(object):
"""Simple tests demonstrating pytest's class-based style."""


class TestClass:
def test_one(self):
x = "this"
assert 'h' in x
assert "h" in x

def test_two(self):
x = "hello"
assert hasattr(x, 'check')
assert hasattr(x, "lower")
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# content of test_sample.py
def inc(x):
"""Sanity-check tests used in the Week 01 lectures."""


def inc(x: int) -> int:
"""Increment ``x`` and return the new value."""

return x + 1


def test_answer():
assert inc(3) == 5
"""The increment helper should add a single unit."""

assert inc(3) == 4
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# content of test_sysexit.py

import pytest


def f():
raise SystemExit(1)


def test_mytest():
with pytest.raises(SystemExit):
f()
f()
Loading