Skip to content
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

2221 public approved repos #625

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
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
18 changes: 10 additions & 8 deletions backend/app/app/api/endpoints/abl.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from typing import List, Optional

from fastapi import APIRouter, Depends
from httpx import AsyncClient
from sqlalchemy.orm import Session

from app.core.auth import RequiresRole
from app.core.auth import RequiresRole, active_user
from app.data_models.models import (
ApprovedBook,
RequestApproveBook,
RequestApproveBooks,
Role,
UserSession,
)
from app.db.utils import get_db
from app.service.abl import add_new_entries, get_abl_info_database
from app.github.client import github_client
from app.service.abl import add_to_abl, get_abl_info_database

router = APIRouter()

Expand All @@ -28,15 +29,16 @@ async def get_abl_info(


@router.post("/", dependencies=[Depends(RequiresRole(Role.ADMIN))])
async def add_to_abl(
async def add_to_abl_request(
*,
user: UserSession = Depends(active_user),
db: Session = Depends(get_db),
info: List[RequestApproveBook],
info: RequestApproveBooks,
):
# Creates a new ApprovedBook
# Fetches rex-web release.json
# Removes extraneous ApprovedBook entries for rex-web
# keeps any version that appears in rex-web
# keeps newest version
async with AsyncClient() as client:
return await add_new_entries(db, info, client)
async with github_client(user) as client:
return await add_to_abl(client, db, info)
15 changes: 14 additions & 1 deletion backend/app/app/api/endpoints/github.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import List

from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session

from app.core.auth import active_user
from app.data_models.models import RepositorySummary, UserSession
from app.db.utils import get_db
from app.github import get_book_repository
from app.github.client import github_client
from app.service.user import user_service

router = APIRouter()
Expand All @@ -16,3 +18,14 @@ async def repositories(
user: UserSession = Depends(active_user), db: Session = Depends(get_db)
):
return user_service.get_user_repositories(db, user)


@router.get("/book-repository/{owner}/{name}")
async def get_book_repository_endpoint(
owner: str,
name: str,
version: str = Query("main", description="Commit or ref to use"),
user: UserSession = Depends(active_user),
):
async with github_client(user) as client:
return await get_book_repository(client, name, owner, version)
12 changes: 11 additions & 1 deletion backend/app/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import json
import os
import re
from typing import Any

# DATABASE SETTINGS
POSTGRES_SERVER = os.getenv("POSTGRES_SERVER")
Expand All @@ -19,7 +22,11 @@
STACK_NAME = os.getenv("STACK_NAME")
DEPLOYED_AT = os.getenv("DEPLOYED_AT")

IS_DEV_ENV = STACK_NAME is None or STACK_NAME == "dev"
IS_PROD = (
isinstance(STACK_NAME, str)
and re.search(r"[\s:_-]prod(uction)?$", STACK_NAME, re.I) is not None
)
IS_DEV_ENV = not IS_PROD

if IS_DEV_ENV:
from dotenv import load_dotenv
Expand All @@ -45,3 +52,6 @@
# To encrypt session cookie
SESSION_SECRET = os.getenv("SESSION_SECRET")
ACCESS_TOKEN_EXPIRE_MINUTES = 480

# Optional features
MAKE_REPO_PUBLIC_ON_APPROVAL = IS_PROD
20 changes: 13 additions & 7 deletions backend/app/app/data_models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,22 @@ class BaseApprovedBook(BaseModel):
uuid: str


class RequestApproveBook(BaseApprovedBook):
class ApprovedBookWithCodeVersion(BaseApprovedBook):
code_version: str


class ApprovedBook(RequestApproveBook):
class RepositoryBase(BaseModel):
name: str
owner: str


class RequestApproveBooks(BaseModel):
books_to_approve: List[ApprovedBookWithCodeVersion]
repository: RepositoryBase
make_repo_public: bool = False


class ApprovedBook(ApprovedBookWithCodeVersion):
created_at: datetime
committed_at: datetime
repository_name: str
Expand Down Expand Up @@ -150,11 +161,6 @@ def get(self, key: str, default: Any = None) -> Any:
return default


class RepositoryBase(BaseModel):
name: str
owner: str


class Repository(RepositoryBase):
class Config:
orm_mode = True
Expand Down
104 changes: 80 additions & 24 deletions backend/app/app/github/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import json
from datetime import datetime
from typing import Any, Dict, List, Tuple
from urllib.parse import urlencode

import httpx
import yaml
from lxml import etree

from app.core.auth import get_user_role
Expand Down Expand Up @@ -39,8 +42,7 @@ async def get_book_repository(
query = f"""
query {{
repository(name: "{repo_name}", owner: "{repo_owner}") {{
databaseId
viewerPermission
{GitHubRepo.graphql_query()}
object(expression: "{version}") {{
oid
... on Commit {{
Expand All @@ -59,25 +61,20 @@ async def get_book_repository(
"""
payload = await graphql(client, query)
repository = payload["data"]["repository"]
repo = GitHubRepo(
name=repo_name,
database_id=repository["databaseId"],
viewer_permission=repository["viewerPermission"],
)
repo = GitHubRepo.from_node(repository)
commit = repository["object"]
if commit is None: # pragma: no cover
raise CustomBaseError(f"Could not find commit '{version}'")
commit_sha = commit["oid"]
commit_timestamp = commit["committedDate"]
fixed_timestamp = f"{commit_timestamp[:-1]}+00:00"
books_xml = commit["file"]["object"]["text"]
meta = parse_xml_doc(books_xml)

books = [
{k: get_attr(el, k) for k in ("slug", "style")}
for el in xpath_some(meta, "//*[local-name()='book']")
]
return (repo, commit_sha, datetime.fromisoformat(fixed_timestamp), books)
return (repo, commit_sha, datetime.fromisoformat(commit_timestamp), books)


async def get_collections(
Expand Down Expand Up @@ -120,6 +117,66 @@ async def get_collections(
}


def contents_path(owner: str, repo: str, path: str, version: str | None = None):
url = f"https://api.github.com/repos/{owner}/{repo}/contents/{path}"
query = {}
if version is not None:
query["ref"] = version
if query:
url += f"?{urlencode(query)}"
return url


async def get_contents(
client: AuthenticatedClient, owner: str, repo: str, path: str, version: str
):
response = await client.get(contents_path(owner, repo, path, version))
response.raise_for_status()
return response.json()


async def get_text_contents(
client: AuthenticatedClient, owner: str, repo: str, path: str, version: str
):
data = await get_contents(client, owner, repo, path, version)
base64content = data["content"]
return base64.b64decode(base64content)


async def make_repo_public(client: AuthenticatedClient, owner: str, repo: str):
branch = "main"
path = ".github/settings.yml"
commit_message = "Change repository visibility to public"
file_exists = True
try:
contents = await get_contents(client, owner, repo, path, branch)
serialized = base64.b64decode(contents["content"]).decode("utf-8")
except httpx.HTTPStatusError as hse:
# TODO: Maybe use settings from template when it is missing?
raise CustomBaseError("Could not get settings file") from hse
settings = yaml.load(serialized, yaml.SafeLoader)
repository = settings.setdefault("repository", {})
if repository.get("private") in (None, True):
repository["private"] = False
else:
raise CustomBaseError(
f"{owner}/{repo}: repository may already be public or the "
"settings.yml file may contain an invalid value for 'private'"
)
serialized = yaml.dump(settings, indent=2)
await push_to_github(
client=client,
path=path,
content=serialized,
owner=owner,
repo=repo,
branch=branch,
commit_message=commit_message,
file_exists=file_exists,
sha=contents["sha"],
)


async def push_to_github(
client: AuthenticatedClient,
path: str,
Expand All @@ -129,9 +186,8 @@ async def push_to_github(
branch: str,
commit_message: str,
file_exists=True,
sha: str = "",
):
url = f"https://api.github.com/repos/{owner}/{repo}/contents/{path}"

base64content = base64.b64encode(content.encode("utf-8"))
message = {
"message": commit_message,
Expand All @@ -140,17 +196,16 @@ async def push_to_github(
}

if file_exists:
response = await client.get(url + "?ref=" + branch)
response.raise_for_status()
data = response.json()
message["sha"] = data["sha"]

if base64content.decode("utf-8").strip() == data["content"].strip():
raise CustomBaseError("No changes to push")
if sha == "":
data = await get_contents(client, owner, repo, path, branch)
sha = data["sha"]
if base64content.decode("utf-8").strip() == data["content"].strip():
raise CustomBaseError("No changes to push")
message["sha"] = sha

response = await client.put(
url,
data=json.dumps(message),
contents_path(owner, repo, path),
content=json.dumps(message),
headers={
**client.headers,
"Content-Type": "application/json",
Expand Down Expand Up @@ -179,9 +234,7 @@ async def get_user_repositories(
edges {{
node {{
... on Repository {{
name
databaseId
viewerPermission
{repo_query}
}}
}}
}}
Expand All @@ -196,7 +249,10 @@ async def get_user_repositories(
"https://api.github.com/graphql",
json={
"query": query.format(
query_args=",".join(":".join(i) for i in query_args.items())
query_args=",".join(
":".join(i) for i in query_args.items()
),
repo_query=GitHubRepo.graphql_query(),
)
},
)
Expand Down
44 changes: 29 additions & 15 deletions backend/app/app/github/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import re
from enum import Enum

from pydantic import BaseModel


def snake_to_camel(s: str):
return re.sub(r"_[a-z]", lambda match: match.group(0)[-1].upper(), s)


def camel_to_snake(s: str):
return re.sub(
r"[a-z][A-Z]",
lambda match: "_".join(c.lower() for c in match.group(0)),
s,
)


class RepositoryPermission(int, Enum):
ADMIN = 1
MAINTAIN = 2
Expand All @@ -11,21 +24,22 @@ class RepositoryPermission(int, Enum):
WRITE = 5


class GitHubRepo(BaseModel):
name: str
database_id: str
viewer_permission: str
class GraphQLModel(BaseModel):
@classmethod
def graphql_fields(cls):
return [snake_to_camel(k) for k in cls.__fields__]

@classmethod
def graphql_query(cls):
return "\n".join(cls.graphql_fields())

@classmethod
def from_node(cls, node: dict):
def to_snake_case(s: str):
ret = []
for c in s:
if c.isupper():
ret.append("_")
ret.append(c.lower())
else:
ret.append(c)
return "".join(ret)

return cls(**{to_snake_case(k): v for k, v in node.items()})
return cls(**{camel_to_snake(k): v for k, v in node.items()})


class GitHubRepo(GraphQLModel):
name: str
database_id: str
viewer_permission: str
visibility: str
Loading
Loading