Fix formatting with black

This commit is contained in:
GotthardG
2024-12-16 10:41:56 +01:00
parent 57763970f9
commit a0be71bdfe
26 changed files with 1657 additions and 645 deletions

View File

@ -5,4 +5,11 @@ from .dewar import router as dewar_router
from .shipment import router as shipment_router
from .auth import router as auth_router
__all__ = ["address_router", "contact_router", "proposal_router", "dewar_router", "shipment_router", "auth_router"]
__all__ = [
"address_router",
"contact_router",
"proposal_router",
"dewar_router",
"shipment_router",
"auth_router",
]

View File

@ -7,23 +7,25 @@ from app.dependencies import get_db
router = APIRouter()
@router.get("/", response_model=List[AddressSchema])
async def get_return_addresses(db: Session = Depends(get_db)):
return db.query(AddressModel).all()
@router.post("/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED)
async def create_return_address(address: AddressCreate, db: Session = Depends(get_db)):
if db.query(AddressModel).filter(AddressModel.city == address.city).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Address in this city already exists."
detail="Address in this city already exists.",
)
db_address = AddressModel(
street=address.street,
city=address.city,
zipcode=address.zipcode,
country=address.country
country=address.country,
)
db.add(db_address)
@ -31,13 +33,15 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge
db.refresh(db_address)
return db_address
@router.put("/{address_id}", response_model=AddressSchema)
async def update_return_address(address_id: int, address: AddressUpdate, db: Session = Depends(get_db)):
async def update_return_address(
address_id: int, address: AddressUpdate, db: Session = Depends(get_db)
):
db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
if not db_address:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Address not found."
status_code=status.HTTP_404_NOT_FOUND, detail="Address not found."
)
for key, value in address.dict(exclude_unset=True).items():
setattr(db_address, key, value)
@ -45,14 +49,14 @@ async def update_return_address(address_id: int, address: AddressUpdate, db: Ses
db.refresh(db_address)
return db_address
@router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_return_address(address_id: int, db: Session = Depends(get_db)):
db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
if not db_address:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Address not found."
status_code=status.HTTP_404_NOT_FOUND, detail="Address not found."
)
db.delete(db_address)
db.commit()
return
return

View File

@ -26,7 +26,10 @@ SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl="/login", tokenUrl="/token/login")
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl="/login", tokenUrl="/token/login"
)
def create_access_token(data: dict) -> str:
to_encode = data.copy()
@ -34,6 +37,7 @@ def create_access_token(data: dict) -> str:
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm="HS256")
async def get_current_user(token: str = Depends(oauth2_scheme)) -> loginData:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@ -60,6 +64,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> loginData:
return token_data
@router.post("/token/login", response_model=loginToken)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = mock_users_db.get(form_data.username)

View File

