Skip to content
Merged
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
47 changes: 47 additions & 0 deletions .github/workflows/sync_roadmap_status.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Sync Roadmap Status

on:
issues:
types: [assigned, unassigned, closed, reopened]
# pull_request_target (not pull_request) so the workflow gets write
# access even for PRs opened from forks. Safe here because we never
# check out or run the fork's code — we only read the PR body
# (already in the event payload) and write to ROADMAP.md on main.
pull_request_target:
types: [opened, ready_for_review, closed]

permissions:
contents: write

jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Update ROADMAP.md
id: update
run: python scripts/roadmap/sync_roadmap.py
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_EVENT_PATH: ${{ github.event_path }}

- name: Commit ROADMAP.md
if: steps.update.outputs.changed == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: sync roadmap status [skip ci]"
file_pattern: ROADMAP.md

- name: Sync Discord
if: steps.update.outputs.changed == 'true'
run: python scripts/roadmap/sync_discord.py
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
DISCORD_MESSAGE_IDS: ${{ vars.DISCORD_MESSAGE_IDS }}
113 changes: 77 additions & 36 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,90 @@ This may be subject to change.

## Legend
`[ ]` - no work has been done
`[-]` - somebody is working on this but it has not yet been completed. There should be a related PR.
`[ ] 🚧` - work in progress (assigned / open PR)
`[x]` - lesson completed

## Roadmap
1. Introduction
* [x] How course works?
* [x] Motivation and Mindset
* [ ] Asking for help
* [ ] Join the Community
* [x] Computers and Programming Languages (CS Basics)
2. Prerequisites
* [x] Installation of required software
* [x] Text Editor basics
* [x] Command Line basics
* [x] Python - What is it? REPL vs Scripts
* [ ] Asking for Help (WIP) — #32
* [ ] Join the Community — #33
* [x] Computer Science basics, Programming and Python
2. Prerequisites
* [x] Software Installation
* [x] Text and Code Editors
* [x] Command Line Basics
* [x] Python - REPL vs Scripts
3. Git Basics
* [x] Introduction to Git
* [x] Setting up Git and Github
* [x] Git basics
4. Python Foundations
* [x] Output (print), Variables, Core Datatypes
* [x] Math, String operations, Comparisons
* [x] User input, Conditionals
* [x] Git Basics
4. Foundations
* [x] Output, Variables, Core Datatypes
* [x] Arithmetic and String Operations, Comparisons
* [x] User Input and Conditionals
* [x] Loops
* [-] Data Structures
* [-] List
* [-] Tuple
* [-] Set
* [-] Dictionary
* [ ] Functions
* [ ] Project 1
5. Modules, Working with Files and Error Handling
* [ ] Working with multiple files (`__init.py__`, directory structure, etc...)
* [ ] Opening, Closing and Manipulating files
* [ ] Error Handling (`try-except-else-finally`)
* [ ] Custom Exceptions
* [ ] Defensive Coding patterns
* [ ] Working with structured data (`json`, `csv`, etc...)
* [ ] Project 2
6. OOP, Virtual Environments and third party libraries, Working with APIs
* [ ] Classes and OOP
* [ ] Usage and setup of virtual environments
* [ ] Installation of third party libraries
* [ ] Working with APIs
* [ ] Project 3 - *probably: * weather app
7. Working on Projects, Solving bugs, Reading errors
5. Data Structures
* [ ] Lists — #57
* [ ] Tuples — #58
* [ ] Sets — #59
* [ ] Dictionaries — #60
* [ ] Coprehensions (`list`, `dict` `set`) — #61
6. Code Organization
* [ ] Functions — #62
* [ ] Scope and Namespaces (LEGB rule) — #63
* [ ] Type Hints — #64
* [ ] Modules — #65
7. Capstone Project 1
* [ ] Problem Solving — #66
* [ ] Problem Solving - Fizz-Buzz Example — #67
* [ ] Capstone Project - **Hangman game** — #68
8. Errors, Debugging, I/O
* [ ] Understanding Errors and Error Handling
* [ ] `logging` module
* [ ] Debugging tehniques
* [ ] File Handling
9. Capstone Project 2
* [ ] Git Workflow: Branches and Pull Requests
* [ ] Revisiting Project 1 (hangman game):
* Creating branch, adding features, merging back to `main`
* Loading *words* from external file
* Catching and logging errors
* [ ] Capstone Project
10. Intermediate Python
* [ ] Decorators
* [ ] Generators
* [ ] Context Managers
11. Capstone Project 3
* [ ] Capstone Project
12. Built-in and Modules
* [ ] `math` module
* [ ] `json` module
* [ ] `random` module
* [ ] `datetime` module
* [ ] `pathlib` / `os` module
* [ ] `csv` module
* [ ] `sys` module
* [ ] `re` module
13. Third-party Modules
* [ ] Virtual Environments
* [ ] Using `pip` and `requirements.txt`
* [ ] Creating python package - `pyproject.toml` file
* [ ] ? Linting, Formatting, Type Checking (`ruff`, `mypy`) ?
14. Capstone Project 4
* [ ] Clean Code Priciples
* [ ] Capstone Project
* [ ] Testing Your Code (`pytest`)
* [ ] Revisit Project 4
* Develop a testing suite for the project
15. Object Oriented Programming - Part 1
* [ ] Introduction to classes and instances
* [ ] Methods (instance, class, static)
* [ ] Dunder Methods (`__str__`, `__repr__`, etc...)
16. Capstone Project 5
* [ ] Capstone Project
17. Object Oriented Programming - Part 2
* [ ] Four Pillars of Object-Oriented Programming (Encapsulation, Abstraction, Inheritance, Polymorphism)
18. Capstone Project 6
* [ ] Capstone Project
20 changes: 0 additions & 20 deletions bot_challenges/01-inventory-system.md

