Skip to content
Merged
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
12 changes: 0 additions & 12 deletions .env.test

This file was deleted.

18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,25 @@ Lenny is a free, open source Lending System for Libraries.

## Installation

First, copy the file `lenny_TEMPLATE.env` to `lenny.env` (gitignored) and edit it to have the correct values (such as desired psql credentials).
```
docker/configure.sh # generates lenny.env
docker compose -p lenny up -d --build
```

Second, run docker compose:
Navigate to localhost:8080 or whatever `$LENNY_PORT` you specified in your `lenny.env`

You may enter the API container via:

```
docker compose -p lenny up -d --build
docker exec -it lenny_api bash
```

Finally, navigate to localhost:8080 or whatever `$LENNY_PORT` you specified in your `lenny.env`
## Rebuilding

```
docker compose -p lenny down
docker compose -p lenny up -d --build
```

## Pilot

Expand Down
5 changes: 4 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ services:
- s3
env_file: lenny.env
environment:
- LENNY_PORT=1337
- LENNY_PORT=1337 # When in docker, run Lenny as 1337, reverse proxies from nginx
- S3_ENDPOINT=s3:9000
volumes:
- .:/app
networks:
- lenny_network

Expand Down
10 changes: 7 additions & 3 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Set only non-sensitive environment variables
# Set non-sensitive environment variables
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app

# Expose ports (FastAPI, PostgreSQL, MinIO API, MinIO Console)
EXPOSE 8080 5432 9000 9001

COPY ./lenny/ /app/lenny/
RUN chmod +x /app/lenny/app.py

# Setup NGINX
RUN apt-get update && apt-get install -y nginx
RUN apt-get update && apt-get install -y \
nginx \
libpq-dev \
postgresql-client
RUN adduser --disabled-password --gecos "" nginx

COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./docker/nginx/conf.d/lenny.conf /etc/nginx/conf.d/lenny.conf

Expand Down
54 changes: 54 additions & 0 deletions docker/configure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/bash

ENV_FILE="lenny.env"

# Exit if the file already exists
if [ -f "$ENV_FILE" ]; then
echo "$ENV_FILE already exists. No changes made."
exit 0
fi

# Use environment variables if they are set, otherwise provide defaults or generate secure values
LENNY_DOMAIN="${LENNY_DOMAIN:-localhost}"
LENNY_HOST="${LENNY_HOST:-0.0.0.0}"
LENNY_PORT="${LENNY_PORT:-8080}"
LENNY_WORKERS="${LENNY_WORKERS:-1}"
LENNY_LOG_LEVEL="${LENNY_LOG_LEVEL:-debug}"
LENNY_RELOAD="${LENNY_RELOAD:-1}"
LENNY_SSL_CRT="${LENNY_SSL_CRT:-}"
LENNY_SSL_KEY="${LENNY_SSL_KEY:-}"

DB_USER="${POSTGRES_USER:-librarian}"
DB_HOST="${POSTGRES_HOST:-127.0.0.1}"
DB_PORT="${POSTGRES_PORT:-5432}"
DB_PASSWORD="${POSTGRES_PASSWORD:-$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 32)}"
DB_NAME="${DB_NAME:-lenny}"

MINIO_ROOT_USER="${MINIO_ROOT_USER:-$(openssl rand -base64 30 | tr -dc 'A-Za-z0-9' | head -c 20)}"
MINIO_ROOT_PASSWORD="${MINIO_ROOT_PASSWORD:-$(openssl rand -base64 60 | tr -dc 'A-Za-z0-9' | head -c 40)}"
MINIO_SECURE="${MINIO_SECURE:-false}"

# Write to lenny.env
cat <<EOF > "$ENV_FILE"
# API App (FastAPI)
LENNY_DOMAIN=$LENNY_DOMAIN
LENNY_HOST=$LENNY_HOST
LENNY_PORT=$LENNY_PORT
LENNY_WORKERS=$LENNY_WORKERS
LENNY_LOG_LEVEL=$LENNY_LOG_LEVEL
LENNY_RELOAD=$LENNY_RELOAD
LENNY_SSL_CRT=$LENNY_SSL_CRT
LENNY_SSL_KEY=$LENNY_SSL_KEY

# DB (PostgreSQL)
DB_USER=$DB_USER
DB_HOST=$DB_HOST
DB_PORT=$DB_PORT
DB_PASSWORD=$DB_PASSWORD
DB_NAME=$DB_NAME

