diff --git a/README.md b/README.md index 2df0eb5..e5d3757 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The Data Submission Portal is a website that provides recommendations, best prac make up ``` -Access the OpenAPI docs at http://0.0.0.0:5002/docs +Access the OpenAPI docs at http://0.0.0.0:5002/redoc #### Run the full stack (with dspfront) A make command exists for building the dspfront image. It temporarily clones the repository with the default branch and builds the image for you. diff --git a/dspback/main.py b/dspback/main.py index 20d19e6..af28dfb 100644 --- a/dspback/main.py +++ b/dspback/main.py @@ -1,5 +1,6 @@ import uvicorn as uvicorn from fastapi import FastAPI, Request, Response +from fastapi.openapi.utils import get_openapi from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware @@ -12,10 +13,12 @@ app.mount("/api/schema", StaticFiles(directory="dspback/schemas"), name="schemas") -app.include_router(authentication.router, prefix="/api", tags=["api"]) -app.include_router(repository_authorization.router, prefix="/api", tags=["api"]) -app.include_router(submissions.router, prefix="/api", tags=["api"]) -app.include_router(metadata_class.router, prefix="/api", tags=["api"]) +app.include_router(authentication.router, prefix="/api", tags=["Authentication"], include_in_schema=False) +app.include_router( + repository_authorization.router, prefix="/api", tags=["Repository Authorization"], include_in_schema=False +) +app.include_router(metadata_class.router, prefix="/api") +app.include_router(submissions.router, prefix="/api", tags=["Submissions"]) @app.middleware("http") @@ -29,5 +32,20 @@ async def db_session_middleware(request: Request, call_next): return response +openapi_schema = get_openapi( + title="Data Submission Portal API", + version="1.0", + description="Standardized interface with validation for managing metadata across repositories", + routes=app.routes, +) +openapi_schema["info"]["contact"] = { + "name": "Learn more about this API", + "url": "https://github.com/cznethub/dspback", + "email": "sblack@cuahsi.org", +} + + +app.openapi_schema = openapi_schema + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=5002) diff --git a/dspback/routers/metadata_class.py b/dspback/routers/metadata_class.py index 56d76b5..eb566f1 100644 --- a/dspback/routers/metadata_class.py +++ b/dspback/routers/metadata_class.py @@ -80,7 +80,14 @@ class HydroShareMetadataRoutes(MetadataRoutes): response_model = ResourceMetadata repository_type = RepositoryType.HYDROSHARE - @router.post('/metadata/hydroshare', response_model_exclude_unset=True, response_model=response_model) + @router.post( + '/metadata/hydroshare', + response_model_exclude_unset=True, + response_model=response_model, + tags=["HydroShare"], + summary="Create a HydroShare resource", + description="Validates the incoming metadata, creates a new HydroShare resource and creates a submission record.", + ) async def create_metadata_repository(self, metadata: request_model): response = requests.post( self.create_url, @@ -98,7 +105,14 @@ async def create_metadata_repository(self, metadata: request_model): return JSONResponse(json_metadata, status_code=201) - @router.put('/metadata/hydroshare/{identifier}', response_model_exclude_unset=True, response_model=response_model) + @router.put( + '/metadata/hydroshare/{identifier}', + response_model_exclude_unset=True, + response_model=response_model, + tags=["HydroShare"], + summary="Update a HydroShare resource", + description="Validates the incoming metadata and updates the HydroShare resource associated with the provided identifier.", + ) async def update_metadata(self, metadata: request_model, identifier): response = requests.put( self.update_url % identifier, @@ -126,14 +140,26 @@ async def _retrieve_metadata_from_repository(self, identifier): json_metadata["additional_metadata"] = [{"key": key, "value": value} for key, value in as_dict.items()] return json_metadata - @router.get('/metadata/hydroshare/{identifier}', response_model_exclude_unset=True, response_model=response_model) + @router.get( + '/metadata/hydroshare/{identifier}', + response_model_exclude_unset=True, + response_model=response_model, + tags=["HydroShare"], + summary="Get a HydroShare resource", + description="Retrieves the metadata for the HydroShare resource.", + ) async def get_metadata_repository(self, identifier): json_metadata = await self._retrieve_metadata_from_repository(identifier) # workaround for rendering dict with key/value forms await self.submit(identifier=identifier, json_metadata=json_metadata) return json_metadata - @router.delete('/metadata/hydroshare/{identifier}') + @router.delete( + '/metadata/hydroshare/{identifier}', + tags=["HydroShare"], + summary="Delete a HydroShare resource", + description="Deletes the HydroShare resource along with the submission record.", + ) async def delete_metadata_repository(self, identifier): response = requests.delete(self.delete_url % identifier, params={"access_token": self.access_token}) @@ -147,6 +173,9 @@ async def delete_metadata_repository(self, identifier): name="submit", response_model_exclude_unset=True, response_model=response_model, + tags=["HydroShare"], + summary="Register a HydroShare resource", + description="Creates a submission record of the HydroShare resource.", ) async def submit_repository_record(self, identifier: str): json_metadata = await self.submit(identifier) @@ -160,7 +189,14 @@ class ZenodoMetadataRoutes(MetadataRoutes): response_model = ZenodoDatasetsSchemaForCzNetV100 repository_type = RepositoryType.ZENODO - @router.post('/metadata/zenodo', response_model_exclude_unset=True, response_model=response_model) + @router.post( + '/metadata/zenodo', + response_model_exclude_unset=True, + response_model=response_model, + tags=["Zenodo"], + summary="Create a Zenodo resource", + description="Validates the incoming metadata, creates a new Zenodo record and creates a submission record.", + ) async def create_metadata_repository(self, metadata: request_model): metadata_json = {"metadata": json.loads(metadata.json(exclude_none=True))} response = requests.post( @@ -183,6 +219,9 @@ async def create_metadata_repository(self, metadata: request_model): '/metadata/zenodo/{identifier}', response_model_exclude_unset=True, response_model=response_model, + tags=["Zenodo"], + summary="Update a Zenodo record", + description="Validates the incoming metadata and updates the Zenodo record associated with the provided identifier.", ) async def update_metadata(self, metadata: request_model, identifier): existing_metadata = await self.get_metadata_repository(identifier) @@ -210,13 +249,25 @@ async def _retrieve_metadata_from_repository(self, identifier): json_metadata = json.loads(response.text) return json_metadata - @router.get('/metadata/zenodo/{identifier}', response_model_exclude_unset=True, response_model=response_model) + @router.get( + '/metadata/zenodo/{identifier}', + response_model_exclude_unset=True, + response_model=response_model, + tags=["Zenodo"], + summary="Get a Zenodo record", + description="Retrieves the metadata for the Zenodo record.", + ) async def get_metadata_repository(self, identifier): json_metadata = await self._retrieve_metadata_from_repository(identifier) await self.submit(identifier=identifier, json_metadata=json_metadata) return json_metadata["metadata"] - @router.delete('/metadata/zenodo/{identifier}') + @router.delete( + '/metadata/zenodo/{identifier}', + tags=["Zenodo"], + summary="Delete a Zenodo record", + description="Deletes the Zenodo record along with the submission record.", + ) async def delete_metadata_repository(self, identifier): response = requests.delete(self.delete_url % identifier, params={"access_token": self.access_token}) @@ -225,7 +276,14 @@ async def delete_metadata_repository(self, identifier): delete_submission(self.db, self.repository_type, identifier, self.user) - @router.put('/submit/zenodo/{identifier}', name="submit", response_model=SubmissionBase) + @router.put( + '/submit/zenodo/{identifier}', + name="submit", + response_model=SubmissionBase, + tags=["Zenodo"], + summary="Register a Zenodo record", + description="Creates a submission record of the Zenodo record.", + ) async def submit_repository_record(self, identifier: str): json_metadata = await self.submit(identifier) return json_metadata["metadata"] @@ -239,26 +297,26 @@ class EarthChemMetadataRoutes(MetadataRoutes): response_model = Ecl20 repository_type = RepositoryType.EARTHCHEM - @router.post('/metadata/earthchem') + @router.post('/metadata/earthchem', tags=["EarthChem"]) async def create_metadata_repository(self, metadata: request_model) -> response_model: raise NotImplementedError("EarthChem metadata endpoints are not implemented yet") - @router.put('/metadata/earthchem/{identifier}') + @router.put('/metadata/earthchem/{identifier}', tags=["EarthChem"]) async def update_metadata(self, metadata: request_model_update, identifier) -> response_model: raise NotImplementedError("EarthChem metadata endpoints are not implemented yet") async def _retrieve_metadata_from_repository(self, identifier): raise NotImplementedError("EarthChem metadata endpoints are not implemented yet") - @router.get('/metadata/earthchem/{identifier}') + @router.get('/metadata/earthchem/{identifier}', tags=["EarthChem"]) async def get_metadata_repository(self, identifier) -> response_model: raise NotImplementedError("EarthChem metadata endpoints are not implemented yet") - @router.delete('/metadata/earthchem/{identifier}') + @router.delete('/metadata/earthchem/{identifier}', tags=["EarthChem"]) async def delete_metadata_repository(self, identifier): raise NotImplementedError("EarthChem metadata endpoints are not implemented yet") - @router.put('/submit/earthchem/{identifier}', name="submit") + @router.put('/submit/earthchem/{identifier}', name="submit", tags=["EarthChem"]) async def submit_repository_record(self, identifier: str): raise NotImplementedError("EarthChem metadata endpoints are not implemented yet") @@ -270,24 +328,50 @@ class ExternalMetadataRoutes(MetadataRoutes): response_model = GenericDatasetSchemaForCzNetDataSubmissionPortalV100 repository_type = RepositoryType.EXTERNAL - @router.post('/metadata/external', response_model_exclude_unset=True, response_model=response_model) + @router.post( + '/metadata/external', + response_model_exclude_unset=True, + response_model=response_model, + tags=["External"], + summary="Create an external record", + description="Create an external record along with the submission record.", + ) async def create_metadata_repository(self, metadata: request_model): metadata.identifier = str(uuid.uuid4()) metadata_json = json.loads(metadata.json()) metadata_json = await self.submit(metadata.identifier, metadata_json) return JSONResponse(metadata_json, status_code=201) - @router.put('/metadata/external/{identifier}', response_model_exclude_unset=True, response_model=response_model) + @router.put( + '/metadata/external/{identifier}', + response_model_exclude_unset=True, + response_model=response_model, + tags=["External"], + summary="Update an external record", + description="update an external record along with the submission record.", + ) async def update_metadata(self, metadata: request_model, identifier): return await self.submit(identifier, metadata.dict()) - @router.get('/metadata/external/{identifier}', response_model_exclude_unset=True, response_model=response_model) + @router.get( + '/metadata/external/{identifier}', + response_model_exclude_unset=True, + response_model=response_model, + tags=["External"], + summary="Get an external record", + description="Get an external record along with the submission record.", + ) async def get_metadata_repository(self, identifier): submission = self.user.submission(self.db, identifier) metadata_json_str = submission.metadata_json metadata_json = json.loads(metadata_json_str) return metadata_json - @router.delete('/metadata/external/{identifier}') + @router.delete( + '/metadata/external/{identifier}', + tags=["External"], + summary="Delete an external record", + description="Deletes an external record.", + ) async def delete_metadata_repository(self, identifier): delete_submission(self.db, self.repository_type, identifier, self.user) diff --git a/nginx/nginx-local.conf b/nginx/nginx-local.conf index 9a8c387..664452c 100644 --- a/nginx/nginx-local.conf +++ b/nginx/nginx-local.conf @@ -28,7 +28,10 @@ http { location /docs { proxy_pass http://dspback:5002; + } + location /redoc { + proxy_pass http://dspback:5002; } location /sockjs-node {