-
Notifications
You must be signed in to change notification settings - Fork 2
Add workflow for syncing course content and utility script for downloading course content #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
pabo99
merged 4 commits into
omegaup:main
from
iqbalcodes6602:Github-action-for-downloading-the-content-from-omegaup.com
Jul 16, 2025
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
8ead7af
Add workflow for syncing course content and utility script for downlo…
iqbalcodes6602 2ea0b78
Refactor course folder handling to ensure clean state before downloads
iqbalcodes6602 3c26df7
Update course alias to "Curso-de-Python-FutureLabs"
iqbalcodes6602 8697022
Update GitHub Actions checkout action to version 4
iqbalcodes6602 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
name: Sync Course Content on PR | ||
|
||
on: | ||
push: | ||
branches: [sync-course] | ||
pull_request: | ||
branches: [sync-course] | ||
|
||
env: | ||
OMEGAUP_API_TOKEN: ${{ secrets.OMEGAUP_API_TOKEN }} | ||
GIT_USERNAME: ${{ github.actor }} | ||
|
||
jobs: | ||
sync-content: | ||
runs-on: ubuntu-22.04 | ||
|
||
steps: | ||
- name: Checkout repository with PR branch | ||
uses: actions/checkout@v4 | ||
with: | ||
ref: ${{ github.head_ref }} | ||
fetch-depth: 0 | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@v5 | ||
with: | ||
python-version: '3.8' | ||
|
||
- name: Set up pipenv | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install pipenv==2023.11.15 | ||
|
||
- name: Install Python dependencies with pipenv | ||
run: | | ||
cd utils | ||
pipenv install | ||
|
||
- name: Run course download script | ||
working-directory: utils | ||
run: pipenv run python3 download_and_sync_courses.py | ||
|
||
- name: Commit and push changes | ||
id: commit | ||
run: | | ||
git config --global user.name "github-actions" | ||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
git add . | ||
# Only commit if there are changes | ||
if git diff --staged --quiet; then | ||
echo "No changes to commit" | ||
else | ||
git commit -m "📝 Auto-sync: Downloaded latest OmegaUp content" | ||
git push | ||
fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"requests":[ | ||
{ | ||
"omegaup_usernmae": "", | ||
"github_username": "", | ||
"requests_number": 0 | ||
} | ||
] | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import argparse | ||
import json | ||
import logging | ||
import os | ||
import zipfile | ||
from typing import Dict, Any | ||
from urllib.parse import urlparse, urljoin | ||
import omegaup.api | ||
import shutil | ||
import http.client | ||
import ssl | ||
|
||
# Create SSL context that skips certificate verification | ||
context = ssl._create_unverified_context() | ||
|
||
logging.basicConfig(level=logging.INFO) | ||
LOG = logging.getLogger(__name__) | ||
|
||
API_CLIENT = None | ||
BASE_URL = None | ||
|
||
# 👇 Add your course aliases here | ||
COURSE_ALIASES = [ | ||
"Curso-de-Python-FutureLabs" | ||
] | ||
|
||
BASE_COURSE_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "Courses")) | ||
|
||
|
||
def handle_input(): | ||
global BASE_URL, API_TOKEN | ||
parser = argparse.ArgumentParser(description="Download and extract problems from multiple course assignments") | ||
parser.add_argument("--url", default="https://omegaup.com", help="omegaUp base URL") | ||
parser.add_argument("--api-token", type=str, default=os.environ.get("OMEGAUP_API_TOKEN"), required=("OMEGAUP_API_TOKEN" not in os.environ)) | ||
args = parser.parse_args() | ||
BASE_URL = args.url | ||
return args.api_token | ||
|
||
|
||
def get_json(endpoint: str, params: Dict[str, str]) -> Dict[str, Any]: | ||
return API_CLIENT.query(endpoint, params) | ||
|
||
|
||
def sanitize_filename(name: str) -> str: | ||
return "".join(c for c in name if c.isalnum() or c in " -_").strip() | ||
|
||
|
||
def get_course_details(course_alias: str, course_base_folder: str) -> Dict[str, Any]: | ||
details = get_json("/api/course/details/", {"alias": course_alias}) | ||
course_folder = os.path.join(course_base_folder, course_alias) | ||
os.makedirs(course_folder, exist_ok=True) | ||
|
||
# Save course_settings.json | ||
course_settings_path = os.path.join(course_folder, "course_settings.json") | ||
with open(course_settings_path, "w", encoding="utf-8") as f: | ||
json.dump(details, f, indent=2, ensure_ascii=False) | ||
|
||
return details | ||
|
||
|
||
def get_assignments(course_alias: str): | ||
return get_json("/api/course/listAssignments/", {"course_alias": course_alias})["assignments"] | ||
|
||
|
||
def get_assignment_details(course_alias: str, assignment_alias: str): | ||
return get_json("/api/course/assignmentDetails/", { | ||
"course": course_alias, | ||
"assignment": assignment_alias | ||
}) | ||
|
||
|
||
def download_and_unzip(problem_alias: str, assignment_folder: str): | ||
try: | ||
download_url = urljoin(BASE_URL, f"/api/problem/download/problem_alias/{problem_alias}/") | ||
parsed_url = urlparse(download_url) | ||
conn = http.client.HTTPSConnection(parsed_url.hostname, context=context) | ||
|
||
headers = {'Authorization': f'token {API_CLIENT.api_token}'} | ||
path = parsed_url.path | ||
|
||
conn.request("GET", path, headers=headers) | ||
response = conn.getresponse() | ||
|
||
if response.status == 404: | ||
LOG.warning(f"⚠️ Problem '{problem_alias}' not found or access denied (404).") | ||
return | ||
elif response.status != 200: | ||
LOG.error(f"❌ Failed to download '{problem_alias}'. HTTP status: {response.status}") | ||
return | ||
|
||
problem_folder = os.path.join(assignment_folder, sanitize_filename(problem_alias)) | ||
os.makedirs(problem_folder, exist_ok=True) | ||
|
||
zip_path = os.path.join(problem_folder, f"{problem_alias}.zip") | ||
with open(zip_path, "wb") as f: | ||
while True: | ||
chunk = response.read(8192) | ||
if not chunk: | ||
break | ||
f.write(chunk) | ||
|
||
try: | ||
with zipfile.ZipFile(zip_path, "r") as zip_ref: | ||
zip_ref.extractall(problem_folder) | ||
os.remove(zip_path) | ||
LOG.info(f"✅ Extracted: {problem_alias} → {problem_folder}") | ||
except zipfile.BadZipFile: | ||
LOG.error(f"❌ Failed to unzip: {zip_path}") | ||
return | ||
|
||
settings_path = os.path.join(problem_folder, "settings.json") | ||
if os.path.exists(settings_path): | ||
try: | ||
with open(settings_path, "r+", encoding="utf-8") as f: | ||
settings = json.load(f) | ||
settings["alias"] = problem_alias | ||
settings["title"] = problem_alias | ||
f.seek(0) | ||
json.dump(settings, f, indent=2, ensure_ascii=False) | ||
f.truncate() | ||
LOG.info(f"🛠️ Updated settings.json with alias: {problem_alias}") | ||
except Exception as e: | ||
LOG.warning(f"⚠️ Failed to update settings.json for '{problem_alias}': {e}") | ||
else: | ||
LOG.warning(f"⚠️ No settings.json found for '{problem_alias}'") | ||
|
||
except Exception as e: | ||
LOG.error(f"❌ Failed to download '{problem_alias}': {e}") | ||
|
||
|
||
def main(): | ||
global API_CLIENT | ||
api_token = handle_input() | ||
API_CLIENT = omegaup.api.Client(api_token=api_token, url=BASE_URL) | ||
|
||
if os.path.exists(BASE_COURSE_FOLDER): | ||
LOG.warning("Delete existing course folder to avoid conflicts") | ||
shutil.rmtree(BASE_COURSE_FOLDER) | ||
|
||
os.makedirs(BASE_COURSE_FOLDER, exist_ok=True) | ||
all_problems = [] | ||
|
||
for course_alias in COURSE_ALIASES: | ||
LOG.info(f"📘 Starting course: {course_alias}") | ||
try: | ||
course_details = get_course_details(course_alias, BASE_COURSE_FOLDER) | ||
assignments = get_assignments(course_alias) | ||
|
||
if not assignments: | ||
LOG.warning(f"No assignments found in {course_alias}.") | ||
continue | ||
|
||
course_folder = os.path.join(BASE_COURSE_FOLDER, course_alias) | ||
|
||
for assignment in assignments: | ||
assignment_alias = assignment["alias"] | ||
assignment_name = assignment["name"] | ||
LOG.info(f"📂 Processing assignment: {assignment_name} ({assignment_alias})") | ||
|
||
try: | ||
details = get_assignment_details(course_alias, assignment_alias) | ||
assignment_folder = os.path.join(course_folder, assignment_alias) | ||
os.makedirs(assignment_folder, exist_ok=True) | ||
|
||
assignment_settings_path = os.path.join(assignment_folder, "assignment_settings.json") | ||
with open(assignment_settings_path, "w", encoding="utf-8") as f: | ||
json.dump(details, f, indent=2, ensure_ascii=False) | ||
|
||
problems = details.get("problems", []) | ||
|
||
for problem in problems: | ||
try: | ||
download_and_unzip(problem["alias"], assignment_folder) | ||
rel_path = os.path.join( | ||
"Courses", course_alias, assignment_alias, sanitize_filename(problem["alias"]) | ||
) | ||
LOG.info(f"📂 Added problem path: {rel_path}") | ||
all_problems.append({"path": rel_path}) | ||
except Exception as e: | ||
LOG.error(f"❌ Error while processing problem '{problem['alias']}': {e}") | ||
|
||
except Exception as e: | ||
LOG.error(f"❌ Failed to process assignment '{assignment_alias}': {e}") | ||
|
||
except Exception as e: | ||
LOG.error(f"❌ Failed to process course '{course_alias}': {e}") | ||
|
||
# ✅ Write problems.json | ||
problems_json_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "problems.json")) | ||
with open(problems_json_path, "w", encoding="utf-8") as f: | ||
LOG.info(f"Writing problems.json to {problems_json_path}") | ||
json.dump({"problems": all_problems}, f, indent=2, ensure_ascii=False) | ||
LOG.info("📝 Created problems.json with all problem paths.") | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.