This file was deleted.

133 changes: 133 additions & 0 deletions scripts/roadmap/sync_discord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
Renders ROADMAP.md into Discord embeds and PATCHes a set of existing
webhook messages, so a Discord channel mirrors the roadmap's current
state. Uses only the standard library — no extra dependency to install
in the workflow.

Requires:
DISCORD_WEBHOOK_URL The webhook's execute URL
(https://discord.com/api/webhooks/<id>/<token>)
DISCORD_MESSAGE_IDS JSON array of message IDs to update, one per
"page" of phases (see PHASES_PER_MESSAGE below)

One-time setup: send an initial message through the webhook manually
(any content) for each page you'll need, note the returned message
IDs, and store them as DISCORD_MESSAGE_IDS. This script only edits
those messages — it never creates new ones — so the roadmap always
lives at the same spot in the channel.
"""

import json
import os
import re
import sys
import urllib.request

ROADMAP_PATH = "ROADMAP.md"
PHASES_PER_MESSAGE = (
9 # 18 phases / 9 = 2 messages; keeps each embed well under Discord's limits
)

STATE_EMOJI = {" ": "⬜", "-": "🚧", "x": "✅"}
WIP_BADGE = "🚧"

PHASE_RE = re.compile(r"^\d+\.\s+(.+)$")
ITEM_RE = re.compile(
rf"^\s*\*\s*\[(?P<check>[ x])\]\s*(?P<badge>{WIP_BADGE}\s*)?(?P<text>.+?)\s*$"
)


def item_state(check: str, badge: str | None) -> str:
if check == "x":
return "x"
return "-" if badge else " "


Phase = tuple[str, list[tuple[str, str]]]


def parse_roadmap(text: str) -> list[Phase]:
phases: list[Phase] = []
current_items: list[tuple[str, str]] = []
current_title: str | None = None

for raw_line in text.splitlines():
phase_match = PHASE_RE.match(raw_line)
if phase_match:
if current_title is not None:
phases.append((current_title, current_items))
current_title = phase_match.group(1)
current_items = []
continue

item_match = ITEM_RE.match(raw_line)
if item_match and current_title is not None:
state = item_state(item_match.group("check"), item_match.group("badge"))
current_items.append((state, item_match.group("text")))

if current_title is not None:
phases.append((current_title, current_items))

return phases


def build_embed(phases: list[Phase]) -> dict:
fields = []
for title, items in phases:
lines = [f"{STATE_EMOJI[state]} {text}" for state, text in items]
value = "\n".join(lines) or "—"
fields.append({"name": title[:256], "value": value[:1024], "inline": False})

return {
"title": "📍 Curriculum Roadmap",
"color": 0x3776AB,
"fields": fields[:25],
}


def patch_message(webhook_url: str, message_id: str, embed: dict) -> None:
url = f"{webhook_url}/messages/{message_id}"
payload = json.dumps({"embeds": [embed]}).encode("utf-8")
req = urllib.request.Request(
url,
data=payload,
method="PATCH",
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
if resp.status not in (200, 204):
print(
f"Unexpected status {resp.status} updating message {message_id}",
file=sys.stderr,
)


def main() -> None:
webhook_url = os.environ["DISCORD_WEBHOOK_URL"]
message_ids = json.loads(os.environ["DISCORD_MESSAGE_IDS"])

with open(ROADMAP_PATH, encoding="utf-8") as f:
phases = parse_roadmap(f.read())

chunks = [
phases[i : i + PHASES_PER_MESSAGE]
for i in range(0, len(phases), PHASES_PER_MESSAGE)
]

if len(chunks) != len(message_ids):
print(
f"Expected {len(chunks)} message(s) for {len(phases)} phases, "
f"but DISCORD_MESSAGE_IDS has {len(message_ids)}. Update the "
f"variable or PHASES_PER_MESSAGE so they match.",
file=sys.stderr,
)
sys.exit(1)

for message_id, chunk in zip(message_ids, chunks):
patch_message(webhook_url, message_id, build_embed(chunk))
print(f"Updated message {message_id} ({len(chunk)} phases)")


if __name__ == "__main__":
main()
Loading
Loading