1+ from enum import Enum
12from typing import Any , List
23
34from fastapi import APIRouter , Depends , HTTPException
45from sqlalchemy import func , or_
56from sqlalchemy .exc import MultipleResultsFound
67from sqlalchemy .orm import Session
8+ from starlette .convertors import Convertor , register_url_convertor
79
810from mavedb import deps
9- from mavedb .lib .identifiers import find_generic_article , find_or_create_publication_identifier
11+ from mavedb .lib .identifiers import find_generic_article
12+ from mavedb .lib .validation .constants .publication import valid_dbnames
1013from mavedb .models .publication_identifier import PublicationIdentifier
1114from mavedb .view_models import publication_identifier
1215from mavedb .view_models .search import TextSearch
1316
17+ # I don't think we can escape the type: ignore hint here on a dynamically created enumerated type.
18+ PublicationDatabases = Enum ("PublicationDataBases" , ((x , x ) for x in valid_dbnames )) # type: ignore
19+
20+
1421router = APIRouter (
1522 prefix = "/api/v1/publication-identifiers" ,
1623 tags = ["publication identifiers" ],
@@ -27,8 +34,31 @@ def list_publications(*, db: Session = Depends(deps.get_db)) -> Any:
2734 return items
2835
2936
37+ # See https://github.com/tiangolo/fastapi/discussions/7328, which describes that slashes are currently un-escapable in FastAPI/Starlette
38+ # implementations.
39+ #
40+ # Workaround here by defining a custom type convertor (see https://www.starlette.io/routing/#path-parameters) whose
41+ # RegEx match matches our accepted publication identifiers. When the API is queried and the string matches the convertor, we enter the
42+ # route on which the convertor captured a match, use the match as our variable of interest, and resolve the route. By capturing the match,
43+ # we retain the ability to add routes such as /db_name/identifier or /identifier/title since we can now match to these unescapable slashes
44+ # without matching the rest of the path as a path type, as the `:path` convertor would have. In the OpenAPI spec, this type will still be
45+ # represented as a 'path'. -capodb 2024.05.30
46+ class PublicationIdentifierConverter (Convertor ):
47+ # RegEx structure: "DOI | PubMed | bioRxiv | medRxiv"
48+ regex = "10[.][0-9]{4,9}\/[-._;()\/:A-z0-9]+|[0-9]{0,8}|[0-9]{4}[.][0-9]{2}[.][0-9]{2}[.][0-9]{6}|[0-9]{4}[.][0-9]{2}[.][0-9]{2}[.][0-9]{8}"
49+
50+ def convert (self , value : str ) -> str :
51+ return value
52+
53+ def to_string (self , value : str ) -> str :
54+ return str (value )
55+
56+
57+ register_url_convertor ("publication" , PublicationIdentifierConverter ())
58+
59+
3060@router .get (
31- "/{identifier}" ,
61+ "/{identifier:publication }" ,
3262 status_code = 200 ,
3363 response_model = publication_identifier .PublicationIdentifier ,
3464 responses = {404 : {}},
@@ -51,14 +81,14 @@ def fetch_publication_by_identifier(*, identifier: str, db: Session = Depends(de
5181
5282
5383@router .get (
54- "/{db_name}/{identifier}" ,
84+ "/{db_name:str }/{identifier:publication }" ,
5585 status_code = 200 ,
5686 response_model = publication_identifier .PublicationIdentifier ,
5787 responses = {404 : {}},
5888)
5989def fetch_publication_by_dbname_and_identifier (
6090 * ,
61- db_name : str ,
91+ db_name : PublicationDatabases ,
6292 identifier : str ,
6393 db : Session = Depends (deps .get_db ),
6494) -> PublicationIdentifier :
@@ -68,18 +98,19 @@ def fetch_publication_by_dbname_and_identifier(
6898 try :
6999 item = (
70100 db .query (PublicationIdentifier )
71- .filter (PublicationIdentifier .identifier == identifier and PublicationIdentifier .db_name == db_name )
101+ .filter (PublicationIdentifier .identifier == identifier )
102+ .filter (PublicationIdentifier .db_name == db_name .name )
72103 .one_or_none ()
73104 )
74105 except MultipleResultsFound :
75106 raise HTTPException (
76107 status_code = 500 ,
77- detail = f"Multiple publications with identifier { identifier } and database name { db_name } were found." ,
108+ detail = f"Multiple publications with identifier { identifier } and database name { db_name . name } were found." ,
78109 )
79110 if not item :
80111 raise HTTPException (
81112 status_code = 404 ,
82- detail = f"No publication with identifier { identifier } and database name { db_name } were found." ,
113+ detail = f"No publication with identifier { identifier } and database name { db_name . name } were found." ,
83114 )
84115 return item
85116
@@ -191,7 +222,7 @@ async def search_publications_by_identifier(*, identifier: str, db: Session = De
191222@router .get (
192223 "/search/{db_name}/{identifier}" ,
193224 status_code = 200 ,
194- response_model = publication_identifier .PublicationIdentifier ,
225+ response_model = list [ publication_identifier .PublicationIdentifier ] ,
195226 responses = {404 : {}, 500 : {}},
196227)
197228async def search_publications_by_identifier_and_db (
@@ -201,7 +232,7 @@ async def search_publications_by_identifier_and_db(
201232 db : Session = Depends (deps .get_db ),
202233) -> Any :
203234 """
204- Search publication identifiers via their identifier and database.
235+ Search all of the publication identifiers via their identifier and database.
205236 """
206237 query = (
207238 db .query (PublicationIdentifier )
0 commit comments