Skip to content

Commit 446cc28

Browse files
add GET /resorts/{id} endpoint and migrate backend to Poetry
- Add GET /resorts/{id} returning full Resort model, 404 on unknown UUID - Replace requirements.txt with pyproject.toml + poetry.lock for isolated backend env - Update Dockerfile to install via Poetry - Add planning/ to root .gitignore, scope poetry.lock ignore to root only Closes #60 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2da4d4b commit 446cc28

8 files changed

Lines changed: 1183 additions & 13 deletions

File tree

.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ cython_debug/
164164
# Mac
165165
.DS_Store
166166

167-
# Poetry
168-
poetry.lock
167+
# Poetry (root-level only; submodule lockfiles are committed for reproducibility)
168+
/poetry.lock
169169

170170
# Streamlit
171171
.streamlit/secrets.toml
@@ -184,6 +184,9 @@ dist/
184184
.env.*.local
185185
*.tsbuildinfo
186186

187+
# Planning / design docs
188+
planning/
189+
187190
# AI coding assistants
188191
CLAUDE.md
189192
.claude/

backend/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.venv/
2+
__pycache__/

backend/Dockerfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ FROM python:3.12-slim
22

33
WORKDIR /app
44

5-
COPY requirements.txt .
6-
RUN pip install --no-cache-dir -r requirements.txt
5+
RUN pip install --no-cache-dir poetry==2.1.3 && \
6+
poetry config virtualenvs.create false
7+
8+
COPY pyproject.toml poetry.lock ./
9+
RUN poetry install --only main --no-interaction
710

811
COPY . .
912

backend/main.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
from contextlib import asynccontextmanager
3-
from fastapi import FastAPI, Query
3+
from fastapi import FastAPI, HTTPException, Query
44
from typing import Optional
55

66
from data import load_resorts
@@ -34,6 +34,14 @@ def _parse_date_list(value: Optional[str]) -> set[str]:
3434
return set()
3535

3636

37+
@app.get("/resorts/{resort_id}", response_model=Resort)
38+
def get_resort(resort_id: str):
39+
for r in _resorts:
40+
if r.resort_id == resort_id:
41+
return r
42+
raise HTTPException(status_code=404, detail="Resort not found")
43+
44+
3745
@app.get("/resorts", response_model=list[ResortSummary])
3846
def get_resorts(
3947
search: Optional[str] = Query(default=None),

backend/poetry.lock

Lines changed: 1078 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/pyproject.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[tool.poetry]
2+
name = "indy-explorer-backend"
3+
version = "0.1.0"
4+
description = "Indy Explorer FastAPI backend"
5+
authors = ["Jon Stelman <jon@jonstelman.com>"]
6+
package-mode = false
7+
8+
[tool.poetry.dependencies]
9+
python = "^3.12"
10+
fastapi = "^0.115.12"
11+
uvicorn = {extras = ["standard"], version = "^0.34.0"}
12+
pandas = "^2.2.3"
13+
pydantic = "^2.12.5"
14+
15+
[tool.poetry.group.dev.dependencies]
16+
pytest = "^8.3.5"
17+
httpx = "^0.28.1"
18+
19+
[tool.black]
20+
skip-string-normalization = true
21+
line-length = 100
22+
23+
[build-system]
24+
requires = ["poetry-core"]
25+
build-backend = "poetry.core.masonry.api"

backend/requirements.txt

Lines changed: 0 additions & 8 deletions
This file was deleted.

backend/tests/test_resort_by_id.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import sys
2+
import os
3+
4+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
5+
6+
from fastapi.testclient import TestClient
7+
from unittest.mock import patch
8+
9+
from main import app
10+
from models import Resort
11+
12+
FAKE_RESORTS = [
13+
Resort(
14+
resort_id='abc-123',
15+
name='Vail',
16+
region='West',
17+
city='Vail',
18+
state='CO',
19+
country='USA',
20+
reservation_status='required',
21+
indy_page='https://example.com/vail',
22+
),
23+
Resort(
24+
resort_id='def-456',
25+
name='Stowe',
26+
region='Northeast',
27+
city='Stowe',
28+
state='VT',
29+
country='USA',
30+
reservation_status='none',
31+
indy_page='https://example.com/stowe',
32+
),
33+
]
34+
35+
client = TestClient(app)
36+
37+
38+
def test_get_resort_by_id_returns_resort():
39+
with patch('main._resorts', FAKE_RESORTS):
40+
response = client.get('/resorts/abc-123')
41+
assert response.status_code == 200
42+
data = response.json()
43+
assert data['resort_id'] == 'abc-123'
44+
assert data['name'] == 'Vail'
45+
46+
47+
def test_get_resort_by_id_returns_full_model():
48+
with patch('main._resorts', FAKE_RESORTS):
49+
response = client.get('/resorts/def-456')
50+
assert response.status_code == 200
51+
data = response.json()
52+
assert data['resort_id'] == 'def-456'
53+
assert data['state'] == 'VT'
54+
55+
56+
def test_get_resort_by_id_not_found():
57+
with patch('main._resorts', FAKE_RESORTS):
58+
response = client.get('/resorts/nonexistent-uuid')
59+
assert response.status_code == 404

0 commit comments

Comments
 (0)