Skip to content

Commit

Permalink
Merge pull request breatheco-de#1498 from tommygonzaleza/asset-context
Browse files Browse the repository at this point in the history
Asset context
  • Loading branch information
tommygonzaleza authored Nov 13, 2024
2 parents bd26bdb + 269f0b4 commit a9f8ee4
Show file tree
Hide file tree
Showing 12 changed files with 495 additions and 13 deletions.
3 changes: 2 additions & 1 deletion breathecode/payments/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
def get_virtual_consumables() -> list[ConsumableType]:
return [
consumable(
service_item=service_item(service=76, unit_type="unit", how_many=-1),
service_item=service_item(service=48, unit_type="unit", how_many=-1),
service_item=service_item(service=93, unit_type="unit", how_many=-1),
),
]
99 changes: 99 additions & 0 deletions breathecode/registry/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,15 @@ def process_asset_config(asset, config):

if "duration" in config:
asset.duration = config["duration"]

if "template_url" in config:
if asset.asset_type != "PROJECT":
asset.log_error("template-url", "Only asset types projects can have templates")
else:
asset.template_url = config["template_url"]
else:
asset.template_url = None

if "difficulty" in config:
asset.difficulty = config["difficulty"].upper()
if "videoSolutions" in config:
Expand Down Expand Up @@ -895,6 +904,96 @@ def pull_learnpack_asset(github, asset: Asset, override_meta):
return asset


def pull_repo_dependencies(github, asset):
"""
Pulls the main programming languages and their versions from a GitHub repository.
Parameters:
- github: Authenticated GitHub client instance (e.g., from PyGithub).
- asset: Asset object with `get_repo_meta()` to retrieve repo metadata.
- override_meta: Optional metadata to override repository details.
Returns:
- languages: Dictionary of main programming languages and their versions.
"""
org_name, repo_name, branch_name = asset.get_repo_meta()

# Access the repository
repo = github.get_repo(f"{org_name}/{repo_name}")

# Retrieve programming languages from GitHub
languages = repo.get_languages()
if not languages:
return "No languages detected by Github"

# Parse version from dependency files
dependency_files = ["requirements.txt", "pyproject.toml", "Pipfile", "package.json"]
language_versions = {}

for file_name in dependency_files:
try:
content_file = repo.get_contents(file_name, ref=branch_name)
content = content_file.decoded_content.decode("utf-8")
detected_version = detect_language_version(file_name, content)
if detected_version:
language_versions.update(detected_version)
except Exception:
continue

# Combine languages and versions
combined = {lang: language_versions.get(lang, "unknown") for lang in languages}
dependencies_str = ",".join(f"{lang.lower()}={version}" for lang, version in combined.items())
return dependencies_str


def detect_language_version(file_name, content):
import tomli

"""
Detects the programming language version from a dependency file.
Returns:
- Dictionary of language and version detected.
"""
if file_name == "requirements.txt":
# Check for Python version in requirements.txt (e.g., python_version marker)
if "python_version" in content:
return {"Python": extract_python_version(content)}

if file_name == "pyproject.toml":
data = tomli.loads(content)
version = data.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python", None)
if version:
return {"Python": version}

if file_name == "package.json":
import json

data = json.loads(content)
engines = data.get("engines", {})
if "node" in engines:
return {"Node.js": engines["node"]}

if file_name == "Pipfile":

data = tomli.loads(content)
version = data.get("requires", {}).get("python_version", None)
if version:
return {"Python": version}

return {}


def extract_python_version(content):
"""
Extracts Python version from requirements.txt content.
"""
for line in content.splitlines():
if "python_version" in line:
return line.split("python_version")[-1].strip(" ()=")
return "unknown"


def pull_quiz_asset(github, asset: Asset):

logger.debug(f"Sync pull_quiz_asset {asset.slug}")
Expand Down
3 changes: 1 addition & 2 deletions breathecode/registry/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def pull_content_from_github(modeladmin, request, queryset):
assets = queryset.all()
for a in assets:
try:
async_pull_from_github.delay(a.slug, request.user.id)
async_pull_from_github.delay(a.slug, request.user.id, override_meta=True)
# async_pull_from_github(a.slug, request.user.id) # uncomment for testing purposes
except Exception as e:
messages.error(request, a.slug + ": " + str(e))
Expand Down Expand Up @@ -486,7 +486,6 @@ def current_status(self, obj):
"OPTIMIZED": "bg-error",
"PENDING_TRANSLATION": "bg-error",
"PENDING": "bg-warning",
"WARNING": "bg-warning",
"NOT_STARTED": "bg-error",
"NEEDS_RESYNC": "bg-error",
"UNLISTED": "bg-warning",
Expand Down
22 changes: 22 additions & 0 deletions breathecode/registry/management/commands/generate_asset_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import logging