# MinIO (S3)
MINIO_ROOT_USER=$MINIO_ROOT_USER
MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD
MINIO_SECURE=$MINIO_SECURE
EOF
1 change: 1 addition & 0 deletions docker/nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ http {
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
client_max_body_size 50M;
include /etc/nginx/conf.d/*.conf;
}

Expand Down
9 changes: 7 additions & 2 deletions lenny/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
#!/usr/bin/env python3

import uvicorn
from fastapi import FastAPI
from lenny.routes import api
from lenny.configs import OPTIONS
from lenny import __version__ as VERSION

app = FastAPI()
app = FastAPI(
title="Lenny API",
description="Lenny: A Free, Open Source Lending System for Libraries",
version=VERSION,
)

app.include_router(api.router, prefix="/v1/api")

if __name__ == "__main__":
import uvicorn
uvicorn.run("lenny.app:app", **OPTIONS)
39 changes: 18 additions & 21 deletions lenny/configs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@
"""

import os
from dotenv import load_dotenv

# Load environment variables from .env.test file
load_dotenv(dotenv_path=".env.test", override= True)

TESTING = os.environ.get("TESTING", "False").lower() == "true"
# Determine environment
TESTING = os.getenv("TESTING", "false").lower() == "true"

# API server configuration
DOMAIN = os.environ.get('LENNY_DOMAIN', '127.0.0.1')
Expand All @@ -37,27 +35,26 @@
OPTIONS['ssl_keyfile'] = SSL_KEY
OPTIONS['ssl_certfile'] = SSL_CRT

DB_CONFIG = {
'user': os.environ.get('DB_USER', 'postgres'),
'password': os.environ.get('DB_PASSWORD'),
'host': os.environ.get('DB_HOST', 'localhost'),
'port': int(os.environ.get('DB_PORT', '5432')),
'dbname': os.environ.get('DB_NAME', 'lenny'),
}

# Database configuration
if TESTING:
DB_URI = "sqlite:///:memory:"
else:
DB_CONFIG = {
'user': os.environ.get('POSTGRES_USER', 'lenny'),
'password': os.environ.get('POSTGRES_PASSWORD', 'lennytest'),
'host': os.environ.get('POSTGRES_HOST', 'postgres'),
'port': int(os.environ.get('POSTGRES_PORT', '5432')),
'dbname': os.environ.get('POSTGRES_DB', 'lending_system'),
}
DB_URI = 'postgresql+psycopg2://{user}:{password}@{host}:{port}/{dbname}'.format(**DB_CONFIG)
DB_URI = (
"sqlite:///:memory:" if TESTING else
'postgresql+psycopg2://{user}:{password}@{host}:{port}/{dbname}'.format(**DB_CONFIG)
)

# MinIO configuration
S3_CONFIG = {
'access_key': os.environ.get('S3_ACCESS_KEY', os.environ.get('MINIO_ROOT_USER')),
'secret_key': os.environ.get('S3_SECRET_KEY', os.environ.get('MINIO_ROOT_PASSWORD')),
'endpoint': f"{os.environ.get('MINIO_HOST', 'minio')}:{os.environ.get('MINIO_PORT', '9000')}",
'secure': False,
'public_bucket': os.environ.get('MINIO_BUCKET', 'lenny') + "-public",
'protected_bucket': os.environ.get('MINIO_BUCKET', 'lenny') + "-protected",
'endpoint': os.environ.get('S3_ENDPOINT'),
'access_key': os.environ.get('MINIO_ROOT_USER'),
'secret_key': os.environ.get('MINIO_ROOT_PASSWORD'),
'secure': os.environ.get('S3_SECURE', 'false').lower() == 'true',
}

__all__ = ['DOMAIN', 'HOST', 'PORT', 'DEBUG', 'OPTIONS', 'DB_URI', 'DB_CONFIG','S3_CONFIG', 'TESTING']
56 changes: 36 additions & 20 deletions lenny/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,42 @@
"""

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from lenny.configs import DB_URI, TESTING
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from minio import Minio

from lenny.configs import DB_URI, S3_CONFIG, DEBUG

Base = declarative_base()

if TESTING:
engine = create_engine("sqlite:///:memory:", connect_args={'check_same_thread': False})
Base.metadata.creat_all(bind=engine)
else:
engine = create_engine(DB_URI)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db():
session = SessionLocal()
try:
yield session
finally:
session.close()
if TESTING:
engine.dispose()

__all__ = ["Base", "SessionLocal", "get_db", "engine"]
# Configure Database Connection
engine = create_engine(DB_URI, echo=DEBUG, client_encoding='utf8')
db = scoped_session(sessionmaker(bind=engine, autocommit=False, autoflush=False))

# Configure S3 Connection
s3 = Minio(
endpoint=S3_CONFIG["endpoint"],
access_key=S3_CONFIG["access_key"],
secret_key=S3_CONFIG["secret_key"],
secure=S3_CONFIG["secure"],
)

# Instantiate s3 buckets
for bucket_name in ["bookshelf-public", "bookshelf-encrypted"]:
if not s3.bucket_exists(bucket_name):
s3.make_bucket(bucket_name)
# Setting public read-only policy
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": ["s3:GetObject"],
"Resource": [f"arn:aws:s3:::{bucket_name}/*"]
}
]
}
s3.set_bucket_policy(bucket_name, json.dumps(policy))

__all__ = ["Base", "db", "s3", "engine"]
24 changes: 0 additions & 24 deletions lenny_TEMPLATE.env

This file was deleted.

31 changes: 26 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
annotated-types==0.7.0
anyio==4.9.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
certifi==2025.4.26
cffi==1.17.1
charset-normalizer==3.4.2
click==8.2.0
dotenv==0.9.9
fastapi==0.115.4
uvicorn==0.32.0
pydantic==2.9.2
SQLAlchemy==2.0.39
greenlet==3.2.2
h11==0.16.0
idna==3.10
minio==7.2.9
psycopg2-binary==2.9.10
pyyaml==6.0.2
pycparser==2.22
pycryptodome==3.22.0
pydantic==2.9.2
pydantic_core==2.23.4
python-dotenv==1.1.0
PyYAML==6.0.2
requests==2.32.3
python-dotenv==1.1.0
typing_extensions==4.12.2
minio==7.2.9
pytest==7.4.2
psycopg2==2.9.10
psycopg2==2.9.10
sniffio==1.3.1
SQLAlchemy==2.0.39
starlette==0.41.3
typing_extensions==4.12.2
urllib3==2.4.0
uvicorn==0.32.0