@ -7,38 +7,48 @@ from app.dependencies import get_db
router = APIRouter()
# Existing routes
@router.get("/", response_model=List[ContactPerson])
async def get_contacts(db: Session = Depends(get_db)):
return db.query(ContactPersonModel).all()
@router.post("/", response_model=ContactPerson, status_code=status.HTTP_201_CREATED)
async def create_contact(contact: ContactPersonCreate, db: Session = Depends(get_db)):
if db.query(ContactPersonModel).filter(ContactPersonModel.email == contact.email).first():
if (
db.query(ContactPersonModel)
.filter(ContactPersonModel.email == contact.email)
.first()
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="This contact already exists."
detail="This contact already exists.",
)
db_contact = ContactPersonModel(
firstname=contact.firstname,
lastname=contact.lastname,
phone_number=contact.phone_number,
email=contact.email
email=contact.email,
)
db.add(db_contact)
db.commit()
db.refresh(db_contact)
return db_contact
# New routes
@router.put("/{contact_id}", response_model=ContactPerson)
async def update_contact(contact_id: int, contact: ContactPersonUpdate, db: Session = Depends(get_db)):
db_contact = db.query(ContactPersonModel).filter(ContactPersonModel.id == contact_id).first()
async def update_contact(
contact_id: int, contact: ContactPersonUpdate, db: Session = Depends(get_db)
):
db_contact = (
db.query(ContactPersonModel).filter(ContactPersonModel.id == contact_id).first()
)
if not db_contact:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Contact not found."
status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found."
)
for key, value in contact.dict(exclude_unset=True).items():
setattr(db_contact, key, value)
@ -46,14 +56,16 @@ async def update_contact(contact_id: int, contact: ContactPersonUpdate, db: Sess
db.refresh(db_contact)
return db_contact
@router.delete("/{contact_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_contact(contact_id: int, db: Session = Depends(get_db)):
db_contact = db.query(ContactPersonModel).filter(ContactPersonModel.id == contact_id).first()
db_contact = (
db.query(ContactPersonModel).filter(ContactPersonModel.id == contact_id).first()
)
if not db_contact:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Contact not found."
status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found."
)
db.delete(db_contact)
db.commit()
return
return

View File

@ -13,7 +13,7 @@ from app.schemas import (
DewarTypeCreate,
DewarSerialNumber as DewarSerialNumberSchema,
DewarSerialNumberCreate,
Shipment as ShipmentSchema # Clearer name for schema
Shipment as ShipmentSchema, # Clearer name for schema
)
from app.models import (
Dewar as DewarModel,
@ -21,7 +21,7 @@ from app.models import (
Sample as SampleModel,
DewarType as DewarTypeModel,
DewarSerialNumber as DewarSerialNumberModel,
Shipment as ShipmentModel # Clearer name for model
Shipment as ShipmentModel, # Clearer name for model
)
from app.dependencies import get_db
import uuid
@ -32,23 +32,32 @@ from PIL import ImageFont, ImageDraw, Image
from reportlab.lib.pagesizes import A5, landscape
from reportlab.lib.units import cm
from reportlab.pdfgen import canvas
from app.crud import get_shipments, get_shipment_by_id # Import CRUD functions for shipment
from app.crud import (
get_shipments,
get_shipment_by_id,
) # Import CRUD functions for shipment
router = APIRouter()
def generate_unique_id(db: Session, length: int = 16) -> str:
while True:
base_string = f"{time.time()}{random.randint(0, 10 ** 6)}"
hash_object = hashlib.sha256(base_string.encode())
hash_digest = hash_object.hexdigest()
unique_id = ''.join(random.choices(hash_digest, k=length))
existing_dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id).first()
unique_id = "".join(random.choices(hash_digest, k=length))
existing_dewar = (
db.query(DewarModel).filter(DewarModel.unique_id == unique_id).first()
)
if not existing_dewar:
break
return unique_id
@router.post("/", response_model=DewarSchema, status_code=status.HTTP_201_CREATED)
async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> DewarSchema:
async def create_dewar(
dewar: DewarCreate, db: Session = Depends(get_db)
) -> DewarSchema:
try:
db_dewar = DewarModel(
dewar_name=dewar.dewar_name,
@ -96,6 +105,7 @@ async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> Dew
logging.error(f"Validation error occurred: {e}")
raise HTTPException(status_code=400, detail="Validation error")
@router.post("/{dewar_id}/generate-qrcode")
async def generate_dewar_qrcode(dewar_id: int, db: Session = Depends(get_db)):
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
@ -109,7 +119,7 @@ async def generate_dewar_qrcode(dewar_id: int, db: Session = Depends(get_db)):
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(dewar.unique_id)
qr.make(fit=True)
img = qr.make_image(fill='black', back_color='white')
img = qr.make_image(fill="black", back_color="white")
buf = io.BytesIO()
img.save(buf)
@ -120,6 +130,7 @@ async def generate_dewar_qrcode(dewar_id: int, db: Session = Depends(get_db)):
return {"message": "QR Code generated", "qrcode": dewar.unique_id}
def generate_label(dewar):
buffer = BytesIO()
# Set page orientation to landscape
@ -138,25 +149,36 @@ def generate_label(dewar):
# Desired logo width in the PDF (you can adjust this size)
desired_logo_width = 4 * cm
desired_logo_height = desired_logo_width / logo_aspect_ratio # maintain aspect ratio
desired_logo_height = (
desired_logo_width / logo_aspect_ratio
) # maintain aspect ratio
# Draw header text
c.setFont("Helvetica-Bold", 16)
c.drawString(2 * cm, page_height - 2 * cm, "Paul Scherrer Institut")
# Draw the Heidi logo with preserved aspect ratio
c.drawImage(png_logo_path, page_width - desired_logo_width - 2 * cm,
page_height - desired_logo_height - 2 * cm,
width=desired_logo_width, height=desired_logo_height, mask='auto')
c.drawImage(
png_logo_path,
page_width - desired_logo_width - 2 * cm,
page_height - desired_logo_height - 2 * cm,
width=desired_logo_width,
height=desired_logo_height,
mask="auto",
)
# Draw details section
c.setFont("Helvetica", 12)
y_position = page_height - 4 * cm # Adjusted to ensure text doesn't overlap with the logo
y_position = (
page_height - 4 * cm
) # Adjusted to ensure text doesn't overlap with the logo
line_height = 0.8 * cm
if dewar.shipment:
c.drawString(2 * cm, y_position, f"Shipment Name: {dewar.shipment.shipment_name}")
c.drawString(
2 * cm, y_position, f"Shipment Name: {dewar.shipment.shipment_name}"
)
y_position -= line_height
c.drawString(2 * cm, y_position, f"Dewar Name: {dewar.dewar_name}")
@ -167,7 +189,11 @@ def generate_label(dewar):
if dewar.contact_person:
contact_person = dewar.contact_person
c.drawString(2 * cm, y_position, f"Contact: {contact_person.firstname} {contact_person.lastname}")
c.drawString(
2 * cm,
y_position,
f"Contact: {contact_person.firstname} {contact_person.lastname}",
)
y_position -= line_height
c.drawString(2 * cm, y_position, f"Email: {contact_person.email}")
y_position -= line_height
@ -191,15 +217,17 @@ def generate_label(dewar):
qr = qrcode.QRCode(version=1, box_size=10, border=4)
qr.add_data(dewar.unique_id)
qr.make(fit=True)
qr_img = qr.make_image(fill='black', back_color='white').convert("RGBA")
qr_img = qr.make_image(fill="black", back_color="white").convert("RGBA")
# Save this QR code to a temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
qr_img.save(temp_file, format='PNG')
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
qr_img.save(temp_file, format="PNG")
temp_file_path = temp_file.name
# Add QR code to PDF
c.drawImage(temp_file_path, page_width - 6 * cm, 5 * cm, width=4 * cm, height=4 * cm)
c.drawImage(
temp_file_path, page_width - 6 * cm, 5 * cm, width=4 * cm, height=4 * cm
)
# Add footer text
c.setFont("Helvetica", 10)
@ -207,7 +235,9 @@ def generate_label(dewar):
# Draw border
c.setLineWidth(1)
c.rect(1 * cm, 1 * cm, page_width - 2 * cm, page_height - 2 * cm) # Adjusted dimensions
c.rect(
1 * cm, 1 * cm, page_width - 2 * cm, page_height - 2 * cm
) # Adjusted dimensions
# Finalize the canvas
c.showPage()
@ -220,25 +250,38 @@ def generate_label(dewar):
return buffer
@router.get("/{dewar_id}/download-label", response_class=Response)
async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)):
dewar = db.query(DewarModel).options(
joinedload(DewarModel.pucks).joinedload(PuckModel.samples),
joinedload(DewarModel.contact_person),
joinedload(DewarModel.return_address),
joinedload(DewarModel.shipment)
).filter(DewarModel.id == dewar_id).first()
dewar = (
db.query(DewarModel)
.options(
joinedload(DewarModel.pucks).joinedload(PuckModel.samples),
joinedload(DewarModel.contact_person),
joinedload(DewarModel.return_address),
joinedload(DewarModel.shipment),
)
.filter(DewarModel.id == dewar_id)
.first()
)
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
if not dewar.unique_id:
raise HTTPException(status_code=404, detail="QR Code not generated for this dewar")
raise HTTPException(
status_code=404, detail="QR Code not generated for this dewar"
)
buffer = generate_label(dewar)
return Response(buffer.getvalue(), media_type="application/pdf", headers={
"Content-Disposition": f"attachment; filename=dewar_label_{dewar.id}.pdf"
})
return Response(
buffer.getvalue(),
media_type="application/pdf",
headers={
"Content-Disposition": f"attachment; filename=dewar_label_{dewar.id}.pdf"
},
)
@router.get("/", response_model=List[DewarSchema])
async def get_dewars(db: Session = Depends(get_db)):
@ -249,13 +292,23 @@ async def get_dewars(db: Session = Depends(get_db)):
logging.error(f"Database error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/dewar-types", response_model=List[DewarTypeSchema])
def get_dewar_types(db: Session = Depends(get_db)):
return db.query(DewarTypeModel).all()
@router.get("/dewar-types/{type_id}/serial-numbers", response_model=List[DewarSerialNumberSchema])
@router.get(
"/dewar-types/{type_id}/serial-numbers",
response_model=List[DewarSerialNumberSchema],
)
def get_serial_numbers(type_id: int, db: Session = Depends(get_db)):
return db.query(DewarSerialNumberModel).filter(DewarSerialNumberModel.dewar_type_id == type_id).all()
return (
db.query(DewarSerialNumberModel)
.filter(DewarSerialNumberModel.dewar_type_id == type_id)
.all()
)
@router.post("/dewar-types", response_model=DewarTypeSchema)
def create_dewar_type(dewar_type: DewarTypeCreate, db: Session = Depends(get_db)):
@ -265,14 +318,18 @@ def create_dewar_type(dewar_type: DewarTypeCreate, db: Session = Depends(get_db)
db.refresh(db_type)
return db_type
@router.post("/dewar-serial-numbers", response_model=DewarSerialNumberSchema)
def create_dewar_serial_number(serial_number: DewarSerialNumberCreate, db: Session = Depends(get_db)):
def create_dewar_serial_number(
serial_number: DewarSerialNumberCreate, db: Session = Depends(get_db)
):
db_serial = DewarSerialNumberModel(**serial_number.dict())
db.add(db_serial)
db.commit()
db.refresh(db_serial)
return db_serial
@router.get("/dewar-serial-numbers", response_model=List[DewarSerialNumberSchema])
def get_all_serial_numbers(db: Session = Depends(get_db)):
try:
@ -282,22 +339,31 @@ def get_all_serial_numbers(db: Session = Depends(get_db)):
logging.error(f"Database error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{dewar_id}", response_model=DewarSchema)
async def get_dewar(dewar_id: int, db: Session = Depends(get_db)):
dewar = db.query(DewarModel).options(
joinedload(DewarModel.pucks).joinedload(PuckModel.samples),
joinedload(DewarModel.contact_person),
joinedload(DewarModel.return_address),
joinedload(DewarModel.shipment)
).filter(DewarModel.id == dewar_id).first()
dewar = (
db.query(DewarModel)
.options(
joinedload(DewarModel.pucks).joinedload(PuckModel.samples),
joinedload(DewarModel.contact_person),
joinedload(DewarModel.return_address),
joinedload(DewarModel.shipment),
)
.filter(DewarModel.id == dewar_id)
.first()
)
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
return DewarSchema.from_orm(dewar)
@router.put("/{dewar_id}", response_model=DewarSchema)
async def update_dewar(dewar_id: int, dewar_update: DewarUpdate, db: Session = Depends(get_db)) -> DewarSchema:
async def update_dewar(
dewar_id: int, dewar_update: DewarUpdate, db: Session = Depends(get_db)
) -> DewarSchema:
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
if not dewar:
@ -311,6 +377,7 @@ async def update_dewar(dewar_id: int, dewar_update: DewarUpdate, db: Session = D
db.refresh(dewar)
return dewar
@router.delete("/{dewar_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_dewar(dewar_id: int, db: Session = Depends(get_db)):
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
@ -322,6 +389,7 @@ async def delete_dewar(dewar_id: int, db: Session = Depends(get_db)):
db.commit()
return
# New routes for shipments
@router.get("/shipments", response_model=List[ShipmentSchema])
async def get_all_shipments(db: Session = Depends(get_db)):
@ -332,6 +400,7 @@ async def get_all_shipments(db: Session = Depends(get_db)):
logging.error(f"Database error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/shipments/{id}", response_model=ShipmentSchema)
async def get_single_shipment(id: int, db: Session = Depends(get_db)):
try:
@ -341,4 +410,4 @@ async def get_single_shipment(id: int, db: Session = Depends(get_db)):
return shipment
except SQLAlchemyError as e:
logging.error(f"Database error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@ -2,7 +2,11 @@ from fastapi import APIRouter, HTTPException, Depends
from pydantic import ValidationError
from sqlalchemy.orm import Session, joinedload
from typing import List, Optional
from ..models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel
from ..models import (
Dewar as DewarModel,
Slot as SlotModel,
LogisticsEvent as LogisticsEventModel,
)
from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema
from ..database import get_db
import logging
@ -14,7 +18,9 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval_hours: int = 1) -> int:
def calculate_time_until_refill(
last_refill: Optional[datetime], refill_interval_hours: int = 1
) -> int:
refill_interval = timedelta(hours=refill_interval_hours)
now = datetime.now()
@ -27,30 +33,54 @@ def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval
@router.post("/dewars/return", response_model=DewarSchema)
async def return_to_storage(data: LogisticsEventCreate, db: Session = Depends(get_db)):
logger.info(f"Returning dewar to storage: {data.dewar_qr_code} at location {data.location_qr_code}")
logger.info(
f"Returning dewar to storage: {data.dewar_qr_code} at location {data.location_qr_code}"
)
try:
# Log the incoming payload
logger.info("Received payload: %s", data.json())
dewar = db.query(DewarModel).filter(DewarModel.unique_id == data.dewar_qr_code).first()
dewar = (
db.query(DewarModel)
.filter(DewarModel.unique_id == data.dewar_qr_code)
.first()
)
if not dewar:
logger.error(f"Dewar not found for unique ID: {data.dewar_qr_code}")
raise HTTPException(status_code=404, detail="Dewar not found")
original_slot = db.query(SlotModel).filter(SlotModel.dewar_unique_id == data.dewar_qr_code).first()
original_slot = (
db.query(SlotModel)
.filter(SlotModel.dewar_unique_id == data.dewar_qr_code)
.first()
)
if original_slot and original_slot.qr_code != data.location_qr_code:
logger.error(f"Dewar {data.dewar_qr_code} is associated with slot {original_slot.qr_code}")
raise HTTPException(status_code=400, detail=f"Dewar {data.dewar_qr_code} is associated with a different slot {original_slot.qr_code}.")
logger.error(
f"Dewar {data.dewar_qr_code} is associated with slot {original_slot.qr_code}"
)
raise HTTPException(
status_code=400,
detail=f"Dewar {data.dewar_qr_code} is associated with a different slot {original_slot.qr_code}.",
)
slot = db.query(SlotModel).filter(SlotModel.qr_code == data.location_qr_code).first()
slot = (
db.query(SlotModel)
.filter(SlotModel.qr_code == data.location_qr_code)
.first()
)
if not slot:
logger.error(f"Slot not found for QR code: {data.location_qr_code}")
raise HTTPException(status_code=404, detail="Slot not found")
if slot.occupied and slot.dewar_unique_id != data.dewar_qr_code:
logger.error(f"Slot {data.location_qr_code} is already occupied by another dewar")
raise HTTPException(status_code=400, detail="Selected slot is already occupied by another dewar")
logger.error(
f"Slot {data.location_qr_code} is already occupied by another dewar"
)
raise HTTPException(
status_code=400,
detail="Selected slot is already occupied by another dewar",
)
# Update slot with dewar information
slot.dewar_unique_id = dewar.unique_id
@ -61,7 +91,9 @@ async def return_to_storage(data: LogisticsEventCreate, db: Session = Depends(ge
log_event(db, dewar.id, slot.id, "returned")
db.commit()
logger.info(f"Dewar {data.dewar_qr_code} successfully returned to storage slot {slot.qr_code}.")
logger.info(
f"Dewar {data.dewar_qr_code} successfully returned to storage slot {slot.qr_code}."
)
db.refresh(dewar)
return dewar
except ValidationError as e:
@ -71,6 +103,7 @@ async def return_to_storage(data: LogisticsEventCreate, db: Session = Depends(ge
logger.error(f"Unexpected error: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/dewar/scan", response_model=dict)
async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)):
logger.info(f"Received event data: {event_data}")
@ -82,7 +115,9 @@ async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get
# Validate Dewar QR Code
if not dewar_qr_code or not dewar_qr_code.strip():
logger.error("Dewar QR Code is null or empty")
raise HTTPException(status_code=422, detail="Dewar QR Code cannot be null or empty")
raise HTTPException(
status_code=422, detail="Dewar QR Code cannot be null or empty"
)
# Retrieve the Dewar
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
@ -92,31 +127,42 @@ async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get
# Check for Outgoing QR Codes and set transaction type
if location_qr_code in ["Outgoing X10-SA", "Outgoing X06-SA"]:
transaction_type = 'outgoing'
transaction_type = "outgoing"
# Retrieve the Slot associated with the Dewar (for outgoing)
slot = None
if transaction_type == 'outgoing':
slot = db.query(SlotModel).filter(SlotModel.dewar_unique_id == dewar.unique_id).first()
if transaction_type == "outgoing":
slot = (
db.query(SlotModel)
.filter(SlotModel.dewar_unique_id == dewar.unique_id)
.first()
)
if not slot:
logger.error(f"No slot associated with dewar for outgoing: {dewar_qr_code}")
raise HTTPException(status_code=404, detail="No slot associated with dewar for outgoing")
raise HTTPException(
status_code=404, detail="No slot associated with dewar for outgoing"
)
# Incoming Logic
if transaction_type == 'incoming':
if transaction_type == "incoming":
slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first()
if not slot or slot.occupied:
logger.error(f"Slot not found or already occupied: {location_qr_code}")
raise HTTPException(status_code=400, detail="Slot not found or already occupied")
raise HTTPException(
status_code=400, detail="Slot not found or already occupied"
)
slot.dewar_unique_id = dewar.unique_id
slot.occupied = True
elif transaction_type == 'outgoing':
elif transaction_type == "outgoing":
if not slot.occupied or slot.dewar_unique_id != dewar.unique_id:
logger.error(f"Slot not valid for outgoing: {location_qr_code}")
raise HTTPException(status_code=400, detail="Dewar not associated with the slot for outgoing")
raise HTTPException(
status_code=400,
detail="Dewar not associated with the slot for outgoing",
)
slot.dewar_unique_id = None
slot.occupied = False
elif transaction_type == 'beamline':
elif transaction_type == "beamline":
slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first()
if not slot:
logger.error(f"Beamline location not found: {location_qr_code}")
@ -128,10 +174,12 @@ async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get
log_event(db, dewar.id, slot.id if slot else None, transaction_type)
db.commit()
logger.info(
f"Transaction completed: {transaction_type} for dewar {dewar_qr_code} in slot {slot.qr_code if slot else 'N/A'}")
f"Transaction completed: {transaction_type} for dewar {dewar_qr_code} in slot {slot.qr_code if slot else 'N/A'}"
)
return {"message": "Status updated successfully"}
@router.get("/slots", response_model=List[SlotSchema])
async def get_all_slots(db: Session = Depends(get_db)):
slots = db.query(SlotModel).options(joinedload(SlotModel.dewar)).all()
@ -147,14 +195,16 @@ async def get_all_slots(db: Session = Depends(get_db)):
if slot.dewar_unique_id:
# Calculate time until refill
last_refill_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
last_refill_event = (
db.query(LogisticsEventModel)
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id)
.filter(
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "refill"
) \
.order_by(LogisticsEventModel.timestamp.desc()) \
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "refill",
)
.order_by(LogisticsEventModel.timestamp.desc())
.first()
)
if last_refill_event:
last_refill = last_refill_event.timestamp
@ -163,21 +213,27 @@ async def get_all_slots(db: Session = Depends(get_db)):
time_until_refill = -1
# Fetch the latest beamline event
last_beamline_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
last_beamline_event = (
db.query(LogisticsEventModel)
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id)
.filter(
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "beamline"
) \
.order_by(LogisticsEventModel.timestamp.desc()) \
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "beamline",
)
.order_by(LogisticsEventModel.timestamp.desc())
.first()
)
if last_beamline_event:
# Set retrievedTimestamp to the timestamp of the beamline event
retrievedTimestamp = last_beamline_event.timestamp.isoformat()
# Fetch the associated slot's label for beamlineLocation
associated_slot = db.query(SlotModel).filter(SlotModel.id == last_beamline_event.slot_id).first()
associated_slot = (
db.query(SlotModel)
.filter(SlotModel.id == last_beamline_event.slot_id)
.first()
)
beamlineLocation = associated_slot.label if associated_slot else None
# Mark as being at a beamline
@ -204,7 +260,11 @@ async def get_all_slots(db: Session = Depends(get_db)):
at_beamline=at_beamline,
retrievedTimestamp=retrievedTimestamp,
beamlineLocation=beamlineLocation,
shipment_name=slot.dewar.shipment.shipment_name if slot.dewar and slot.dewar.shipment else None,
shipment_name=(
slot.dewar.shipment.shipment_name
if slot.dewar and slot.dewar.shipment
else None
),
contact_person=contact_person,
local_contact="local contact placeholder",
)
@ -214,7 +274,6 @@ async def get_all_slots(db: Session = Depends(get_db)):
return slots_with_refill_time
@router.post("/dewar/refill", response_model=dict)
async def refill_dewar(qr_code: str, db: Session = Depends(get_db)):
logger.info(f"Refilling dewar with QR code: {qr_code}")
@ -236,9 +295,14 @@ async def refill_dewar(qr_code: str, db: Session = Depends(get_db)):
db.commit()
time_until_refill_seconds = calculate_time_until_refill(now)
logger.info(f"Dewar refilled successfully with time_until_refill: {time_until_refill_seconds}")
logger.info(
f"Dewar refilled successfully with time_until_refill: {time_until_refill_seconds}"
)
return {"message": "Dewar refilled successfully", "time_until_refill": time_until_refill_seconds}
return {
"message": "Dewar refilled successfully",
"time_until_refill": time_until_refill_seconds,
}
@router.get("/dewars", response_model=List[DewarSchema])
@ -250,7 +314,9 @@ async def get_all_dewars(db: Session = Depends(get_db)):
@router.get("/dewar/{unique_id}", response_model=DewarSchema)
async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)):
logger.info(f"Received request for dewar with unique_id: {unique_id}")
dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id.strip()).first()
dewar = (
db.query(DewarModel).filter(DewarModel.unique_id == unique_id.strip()).first()
)
if not dewar:
logger.warning(f"Dewar with unique_id '{unique_id}' not found.")
raise HTTPException(status_code=404, detail="Dewar not found")
@ -263,8 +329,10 @@ def log_event(db: Session, dewar_id: int, slot_id: Optional[int], event_type: st
dewar_id=dewar_id,
slot_id=slot_id,
event_type=event_type,
timestamp=datetime.now()
timestamp=datetime.now(),
)
db.add(new_event)
db.commit()
logger.info(f"Logged event: {event_type} for dewar: {dewar_id} in slot: {slot_id if slot_id else 'N/A'}")
logger.info(
f"Logged event: {event_type} for dewar: {dewar_id} in slot: {slot_id if slot_id else 'N/A'}"
)

