|
|
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)
|