Skip to content

Commit

Permalink
Merge pull request #799 from NeurodataWithoutBorders/break_up_endpoints
Browse files Browse the repository at this point in the history
[Refactor Flask II] Break up top-level
  • Loading branch information
CodyCBakerPhD authored May 28, 2024
2 parents 37d037e + 2b49554 commit 6972133
Show file tree
Hide file tree
Showing 15 changed files with 274 additions and 154 deletions.
4 changes: 2 additions & 2 deletions src/electron/renderer/src/server/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const activateServer = () => {
export const serverGlobals = {
species: new Promise((res, rej) => {
onServerOpen(() => {
fetch(new URL("get-recommended-species", baseUrl))
fetch(new URL("/dandi/get-recommended-species", baseUrl))
.then((res) => res.json())
.then((species) => {
res(species)
Expand All @@ -67,7 +67,7 @@ export const serverGlobals = {
}),
cpus: new Promise((res, rej) => {
onServerOpen(() => {
fetch(new URL("cpus", baseUrl))
fetch(new URL("/system/cpus", baseUrl))
.then((res) => res.json())
.then((cpus) => {
res(cpus)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ export class PreviewPage extends Page {

updatePath = async (path) => {
if (path) {
const result = await fetch(`${baseUrl}/files/${path}`, { method: "POST" }).then((res) => res.text());
// Enable access to the explicit file path
const result = await fetch(`${baseUrl}/files/${path}`, {
method: "POST",
}).then((res) => res.text());

// Set Neurosift to access the returned URL
if (result) this.neurosift.url = result;
} else this.neurosift.url = undefined;
};
Expand Down
5 changes: 4 additions & 1 deletion src/electron/renderer/src/stories/preview/NWBFilePreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ class NWBPreviewInstance extends LitElement {
const isOnline = navigator.onLine;

return isOnline
? new Neurosift({ url: getURLFromFilePath(this.file, this.project), fullscreen: false })
? new Neurosift({
url: getURLFromFilePath(this.file, this.project),
fullscreen: false,
})
: until(
(async () => {
const htmlRep = await run("html", { nwbfile_path: this.file }, { swal: false });
Expand Down
4 changes: 3 additions & 1 deletion src/electron/renderer/src/stories/preview/Neurosift.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ export class Neurosift extends LitElement {
render() {
return this.url
? html` <div class="loader-container">
${new Loader({ message: `Loading Neurosift view...<br/><small>${this.url}</small>` })}
${new Loader({
message: `Loading Neurosift view...<br/><small>${this.url}</small>`,
})}
</div>
${this.fullscreen ? new FullScreenToggle({ target: this }) : ""}
<iframe
Expand Down
3 changes: 0 additions & 3 deletions src/pyflask/apis/__init__.py

This file was deleted.

96 changes: 41 additions & 55 deletions src/pyflask/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""The primary Flask server for the Python backend."""

import collections
import json
import multiprocessing
import sys
Expand All @@ -10,13 +11,12 @@
from os.path import isabs
from pathlib import Path
from signal import SIGINT
from typing import Union
from urllib.parse import unquote

# https://stackoverflow.com/questions/32672596/pyinstaller-loads-script-multiple-times#comment103216434_32677108
multiprocessing.freeze_support()


from apis import data_api, neuroconv_api, startup_api
from flask import Flask, request, send_file, send_from_directory
from flask_cors import CORS
from flask_restx import Api, Resource
Expand All @@ -27,12 +27,22 @@
is_packaged,
resource_path,
)
from namespaces import (
dandi_namespace,
data_namespace,
neuroconv_namespace,
neurosift_namespace,
startup_namespace,
system_namespace,
)

app = Flask(__name__)
neurosift_file_registry = collections.defaultdict(bool)

flask_app = Flask(__name__)

# Always enable CORS to allow distinct processes to handle frontend vs. backend
CORS(app)
app.config["CORS_HEADERS"] = "Content-Type"
CORS(flask_app)
flask_app.config["CORS_HEADERS"] = "Content-Type"

# Create logger configuration
LOG_FOLDER = Path(GUIDE_ROOT_FOLDER, "logs")
Expand All @@ -50,63 +60,39 @@
title="NWB GUIDE API",
description="The REST API for the NWB GUIDE provided by the Python Flask Server.",
)
api.add_namespace(startup_api)
api.add_namespace(neuroconv_api)
api.add_namespace(data_api)
api.init_app(app)
api.add_namespace(startup_namespace)
api.add_namespace(neuroconv_namespace)
api.add_namespace(data_namespace)
api.add_namespace(system_namespace)
api.add_namespace(dandi_namespace)
# api.add_namespace(neurosift_namespace) # TODO: enable later
api.init_app(flask_app)

registered = {}

@flask_app.route("/preview/<path:file_path>")
def send_preview(file_path):
return send_from_directory(directory=STUB_SAVE_FOLDER_PATH, path=file_path)

@app.route("/files")
def get_all_files():
return list(registered.keys())

@flask_app.route("/files/<path:file_path>", methods=["GET", "POST"])
def handle_file_request(file_path) -> Union[str, None]:
if ".nwb" not in file_path:
raise ValueError("This endpoint must be called on an NWB file!")

@app.route("/files/<path:path>", methods=["GET", "POST"])
def handle_file_request(path):
if request.method == "GET":
if registered[path]:
path = unquote(path)
if not isabs(path):
path = f"/{path}"
return send_file(path)
if neurosift_file_registry[file_path] is True:
file_path = unquote(file_path)
if not isabs(file_path):
file_path = f"/{file_path}"

return send_file(path_or_file=file_path)
else:
app.abort(404, "Resource is not accessible.")

else:
if ".nwb" in path:
registered[path] = True
return request.base_url
else:
app.abort(400, str("Path does not point to an NWB file."))


@app.route("/conversions/<path:path>")
def send_conversions(path):
return send_from_directory(CONVERSION_SAVE_FOLDER_PATH, path)


@app.route("/preview/<path:path>")
def send_preview(path):
return send_from_directory(STUB_SAVE_FOLDER_PATH, path)


@app.route("/cpus")
def get_cpu_count():
from psutil import cpu_count

physical = cpu_count(logical=False)
logical = cpu_count()

return dict(physical=physical, logical=logical)


@app.route("/get-recommended-species")
def get_species():
from dandi.metadata.util import species_map
neurosift_file_registry[file_path] = True

return species_map
return request.base_url


@api.route("/log")
Expand Down Expand Up @@ -154,13 +140,13 @@ def get(self):
)
log_handler.setFormatter(log_formatter)

app.logger.addHandler(log_handler)
app.logger.setLevel(DEBUG)
flask_app.logger.addHandler(log_handler)
flask_app.logger.setLevel(DEBUG)

app.logger.info(f"Logging to {LOG_FILE_PATH}")
flask_app.logger.info(f"Logging to {LOG_FILE_PATH}")

# Run the server
api.logger.info(f"Starting server on port {port}")
app.run(host="127.0.0.1", port=port)
flask_app.run(host="127.0.0.1", port=port)
else:
raise Exception("No port provided for the NWB GUIDE backend.")
6 changes: 6 additions & 0 deletions src/pyflask/namespaces/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .dandi import dandi_namespace
from .data import data_namespace
from .neuroconv import neuroconv_namespace
from .neurosift import neurosift_namespace
from .startup import startup_namespace
from .system import system_namespace
24 changes: 24 additions & 0 deletions src/pyflask/namespaces/dandi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""An API for handling requests to the DANDI Python API."""

from typing import List, Tuple, Union

import flask_restx

dandi_namespace = flask_restx.Namespace(
name="dandi", description="Request various static listings from the DANDI Python API."
)


@dandi_namespace.route("/get-recommended-species")
class SupportedSpecies(flask_restx.Resource):

@dandi_namespace.doc(
description=(
"Request the list of currently supported species (by Latin Binomial name) for DANDI. Note that any "
"explicit NCBI taxonomy link is also supported."
),
)
def get(self) -> Union[List[Tuple[List[str], str, str, str]], None]:
from dandi.metadata.util import species_map

return species_map
20 changes: 10 additions & 10 deletions src/pyflask/apis/data.py → src/pyflask/namespaces/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from flask_restx import Namespace, Resource, reqparse
from manageNeuroconv import generate_dataset, generate_test_data

data_api = Namespace("data", description="API route for dataset generation in the NWB GUIDE.")
data_namespace = Namespace("data", description="API route for dataset generation in the NWB GUIDE.")


@data_api.errorhandler(Exception)
@data_namespace.errorhandler(Exception)
def exception_handler(error):
exceptiondata = traceback.format_exception(type(error), error, error.__traceback__)
return {"message": exceptiondata[-1], "traceback": "".join(exceptiondata)}
Expand All @@ -18,16 +18,16 @@ def exception_handler(error):
generate_test_data_parser.add_argument("output_path", type=str, required=True)


@data_api.route("/generate")
@data_api.expect(generate_test_data_parser)
@data_namespace.route("/generate")
@data_namespace.expect(generate_test_data_parser)
class GeneratetestData(Resource):
@data_api.doc(responses={200: "Success", 400: "Bad Request", 500: "Internal server error"})
@data_namespace.doc(responses={200: "Success", 400: "Bad Request", 500: "Internal server error"})
def post(self):
try:
arguments = generate_test_data_parser.parse_args()
generate_test_data(output_path=arguments["output_path"])
except Exception as exception:
data_api.abort(500, str(exception))
data_namespace.abort(500, str(exception))
raise exception


Expand All @@ -36,14 +36,14 @@ def post(self):
generate_test_dataset_parser.add_argument("input_path", type=str, required=True)


@data_api.route("/generate/dataset")
@data_api.expect(generate_test_data_parser)
@data_namespace.route("/generate/dataset")
@data_namespace.expect(generate_test_data_parser)
class GenerateDataset(Resource):
@data_api.doc(responses={200: "Success", 400: "Bad Request", 500: "Internal server error"})
@data_namespace.doc(responses={200: "Success", 400: "Bad Request", 500: "Internal server error"})
def post(self):
try:
arguments = generate_test_dataset_parser.parse_args()
return generate_dataset(input_path=arguments["input_path"], output_path=arguments["output_path"])

except Exception as exception:
data_api.abort(500, str(exception))
data_namespace.abort(500, str(exception))
Loading

0 comments on commit 6972133

Please sign in to comment.