<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[let's do AI]]></title><description><![CDATA[this and that about AI, LLMs, agentic workflows, tools and everything else]]></description><link>https://www.letsdoai.org</link><image><url>https://substackcdn.com/image/fetch/$s_!Gr5w!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd580e33f-8e7a-4411-9d49-2dd98ea47343_1280x1280.png</url><title>let&apos;s do AI</title><link>https://www.letsdoai.org</link></image><generator>Substack</generator><lastBuildDate>Sun, 19 Apr 2026 06:06:24 GMT</lastBuildDate><atom:link href="https://www.letsdoai.org/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Sumit Gaur]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[allthingsagentic@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[allthingsagentic@substack.com]]></itunes:email><itunes:name><![CDATA[Sumit Gaur]]></itunes:name></itunes:owner><itunes:author><![CDATA[Sumit Gaur]]></itunes:author><googleplay:owner><![CDATA[allthingsagentic@substack.com]]></googleplay:owner><googleplay:email><![CDATA[allthingsagentic@substack.com]]></googleplay:email><googleplay:author><![CDATA[Sumit Gaur]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Building a Production Ready REST API with FastAPI]]></title><description><![CDATA[Because life's too short for Flask boilerplate and Django's kitchen sink approach]]></description><link>https://www.letsdoai.org/p/building-a-production-ready-rest</link><guid isPermaLink="false">https://www.letsdoai.org/p/building-a-production-ready-rest</guid><dc:creator><![CDATA[Sumit Gaur]]></dc:creator><pubDate>Sun, 16 Nov 2025 05:28:25 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By the end of this post, you'll have a fully functional API with CRUD operations, automatic Swagger docs, PostgreSQL database, and everything wrapped up in a Docker container.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="4608" height="3456" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3456,&quot;width&quot;:4608,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;lighted light on road during nighttime&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="lighted light on road during nighttime" title="lighted light on road during nighttime" srcset="https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1555608165-904b52e0e172?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzMHx8ZmFzdHxlbnwwfHx8fDE3NjMyNzAxMjV8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@sharadmbhat">Sharad Bhat</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><h2>What We're Building</h2><p>We're creating an Employee Management API that can:</p><ul><li><p>Create new employee records</p></li><li><p>Read employee data (all or by ID)</p></li><li><p>Update existing employees</p></li><li><p>Delete employees (sorry, downsizing happens)</p></li></ul><p>And the best part? <a href="https://fastapi.tiangolo.com/">FastAPI</a> will generate interactive API documentation automatically.</p><h2>Setting Up the Project with uv</h2><p>Assuming you already have <a href="https://docs.astral.sh/uv/getting-started/installation/">uv</a> installed on your machine, execute the following commands to bootstrap a new project and add some dependencies</p><pre><code><code># Create project directory
mkdir employee-api
cd employee-api

# Initialize a new Python project
uv init

uv add fastapi email-validator uvicorn sqlalchemy psycopg2-binary alembic python-dotenv</code></code></pre><p>Quick breakdown of what we just installed:</p><ul><li><p><strong>fastapi</strong>: Our star performer, the web framework</p></li><li><p><strong>uvicorn</strong>: ASGI (Asynchronous Server Gateway Interface) server to run our app</p></li><li><p><strong>sqlalchemy</strong>: ORM for database operations</p></li><li><p><strong>psycopg2-binary</strong>: PostgreSQL adapter</p></li><li><p><strong>alembic</strong>: Database migration tool</p></li><li><p><strong>python-dotenv</strong>: For managing environment variables</p></li></ul><h2>Project Structure</h2><p>Let's organize our project like <a href="https://en.wikipedia.org/wiki/Marie_Kondo">Marie Kondo</a> would organize a closet</p><pre><code><code>employee-api/
&#9500;&#9472;&#9472; app/
&#9474;   &#9500;&#9472;&#9472; __init__.py
&#9474;   &#9500;&#9472;&#9472; main.py
&#9474;   &#9500;&#9472;&#9472; models.py
&#9474;   &#9500;&#9472;&#9472; schemas.py
&#9474;   &#9500;&#9472;&#9472; database.py
&#9474;   &#9492;&#9472;&#9472; crud.py
&#9500;&#9472;&#9472; .env
&#9500;&#9472;&#9472; docker-compose.yml
&#9500;&#9472;&#9472; Dockerfile
&#9500;&#9472;&#9472; pyproject.toml
&#9492;&#9472;&#9472; README.md</code></code></pre><p>We do this by running the following commands and creating files as required:</p><pre><code># create app dir
mkdir app

# create files
touch app/__init__.py app/main.py app/models.py app/schemas.py \
app/database.py app/crud.py

touch .env docker-compose.yml Dockerfile</code></pre><h2>Database Configuration</h2><p>Let's set up our PostgreSQL connection. First, create a <code>.env</code> file and add the following configurations to it. The credentials to access the <a href="https://www.postgresql.org/">PostgreSql</a> are aligned with the Docker container setup as mentioned in later sections:</p><pre><code># .env
DATABASE_URL=postgresql://employee_user:employee_pass@db:5432/employee_db
POSTGRES_USER=employee_user
POSTGRES_PASSWORD=employee_pass
POSTGRES_DB=employee_db</code></pre><blockquote><p>Never commit your <code>.env</code> file to Git as it usually contains some confidential data. Additionally, add it to <code>.gitignore</code> so as to avoid any unintentional commits.</p></blockquote><p>Now, let's create our database configuration in <code>app/database.py</code></p><pre><code><code># app/database.py
import os

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from dotenv import load_dotenv

load_dotenv()

DATABASE_URL = os.getenv(&#8221;DATABASE_URL&#8221;)

# Create database engine
engine = create_engine(DATABASE_URL)

# Create session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Base class for models
Base = declarative_base()

# Dependency to get database session
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()</code></code></pre><h2>Define the Employee Model</h2><p>Time to create our database model in <code>app/models.py</code></p><pre><code># app/models.py
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean
from sqlalchemy.sql import func
from app.database import Base

class Employee(Base):
    __tablename__ = &#8220;employees&#8221;
    
    id = Column(Integer, primary_key=True, index=True)
    first_name = Column(String(50), nullable=False)
    last_name = Column(String(50), nullable=False)
    email = Column(String(100), unique=True, index=True, nullable=False)
    department = Column(String(50), nullable=False)
    position = Column(String(100), nullable=False)
    salary = Column(Float, nullable=False)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    updated_at = Column(DateTime(timezone=True), onupdate=func.now())</code></pre><p>This model represents our employee table with all the essential fields. Notice the <code>created_at</code> and <code>updated_at</code> timestamps? These are like those "seen at" timestamps in messaging apps, useful for tracking changes.</p><h2>Create Pydantic Schemas</h2><p><a href="https://docs.pydantic.dev/latest/">Pydantic</a> schemas are like bouncers at a club; they make sure only the right data gets in. Create <code>app/schemas.py</code></p><pre><code># app/schemas.py
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
from typing import Optional

class EmployeeBase(BaseModel):
    first_name: str = Field(..., min_length=1, max_length=50)
    last_name: str = Field(..., min_length=1, max_length=50)
    email: EmailStr
    department: str = Field(..., min_length=1, max_length=50)
    position: str = Field(..., min_length=1, max_length=100)
    salary: float = Field(..., gt=0)
    is_active: bool = True

class EmployeeCreate(EmployeeBase):
    pass

class EmployeeUpdate(BaseModel):
    first_name: Optional[str] = Field(None, min_length=1, max_length=50)
    last_name: Optional[str] = Field(None, min_length=1, max_length=50)
    email: Optional[EmailStr] = None
    department: Optional[str] = Field(None, min_length=1, max_length=50)
    position: Optional[str] = Field(None, min_length=1, max_length=100)
    salary: Optional[float] = Field(None, gt=0)
    is_active: Optional[bool] = None

class EmployeeResponse(EmployeeBase):
    id: int
    created_at: datetime
    updated_at: Optional[datetime] = None
    
    class Config:
        from_attributes = True</code></pre><p><strong>What's happening here:</strong></p><ul><li><p><code>EmployeeBase</code>: Shared fields for all schemas</p></li><li><p><code>EmployeeCreate</code>: For creating new employees</p></li><li><p><code>EmployeeUpdate</code>: For partial updates (all fields optional)</p></li><li><p><code>EmployeeResponse</code>: What we send back to clients</p></li></ul><h2>CRUD Operations</h2><p>Now for the fun part, let's create our CRUD operations in <code>app/crud.py</code></p><pre><code># app/crud.py
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from app import models, schemas
from fastapi import HTTPException, status
from typing import List, Optional

def create_employee(db: Session, employee: schemas.EmployeeCreate) -&gt; models.Employee:
    &#8220;&#8221;&#8220;Create a new employee&#8221;&#8220;&#8221;
    db_employee = models.Employee(**employee.model_dump())
    try:
        db.add(db_employee)
        db.commit()
        db.refresh(db_employee)
        return db_employee
    except IntegrityError:
        db.rollback()
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=&#8221;Employee with this email already exists&#8221;
        )

def get_employee(db: Session, employee_id: int) -&gt; Optional[models.Employee]:
    &#8220;&#8221;&#8220;Get employee by ID&#8221;&#8220;&#8221;
    employee = db.query(models.Employee).filter(models.Employee.id == employee_id).first()
    if not employee:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f&#8221;Employee with ID {employee_id} not found&#8221;
        )
    return employee

