diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..d6eafde --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,24 @@ +FROM mcr.microsoft.com/devcontainers/python:3.12 + +# Install GitHub CLI +RUN rm -f /etc/apt/sources.list.d/yarn*.list || true && \ + rm -f /etc/apt/trusted.gpg.d/yarn*.gpg || true && \ + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \ + echo "deb [signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list && \ + apt-get update && \ + apt-get install -y gh && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Railway CLI +RUN curl -fsSL https://railway.app/install.sh | bash + +# Configure Git +RUN git config --global pull.ff true + +# Install Poetry +RUN pip install poetry && \ + poetry config virtualenvs.in-project true && \ + poetry config virtualenvs.create true + +# Install poetry-shell plugin +RUN poetry self add poetry-plugin-shell diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0647e03..b859de3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,8 @@ { "name": "Python Flask DevContainer", - "image": "mcr.microsoft.com/devcontainers/python:3.12", + "build": { + "dockerfile": "Dockerfile" + }, "customizations": { "vscode": { "settings": { @@ -12,6 +14,6 @@ ] } }, - "postCreateCommand": "bash .devcontainer/post-create.sh", + "postCreateCommand": "poetry install --no-root", "remoteUser": "vscode" -} +} \ No newline at end of file diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh deleted file mode 100644 index fbc3c52..0000000 --- a/.devcontainer/post-create.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# Configure Git to use fast-forward pulls only -git config pull.ff true - -# Install Poetry -pip install poetry - -# Install poetry-shell plugin -poetry self add poetry-plugin-shell - -# Install GitHub CLI -# Remove any problematic third-party apt sources (e.g., yarn) that may have broken GPG keys -rm -f /etc/apt/sources.list.d/yarn*.list || true -rm -f /etc/apt/trusted.gpg.d/yarn*.gpg || true - -# Import GitHub CLI GPG key and add repository -curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg -echo "deb [signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list -apt-get update -apt-get install -y gh - -# Install project dependencies -poetry install - -# Activate poetry venv (for pylance auto-import) -poetry shell diff --git a/campus_python/api/v1/submissions.py b/campus_python/api/v1/submissions.py index 069d74e..2b6d82d 100644 --- a/campus_python/api/v1/submissions.py +++ b/campus_python/api/v1/submissions.py @@ -173,7 +173,7 @@ def update( def submit(self) -> None: """Finalize/submit this submission (marks submitted_at timestamp).""" - path = f"{self.make_path()}/submit" + path = self.make_path("submit", end_slash=False) resp = self.client.post(path) resp.raise_for_status() return None diff --git a/campus_python/api/v1/timetable.py b/campus_python/api/v1/timetable.py index 75b8eac..bc0c527 100644 --- a/campus_python/api/v1/timetable.py +++ b/campus_python/api/v1/timetable.py @@ -62,13 +62,13 @@ def __getitem__(self, timetable_id: str) -> "Timetables.Timetable": def get_current(self) -> str: """Get the timetable ID of current timetable.""" - resp = self.client.get(self.make_path("current")) + resp = self.client.get(self.make_path("current", end_slash=False)) resp.raise_for_status() return resp.json()["value"] def get_next(self) -> str: """Get the timetable ID of next timetable.""" - resp = self.client.get(self.make_path("next")) + resp = self.client.get(self.make_path("next", end_slash=False)) resp.raise_for_status() return resp.json()["value"] @@ -79,7 +79,7 @@ def set_current(self, timetable_id: str) -> None: timetable_id: ID of the timetable to set as current """ resp = self.client.put( - self.make_path("current"), + self.make_path("current", end_slash=False), json={"value": timetable_id} ) resp.raise_for_status() @@ -91,7 +91,7 @@ def set_next(self, timetable_id: str) -> None: timetable_id: ID of the timetable to set as next """ resp = self.client.put( - self.make_path("next"), + self.make_path("next", end_slash=False), json={"value": timetable_id} ) resp.raise_for_status() diff --git a/campus_python/auth/v1/clients.py b/campus_python/auth/v1/clients.py index b5e18fa..41aa44a 100644 --- a/campus_python/auth/v1/clients.py +++ b/campus_python/auth/v1/clients.py @@ -66,7 +66,7 @@ def revoke(self) -> str: Returns: The newly generated client secret. """ - resp = self.client.post(self.make_path("revoke")) + resp = self.client.post(self.make_path("revoke", end_slash=False)) # Raise error if status code is not 2XX or 3XX resp.raise_for_status() return resp.json()["secret"] @@ -105,7 +105,7 @@ def grant( vault: str, permission: int, ) -> JsonDict: - resp = self.client.post(self.make_path("grant"), json={ + resp = self.client.post(self.make_path("grant", end_slash=False), json={ "vault": vault, "permission": permission, }) @@ -118,7 +118,7 @@ def revoke( vault: str, permission: int, ) -> JsonDict: - resp = self.client.post(self.make_path("revoke"), json={ + resp = self.client.post(self.make_path("revoke", end_slash=False), json={ "vault": vault, "permission": permission, }) diff --git a/campus_python/auth/v1/sessions.py b/campus_python/auth/v1/sessions.py index 7bc746f..39654ee 100644 --- a/campus_python/auth/v1/sessions.py +++ b/campus_python/auth/v1/sessions.py @@ -32,7 +32,7 @@ def __getitem__(self, session_id: str) -> "CampusSessions.Session": def from_code(self, code: str) -> campus.model.AuthSession: """Get a session using authorization code.""" resp = self.client.post( - self.make_path("authorization_code"), + self.make_path("authorization_code", end_slash=False), json={"code": code} ) resp.raise_for_status() @@ -85,7 +85,7 @@ def sweep(self, at_time: datetime | str | None = None) -> int: json_data["at_time"] = schema.DateTime(at_time) case None: json_data["at_time"] = schema.DateTime.utcnow() - resp = self.client.post(self.make_path("sweep"), json=json_data) + resp = self.client.post(self.make_path("sweep", end_slash=False), json=json_data) resp.raise_for_status() return int(resp.json()["swept_count"]) diff --git a/campus_python/auth/v1/users.py b/campus_python/auth/v1/users.py index 4b4ade5..678007b 100644 --- a/campus_python/auth/v1/users.py +++ b/campus_python/auth/v1/users.py @@ -41,7 +41,7 @@ class User(Resource): """Single vault user resource.""" def activate(self) -> campus.model.User: - resp = self.client.post(self.make_path("activate")) + resp = self.client.post(self.make_path("activate", end_slash=False)) resp.raise_for_status() return campus.model.User.from_resource(resp.json()) diff --git a/campus_python/interface.py b/campus_python/interface.py index 1a53b0f..f3c5782 100644 --- a/campus_python/interface.py +++ b/campus_python/interface.py @@ -90,16 +90,19 @@ def client(self) -> JsonClient: return self.root.client raise AttributeError(f"No client defined for {self}") - def make_path(self, part: str | None = None) -> str: + def make_path(self, part: str | None = None, end_slash: bool = True) -> str: """Create a full path for a resource collection. - Resource collection paths always end in a /. + Args: + part: Optional sub-resource or action path. + end_slash: Whether to add a trailing slash (default: True for collections). + + Returns: + Full path for the resource collection or sub-resource. """ if part: - return ( - f"/{self.root.make_path(self.path).strip(SLASH)}" - f"/{part.strip(SLASH)}/" - ) + base = f"/{self.root.make_path(self.path).strip(SLASH)}/{part.strip(SLASH)}" + return f"{base}/" if end_slash else base else: return f"/{self.root.make_path(self.path).strip(SLASH)}/" diff --git a/pyproject.toml b/pyproject.toml index 395d097..b16091f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "campus-api-python" -version = "0.1.60" +version = "0.1.61" description = "Campus API for Python projects" authors = ["NYJC Computing "]