Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor Flask II] Break up top-level #799

Merged
merged 15 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading