Skip to content

Commit

Permalink
feat: add environment variable support and Docker configuration\n\n- …
Browse files Browse the repository at this point in the history
…Added .env support for the application using python-dotenv.\n- Updated Dockerfile to copy .env file for Docker configuration.\n- Added configuration module to handle environment variables.\n- Refactored database configuration to use environment variables.\n- Updated README.md to include information about environment variable configuration.\n- Removed Python version backport for importlib.metadata.\n- Added new run_gull_api module for running the app with uvicorn with CLI arguments.\n- Added tests for the new changes.
  • Loading branch information
mdbecker committed Jun 26, 2023
1 parent d943048 commit 858fd20
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 47 deletions.
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ COPY gull_api/ ./gull_api
# Copy the mock LLM cli app to the working directory
COPY echo_args.sh ./

# Copy the .env file to set the DB_URI for Docker
COPY docker.env ./.env

# Expose the port the app runs on
EXPOSE 8000

Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,19 @@ In test mode, an included script echo_args.sh is used instead of a real LLM. Thi
poetry install
```

4. Run the application:
4. Configure Environment Variables (Optional):

`GULL-API` can be configured using environment variables. To do this, create a file named `.env` in the root of the project directory, and set the environment variables there. For example:

```
DB_URI=sqlite:///mydatabase.db
CLI_JSON_PATH=/path/to/cli.json
```

`GULL-API` uses the `python-dotenv` package to load these environment variables when the application starts.


5. Run the application:

```
uvicorn gull_api.main:app --host 0.0.0.0 --port 8000
Expand Down
1 change: 1 addition & 0 deletions docker.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DB_URI=sqlite:////app/data/database.db
8 changes: 2 additions & 6 deletions gull_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
try:
from importlib.metadata import version, PackageNotFoundError
except ImportError:
# For Python versions < 3.8, use the backport module
from importlib_metadata import version, PackageNotFoundError
from importlib.metadata import version, PackageNotFoundError

try:
# Change 'gull-api' to the name of your package as it appears in pyproject.toml
__version__ = version('gull-api')
except PackageNotFoundError:
# Package is not installed
__version__ = None
__version__ = None
9 changes: 9 additions & 0 deletions gull_api/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import os
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

# Define configuration variables
CLI_JSON_PATH = os.getenv("CLI_JSON_PATH", "cli.json")
DB_URI = os.getenv("DB_URI", "sqlite:///./database.db")
11 changes: 2 additions & 9 deletions gull_api/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from sqlalchemy import create_engine, Column, Integer, String, Boolean
import sqlalchemy.orm
from sqlalchemy.exc import SQLAlchemyError
from gull_api import config

Base = sqlalchemy.orm.declarative_base()

Expand All @@ -15,16 +16,8 @@ class APIRequestLog(Base):
error_occurred = Column(Boolean)
error_details = Column(String)

def load_db_config(filename="db_config.json"):
try:
with open(filename) as f:
return json.load(f)
except FileNotFoundError:
return {"db_uri": "sqlite:////app/data/database.db"}

def get_engine():
db_config = load_db_config()
return create_engine(db_config.get("db_uri", "sqlite:///:memory:"))
return create_engine(config.DB_URI)

def get_session_maker(engine=None):
if engine is None:
Expand Down
3 changes: 2 additions & 1 deletion gull_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import subprocess
import asyncio
from gull_api.db import APIRequestLog, SessionManager
from gull_api import config

app = FastAPI()

def get_single_key(dictionary: Dict[str, Any]) -> str:
return list(dictionary.keys())[0]

def load_cli_json():
with open("cli.json", "r") as f:
with open(config.CLI_JSON_PATH, "r") as f:
return json.load(f)

def create_llm_request_model(cli_json: Dict[str, Any]) -> BaseModel:
Expand Down
41 changes: 41 additions & 0 deletions gull_api/run_gull_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import argparse


def create_parser():
parser = argparse.ArgumentParser(description='Run GULL-API using uvicorn.')

# Add arguments similar to what uvicorn accepts
parser.add_argument('--host', default='0.0.0.0', type=str, help='Bind socket to this host (default: 0.0.0.0)')
parser.add_argument('--port', default=8000, type=int, help='Bind socket to this port (default: 8000)')
parser.add_argument('--log-level', default='info', type=str, help='Log level (default: info)')
parser.add_argument('--workers', default=1, type=int, help='Number of worker processes (default: 1)')
parser.add_argument('--reload', action='store_true', help='Enable auto-reload')
parser.add_argument('--reload-dir', default=None, type=str, help='Set reload directories explicitly, instead of using the current working directory')

return parser


def run_uvicorn(args, uvicorn_module):
# Call uvicorn.run with the user-specified options
uvicorn_module.run(
"gull_api.main:app",
host=args.host,
port=args.port,
log_level=args.log_level,
workers=args.workers,
reload=args.reload,
reload_dirs=[args.reload_dir] if args.reload_dir else None
)


def main():
parser = create_parser()
args = parser.parse_args()

# Import uvicorn here so it can be mocked in tests
import uvicorn
run_uvicorn(args, uvicorn)


if __name__ == "__main__": # pragma: no cover
main()
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ SQLAlchemy = "^2.0.16"
fastapi = "^0.97.0"
pydantic = "^1.10.9"
uvicorn = "^0.22.0"
python-dotenv = "^1.0.0"

