Skip to content

Commit

Permalink
Merge pull request #1455 from breatheco-de/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
tommygonzaleza authored Sep 9, 2024
2 parents 42f3c91 + 430a328 commit 6a4d279
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 30 deletions.
57 changes: 34 additions & 23 deletions .gitpod.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
FROM gitpod/workspace-postgres:latest
# https://github.com/gitpod-io/workspace-images/blob/main/chunks/tool-postgresql/Dockerfile
FROM gitpod/workspace-python-3.12

# Dazzle does not rebuild a layer until one of its lines are changed. Increase this counter to rebuild this layer.
ENV TRIGGER_REBUILD=4
ENV PGWORKSPACE="/workspace/.pgsql"
ENV PGDATA="$PGWORKSPACE/data"

# Install PostgreSQL
RUN sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \
wget --quiet -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - && \
echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-18 main" | sudo tee /etc/apt/sources.list.d/llvm.list && \
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - && \
sudo install-packages postgresql-16 postgresql-contrib-16 redis-server

# Setup PostgreSQL server for user gitpod
ENV PATH="/usr/lib/postgresql/16/bin:$PATH"

SHELL ["/usr/bin/bash", "-c"]
RUN PGDATA="${PGDATA//\/workspace/$HOME}" \
&& mkdir -p ~/.pg_ctl/bin ~/.pg_ctl/sockets $PGDATA \
&& initdb -D $PGDATA \
&& printf '#!/bin/bash\npg_ctl -D $PGDATA -l ~/.pg_ctl/log -o "-k ~/.pg_ctl/sockets" start\n' > ~/.pg_ctl/bin/pg_start \
&& printf '#!/bin/bash\npg_ctl -D $PGDATA -l ~/.pg_ctl/log -o "-k ~/.pg_ctl/sockets" stop\n' > ~/.pg_ctl/bin/pg_stop \
&& chmod +x ~/.pg_ctl/bin/*
ENV PATH="$HOME/.pg_ctl/bin:$PATH"
ENV DATABASE_URL="postgresql://gitpod@localhost"
ENV PGHOSTADDR="127.0.0.1"
ENV PGDATABASE="postgres"
COPY --chown=gitpod:gitpod postgresql-hook.bash $HOME/.bashrc.d/200-postgresql-launch

# RUN pyenv install 3.12.3 && pyenv global 3.12.3
# RUN pip install pipenv

SHELL ["/bin/bash", "-c"]

RUN sudo apt-get update \
&& sudo apt-get update \
&& sudo apt-get install -y redis-server \
&& sudo apt-get clean \
&& sudo rm -rf /var/cache/apt/* /var/lib/apt/lists/* /tmp/*



# That Gitpod install pyenv for me? no, thanks
# WORKDIR /home/gitpod/
# RUN rm .pyenv -Rf
# RUN rm .gp_pyenv.d -Rf
# RUN curl https://pyenv.run | bash


# RUN pyenv update && pyenv install 3.12.2 && pyenv global 3.12.2
RUN pyenv install 3.12.2 && pyenv global 3.12.2
RUN pip install pipenv yapf

# remove PIP_USER environment
USER gitpod

RUN if ! grep -q "export PIP_USER=no" "$HOME/.bashrc"; then printf '%s\n' "export PIP_USER=no" >> "$HOME/.bashrc"; fi
RUN echo "" >> $HOME/.bashrc
RUN echo "unset DATABASE_URL" >> $HOME/.bashrc
Expand Down
12 changes: 5 additions & 7 deletions .gitpod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,17 @@ ports:
tasks:
- command: redis-server
- init: |
pyenv global 3.12.2 &&
python -m scripts.install &&
gp sync-done deps
command: pyenv global 3.12.2 && pipenv run celery
command: pipenv run celery
- init: gp sync-await deps
command: >
pyenv global 3.12.2 &&
(psql -U gitpod -c 'CREATE DATABASE breathecode;' || true) &&
(psql -U gitpod -c 'CREATE EXTENSION unaccent;' -d breathecode || true) &&
pipenv run migrate &&
pipenv run python manage.py loaddata breathecode/*/fixtures/dev_*.json &&
pipenv run python manage.py create_academy_roles &&
pipenv run start;
CACHE=0 pipenv run migrate &&
CACHE=0 pipenv run python manage.py loaddata breathecode/*/fixtures/dev_*.json &&
CACHE=0 pipenv run python manage.py create_academy_roles &&
CACHE=1 pipenv run start;
- init: gp sync-await deps

vscode:
Expand Down
178 changes: 178 additions & 0 deletions breathecode/assignments/tests/urls/tests_completion_job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""
Test /answer
"""

import json
import random
from unittest.mock import MagicMock, call, patch

import aiohttp
import pytest
import requests
from django.urls.base import reverse_lazy
from linked_services.django.actions import reset_app_cache
from linked_services.django.service import Service
from rest_framework import status
from rest_framework.test import APIClient

from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode


@pytest.fixture(autouse=True)
def setup(db):
reset_app_cache()
yield


class StreamReaderMock:

def __init__(self, data):
self.data = data

async def read(self):
return self.data


class ResponseMock:

def __init__(self, data, status=200, headers={}):
self.content = data
self.status = status
self.headers = headers

async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc, tb):
pass

def __enter__(self):
return self

def __exit__(self, exc_type, exc, tb):
pass


@pytest.fixture(autouse=True)
def patch_post(monkeypatch):

def handler(expected, code, headers):

reader = StreamReaderMock(json.dumps(expected).encode())
monkeypatch.setattr("aiohttp.ClientSession.post", MagicMock(return_value=ResponseMock(reader, code, headers)))

yield handler


@pytest.fixture
def get_jwt(bc: Breathecode, monkeypatch):
token = bc.random.string(lower=True, upper=True, symbol=True, number=True, size=20)
monkeypatch.setattr("linked_services.django.actions.get_jwt", MagicMock(return_value=token))
yield token


# When: no auth
# Then: response 401
def test_no_auth(bc: Breathecode, client: APIClient):
url = reverse_lazy("assignments:completion_job", kwargs={"task_id": 1})
response = client.post(url)

json = response.json()
expected = {"detail": "Authentication credentials were not provided.", "status_code": 401}

assert json == expected
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert bc.database.list_of("assignments.Task") == []


# When: no task
# Then: response 404
def test_task_not_found(bc: Breathecode, client: APIClient):
url = reverse_lazy("assignments:completion_job", kwargs={"task_id": 1})
model = bc.database.create(profile_academy=1)
client.force_authenticate(model.user)
response = client.post(url)

json = response.json()
expected = {"detail": "task-not-found", "status_code": 404}

assert json == expected
assert response.status_code == status.HTTP_404_NOT_FOUND
assert bc.database.list_of("assignments.Task") == []


# When: no asset
# Then: response 404
def test_asset_not_found(bc: Breathecode, client: APIClient):
url = reverse_lazy("assignments:completion_job", kwargs={"task_id": 1})
model = bc.database.create(profile_academy=1, task=1)
client.force_authenticate(model.user)
response = client.post(url)

json = response.json()
expected = {"detail": "asset-not-found", "status_code": 404}

assert json == expected
assert response.status_code == status.HTTP_404_NOT_FOUND
assert bc.database.list_of("assignments.Task") == [bc.format.to_dict(model.task)]


# When: auth and asset
# Then: response 200
def test_with_asset_and_task(bc: Breathecode, client: APIClient, patch_post, get_jwt):
url = reverse_lazy("assignments:completion_job", kwargs={"task_id": 1})
model = bc.database.create(
profile_academy=1,
cohort=1,
syllabus_version=1,
syllabus={"name": "syllabus"},
task={"associated_slug": "slayer"},
asset={"slug": "slayer", "asset_type": "LESSON"},
app={"slug": "rigobot"},
)
client.force_authenticate(model.user)

expected = {
"id": 62,
"status": "PENDING",
"status_text": None,
"template": {"id": 5, "name": "Create post from document"},
"inputs": {
"asset_type": "LESSON",
"title": "Learnpack",
"syllabus_name": "Full-Stack Software Developer",
"asset_markdown_body": "",
},
"started_at": "2024-09-06T20:07:31.668065Z",
}

headers = {"Content-Type": "application/json"}

patch_post(expected, 201, headers)
response = client.post(url, format="json")

body = {
"inputs": {
"asset_type": model.task.task_type,
"title": model.task.title,
"syllabus_name": "syllabus",
"asset_mardown_body": None,
},
"include_organization_brief": False,
"include_purpose_objective": True,
"execute_async": False,
"just_format": True,
}

assert aiohttp.ClientSession.post.call_args_list == [
call(
f"{model.app.app_url}/v1/prompting/completion/linked/5/",
json=body,
data=None,
headers={"Authorization": f"Link App=breathecode,Token={get_jwt}"},
)
]

assert response.getvalue().decode("utf-8") == json.dumps(expected)
assert response.status_code == status.HTTP_201_CREATED
assert bc.database.list_of("assignments.Task") == [bc.format.to_dict(model.task)]
2 changes: 2 additions & 0 deletions breathecode/assignments/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
sync_cohort_tasks_view,
AssignmentTelemetryView,
FinalProjectCohortView,
CompletionJobView,
SyncTasksView,
)

Expand Down Expand Up @@ -73,4 +74,5 @@
path("task/<int:task_id>/attachment", TaskMeAttachmentView.as_view(), name="task_id_attachment"),
path("task/<int:task_id>", TaskMeView.as_view(), name="task_id"),
path("sync/cohort/<int:cohort_id>/task", sync_cohort_tasks_view, name="sync_cohort_id_task"),
path("completion_job/<int:task_id>", CompletionJobView.as_view(), name="completion_job"),
]
35 changes: 35 additions & 0 deletions breathecode/assignments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from breathecode.assignments.permissions.consumers import code_revision_service
from breathecode.authenticate.actions import aget_user_language, get_user_language
from breathecode.authenticate.models import ProfileAcademy, Token
from breathecode.registry.models import Asset
from breathecode.services.learnpack import LearnPack
from breathecode.utils import GenerateLookupsMixin, capable_of, num_to_roman, response_207
from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions
Expand Down Expand Up @@ -1005,6 +1006,40 @@ def put(self, request, task_id):
return Response(item.subtasks)


class CompletionJobView(APIView):
@sync_to_async
def get_task_syllabus(self, task):

return task.cohort.syllabus_version.syllabus.name

async def post(self, request, task_id):
task = await Task.objects.filter(id=task_id).afirst()
if task is None:
raise ValidationException("Task not found", code=404, slug="task-not-found")

asset = await Asset.objects.filter(slug=task.associated_slug).afirst()
if asset is None:
raise ValidationException("Asset not found", code=404, slug="asset-not-found")

syllabus_name = await self.get_task_syllabus(task)

data = {
"inputs": {
"asset_type": task.task_type,
"title": task.title,
"syllabus_name": syllabus_name,
"asset_mardown_body": Asset.decode(asset.readme),
},
"include_organization_brief": False,
"include_purpose_objective": True,
"execute_async": False,
"just_format": True,
}

async with Service("rigobot", request.user.id, proxy=True) as s:
return await s.post("/v1/prompting/completion/linked/5/", json=data)


class MeCodeRevisionView(APIView):

@sync_to_async
Expand Down
27 changes: 27 additions & 0 deletions postgresql-hook.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Auto-start PostgreSQL server
(
if mkdir /tmp/.pgsql_lock 2>/dev/null; then {
target="${PGWORKSPACE}"
source="${target//\/workspace/$HOME}"

if test -e "$source"; then {

if test ! -e "$target"; then {
mv "$source" "$target"
}; fi

if ! [[ "$(pg_ctl status)" =~ PID ]]; then {
printf 'INFO: %s\n' "Executing command: pg_start"
pg_start
trap "pg_stop" TERM EXIT
exec {sfd}<> <(:)
printf 'INFO: %s\n' \
"Please create another terminal" \
"this one is monitoring postgres server for gracefully shutting down when needed"
until read -r -t 3600 -u $sfd; do continue; done
}; fi

}; fi
}; fi &
)

0 comments on commit 6a4d279

Please sign in to comment.