From 29915e36ea8c3432c64ad5538f24ca93eee6d9a8 Mon Sep 17 00:00:00 2001 From: Tyler Nullmeier Date: Fri, 25 Oct 2024 15:56:45 -0500 Subject: [PATCH] Get rex release version from github repository --- backend/app/app/core/config.py | 5 ++++ backend/app/app/github/api.py | 37 ++++++++++++++++++++++++++ backend/app/app/service/abl.py | 24 ++++++++++------- backend/app/tests/unit/test_abl.py | 42 ++++++++++-------------------- 4 files changed, 70 insertions(+), 38 deletions(-) diff --git a/backend/app/app/core/config.py b/backend/app/app/core/config.py index 16945218..a2b98b77 100644 --- a/backend/app/app/core/config.py +++ b/backend/app/app/core/config.py @@ -36,6 +36,11 @@ REX_WEB_RELEASE_URL = os.getenv( "REX_WEB_RELEASE_URL", "https://openstax.org/rex/release.json" ) +REX_WEB_ARCHIVE_CONFIG = os.getenv( + "REX_WEB_ARCHIVE_CONFIG", + # owner:repo:path + "openstax:rex-web:src/config.archive-url.json", +) # GITHUB OAUTH CLIENT_ID = os.getenv("GITHUB_OAUTH_ID") diff --git a/backend/app/app/github/api.py b/backend/app/app/github/api.py index 05a7c51f..b0724f6a 100644 --- a/backend/app/app/github/api.py +++ b/backend/app/app/github/api.py @@ -2,6 +2,7 @@ import json from datetime import datetime from typing import Any, Dict, List, Tuple +from urllib.parse import urlencode from lxml import etree @@ -120,6 +121,42 @@ async def get_collections( } +def normpath(*parts: str): + return tuple(p.strip("/") for p in parts) + + +def build_url(*parts: str, **kwargs: str | None): + path = "/".join(("https://api.github.com", *normpath(*parts))) + kwargs = {k: v for k, v in kwargs.items() if v} + if kwargs: + path = "?".join((path, urlencode(kwargs))) + return path + + +async def get_file_response( + client: AuthenticatedClient, + owner: str, + repo: str, + path: str, + ref: str | None = None, +): + url = build_url("repos", owner, repo, "contents", path, ref=ref) + response = await client.get(url) + response.raise_for_status() + return response + + +async def get_file_content( + client: AuthenticatedClient, + owner: str, + repo: str, + path: str, + ref: str | None = None, +): + payload = (await get_file_response(client, owner, repo, path, ref)).json() + return base64.b64decode(payload["content"]) + + async def push_to_github( client: AuthenticatedClient, path: str, diff --git a/backend/app/app/service/abl.py b/backend/app/app/service/abl.py index 8e470ec8..f09b16ed 100644 --- a/backend/app/app/service/abl.py +++ b/backend/app/app/service/abl.py @@ -1,4 +1,4 @@ -import re +import json from typing import Any, Dict, List, Optional from httpx import AsyncClient, HTTPStatusError @@ -16,6 +16,7 @@ Consumer, Repository, ) +from app.github.api import get_file_content async def get_rex_release_json(client: AsyncClient): @@ -38,15 +39,18 @@ async def get_rex_books(client: AsyncClient): async def get_rex_release_version(client: AsyncClient): - rex_release = await get_rex_release_json(client) - archive_url = rex_release.get("archiveUrl", "").strip() - if archive_url == "": - raise CustomBaseError("Could not find valid REX archive URL") - # Search for: %Y%m%d.%H%M%S - version_matches = re.findall(r"\d{8}\.\d{6}", archive_url) - if len(version_matches) != 1: - raise CustomBaseError("Could not determine REX release version") - return version_matches[0] + owner, repo, path = config.REX_WEB_ARCHIVE_CONFIG.split(":", 2) + try: + raw_contents = await get_file_content(client, owner, repo, path) + except HTTPStatusError as he: + raise CustomBaseError( + f"Failed to fetch rex release version: {he.response.status_code}" + ) from he + contents = json.loads(raw_contents) + version = contents.get("REACT_APP_ARCHIVE", "").strip() + if not version: + raise CustomBaseError("Could not find valid REX version") + return version def get_rex_book_versions(rex_books: Dict[str, Any], book_uuids: List[str]): diff --git a/backend/app/tests/unit/test_abl.py b/backend/app/tests/unit/test_abl.py index 3c2af07f..b1c8188a 100644 --- a/backend/app/tests/unit/test_abl.py +++ b/backend/app/tests/unit/test_abl.py @@ -1,3 +1,6 @@ +import base64 +import json + import pytest from app.core import config @@ -216,11 +219,16 @@ def test_get_rex_book_versions(rex_books, book_uuids, expected): @pytest.mark.asyncio async def test_get_rex_release_version(mock_http_client): + url = "https://api.github.com/repos/openstax/rex-web/contents/src/config.archive-url.json" + fake_api_response = { + "content": base64.b64encode( + json.dumps({"REACT_APP_ARCHIVE": "20240101.000001"}).encode() + ).decode() + } + # GIVEN: A valid response mock_client: MockAsyncClient = mock_http_client( - get={ - config.REX_WEB_RELEASE_URL: {"archiveUrl": "a/b/20240101.000001/c"} - } + get={url: fake_api_response} ) # WHEN: A request is made version = await get_rex_release_version(mock_client) @@ -231,39 +239,17 @@ async def test_get_rex_release_version(mock_http_client): # THEN: The expected version is matched assert version == "20240101.000001" - # GIVEN: An invalid response with zero matches - mock_client: MockAsyncClient = mock_http_client( - get={config.REX_WEB_RELEASE_URL: {"archiveUrl": "a/b/c"}} - ) - # WHEN: A request is made - # THEN: An error is raised - with pytest.raises(CustomBaseError) as cbe: - await get_rex_release_version(mock_client) - assert len(mock_client.responses) == 1 - assert cbe.match("Could not determine REX release version") - # GIVEN: An invalid response with more than one match + # GIVEN: An invalid response mock_client: MockAsyncClient = mock_http_client( get={ - config.REX_WEB_RELEASE_URL: { - "archiveUrl": "a/b/c/20240101.000001/d/12345678.123456" - } + url: {"content": base64.b64encode(json.dumps({}).encode()).decode()} } ) # WHEN: A request is made # THEN: An error is raised with pytest.raises(CustomBaseError) as cbe: await get_rex_release_version(mock_client) - assert len(mock_client.responses) == 1 - assert cbe.match("Could not determine REX release version") - # GIVEN: An invalid response - mock_client: MockAsyncClient = mock_http_client( - get={config.REX_WEB_RELEASE_URL: {}} - ) - # WHEN: A request is made - # THEN: An error is raised - with pytest.raises(CustomBaseError) as cbe: - await get_rex_release_version(mock_client) - assert cbe.match("Could not find valid REX archive URL") + assert cbe.match("Could not find valid REX version") # GIVEN: A no response mock_client: MockAsyncClient = mock_http_client() # WHEN: A request is made