From b4b4c6184e7f0dad268125240f49c53fa31277a3 Mon Sep 17 00:00:00 2001 From: Charlie Date: Fri, 30 Jan 2026 13:29:59 -0500 Subject: [PATCH] Add URL prefix support for proxy deployments --- README.md | 6 ++++-- sample_registry/app.py | 48 ++++++++++++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f1df3de..c937ea5 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,9 @@ python sample_registry/app.py How you want to deploy this will depend on your needs, facilities, and ability. We have it deployed by a Kubernetes cluster but you could also 1) just run it in development mode from a lab computer or 2) setup Nginx/Apache on a dedicated server or 3) run it serverlessly in the cloud (e.g. with [Zappa](https://github.com/zappa/Zappa) on AWS) or 4) do something else. There are lots of well documented examples of deploying Flask sites out there, look around and find what works best for you. -When running, it will default to using a SQLite3 database located in the root of this repository (automatically created if it doesn't already exist). You can change to use a different backend by setting the `SAMPLE_REGISTRY_DB_URI` environment variable before running the app. For example, another sqlite database could be specified with a URI like this: `export SAMPLE_REGISTRY_DB_URI=sqlite:////path/to/db.sqlite`. +When running, it will default to using a SQLite3 database located in the root of this repository (automatically created if it doesn't already exist). You can change to use a different backend by setting the `SAMPLE_REGISTRY_DB_URI` environment variable before running the app. For example, another sqlite database could be specified with a URI like this: `export SAMPLE_REGISTRY_DB_URI=sqlite:////path/to/db.sqlite`. + +If you're deploying behind a reverse proxy at a URL prefix, set `SAMPLE_REGISTRY_URL_PREFIX` (for example, `/sample_registry`). The WSGI entrypoint `sample_registry.app:application` will mount the Flask app at that prefix while leaving local development (`python sample_registry/app.py`) available at `/`. ## Using the library @@ -36,4 +38,4 @@ The `sample_registry` library can be installed and run anywhere by following the If you want to iterate over a feature you can only test on the K8s deployment, you can manually build the Docker image instead of relying on the release workflow. Use `docker build -t ctbushman/sample_registry:latest -f Dockerfile .` to build the image and then `docker push ctbushman/sample_registry:latest` to push it to DockerHub. You can then trigger the K8s deployment to grab the new image. -N.B. You might want to use a different tag than `latest` if you're testing something volatile so that if someone else is trying to use the image as you're developing, they won't pull your wonky changes. \ No newline at end of file +N.B. You might want to use a different tag than `latest` if you're testing something volatile so that if someone else is trying to use the image as you're developing, they won't pull your wonky changes. diff --git a/sample_registry/app.py b/sample_registry/app.py index af95f7a..e058e9d 100644 --- a/sample_registry/app.py +++ b/sample_registry/app.py @@ -19,29 +19,45 @@ from pathlib import Path from sample_registry import ARCHIVE_ROOT, SQLALCHEMY_DATABASE_URI from sample_registry.models import Base, Annotation, Run, Sample -from sample_registry.db import run_to_dataframe, query_tag_stats, STANDARD_TAGS -from sample_registry.standards import STANDARD_HOST_SPECIES, STANDARD_SAMPLE_TYPES -from werkzeug.middleware.proxy_fix import ProxyFix - -app = Flask(__name__) -app.secret_key = os.urandom(12) +from sample_registry.db import run_to_dataframe, query_tag_stats, STANDARD_TAGS +from sample_registry.standards import STANDARD_HOST_SPECIES, STANDARD_SAMPLE_TYPES +from werkzeug.middleware.dispatcher import DispatcherMiddleware +from werkzeug.middleware.proxy_fix import ProxyFix + +app = Flask(__name__) +app.secret_key = os.urandom(12) # This line is only used in production mode on a nginx server, follow instructions to setup forwarding for # whatever production server you are using instead. It's ok to leave this in when running the dev server. app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) -# Sanitize and RO db connection -SQLALCHEMY_DATABASE_URI = f"{SQLALCHEMY_DATABASE_URI.split('?')[0]}?mode=ro" -app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI -print(SQLALCHEMY_DATABASE_URI) +# Sanitize and RO db connection +SQLALCHEMY_DATABASE_URI = f"{SQLALCHEMY_DATABASE_URI.split('?')[0]}?mode=ro" +app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI +print(SQLALCHEMY_DATABASE_URI) # Ensure SQLite explicitly opens in read-only mode app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {"connect_args": {"uri": True}} -db = SQLAlchemy(model_class=Base) -db.init_app(app) - - -@app.route("/favicon.ico") -def favicon(): +db = SQLAlchemy(model_class=Base) +db.init_app(app) + +def _normalize_url_prefix(raw_prefix: str) -> str: + prefix = (raw_prefix or "").strip() + if not prefix or prefix == "/": + return "" + if not prefix.startswith("/"): + prefix = f"/{prefix}" + return prefix.rstrip("/") + + +URL_PREFIX = _normalize_url_prefix(os.getenv("SAMPLE_REGISTRY_URL_PREFIX")) +if URL_PREFIX: + application = DispatcherMiddleware(Flask("dummy_root"), {URL_PREFIX: app}) +else: + application = app + + +@app.route("/favicon.ico") +def favicon(): return send_from_directory( Path(app.root_path) / "static", "favicon.ico",