from django.core.management.base import BaseCommand

from ...models import Asset, AssetContext
from ...tasks import async_build_asset_context

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = "Generate asset context for all assets."

def handle(self, *args, **options):
assets = Asset.objects.filter(assetcontext__isnull=True)
for asset in assets:
try:
AssetContext.objects.update_or_create(asset=asset, defaults={"status": "PROCESSING"})
async_build_asset_context.delay(asset.id)

except Exception as e:
AssetContext.objects.update_or_create(asset=asset, defaults={"status": "ERROR", "status_text": e})
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 5.1.1 on 2024-11-12 15:29

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("registry", "0047_asset_preview_in_tutorial_alter_asset_preview"),
]

operations = [
migrations.AddField(
model_name="asset",
name="dependencies",
field=models.CharField(
blank=True,
default=None,
help_text="Automatically calculated based on the package.json, pipfile or alternatives. String like: python=3.10,node=16.0",
max_length=50,
null=True,
),
),
migrations.AddField(
model_name="asset",
name="is_template",
field=models.BooleanField(
db_index=True,
default=False,
help_text="Automatically set by the system, if true, it means that this asset is set as template by another asset",
),
),
migrations.AddField(
model_name="asset",
name="template_url",
field=models.URLField(
blank=True,
default=None,
help_text="This template will be used to open the asset (only applied for projects)",
null=True,
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 5.1.1 on 2024-11-12 18:32

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("registry", "0048_asset_dependencies_asset_is_template_and_more"),
]

operations = [
migrations.RemoveField(
model_name="asset",
name="is_template",
),
migrations.AlterField(
model_name="asset",
name="asset_type",
field=models.CharField(
choices=[
("PROJECT", "Project"),
("STARTER", "Starter Template"),
("EXERCISE", "Exercise"),
("QUIZ", "Quiz"),
("LESSON", "Lesson"),
("VIDEO", "Video"),
("ARTICLE", "Article"),
],
db_index=True,
max_length=20,
),
),
migrations.AlterField(
model_name="asseterrorlog",
name="asset_type",
field=models.CharField(
blank=True,
choices=[
("PROJECT", "Project"),
("STARTER", "Starter Template"),
("EXERCISE", "Exercise"),
("QUIZ", "Quiz"),
("LESSON", "Lesson"),
("VIDEO", "Video"),
("ARTICLE", "Article"),
],
default=None,
max_length=20,
null=True,
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 5.1.2 on 2024-11-13 16:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("registry", "0049_remove_asset_is_template_alter_asset_asset_type_and_more"),
]

operations = [
migrations.AddField(
model_name="assetcontext",
name="status",
field=models.CharField(
choices=[("PENDING", "Pending"), ("PROCESSING", "Processing"), ("DONE", "Done"), ("ERROR", "Error")],
db_index=True,
default="PENDING",
help_text="If pending, it means it hasn't been generated yet, processing means that is being generated at this moment, done means it has been generated",
max_length=20,
),
),
migrations.AddField(
model_name="assetcontext",
name="status_text",
field=models.TextField(
blank=True,
default=None,
help_text="Status details, it may be set automatically if enough error information",
null=True,
),
),
]
24 changes: 24 additions & 0 deletions breathecode/registry/migrations/0051_alter_assetcontext_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.2 on 2024-11-13 22:19

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("registry", "0050_assetcontext_status_assetcontext_status_text"),
]

operations = [
migrations.AlterField(
model_name="assetcontext",
name="status",
field=models.CharField(
choices=[("PENDING", "PENDING"), ("PROCESSING", "PROCESSING"), ("DONE", "DONE"), ("ERROR", "ERROR")],
db_index=True,
default="PENDING",
help_text="If pending, it means it hasn't been generated yet, processing means that is being generated at this moment, done means it has been generated",
max_length=20,
),
),
]
Loading

0 comments on commit a9f8ee4

Please sign in to comment.