def get_employees(
    db: Session, 
    skip: int = 0, 
    limit: int = 100,
    department: Optional[str] = None,
    is_active: Optional[bool] = None
) -&gt; List[models.Employee]:
    &#8220;&#8221;&#8220;Get all employees with optional filtering&#8221;&#8220;&#8221;
    query = db.query(models.Employee)
    
    if department:
        query = query.filter(models.Employee.department == department)
    if is_active is not None:
        query = query.filter(models.Employee.is_active == is_active)
    
    return query.offset(skip).limit(limit).all()

def update_employee(
    db: Session, 
    employee_id: int, 
    employee_update: schemas.EmployeeUpdate
) -&gt; models.Employee:
    &#8220;&#8221;&#8220;Update an employee&#8221;&#8220;&#8221;
    db_employee = get_employee(db, employee_id)
    
    update_data = employee_update.model_dump(exclude_unset=True)
    
    for field, value in update_data.items():
        setattr(db_employee, field, value)
    
    try:
        db.commit()
        db.refresh(db_employee)
        return db_employee
    except IntegrityError:
        db.rollback()
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=&#8221;Email already exists&#8221;
        )

def delete_employee(db: Session, employee_id: int) -&gt; dict:
    &#8220;&#8221;&#8220;Delete an employee (soft delete by setting is_active to False)&#8221;&#8220;&#8221;
    db_employee = get_employee(db, employee_id)
    db_employee.is_active = False
    db.commit()
    return {&#8221;message&#8221;: f&#8221;Employee {employee_id} has been deactivated&#8221;}

def hard_delete_employee(db: Session, employee_id: int) -&gt; dict:
    &#8220;&#8221;&#8220;Permanently delete an employee&#8221;&#8220;&#8221;
    db_employee = get_employee(db, employee_id)
    db.delete(db_employee)
    db.commit()
    return {&#8221;message&#8221;: f&#8221;Employee {employee_id} has been permanently deleted&#8221;}</code></pre><p>Notice we have both soft delete (deactivation) and hard delete? It's like the difference between un-following someone and blocking them. Sometimes you want to keep the data for auditing purposes.</p><h2>FastAPI Application</h2><p>Now let's tie it all together in <code>app/main.py</code></p><pre><code># app/main.py
from fastapi import FastAPI, Depends, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from app import crud, models, schemas
from app.database import engine, get_db

# Create database tables
models.Base.metadata.create_all(bind=engine)

# Initialize FastAPI app
app = FastAPI(
    title=&#8221;Employee Management API&#8221;,
    description=&#8221;API for managing employee data with CRUD operations&#8221;,
    version=&#8221;1.0.0&#8221;,
    docs_url=&#8221;/docs&#8221;,  # Swagger UI
    redoc_url=&#8221;/redoc&#8221;  # ReDoc
)

@app.get(&#8221;/&#8221;, tags=[&#8221;Root&#8221;])
def read_root():
    &#8220;&#8221;&#8220;Welcome endpoint&#8221;&#8220;&#8221;
    return {
        &#8220;message&#8221;: &#8220;Welcome to Employee Management API&#8221;,
        &#8220;docs&#8221;: &#8220;/docs&#8221;,
        &#8220;redoc&#8221;: &#8220;/redoc&#8221;
    }

@app.post(&#8221;/employees/&#8221;, response_model=schemas.EmployeeResponse, status_code=201, tags=[&#8221;Employees&#8221;])
def create_employee(
    employee: schemas.EmployeeCreate,
    db: Session = Depends(get_db)
):
    &#8220;&#8221;&#8220;
    Create a new employee with the following information:
    - **first_name**: Employee&#8217;s first name
    - **last_name**: Employee&#8217;s last name
    - **email**: Unique email address
    - **department**: Department name
    - **position**: Job position
    - **salary**: Annual salary (must be positive)
    &#8220;&#8221;&#8220;
    return crud.create_employee(db=db, employee=employee)

@app.get(&#8221;/employees/&#8221;, response_model=List[schemas.EmployeeResponse], tags=[&#8221;Employees&#8221;])
def read_employees(
    skip: int = Query(0, ge=0, description=&#8221;Number of records to skip&#8221;),
    limit: int = Query(100, ge=1, le=100, description=&#8221;Maximum number of records to return&#8221;),
    department: Optional[str] = Query(None, description=&#8221;Filter by department&#8221;),
    is_active: Optional[bool] = Query(None, description=&#8221;Filter by active status&#8221;),
    db: Session = Depends(get_db)
):
    &#8220;&#8221;&#8220;
    Retrieve all employees with optional filtering:
    - Pagination support via skip and limit
    - Filter by department
    - Filter by active status
    &#8220;&#8221;&#8220;
    employees = crud.get_employees(
        db, skip=skip, limit=limit, 
        department=department, is_active=is_active
    )
    return employees

@app.get(&#8221;/employees/{employee_id}&#8221;, response_model=schemas.EmployeeResponse, tags=[&#8221;Employees&#8221;])
def read_employee(employee_id: int, db: Session = Depends(get_db)):
    &#8220;&#8221;&#8220;Get a specific employee by ID&#8221;&#8220;&#8221;
    return crud.get_employee(db=db, employee_id=employee_id)

@app.put(&#8221;/employees/{employee_id}&#8221;, response_model=schemas.EmployeeResponse, tags=[&#8221;Employees&#8221;])
def update_employee(
    employee_id: int,
    employee: schemas.EmployeeUpdate,
    db: Session = Depends(get_db)
):
    &#8220;&#8221;&#8220;
    Update an employee&#8217;s information.
    Only provided fields will be updated (partial update supported).
    &#8220;&#8221;&#8220;
    return crud.update_employee(db=db, employee_id=employee_id, employee_update=employee)

@app.delete(&#8221;/employees/{employee_id}&#8221;, tags=[&#8221;Employees&#8221;])
def delete_employee(employee_id: int, db: Session = Depends(get_db)):
    &#8220;&#8221;&#8220;Soft delete an employee (sets is_active to False)&#8221;&#8220;&#8221;
    return crud.delete_employee(db=db, employee_id=employee_id)

@app.delete(&#8221;/employees/{employee_id}/permanent&#8221;, tags=[&#8221;Employees&#8221;])
def hard_delete_employee(employee_id: int, db: Session = Depends(get_db)):
    &#8220;&#8221;&#8220;Permanently delete an employee from the database&#8221;&#8220;&#8221;
    return crud.hard_delete_employee(db=db, employee_id=employee_id)

@app.get(&#8221;/health&#8221;, tags=[&#8221;Health&#8221;])
def health_check():
    &#8220;&#8221;&#8220;Health check endpoint for monitoring&#8221;&#8220;&#8221;
    return {&#8221;status&#8221;: &#8220;healthy&#8221;}</code></pre><p><strong>Swagger Documentation Magic:</strong> Notice those docstrings and the <code>tags</code> parameter? FastAPI automatically converts these into API documentation and an interactive playground.</p><h2>Docker Configuration</h2><p>Let's containerize the application. First, create <code>docker-compose.yml</code></p><pre><code># docker-compose.yml
version: &#8216;3.8&#8217;

services:
  db:
    image: postgres:15-alpine
    container_name: employee_db
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    ports:
      - &#8220;5432:5432&#8221;
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: [&#8221;CMD-SHELL&#8221;, &#8220;pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}&#8221;]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    build: .
    container_name: employee_api
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    volumes:
      - .:/app
    ports:
      - &#8220;8000:8000&#8221;
    environment:
      DATABASE_URL: ${DATABASE_URL}
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

volumes:
  postgres_data:</code></pre><p>Now let's create Dockerfile</p><pre><code># Dockerfile
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update &amp;&amp; apt-get install -y \
    gcc \
    postgresql-client \
    &amp;&amp; rm -rf /var/lib/apt/lists/*

# Copy uv installer and install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# Copy dependency files
COPY pyproject.toml .

# Install dependencies using uv
RUN uv pip install --system -r pyproject.toml

# Copy application code
COPY . .

# Expose port
EXPOSE 8000

# Run the application
CMD [&#8221;uvicorn&#8221;, &#8220;app.main:app&#8221;, &#8220;--host&#8221;, &#8220;0.0.0.0&#8221;, &#8220;--port&#8221;, &#8220;8000&#8221;]</code></pre><h2>Running the Application</h2><pre><code># Build and start the containers
docker-compose up --build

# Or run in detached mode
docker-compose up -d --build</code></pre><p>Once everything is running, visit:</p><ul><li><p><strong>Swagger UI</strong>: http://localhost:8000/docs</p></li><li><p><strong>ReDoc</strong>: http://localhost:8000/redoc</p></li><li><p><strong>API Root</strong>: http://localhost:8000</p></li></ul><h2>Testing the API</h2><p>Let's take our API for a test drive! Here are some example requests using Python</p><pre><code># test_api.py
import requests

BASE_URL = "http://localhost:8000"

# Create an employee
new_employee = {
    "first_name": "John",
    "last_name": "Doe",
    "email": "john.doe@company.com",
    "department": "Engineering",
    "position": "Senior Developer",
    "salary": 95000.00,
    "is_active": True
}

response = requests.post(f"{BASE_URL}/employees/", json=new_employee)
print(f"Created Employee: {response.json()}")

# Get all employees
response = requests.get(f"{BASE_URL}/employees/")
print(f"All Employees: {response.json()}")

# Get specific employee
employee_id = 1
response = requests.get(f"{BASE_URL}/employees/{employee_id}")
print(f"Employee {employee_id}: {response.json()}")

# Update employee
update_data = {"salary": 105000.00, "position": "Lead Developer"}
response = requests.put(f"{BASE_URL}/employees/{employee_id}", json=update_data)
print(f"Updated Employee: {response.json()}")

# Filter by department
response = requests.get(f"{BASE_URL}/employees/?department=Engineering")
print(f"Engineering Employees: {response.json()}")

# Soft delete employee
response = requests.delete(f"{BASE_URL}/employees/{employee_id}")
print(f"Delete Response: {response.json()}")</code></pre><p>You can also use the interactive Swagger playground at <code>/docs</code> to test all endpoints without writing any code. Just click, fill, and submit.</p><div><hr></div><h2>What's Next?</h2><p>Congratulations! You've built a production-ready REST API. Here are some ideas to level up:</p><ol><li><p><strong>Add Authentication</strong>: Implement JWT tokens for securing your endpoints</p></li><li><p><strong>Database Migrations</strong>: Use Alembic for managing schema changes</p></li><li><p><strong>Testing</strong>: Add pytest for unit and integration tests</p></li><li><p><strong>Logging</strong>: Implement structured logging for better debugging</p></li><li><p><strong>Monitoring</strong>: Add Prometheus metrics and health checks</p></li><li><p><strong>API Versioning</strong>: Prepare for future changes with <code>/v1/employees/</code></p></li><li><p><strong>Rate Limiting</strong>: Prevent abuse with request throttling</p></li><li><p><strong>CORS Configuration</strong>: Enable front-end applications to consume your API</p></li></ol><div><hr></div><p>A complete working example of the post is available in <a href="https://github.com/gaurscode/fastapi-demo">Github</a>. </p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[A Practical Guide to uv]]></title><description><![CDATA[uv is a blazingly fast Python package & project manager written in Rust, created by Astral (the folks behind ruff)]]></description><link>https://www.letsdoai.org/p/uv</link><guid isPermaLink="false">https://www.letsdoai.org/p/uv</guid><dc:creator><![CDATA[Sumit Gaur]]></dc:creator><pubDate>Sat, 15 Nov 2025 17:12:13 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="724.7000122070312" height="483.37636629100774" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:5304,&quot;width&quot;:7952,&quot;resizeWidth&quot;:724.7000122070312,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;brown cardboard boxes on black plastic crate&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="brown cardboard boxes on black plastic crate" title="brown cardboard boxes on black plastic crate" srcset="https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1580674285054-bed31e145f59?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxwYWNrYWdlfGVufDB8fHx8MTc2MzIyNjQ2NXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@purzlbaum">Claudio Schwarz</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>A package manager is basically the app store for your code. Instead of manually hunting libraries, downloading zips, and praying versions match, you just say:</p><pre><code>uv add openai</code></pre><p>&#8230;and <em>boom</em>, dependency installed, version resolved, ready to use.</p><div><hr></div><h2>What&#8217;s a package manager, and why do we need one?</h2><p>A <strong>package manager</strong>:</p><ul><li><p>Downloads libraries (e.g. <code>openai</code>, <code>httpx</code>, <code>langchain</code>)</p></li><li><p>Resolves dependencies and versions</p></li><li><p>Keeps different projects&#8217; dependencies isolated (so your old project doesn&#8217;t break when you upgrade something for a new one)</p></li></ul><p>Without a package manager, you&#8217;d be:</p><ul><li><p>Manually downloading packages from the web</p></li><li><p>Managing version conflicts yourself</p></li><li><p>Copy&#8211;pasting libraries between projects (which is.. chaos)</p></li></ul><p>For AI &amp; agentic projects, this gets even more important because:</p><ul><li><p>You quickly pile up dependencies: <code>openai</code>, <code>anthropic</code>, <code>pydantic</code>, <code>fastapi</code>, <code>httpx</code>, <code>langchain</code>, <code>playwright</code>, vector DB clients, etc.</p></li><li><p>You often need <strong>different versions</strong> of the same packages across projects.</p></li><li><p>You care a LOT about reproducibility: if you&#8217;re shipping an agent to production, &#8220;it worked on my machine&#8221; is not good enough.</p></li></ul><h2>What is <code>uv</code>?</h2><p><code>uv</code> is a <strong>blazingly fast</strong> Python package &amp; project manager written in Rust, created by Astral (the folks behind <code>ruff</code>). It&#8217;s designed as:</p><ul><li><p>A <strong>drop-in replacement</strong> for <code>pip</code> and <code>pip-tools</code></p></li><li><p>A combined tool for:</p><ul><li><p>Managing dependencies</p></li><li><p>Creating virtual environments</p></li><li><p>Managing Python versions</p></li><li><p>Initializing and building projects</p></li></ul></li></ul><p>Benchmarks show uv can be <strong>10-100x faster</strong> than pip in many workflows, especially with caching.</p><h2>Installing <code>uv</code></h2><p>There are multiple ways to install <strong>uv</strong> on your machine. Refer to the latest <a href="https://docs.astral.sh/uv/getting-started/installation/">documentation</a> for the most relevant option for your setup. </p><h2>uv basics: the mental model</h2><p>Here&#8217;s the core workflow you&#8217;ll use 90% of the time:</p><ol><li><p>Create a project</p></li><li><p>Install / add dependencies</p></li><li><p>Run your code in that environment</p></li><li><p>Lock / sync dependencies for reproducibility</p></li></ol><p>We&#8217;ll walk through each, then we&#8217;ll plug this into real AI/agent examples.</p><h2>Creating a new project with <code>uv init</code></h2><p><code>uv</code> can scaffold a project for you.</p><pre><code><code>uv init ai-agent-demo
cd ai-agent-demo</code></code></pre><p>This creates something like:</p><pre><code>ai-agent-demo/
&#9500;&#9472;&#9472; .gitignore
&#9500;&#9472;&#9472; .python-version
&#9500;&#9472;&#9472; README.md
&#9500;&#9472;&#9472; main.py
&#9492;&#9472;&#9472; pyproject.toml</code></pre><p>You already have a <code>main.py</code> and a <code>pyproject.toml</code> ready.</p><p>Run the starter script:</p><pre><code>uv run main.py</code></pre><p><code>uv run</code> makes sure:</p><ul><li><p>The environment exists</p></li><li><p>Dependencies match the lockfile (if any)</p></li><li><p>Your script runs inside that environment</p></li></ul><h2>Installing Python with <code>uv</code></h2><p>You don&#8217;t even need Python installed beforehand, <code>uv</code> can manage Python versions for you.</p><p>Install the latest Python:</p><pre><code>uv python install</code></pre><p>Or a specific version (useful for AI libraries that want 3.10+ etc.):</p><pre><code>uv python install 3.12</code></pre><p>Then <code>uv</code> uses that in your projects automatically.</p><h2>Managing dependencies with <code>uv add</code></h2><p>Inside your project folder:</p><pre><code>uv add httpx</code></pre><p>This will:</p><ul><li><p>Add <code>httpx</code> to <code>pyproject.toml</code></p></li><li><p>Create a virtual environment (if needed)</p></li><li><p>Create/update <code>uv.lock</code></p></li><li><p>Install <code>httpx</code> into that environment</p></li></ul><p>You can also install multiple in one go:</p><pre><code><code>uv add openai python-dotenv httpx</code></code></pre><h2>Example - a simple AI script calling an LLM API</h2><p>Let&#8217;s say we&#8217;re building a tiny script that calls the OpenAI API (or any LLM API with a Python client).</p><h3>Set up the project</h3><p>This gives you an isolated environment with:</p><pre><code><code>uv init ai-chat
cd ai-chat
uv add openai python-dotenv</code></code></pre><ul><li><p><code>openai</code> &#8211; for the API</p></li><li><p><code>python-dotenv</code> &#8211; for <code>.env</code>-based secrets</p></li></ul><h3>Create <code>.env</code></h3><pre><code>OPENAI_API_KEY=sk-your-key-here</code></pre><h3>Write <code>main.py</code></h3><pre><code>from dotenv import load_dotenv
import os
from openai import OpenAI

load_dotenv()

client = OpenAI(api_key=os.getenv(&#8221;OPENAI_API_KEY&#8221;))

def ask_llm(prompt: str) -&gt; str:
    response = client.chat.completions.create(
        model=&#8221;gpt-4.1-mini&#8221;,
        messages=[{&#8221;role&#8221;: &#8220;user&#8221;, &#8220;content&#8221;: prompt}],
        temperature=0.2,
    )
    return response.choices[0].message.content

if __name__ == &#8220;__main__&#8221;:
    answer = ask_llm(&#8221;Explain uv package manager in one sentence.&#8221;)
    print(answer)</code></pre><h3>Run it with uv</h3><pre><code>uv run main.py</code></pre><p>You now have:</p><ul><li><p>A reproducible environment (thanks to <code>pyproject.toml</code> + <code>uv.lock</code>)</p></li><li><p>A clean way to call an external API</p></li></ul><h2>Locking, syncing, and reproducibility</h2><p>For real-world AI systems (especially agentic ones that call many tools), you <em>really</em> want reproducible environments.</p><p>Key uv concepts:</p><ul><li><p><code>pyproject.toml</code> &#8211; what you <em>want</em> to install</p></li><li><p><code>uv.lock</code> &#8211; the exact versions you <em>actually</em> installed</p></li><li><p><code>.venv/</code> &#8211; the environment itself</p></li></ul><h3>Sync/recreate env from lockfile</h3><p>On CI, a new machine, or when on-boarding teammates:</p><pre><code>uv sync</code></pre><p><code>uv sync</code> uses <code>uv.lock</code> to recreate the environment exactly, avoiding the &#8220;it works on my machine&#8221; drama.</p><h3>Updating dependencies</h3><p>If you change versions in <code>pyproject.toml</code> or add new ones:</p><pre><code>uv lock   # updates uv.lock
uv sync   # syncs the env to match the lock</code></pre><p>This workflow is especially good for:</p><ul><li><p><strong>Agent frameworks</strong> (LangChain, LlamaIndex, etc.)</p></li><li><p><strong>Vector DB clients</strong> (e.g. <code>qdrant-client</code>, <code>weaviate-client</code>)</p></li><li><p><strong>Browser automation tools</strong> (<code>playwright</code>, <code>selenium</code>)</p></li></ul><p>where version mismatches can cause subtle runtime bugs.</p><h2>Managing multiple Python versions</h2><p>You might have:</p><ul><li><p>One agent project requiring Python 3.10+</p></li><li><p>Another legacy ML project stuck on 3.9</p></li></ul><p><code>uv</code> can juggle this with ease.</p><pre><code># Install multiple Python versions
uv python install 3.10 3.12

# Pin a specific version for this project
echo &#8220;3.12&#8221; &gt; .python-version

# Create a venv using that version
uv venv</code></pre><p>Whenever you run </p><pre><code>uv run main.py</code></pre><p>uv will use the Python version pinned in <code>.python-version</code>.</p><h2>Using uv in Docker</h2><p>Common deployment pattern for AI backends:</p><ol><li><p>Use <code>uv</code> to install and lock dependencies.</p></li><li><p>Copy <code>pyproject.toml</code> + <code>uv.lock</code> into an image.</p></li><li><p>Run <code>uv sync</code> in the container.</p></li></ol><p>Super-rough Dockerfile sketch (just conceptual):</p><pre><code>FROM python:3.12-slim

# Install uv (one way)
RUN pip install uv

WORKDIR /app

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen   # install deps from lockfile

COPY . .

CMD [&#8221;uv&#8221;, &#8220;run&#8221;, &#8220;server.py&#8221;]</code></pre><p>This makes your container builds:</p><ul><li><p>Fast (thanks to uv caching &amp; fast resolver)</p></li><li><p>Reproducible (same <code>uv.lock</code> everywhere)</p></li></ul><h2>Common use cases</h2><p><strong>LLM-powered backend with tools:</strong> Chat endpoint that can call external APIs (tools), then compose responses using LLMs:</p><pre><code>uv add openai httpx pydantic fastapi uvicorn python-dotenv</code></pre><p><strong>Retrieval-augmented generation (RAG) app: </strong>Document Q&amp;A app where:</p><ul><li><p><code>langchain-community</code>/similar handles chains &amp; tools</p></li><li><p><code>chromadb</code> or other vector client is your store</p></li></ul><pre><code>uv add openai langchain-community chromadb tiktoken python-dotenv</code></pre><p><strong>Browser-based agent: </strong>Agent that can browse the web, click buttons, extract data, then summarize via LLM.</p><pre><code>uv add playwright openai python-dotenv pydantic</code></pre><p><strong>Multi-agent orchestration: </strong>Multiple agents pulling jobs from a queue (Redis), sharing tools, and using LLMs to decide next actions.</p><pre><code>uv add fastapi openai httpx pydantic redis rq python-dotenv</code></pre><h2>Where to learn more</h2><p>Some nice references if you want to go deeper:</p><ul><li><p><a href="https://docs.astral.sh">Official uv docs</a> &#8211; <strong>features &amp; guides</strong></p></li><li><p><a href="https://www.datacamp.com/tutorial/python-uv">Python UV: The Ultimate Guide to the Fastest Python Package Manager</a></p></li><li><p><a href="https://realpython.com/python-uv">Real Python &#8211; Managing Python Projects With uv: An All-in-One Solution</a></p></li><li><p><a href="https://astral.sh/blog/uv">Astral blog posts about uv and unified Python packaging</a></p></li></ul>]]></content:encoded></item></channel></rss>