from datetime import datetime from re import IGNORECASE, sub as substitute from typing import List, Optional from urllib.parse import unquote_plus from fastapi import Depends, FastAPI, File, HTTPException, UploadFile from sqlalchemy.orm import Session from . import crud, models, schemas, spreadsheet from .database import SessionLocal, engine app = FastAPI() # Dependency def get_db(): db = SessionLocal() try: yield db finally: db.close() @app.post( "/items/", responses={ 422: { "description": "Загружен некорректный файл (ожидался .xlsx). " "Неизвестный заголовок. Или обнаружены данные в столбце без заголовка. " "Необходимо использовать файлы установленного образца." }, }, response_model=schemas.InsertStatus, ) def create_items(file: UploadFile = File(...), db: Session = Depends(get_db)): """импорт в базу данных Описаний наборов данных из Excel файлов""" try: start = datetime.now() # parse spreadsheet into a collection of header and data iterators # TODO: Multiple worksheets per Workbook or one? [3] # assume one for now (second one have strange formatting) with spreadsheet.parse(file=file) as spreadsheet_parse: sheet = spreadsheet_parse[0] # TODO use fullfledged validation framework? [6] # ad-hoc for now: # fetch known headersfrom the database # NB: we never check the actual order of the columns, so we might suffer for it # we have to construct a collection of known headers anyway, # might as well make it a set sheet_header_set = set(sheet["header"]) unknown_headers = sheet_header_set - set( dbh.spreadsheet for dbh in crud.get_headers(db=db) ) missing_headers = ( set(dbh.spreadsheet for dbh in crud.get_headers(db=db)) - sheet_header_set ) if unknown_headers: raise HTTPException( status_code=422, detail="Unknown headers in the spreadsheet: {}. Missing headers from the database: {}. Check the coordinated format".format( unknown_headers, missing_headers ), ) # construct a list of schemas.Item items spreadsheet_item_list = [ schemas.ItemCreate( **{ key: row[i] for i, key in enumerate(schemas.ItemCreate.__fields__.keys()) } ) for row in sheet["data"] ] # dump all the data into database accepted, processed = crud.insert_items(db=db, items=spreadsheet_item_list) except spreadsheet.InvalidFileException: raise HTTPException( status_code=422, detail="Invalid file upload (expected .xlsx)" ) except spreadsheet.DataInUnnamedColumnException: raise HTTPException( status_code=422, detail="Data is found in a column with empty header" ) return schemas.InsertStatus( status="Success" if accepted == processed else "Warning", accepted=accepted, processed=processed, process_time=datetime.now() - start, ) @app.get( "/item/{item_id}", response_model=schemas.Item, responses={404: {"description": "Такой набор данных отсутствует"}}, ) def read_item(item_id: int, db: Session = Depends(get_db)): """индивидуальные страницы для каждого Описания набора данных""" db_item = crud.get_item(db=db, item_id=item_id) if db_item is None: raise HTTPException(status_code=404, detail="Item not found") return db_item @app.get("/items/", response_model=List[schemas.Item]) def read_items( categ: Optional[str] = None, skip: int = 0, limit: int = 20, db: Session = Depends(get_db), ): """список доступных в системе Описаний наборов данных опционально, с указанием конкретной категории """ if categ is None or str(categ).strip().lower() in ("any", "all"): resp = crud.get_items(db=db, skip=skip, limit=limit) else: # heuristically adjust query capitalization to fit the spreadsheet form (e.g "Geology") category = str(categ).strip().capitalize() resp = crud.get_items_by_category( category=category, db=db, skip=skip, limit=limit ) return resp @app.get( "/search/", response_model=List[schemas.Item], responses={400: {"description": "Запрос слишком короткий (минимум 3 символа)"}}, ) def search(q: str, skip: int = 0, limit: int = 20, db: Session = Depends(get_db)): """фильтры для поиска Описаний наборов данных по атрибутам дополнительный возможный синтаксис в запросах преобразуется в поисковый запрос содержащий: - простой текст: переданные слова в любом порядке - "текст в кавычках": переданные слова в указанном порядке - OR ИЛИ: какие-либо из переданных слов - -: не содержащий данного слова """ q = unquote_plus(q) # replace all full ИЛИ words with OR q = substitute(r"\bИЛИ\b", "OR", q, flags=IGNORECASE) if len(q) < 3: raise HTTPException(status_code=400, detail="Query too short") return crud.get_item_by_description(db=db, needle=q, skip=skip, limit=limit) @app.post( "/detailed_search/", response_model=List[schemas.Item], ) def detailed_search( query: schemas.DetailedSearchQuery, skip: int = 0, limit: int = 20, db: Session = Depends(get_db), ): """фильтры для поиска Описаний наборов данных по атрибутам дополнительный возможный синтаксис в запросах преобразуется в поисковый запрос содержащий: - простой текст: переданные слова в любом порядке - "текст в кавычках": переданные слова в указанном порядке - OR ИЛИ: какие-либо из переданных слов - -: не содержащий данного слова дополнительные поля ипользуются напрямую """ if query.main_query is not None: # replace all full ИЛИ words with OR query.main_query = substitute( r"\bИЛИ\b", "OR", query.main_query, flags=IGNORECASE ) return crud.get_item_by_detailed_description( db=db, query=query, skip=skip, limit=limit ) @app.get("/headers/", response_model=List[schemas.Header]) def read_headers(db: Session = Depends(get_db)): """полные наименования столбцов таблиц""" return crud.get_headers(db=db)