-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/basic pytests #12
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
Changes from all commits
4041b0e
639f6e9
d127e5e
1516d1a
043b87c
937753b
f609825
d35891b
e3300c1
c9b2b60
206fd6f
8050e72
0352eea
bba01c0
ba0c60c
75cb26e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,13 @@ | ||
| { | ||
| "version": "0.2.0", | ||
| "configurations": [ | ||
| { | ||
| "name": "Python Debugger: Current File", | ||
| "type": "debugpy", | ||
| "request": "launch", | ||
| "program": "${file}", | ||
| "console": "integratedTerminal" | ||
| }, | ||
| { | ||
| "name": "Attach to Python Functions", | ||
| "type": "python", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code Review:
Improvements:
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,5 +5,10 @@ | |
| // "azureFunctions.pythonVenv": "${workspaceFolder}/backend_azure_function/.venv_azure_func", | ||
| "azureFunctions.projectLanguage": "Python", | ||
| "azureFunctions.projectRuntime": "~4", | ||
| "debug.internalConsoleOptions": "neverOpen" | ||
| "debug.internalConsoleOptions": "neverOpen", | ||
| "python.testing.pytestArgs": [ | ||
| "backend" | ||
| ], | ||
| "python.testing.unittestEnabled": false, | ||
| "python.testing.pytestEnabled": true | ||
| } | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,11 +38,41 @@ GPTeasers is a webapp that generates quiz-style questions based on the topic you | |
| 3. Azure Container Apps: Once triggered, the FastAPI containers communicates with the OpenAI API, sending requests and receiving responses. | ||
| 4. OpenAI API: Processes the request and sends back a response. | ||
|
|
||
| ## Contribute 🤲 | ||
| ## Docker Compose Setup for Local Testing | ||
|
|
||
| Love **GPTeasers**? Want to make it even better? We welcome contributions! | ||
| This project uses Docker Compose to run both the FastAPI backend and the frontend services locally. | ||
|
|
||
| 1. **Fork** this repo 🍴. | ||
| 2. Make your changes 🛠️. | ||
| 3. Submit a **pull request** 👥. | ||
| ### Services | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code Review and Suggestions:Bugs/Risks:
Improvements/Suggestions:
Enhancements could improve readability and ensure that contributors have all necessary details to run tests effectively and understand the contribution process better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| - **fastapi_generate_quiz**: | ||
| The FastAPI backend that serves the GPTeasers API. This container is responsible for handling requests from the frontend and interacting with the OpenAI API to generate quizzes. | ||
|
|
||
| - **frontend**: | ||
| A static frontend application. Although the site is hosted on GitHub Pages, this container allows you to test it locally. | ||
|
|
||
| ### Running Locally | ||
|
|
||
| 1. **Set Environment Variables** | ||
| Ensure that the `OPENAI_API_KEY` is set in your environment or in a `.env` file at the project root: | ||
| ```sh | ||
| export OPENAI_API_KEY=your_openai_api_key_here | ||
| ``` | ||
| or create a `.env` file with: | ||
| ``` | ||
| OPENAI_API_KEY=your_openai_api_key_here | ||
| ``` | ||
|
|
||
| 2. **Build and Run the Containers** | ||
| From the project root, run: | ||
| ```sh | ||
| docker-compose up --build | ||
| ``` | ||
| This command builds both the backend and frontend images and starts the containers. | ||
|
|
||
| 3. **Access the Services** | ||
| - **Backend API (FastAPI)**: | ||
| Access via [http://localhost:8000](http://localhost:8000) | ||
| - **Frontend**: | ||
| Access via [http://localhost:8080](http://localhost:8080) | ||
|
|
||
| By following these steps, you can easily test both your backend API and your static frontend locally using Docker Compose. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code Review:
Overall, the code patch provides a clear and concise guide for setting up and running the project locally using Docker Compose. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,4 +66,41 @@ To debug locally, follow these steps: | |
| docker push ghcr.io/djsaunders1997/fastapi_generate_quiz:latest | ||
| ``` | ||
|
|
||
|
|
||
| ## Running Tests | ||
|
|
||
| Our test suite is divided into **unit tests** and **integration tests**. | ||
|
|
||
| - **Unit Tests:** | ||
| These tests use mocks to simulate API responses. They run quickly and do not require real API calls. | ||
|
|
||
| - **Integration Tests:** | ||
| These tests make real API calls (e.g., to the OpenAI API) and require a valid API key. They are intended to be run manually or in a staging environment. | ||
|
|
||
| ### Default Behavior | ||
|
|
||
| By default, integration tests are **excluded** from the test run. This is achieved by configuring `pytest` in our `pytest.ini` file (located in the `backend` directory): | ||
|
|
||
| ```ini | ||
| [pytest] | ||
| markers = | ||
| integration: mark test as an integration test. | ||
| addopts = -m "not integration" | ||
| ``` | ||
|
|
||
| This configuration tells `pytest` to skip any test marked with `@pytest.mark.integration` when you run: | ||
|
|
||
| ```bash | ||
| pytest -v | ||
| ``` | ||
|
|
||
| ### Running Integration Tests | ||
|
|
||
| To run the integration tests, override the default marker filter by using the `-m` option: | ||
|
|
||
| ```bash | ||
| pytest -m integration | ||
| ``` | ||
|
|
||
| > **Note:** Integration tests make real API calls and require the `OPENAI_API_KEY` environment variable to be set. Make sure you have this environment variable configured before running these tests. | ||
|
|
||
| --- | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The provided code change adds documentation regarding testing procedures in a project. Here are some observations and suggestions for improvement:
Overall, the code change appears to be well-documented and provides valuable information for developers working on the project, covering the basics of testing procedures effectively. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,10 @@ | ||
| # Example of openai streaming | ||
| # Example of openai streaming | ||
| # https://platform.openai.com/docs/api-reference/streaming | ||
| import logging | ||
| from generate_quiz import QuizGenerator | ||
| from generate_image import generate_image | ||
| from generate_image import ImageGenerator | ||
| from fastapi import FastAPI, Request | ||
| from fastapi.responses import (StreamingResponse, JSONResponse) | ||
| from fastapi.responses import StreamingResponse, JSONResponse | ||
| from fastapi.middleware.cors import CORSMiddleware | ||
|
|
||
| # Copy Azure Docs Example | ||
|
|
@@ -20,9 +20,10 @@ | |
| allow_headers=["*"], # Allows all headers | ||
| ) | ||
|
|
||
|
|
||
| @app.get("/GenerateQuiz") | ||
| async def generate_quiz_endpoint(request: Request) -> JSONResponse: | ||
| """ | ||
| """ | ||
| FastAPI App to generate an image based on a provided prompt. | ||
|
|
||
| The function expects a 'prompt' parameter in the HTTP request query | ||
|
|
@@ -42,9 +43,11 @@ async def generate_quiz_endpoint(request: Request) -> JSONResponse: | |
| difficulty = request.query_params.get("difficulty") | ||
| n_questions = request.query_params.get("n_questions") | ||
|
|
||
| logging.info(f"Python HTTP trigger function processed a request with {topic=} {difficulty=}, {n_questions=}.") | ||
| logging.info( | ||
| f"Python HTTP trigger function processed a request with {topic=} {difficulty=}, {n_questions=}." | ||
| ) | ||
|
|
||
| # If either 'topic' or 'difficulty' is not provided in the request, | ||
| # If either 'topic' or 'difficulty' is not provided in the request, | ||
| # the function will return an error message and a 400 status code. | ||
| # n_questions is optional | ||
| if not topic or not difficulty: | ||
|
|
@@ -58,7 +61,7 @@ async def generate_quiz_endpoint(request: Request) -> JSONResponse: | |
| # Set default value if not set | ||
| if not n_questions: | ||
| n_questions = 10 | ||
|
|
||
| logging.info( | ||
| f"Generating quiz for topic: {topic} with difficulty: {difficulty} with number of questions: {n_questions}" | ||
| ) | ||
|
|
@@ -72,9 +75,10 @@ async def generate_quiz_endpoint(request: Request) -> JSONResponse: | |
|
|
||
| return StreamingResponse(generator, media_type="text/event-stream") | ||
|
|
||
|
|
||
| @app.get("/GenerateImage") | ||
| async def generate_image_endpoint(request: Request) -> JSONResponse: | ||
| """ | ||
| """ | ||
| FastAPI App to generate an image based on a provided prompt. | ||
|
|
||
| The function expects a 'prompt' parameter in the HTTP request query | ||
|
|
@@ -100,7 +104,8 @@ async def generate_image_endpoint(request: Request) -> JSONResponse: | |
| return JSONResponse(content={"error": error_message}, status_code=400) | ||
|
|
||
| logging.info(f"Received prompt: {prompt}") | ||
| image_url = generate_image(prompt) | ||
| image_generator = ImageGenerator() | ||
| image_url = image_generator.generate_image(prompt) | ||
|
|
||
| if image_url is None: | ||
| error_message = "Error - Image generation failed." | ||
|
|
@@ -111,7 +116,8 @@ async def generate_image_endpoint(request: Request) -> JSONResponse: | |
| logging.info(f"Generated image for prompt {prompt}: {image_url}") | ||
| return JSONResponse(content={"image_url": image_url}, status_code=200) | ||
|
|
||
|
|
||
| # Run with uvicorn fastapi_generate_quiz:app --reload --host 0.0.0.0 --port 8000 --log-level debug | ||
| # Access with curl "http://localhost:8000/GenerateQuiz?topic=UK%20History&difficulty=easy&n_questions=3" | ||
| # Access with curl "http://localhost:8000/GenerateImage?prompt=A%20Juicy%20Burger" | ||
| # This simple example works! | ||
| # This simple example works! | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code Review:
Overall, the code appears to be on the right track but could benefit from improvements in error handling, logging, and input validation. Ensure that the application is robust and can handle unexpected scenarios gracefully. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,53 +1,92 @@ | ||
| from openai import OpenAI | ||
|
|
||
| import logging | ||
| import os | ||
| import logging | ||
| from typing import Optional | ||
| from openai import OpenAI | ||
|
|
||
| # Set up logging | ||
| logger = logging.getLogger(__name__) | ||
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') | ||
|
|
||
| # Set up OpenAI API key from environment variables | ||
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") | ||
| if not OPENAI_API_KEY: | ||
| raise ValueError( | ||
| "Environment variable OPENAI_API_KEY is not set. " | ||
| "Please ensure it's set and try again." | ||
| ) | ||
| logging.basicConfig( | ||
| level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" | ||
| ) | ||
|
|
||
|
|
||
| class ImageGenerator: | ||
| @classmethod | ||
| def get_api_key_from_env(cls) -> str: | ||
| """Retrieves the OpenAI API key from environment variables. | ||
|
|
||
| Returns: | ||
| str: The API key from the environment variable OPENAI_API_KEY. | ||
|
|
||
| Raises: | ||
| ValueError: If the environment variable is not set or empty. | ||
| """ | ||
| api_key = os.getenv("OPENAI_API_KEY") | ||
| if not api_key: | ||
| raise ValueError( | ||
| "Environment variable OPENAI_API_KEY is not set. " | ||
| "Please ensure it's set and try again." | ||
| ) | ||
| return api_key | ||
|
|
||
| def __init__(self, api_key: Optional[str] = None): | ||
| """Initialises the ImageGenerator. | ||
|
|
||
| If `api_key` is not provided, it is retrieved from the environment | ||
| using `get_api_key_from_env`. | ||
|
|
||
| client = OpenAI(api_key=OPENAI_API_KEY) | ||
| Args: | ||
| api_key (str, optional): The OpenAI API key to use. Defaults to None. | ||
| """ | ||
| if api_key is None: | ||
| api_key = self.get_api_key_from_env() | ||
|
|
||
| self.client = OpenAI(api_key=api_key) | ||
|
|
||
| def generate_image(prompt: str, n: int = 1, size: str = "256x256") -> str: | ||
| """ | ||
| Generates an image using OpenAI's Image API based on a given prompt. | ||
| def generate_image( | ||
| self, prompt: str, n: int = 1, size: str = "256x256" | ||
| ) -> Optional[str]: | ||
| """Generates an image based on the provided prompt. | ||
|
|
||
| Parameters: | ||
| - prompt (str): The textual description for the image to be generated. | ||
| - n (int): The number of images to generate. Default is 1. | ||
| - size (str): The size of the generated image. Default is "256x256". | ||
| Args: | ||
| prompt (str): The textual description for the image to be generated. | ||
| n (int, optional): The number of images to generate. Defaults to 1. | ||
| size (str, optional): The size of the generated image. Defaults to "256x256". | ||
|
|
||
| Returns: | ||
| - str: URL of generated image, in JSON dict with key URL | ||
| Returns: | ||
| Optional[str]: The URL of the generated image if successful, | ||
| or `None` if an error occurred. | ||
| """ | ||
| logger.info(f"Generating image with prompt: {prompt=}") | ||
| image_url = self._get_image_url(prompt, n, size) | ||
| logger.info(f"Generated image URL: {image_url}") | ||
| return image_url | ||
|
|
||
| Raises: | ||
| - openai.error.OpenAIError: If there's an error in the request. | ||
| """ | ||
| def _get_image_url(self, prompt: str, n: int, size: str) -> Optional[str]: | ||
| """Makes the API call to generate images using OpenAI and returns the URL. | ||
|
|
||
| logging.info(f"{prompt=}") | ||
| Args: | ||
| prompt (str): The textual description for the image to be generated. | ||
| n (int): The number of images to generate. | ||
| size (str): The size of the generated image (e.g., "256x256"). | ||
|
|
||
| try: | ||
| response = client.images.generate(prompt=prompt, n=n, size=size) | ||
| return response.data[0].url | ||
| except Exception as e: | ||
| logger.error(f"Non-OpenAI Error when calling OpenAI api: {e}") | ||
| return None | ||
| Returns: | ||
| Optional[str]: The URL of the first generated image, | ||
| or `None` if an error occurred. | ||
| """ | ||
| try: | ||
| response = self.client.images.generate(prompt=prompt, n=n, size=size) | ||
| return response.data[0].url | ||
| except Exception as e: | ||
| logger.error(f"Error when calling OpenAI API: {e}") | ||
| return None | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| image_description = ( | ||
| "Crested Gecko showcasing its distinct crests and coloration. Pixel Art" | ||
| # Example usage: | ||
| image_generator = ( | ||
| ImageGenerator() | ||
| ) # Uses environment variable if no API key is provided | ||
| prompt_text = ( | ||
| "Crested Gecko showcasing its distinct crests and colouration. Pixel Art" | ||
| ) | ||
| image_url = generate_image(image_description) | ||
| if image_url: | ||
| print(f"Generated Image URL: {image_url}") | ||
| image_url = image_generator.generate_image(prompt_text) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a brief code review for the provided patch:
Improvements:
Suggestions:
Bug Risk:
Overall, the code review looks good, covers essential aspects, and enhances the CI process by including linting, type checking, and testing steps.