[tool.poetry.dev-dependencies]
pytest = "^7.3.2"
pytest-asyncio = "^0.21.0"
pytest-cov = "^4.1.0"

[tool.poetry.scripts]
gull-api = 'gull_api.run_gull_api:main'
42 changes: 12 additions & 30 deletions tests/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@
from unittest.mock import patch, MagicMock, mock_open
from sqlalchemy.orm import Session, sessionmaker
import gull_api.db as db
from gull_api import config
from sqlalchemy.exc import SQLAlchemyError


@patch('gull_api.db.create_engine')
@patch('gull_api.db.load_db_config')
def test_get_engine_initialization(mock_load_db_config, mock_create_engine):
def test_get_engine_initialization(mock_create_engine):
mock_engine = MagicMock()
mock_create_engine.return_value = mock_engine

# mocking the load_db_config function to return a dummy db_uri
mock_load_db_config.return_value = {"db_uri": "mock_db_uri"}


# Calling the get_engine function, which should call create_engine with the correct db_uri
returned_engine = db.get_engine()

# Check if the create_engine was called with the correct db_uri
mock_create_engine.assert_called_once_with("mock_db_uri")
mock_create_engine.assert_called_once_with(config.DB_URI)

# Check if the correct engine is returned
assert returned_engine == mock_engine

Expand Down Expand Up @@ -51,16 +47,6 @@ def test_session_manager_with_error():

mock_session.rollback.assert_called_once()

@patch("builtins.open", new_callable=mock_open, read_data='{"db_uri": "test_db_uri"}')
def test_load_db_config_file_exists(mock_open):
result = db.load_db_config()
assert result == {"db_uri": "test_db_uri"}

@patch("builtins.open", side_effect=FileNotFoundError())
def test_load_db_config_file_not_found(mock_open):
result = db.load_db_config()
assert result == {"db_uri": "sqlite:////app/data/database.db"}

@patch('gull_api.db.sqlalchemy.exc.SQLAlchemyError', new_callable=MagicMock)
def test_session_manager_with_context_error(mock_exception):
mock_session = MagicMock(spec=Session)
Expand Down Expand Up @@ -89,25 +75,21 @@ def test_session_close_on_exit(mock_exception):
mock_session.close.assert_called_once() # Session should be closed on exit

@patch('gull_api.db.create_engine')
@patch('gull_api.db.load_db_config')
@patch.object(db.Base.metadata, 'create_all')
def test_get_session_maker_without_engine(mock_create_all, mock_load_db_config, mock_create_engine):
def test_get_session_maker_without_engine(mock_create_all, mock_create_engine):
# Mock the engine
mock_engine = MagicMock()
mock_create_engine.return_value = mock_engine

# Mock the db_config
mock_load_db_config.return_value = {"db_uri": "mock_db_uri"}


# Calling the get_session_maker function without providing an engine
returned_session_maker = db.get_session_maker()

# Check if get_engine was called
mock_create_engine.assert_called_once_with("mock_db_uri")

mock_create_engine.assert_called_once_with(config.DB_URI)
# Check if session_maker has been called with the correct engine
assert isinstance(returned_session_maker, sessionmaker)
assert returned_session_maker.kw['bind'] == mock_engine

# Check if metadata.create_all was called with the correct engine
mock_create_all.assert_called_once_with(bind=mock_engine)
19 changes: 19 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest
import importlib
from importlib.metadata import PackageNotFoundError
import gull_api


def raise_package_not_found_error(name):
raise PackageNotFoundError("Simulated PackageNotFoundError")


def test_package_not_found(monkeypatch):
# Use monkeypatch to override importlib.metadata.version function to raise PackageNotFoundError
monkeypatch.setattr('importlib.metadata.version', raise_package_not_found_error)

# Reload gull_api to force re-execution of its __init__.py with the mocked version function
importlib.reload(gull_api)

# Check that __version__ is set to None when PackageNotFoundError is raised
assert gull_api.__version__ is None
40 changes: 40 additions & 0 deletions tests/test_run_gull_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
from unittest.mock import Mock, patch, ANY
from gull_api.run_gull_api import create_parser, run_uvicorn, main


def test_run_uvicorn():
parser = create_parser()
args = parser.parse_args(['--host', '127.0.0.1', '--port', '8080'])

mock_uvicorn = Mock()

run_uvicorn(args, mock_uvicorn)

mock_uvicorn.run.assert_called_once_with(
"gull_api.main:app",
host='127.0.0.1',
port=8080,
log_level='info',
workers=1,
reload=False,
reload_dirs=None
)


def test_main_function():
with patch('gull_api.run_gull_api.run_uvicorn') as mock_run_uvicorn:
with patch('argparse.ArgumentParser.parse_args') as mock_parse_args:
mock_args = Mock()
mock_args.host = '0.0.0.0'
mock_args.port = 8000
mock_args.log_level = 'info'
mock_args.workers = 1
mock_args.reload = False
mock_args.reload_dir = None
mock_parse_args.return_value = mock_args

main()

# Assert that the run_uvicorn function was called with the correct arguments
mock_run_uvicorn.assert_called_once_with(mock_args, ANY)

0 comments on commit 858fd20

Please sign in to comment.