View File

@ -8,6 +8,7 @@ from app.dependencies import get_db
router = APIRouter()
@router.get("/", response_model=List[ProposalSchema])
async def get_proposals(db: Session = Depends(get_db)):
return db.query(ProposalModel).all()
return db.query(ProposalModel).all()

View File

@ -2,8 +2,21 @@ from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from typing import List
import uuid
from app.schemas import Puck as PuckSchema, PuckCreate, PuckUpdate, SetTellPosition, PuckEvent
from app.models import Puck as PuckModel, Sample as SampleModel, PuckEvent as PuckEventModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel, Dewar as DewarModel
from app.schemas import (
Puck as PuckSchema,
PuckCreate,
PuckUpdate,
SetTellPosition,
PuckEvent,
)
from app.models import (
Puck as PuckModel,
Sample as SampleModel,
PuckEvent as PuckEventModel,
Slot as SlotModel,
LogisticsEvent as LogisticsEventModel,
Dewar as DewarModel,
)
from app.dependencies import get_db
from datetime import datetime
import logging
@ -13,6 +26,7 @@ router = APIRouter()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@router.get("/", response_model=List[PuckSchema])
async def get_pucks(db: Session = Depends(get_db)):
return db.query(PuckModel).all()
@ -35,8 +49,7 @@ async def get_pucks_with_tell_position(db: Session = Depends(get_db)):
if not pucks:
logger.info("No pucks with tell_position found.") # Log for debugging
raise HTTPException(
status_code=404,
detail="No pucks with a `tell_position` found."
status_code=404, detail="No pucks with a `tell_position` found."
)
result = []
@ -67,6 +80,7 @@ async def get_pucks_with_tell_position(db: Session = Depends(get_db)):
return result
@router.get("/{puck_id}", response_model=PuckSchema)
async def get_puck(puck_id: str, db: Session = Depends(get_db)):
puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first()
@ -77,13 +91,13 @@ async def get_puck(puck_id: str, db: Session = Depends(get_db)):
@router.post("/", response_model=PuckSchema, status_code=status.HTTP_201_CREATED)
async def create_puck(puck: PuckCreate, db: Session = Depends(get_db)) -> PuckSchema:
puck_id = f'PUCK-{uuid.uuid4().hex[:8].upper()}'
puck_id = f"PUCK-{uuid.uuid4().hex[:8].upper()}"
db_puck = PuckModel(
id=puck_id,
puck_name=puck.puck_name,
puck_type=puck.puck_type,
puck_location_in_dewar=puck.puck_location_in_dewar,
dewar_id=puck.dewar_id
dewar_id=puck.dewar_id,
)
db.add(db_puck)
db.commit()
@ -92,7 +106,9 @@ async def create_puck(puck: PuckCreate, db: Session = Depends(get_db)) -> PuckSc
@router.put("/{puck_id}", response_model=PuckSchema)
async def update_puck(puck_id: str, updated_puck: PuckUpdate, db: Session = Depends(get_db)):
async def update_puck(
puck_id: str, updated_puck: PuckUpdate, db: Session = Depends(get_db)
):
puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first()
if not puck:
raise HTTPException(status_code=404, detail="Puck not found")
@ -115,17 +131,18 @@ async def delete_puck(puck_id: str, db: Session = Depends(get_db)):
db.commit()
return
@router.put("/{puck_id}/tell_position", status_code=status.HTTP_200_OK)
async def set_tell_position(
puck_id: int,
request: SetTellPosition,
db: Session = Depends(get_db)
puck_id: int, request: SetTellPosition, db: Session = Depends(get_db)
):
# Get the requested tell_position
tell_position = request.tell_position
# Define valid positions
valid_positions = [f"{letter}{num}" for letter in "ABCDEF" for num in range(1, 6)] + ["null", None]
valid_positions = [
f"{letter}{num}" for letter in "ABCDEF" for num in range(1, 6)
] + ["null", None]
# Validate tell_position
if tell_position not in valid_positions:
@ -161,7 +178,10 @@ async def get_last_tell_position(puck_id: str, db: Session = Depends(get_db)):
# Query the most recent tell_position_set event for the given puck_id
last_event = (
db.query(PuckEventModel)
.filter(PuckEventModel.puck_id == puck_id, PuckEventModel.event_type == "tell_position_set")
.filter(
PuckEventModel.puck_id == puck_id,
PuckEventModel.event_type == "tell_position_set",
)
.order_by(PuckEventModel.timestamp.desc())
.first()
)
@ -182,10 +202,7 @@ async def get_last_tell_position(puck_id: str, db: Session = Depends(get_db)):
@router.get("/slot/{slot_identifier}", response_model=List[dict])
async def get_pucks_by_slot(
slot_identifier: str,
db: Session = Depends(get_db)
):
async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db)):
"""
Retrieve all pucks associated with all dewars linked to the given slot
(by ID or keyword) via 'beamline' events.
@ -200,28 +217,29 @@ async def get_pucks_by_slot(
"PXIII": 49,
"X06SA": 47,
"X10SA": 48,
"X06DA": 49
"X06DA": 49,
}
# Check if the slot identifier is an alias or ID
try:
slot_id = int(slot_identifier) # If the user provided a numeric ID
alias = next((k for k, v in slot_aliases.items() if v == slot_id), slot_identifier)
alias = next(
(k for k, v in slot_aliases.items() if v == slot_id), slot_identifier
)
except ValueError:
slot_id = slot_aliases.get(slot_identifier.upper()) # Try mapping alias
alias = slot_identifier.upper() # Keep alias as-is for error messages
if not slot_id:
raise HTTPException(
status_code=400,
detail="Invalid slot identifier. Must be an ID or one of the following: PXI, PXII, PXIII, X06SA, X10SA, X06DA."
detail="Invalid slot identifier. Must be an ID or one of the following: PXI, PXII, PXIII, X06SA, X10SA, X06DA.",
)
# Verify that the slot exists
slot = db.query(SlotModel).filter(SlotModel.id == slot_id).first()
if not slot:
raise HTTPException(
status_code=404,
detail=f"Slot not found for identifier '{alias}'."
status_code=404, detail=f"Slot not found for identifier '{alias}'."
)
logger.info(f"Slot found: ID={slot.id}, Label={slot.label}")
@ -231,7 +249,7 @@ async def get_pucks_by_slot(
db.query(LogisticsEventModel)
.filter(
LogisticsEventModel.slot_id == slot_id,
LogisticsEventModel.event_type == "beamline"
LogisticsEventModel.event_type == "beamline",
)
.order_by(LogisticsEventModel.timestamp.desc())
.all()
@ -240,8 +258,7 @@ async def get_pucks_by_slot(
if not beamline_events:
logger.warning(f"No dewars associated to this beamline '{alias}'.")
raise HTTPException(
status_code=404,
detail=f"No dewars found for the given beamline '{alias}'."
status_code=404, detail=f"No dewars found for the given beamline '{alias}'."
)
logger.info(f"Found {len(beamline_events)} beamline events for slot_id={slot_id}.")
@ -253,8 +270,7 @@ async def get_pucks_by_slot(
if not dewars:
logger.warning(f"No dewars found for beamline '{alias}'.")
raise HTTPException(
status_code=404,
detail=f"No dewars found for beamline '{alias}'."
status_code=404, detail=f"No dewars found for beamline '{alias}'."
)
logger.info(f"Found {len(dewars)} dewars for beamline '{alias}'.")
@ -273,7 +289,7 @@ async def get_pucks_by_slot(
logger.warning(f"No pucks found for dewars associated with beamline '{alias}'.")
raise HTTPException(
status_code=404,
detail=f"No pucks found for dewars associated with beamline '{alias}'."
detail=f"No pucks found for dewars associated with beamline '{alias}'.",
)
logger.info(f"Found {len(puck_list)} pucks for beamline '{alias}'.")
@ -285,10 +301,10 @@ async def get_pucks_by_slot(
"puck_name": puck.puck_name,
"puck_type": puck.puck_type,
"dewar_id": puck.dewar_id,
"dewar_name": dewar_mapping.get(puck.dewar_id) # Link dewar_name
"dewar_name": dewar_mapping.get(puck.dewar_id), # Link dewar_name
}
for puck in puck_list
]
# Return the list of pucks with their associated dewar names
return puck_output
return puck_output

View File

@ -2,7 +2,11 @@ from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from typing import List
from app.schemas import Puck as PuckSchema, Sample as SampleSchema, SampleEventCreate
from app.models import Puck as PuckModel, Sample as SampleModel, SampleEvent as SampleEventModel
from app.models import (
Puck as PuckModel,
Sample as SampleModel,
SampleEvent as SampleEventModel,
)
from app.dependencies import get_db
import logging
@ -18,10 +22,15 @@ async def get_samples_with_events(puck_id: str, db: Session = Depends(get_db)):
samples = db.query(SampleModel).filter(SampleModel.puck_id == puck_id).all()
for sample in samples:
sample.events = db.query(SampleEventModel).filter(SampleEventModel.sample_id == sample.id).all()
sample.events = (
db.query(SampleEventModel)
.filter(SampleEventModel.sample_id == sample.id)
.all()
)
return samples
@router.get("/pucks-samples", response_model=List[PuckSchema])
async def get_all_pucks_with_samples_and_events(db: Session = Depends(get_db)):
logging.info("Fetching all pucks with samples and events")
@ -32,5 +41,7 @@ async def get_all_pucks_with_samples_and_events(db: Session = Depends(get_db)):
logging.info(f"Puck ID: {puck.id}, Name: {puck.puck_name}")
if not pucks:
raise HTTPException(status_code=404, detail="No pucks found in the database") # More descriptive
raise HTTPException(
status_code=404, detail="No pucks found in the database"
) # More descriptive
return pucks

View File

@ -6,10 +6,27 @@ from pydantic import BaseModel, ValidationError
from datetime import date
from sqlalchemy.exc import SQLAlchemyError
from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, \
Proposal as ProposalModel, Dewar as DewarModel, Puck as PuckModel, Sample as SampleModel
from app.schemas import ShipmentCreate, UpdateShipmentComments, Shipment as ShipmentSchema, DewarUpdate, \
ContactPerson as ContactPersonSchema, Sample as SampleSchema, DewarCreate, PuckCreate, SampleCreate, DewarSchema
from app.models import (
Shipment as ShipmentModel,
ContactPerson as ContactPersonModel,
Address as AddressModel,
Proposal as ProposalModel,
Dewar as DewarModel,
Puck as PuckModel,
Sample as SampleModel,
)
from app.schemas import (
ShipmentCreate,
UpdateShipmentComments,
Shipment as ShipmentSchema,
DewarUpdate,
ContactPerson as ContactPersonSchema,
Sample as SampleSchema,
DewarCreate,
PuckCreate,
SampleCreate,
DewarSchema,
)
from app.database import get_db
from app.crud import get_shipments, get_shipment_by_id
@ -23,7 +40,9 @@ def default_serializer(obj):
@router.get("", response_model=List[ShipmentSchema])
async def fetch_shipments(id: Optional[int] = Query(None), db: Session = Depends(get_db)):
async def fetch_shipments(
id: Optional[int] = Query(None), db: Session = Depends(get_db)
):
if id:
shipment = get_shipment_by_id(db, id)
if not shipment:
@ -35,9 +54,12 @@ async def fetch_shipments(id: Optional[int] = Query(None), db: Session = Depends
shipments = get_shipments(db)
logging.info(f"Total shipments fetched: {len(shipments)}")
for shipment in shipments:
logging.info(f"Shipment ID: {shipment.id}, Shipment Name: {shipment.shipment_name}")
logging.info(
f"Shipment ID: {shipment.id}, Shipment Name: {shipment.shipment_name}"
)
return shipments
@router.get("/{shipment_id}/dewars", response_model=List[DewarSchema])
async def get_dewars_by_shipment_id(shipment_id: int, db: Session = Depends(get_db)):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
@ -51,12 +73,21 @@ async def get_dewars_by_shipment_id(shipment_id: int, db: Session = Depends(get_
return dewars
@router.post("", response_model=ShipmentSchema, status_code=status.HTTP_201_CREATED)
async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db)):
contact_person = db.query(ContactPersonModel).filter(ContactPersonModel.id == shipment.contact_person_id).first()
return_address = db.query(AddressModel).filter(AddressModel.id == shipment.return_address_id).first()
proposal = db.query(ProposalModel).filter(ProposalModel.id == shipment.proposal_id).first()
contact_person = (
db.query(ContactPersonModel)
.filter(ContactPersonModel.id == shipment.contact_person_id)
.first()
)
return_address = (
db.query(AddressModel)
.filter(AddressModel.id == shipment.return_address_id)
.first()
)
proposal = (
db.query(ProposalModel).filter(ProposalModel.id == shipment.proposal_id).first()
)
if not (contact_person or return_address or proposal):
raise HTTPException(status_code=404, detail="Associated entity not found")
@ -97,17 +128,29 @@ async def delete_shipment(shipment_id: int, db: Session = Depends(get_db)):
@router.put("/{shipment_id}", response_model=ShipmentSchema)
async def update_shipment(shipment_id: int, updated_shipment: ShipmentCreate, db: Session = Depends(get_db)):
print("Received payload:", json.dumps(updated_shipment.dict(), indent=2, default=default_serializer))
async def update_shipment(
shipment_id: int, updated_shipment: ShipmentCreate, db: Session = Depends(get_db)
):
print(
"Received payload:",
json.dumps(updated_shipment.dict(), indent=2, default=default_serializer),
)
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
# Validate relationships by IDs
contact_person = db.query(ContactPersonModel).filter(
ContactPersonModel.id == updated_shipment.contact_person_id).first()
return_address = db.query(AddressModel).filter(AddressModel.id == updated_shipment.return_address_id).first()
contact_person = (
db.query(ContactPersonModel)
.filter(ContactPersonModel.id == updated_shipment.contact_person_id)
.first()
)
return_address = (
db.query(AddressModel)
.filter(AddressModel.id == updated_shipment.return_address_id)
.first()
)
if not contact_person:
raise HTTPException(status_code=404, detail="Contact person not found")
if not return_address:
@ -123,25 +166,39 @@ async def update_shipment(shipment_id: int, updated_shipment: ShipmentCreate, db
# Process and update dewars' details
for dewar_data in updated_shipment.dewars:
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_data.dewar_id).first()
dewar = (
db.query(DewarModel).filter(DewarModel.id == dewar_data.dewar_id).first()
)
if not dewar:
raise HTTPException(status_code=404, detail=f"Dewar with ID {dewar_data.dewar_id} not found")
raise HTTPException(
status_code=404, detail=f"Dewar with ID {dewar_data.dewar_id} not found"
)
update_fields = dewar_data.dict(exclude_unset=True)
for key, value in update_fields.items():
if key == 'contact_person_id':
contact_person = db.query(ContactPersonModel).filter(ContactPersonModel.id == value).first()
if key == "contact_person_id":
contact_person = (
db.query(ContactPersonModel)
.filter(ContactPersonModel.id == value)
.first()
)
if not contact_person:
raise HTTPException(status_code=404,
detail=f"Contact person with ID {value} for Dewar {dewar_data.dewar_id} not found")
if key == 'return_address_id':
address = db.query(AddressModel).filter(AddressModel.id == value).first()
raise HTTPException(
status_code=404,
detail=f"Contact person with ID {value} for Dewar {dewar_data.dewar_id} not found",
)
if key == "return_address_id":
address = (
db.query(AddressModel).filter(AddressModel.id == value).first()
)
if not address:
raise HTTPException(status_code=404,
detail=f"Address with ID {value} for Dewar {dewar_data.dewar_id} not found")
raise HTTPException(
status_code=404,
detail=f"Address with ID {value} for Dewar {dewar_data.dewar_id} not found",
)
for key, value in update_fields.items():
if key != 'dewar_id':
if key != "dewar_id":
setattr(dewar, key, value)
db.commit()
@ -150,7 +207,9 @@ async def update_shipment(shipment_id: int, updated_shipment: ShipmentCreate, db
@router.post("/{shipment_id}/add_dewar", response_model=ShipmentSchema)
async def add_dewar_to_shipment(shipment_id: int, dewar_id: int, db: Session = Depends(get_db)):
async def add_dewar_to_shipment(
shipment_id: int, dewar_id: int, db: Session = Depends(get_db)
):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
@ -166,14 +225,18 @@ async def add_dewar_to_shipment(shipment_id: int, dewar_id: int, db: Session = D
@router.delete("/{shipment_id}/remove_dewar/{dewar_id}", response_model=ShipmentSchema)
async def remove_dewar_from_shipment(shipment_id: int, dewar_id: int, db: Session = Depends(get_db)):
async def remove_dewar_from_shipment(
shipment_id: int, dewar_id: int, db: Session = Depends(get_db)
):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
dewar_exists = any(dw.id == dewar_id for dw in shipment.dewars)
if not dewar_exists:
raise HTTPException(status_code=404, detail=f"Dewar with ID {dewar_id} not found in shipment")
raise HTTPException(
status_code=404, detail=f"Dewar with ID {dewar_id} not found in shipment"
)
shipment.dewars = [dw for dw in shipment.dewars if dw.id != dewar_id]
db.commit()
@ -201,8 +264,13 @@ async def get_samples_in_shipment(shipment_id: int, db: Session = Depends(get_db
return samples
@router.get("/shipments/{shipment_id}/dewars/{dewar_id}/samples", response_model=List[SampleSchema])
async def get_samples_in_dewar(shipment_id: int, dewar_id: int, db: Session = Depends(get_db)):
@router.get(
"/shipments/{shipment_id}/dewars/{dewar_id}/samples",
response_model=List[SampleSchema],
)
async def get_samples_in_dewar(
shipment_id: int, dewar_id: int, db: Session = Depends(get_db)
):
shipment = get_shipment_by_id(db, shipment_id)
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
@ -220,8 +288,11 @@ async def get_samples_in_dewar(shipment_id: int, dewar_id: int, db: Session = De
@router.put("/{shipment_id}/comments", response_model=ShipmentSchema)
async def update_shipment_comments(shipment_id: int, comments_data: UpdateShipmentComments,
db: Session = Depends(get_db)):
async def update_shipment_comments(
shipment_id: int,
comments_data: UpdateShipmentComments,
db: Session = Depends(get_db),
):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
@ -232,15 +303,25 @@ async def update_shipment_comments(shipment_id: int, comments_data: UpdateShipme
return shipment
@router.post("/{shipment_id}/add_dewar_puck_sample", response_model=ShipmentSchema, status_code=status.HTTP_201_CREATED)
def add_dewar_puck_sample_to_shipment(shipment_id: int, payload: DewarCreate, db: Session = Depends(get_db)):
@router.post(
"/{shipment_id}/add_dewar_puck_sample",
response_model=ShipmentSchema,
status_code=status.HTTP_201_CREATED,
)
def add_dewar_puck_sample_to_shipment(
shipment_id: int, payload: DewarCreate, db: Session = Depends(get_db)
):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
try:
for dewar_data in payload.dewars:
dewar = db.query(DewarModel).filter(DewarModel.dewar_name == dewar_data.dewar_name).first()
dewar = (
db.query(DewarModel)
.filter(DewarModel.dewar_name == dewar_data.dewar_name)
.first()
)
if dewar:
# Update existing dewar
dewar.tracking_number = dewar_data.tracking_number
@ -284,4 +365,4 @@ def add_dewar_puck_sample_to_shipment(shipment_id: int, payload: DewarCreate, db
except ValidationError as e:
raise HTTPException(status_code=400, detail=f"Validation error: {e}")
return shipment
return shipment

View File

@ -1,7 +1,10 @@
from app.sample_models import SpreadsheetModel, SpreadsheetResponse
from fastapi import APIRouter, UploadFile, File, HTTPException
import logging
from app.services.spreadsheet_service import SampleSpreadsheetImporter, SpreadsheetImportError
from app.services.spreadsheet_service import (
SampleSpreadsheetImporter,
SpreadsheetImportError,
)
from fastapi.responses import FileResponse
import os
from pydantic import ValidationError # Import ValidationError here
@ -10,20 +13,27 @@ from app.row_storage import row_storage # Import the RowStorage instance
router = APIRouter()
logger = logging.getLogger(__name__)
importer = SampleSpreadsheetImporter() # assuming this is a singleton or manageable instance
importer = (
SampleSpreadsheetImporter()
) # assuming this is a singleton or manageable instance
@router.get("/download-template", response_class=FileResponse)
async def download_template():
"""Serve a template file for spreadsheet upload."""
current_dir = os.path.dirname(__file__)
template_path = os.path.join(current_dir, "../../downloads/V7_TELLSamplesSpreadsheetTemplate.xlsx")
template_path = os.path.join(
current_dir, "../../downloads/V7_TELLSamplesSpreadsheetTemplate.xlsx"
)
if not os.path.exists(template_path):
raise HTTPException(status_code=404, detail="Template file not found.")
return FileResponse(template_path, filename="template.xlsx",
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
return FileResponse(
template_path,
filename="template.xlsx",
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
@router.post("/upload", response_model=SpreadsheetResponse)
@ -33,17 +43,24 @@ async def upload_file(file: UploadFile = File(...)):
logger.info(f"Received file: {file.filename}")
# Validate file format
if not file.filename.endswith('.xlsx'):
if not file.filename.endswith(".xlsx"):
logger.error("Invalid file format")
raise HTTPException(status_code=400, detail="Invalid file format. Please upload an .xlsx file.")
raise HTTPException(
status_code=400,
detail="Invalid file format. Please upload an .xlsx file.",
)
# Initialize the importer and process the spreadsheet
validated_model, errors, raw_data, headers = importer.import_spreadsheet_with_errors(file)
validated_model, errors, raw_data, headers = (
importer.import_spreadsheet_with_errors(file)
)
# Extract unique values for dewars, pucks, and samples
dewars = {sample.dewarname for sample in validated_model if sample.dewarname}
pucks = {sample.puckname for sample in validated_model if sample.puckname}
samples = {sample.crystalname for sample in validated_model if sample.crystalname}
samples = {
sample.crystalname for sample in validated_model if sample.crystalname
}
# Construct the response model with the processed data
response_data = SpreadsheetResponse(
@ -56,7 +73,7 @@ async def upload_file(file: UploadFile = File(...)):
pucks=list(pucks),
samples_count=len(samples),
samples=list(samples),
headers=headers # Include headers in the response
headers=headers, # Include headers in the response
)
# Store row data for future use
@ -64,16 +81,23 @@ async def upload_file(file: UploadFile = File(...)):
row_num = idx + 4 # Adjust row numbering if necessary
row_storage.set_row(row_num, row.dict())
logger.info(f"Returning response with {len(validated_model)} records and {len(errors)} errors.")
logger.info(
f"Returning response with {len(validated_model)} records and {len(errors)} errors."
)
return response_data
except SpreadsheetImportError as e:
logger.error(f"Spreadsheet import error: {str(e)}")
raise HTTPException(status_code=400, detail=f"Error processing spreadsheet: {str(e)}")
raise HTTPException(
status_code=400, detail=f"Error processing spreadsheet: {str(e)}"
)
except Exception as e:
logger.error(f"Unexpected error occurred: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to upload file. Please try again. Error: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Failed to upload file. Please try again. Error: {str(e)}",
)
@router.post("/validate-cell")
@ -86,7 +110,9 @@ async def validate_cell(data: dict):
current_row_data = row_storage.get_row(row_num)
# Update the cell value
current_row_data[col_name] = importer._clean_value(value, importer.get_expected_type(col_name))
current_row_data[col_name] = importer._clean_value(
value, importer.get_expected_type(col_name)
)
# Temporarily store the updated row data
row_storage.set_row(row_num, current_row_data)
@ -100,6 +126,8 @@ async def validate_cell(data: dict):
return {"is_valid": True, "message": ""}
except ValidationError as e:
# Extract the first error message
message = e.errors()[0]['msg']
logger.error(f"Validation failed for row {row_num}, column {col_name}: {message}")
message = e.errors()[0]["msg"]
logger.error(
f"Validation failed for row {row_num}, column {col_name}: {message}"
)
return {"is_valid": False, "message": message}