You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
geodata-catalog/src/main.py

188 lines
7.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)