From 65744dd6aa6481a076146f7a29df3cb45cf2479e Mon Sep 17 00:00:00 2001 From: Danny Shin Date: Thu, 4 Dec 2025 10:46:23 -0800 Subject: [PATCH] completed localhost compatability --- examples/chem-sync-local-flask/Dockerfile | 2 +- examples/chem-sync-local-flask/README.md | 66 +++++++++---------- .../chem-sync-local-flask/docker-compose.yaml | 21 ++++-- .../chem-sync-local-flask/local_app/app.py | 9 +-- .../local_app/benchling_app/setup.py | 9 ++- .../tests/unit/local_app/test_app.py | 10 +-- 6 files changed, 61 insertions(+), 56 deletions(-) diff --git a/examples/chem-sync-local-flask/Dockerfile b/examples/chem-sync-local-flask/Dockerfile index 4d288b8..886d437 100644 --- a/examples/chem-sync-local-flask/Dockerfile +++ b/examples/chem-sync-local-flask/Dockerfile @@ -7,4 +7,4 @@ COPY requirements.txt ./ RUN pip install -r requirements.txt COPY ./local_app /src/local_app WORKDIR /src/local_app -CMD ["flask", "run", "--host", "0.0.0.0"] \ No newline at end of file +CMD ["flask", "run", "--host", "0.0.0.0", "--port", "5004"] \ No newline at end of file diff --git a/examples/chem-sync-local-flask/README.md b/examples/chem-sync-local-flask/README.md index 73eb168..08109ef 100644 --- a/examples/chem-sync-local-flask/README.md +++ b/examples/chem-sync-local-flask/README.md @@ -16,7 +16,7 @@ This app is optimized as a minimal local development experience using [Docker](h > ⚠️ **Development Only**: This example is not meant to be copied into production as-is. There are additional deployment, scale, and security concerns that should be addressed before deploying an app based on this example to production. It relies on a few other tools that will be installed for you within Docker containers: -* [Cloudflare-tunnel](https://www.cloudflare.com/products/tunnel/) - expose a public webhook URL and forward the results locally. ⚠️ *Not for production or real data!* +* [ngrok](https://ngrok.com/) - expose a public webhook URL and forward the results locally. ⚠️ *Not for production or real data!* * [Flask](https://flask.palletsprojects.com/) - A simple Python web application framework ## Getting Started @@ -33,6 +33,23 @@ Windows example: echo.> .client_secret ``` +Create an `.env` file for Docker environment variables. *nix example: + +```bash +touch .env +``` + +Windows example: + +```cmd +echo.> .env +``` + +Then, add your `ngrok` auth token under the alias `NGROK_AUTHTOKEN` which can be found on [ngrok your-authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) like so: +```` +NGROK_AUTHTOKEN= +```` + Start Docker: ```bash @@ -54,28 +71,18 @@ curl localhost:8000/health If Flask is running, you should see `OK` printed. -Be sure to note the URL created for you by `cloudflare-tunnel`. The log line should look something like this: - -``` -cloudflare-tunnel-1 | 2025-03-11T18:45:24Z INF +--------------------------------------------------------------------------------------------+ -cloudflare-tunnel-1 | 2025-03-11T18:45:24Z INF | Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): | -cloudflare-tunnel-1 | 2025-03-11T18:45:24Z INF | https://pioneer-rights-hardly-disorder.trycloudflare.com | -cloudflare-tunnel-1 | 2025-03-11T18:45:24Z INF +--------------------------------------------------------------------------------------------+ -``` - -On *nix systems, you can easily obtain _just_ the URL via: - -``` -docker compose logs cloudflare-tunnel | grep -o 'https://[^ ]*trycloudflare.com[^ ]*' | tail -n 1 +You can obtain the public facing url generated by `ngrok` with: +```bash +curl localhost:4040/api/tunnels ``` Example Output: +```` +{"tunnels":[{"name":"command_line","ID":>,"uri":"/api/tunnels/command_line","public_url":"https://your-public-url.ngrok-free.dev" +... +```` -``` -https://pioneer-rights-hardly-disorder.trycloudflare.com -``` - -> 💡 Don't forget to append `/1/webhooks`, making the full URL given to Benchling `https://pioneer-rights-hardly-disorder.trycloudflare.com/1/webhooks` +> 💡 Don't forget to append `/1/webhooks`, making the full URL given to Benchling `https://your-public-url.ngrok-free.dev/1/webhooks` ## Setting Up Your App in Benchling @@ -98,17 +105,17 @@ When prompted to upload a file, select `manifest.yaml` and click "Create." ### Update the Webhook URL -Every time we restart the `cloudflare-tunnel` Docker container, it will provision +Every time we restart the `ngrok` Docker container, it will provision a new public webhook URL. Update the Benchling App's Webhook URL in the UI with the new server and append the path our Flask route expects (see `local_app/app.py`). -For example, if our `cloudflare-tunnel` generated URL is `https://pioneer-rights-hardly-disorder.trycloudflare.com`, +For example, if our `ngrok` generated URL is `https://your-public-url.ngrok-free.dev`, the webhook URL in Benchling should be: ``` -https://https://pioneer-rights-hardly-disorder.trycloudflare.com/1/webhooks +https://your-public-url.ngrok-free.dev/1/webhooks ``` ![image info](./docs/update-webhook-url.gif) @@ -139,26 +146,15 @@ You'll then need to restart _just_ the `benchling-app` Docker service to pick up docker-compose up -d ``` -If you restart both containers, be sure to update your App in Benchling with the new webhook URL from cloudflare-tunnel. +If you restart both containers, be sure to update your App in Benchling with the new webhook URL from ngrok. ### Setting Client ID Our App needs a Client ID to pair with the Client Secret for authentication to Benchling. In this case, we've created our App to accept `CLIENT_ID` as an environment variable. -One easy way to set an environment variables for Docker is to add a `.env` file. - -```bash -touch .env -``` - -Windows example: - -```cmd -echo.> .env -``` -Open it in an editor of your choice and set the values with the plaintext client ID +Open the `.env` file in an editor of your choice and set the values with the plaintext client ID for your App. For example: ``` diff --git a/examples/chem-sync-local-flask/docker-compose.yaml b/examples/chem-sync-local-flask/docker-compose.yaml index 660e518..0774705 100644 --- a/examples/chem-sync-local-flask/docker-compose.yaml +++ b/examples/chem-sync-local-flask/docker-compose.yaml @@ -1,8 +1,6 @@ services: benchling-app: build: . - ports: - - "8000:5000" volumes: - type: bind source: . @@ -21,16 +19,29 @@ services: - CLIENT_SECRET_FILE=/run/secrets/app_client_secret secrets: - app_client_secret + network_mode: host + # FOR LOCAL DEVELOPMENT ONLY! # Free tool for providing a public URL to forward webhooks to our Benchling App running locally # Do not do this in production or use with any sensitive data. # Benchling has not vetted this tool for use in production or in sensitive systems. # Conduct your own due diligence before choosing a tool for production use. - cloudflare-tunnel: - image: cloudflare/cloudflared + tunnel: + image: ngrok/ngrok:3.7.0-alpine restart: unless-stopped - command: tunnel --url http://benchling-app:5000 + command: "http localhost:5004" + volumes: + # For Mac and Linux + - type: bind + source: /run/host-services/ssh-auth.sock + target: /run/host-services/ssh-auth.sock + environment: + # For Mac and Linux + - SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock + # Note: You'll need to set NGROK_AUTHTOKEN as an environment variable, for example in .env + - NGROK_AUTHTOKEN + network_mode: host secrets: app_client_secret: diff --git a/examples/chem-sync-local-flask/local_app/app.py b/examples/chem-sync-local-flask/local_app/app.py index 767decc..3e37cfb 100644 --- a/examples/chem-sync-local-flask/local_app/app.py +++ b/examples/chem-sync-local-flask/local_app/app.py @@ -1,10 +1,10 @@ from threading import Thread -from benchling_sdk.apps.helpers.webhook_helpers import verify from flask import Flask, request from local_app.benchling_app.handler import handle_webhook -from local_app.benchling_app.setup import app_definition_id + +# from local_app.benchling_app.setup import app_definition_id # noqa: ERA001 from local_app.lib.logger import get_logger logger = get_logger() @@ -21,11 +21,12 @@ def health_check() -> tuple[str, int]: @app.route("/1/webhooks/", methods=["POST"]) def receive_webhooks(target: str) -> tuple[str, int]: # noqa: ARG001 # For security, don't do anything else without first verifying the webhook - app_def_id = app_definition_id() + # app_def_id = app_definition_id() # noqa: ERA001 # Important! To verify webhooks, we need to pass the body as an unmodified string # Flask's request.data is bytes, so decode to string. Passing bytes or JSON won't work - verify(app_def_id, request.data.decode("utf-8"), request.headers) + # Disable when using local instance of Benchling Monolith + # verify(app_def_id, request.data.decode("utf-8"), request.headers) # noqa: ERA001 logger.debug("Received webhook message: %s", request.json) # Dispatch work and ACK webhook as quickly as possible diff --git a/examples/chem-sync-local-flask/local_app/benchling_app/setup.py b/examples/chem-sync-local-flask/local_app/benchling_app/setup.py index 9bcf448..592a1cb 100644 --- a/examples/chem-sync-local-flask/local_app/benchling_app/setup.py +++ b/examples/chem-sync-local-flask/local_app/benchling_app/setup.py @@ -6,6 +6,7 @@ from benchling_sdk.auth.client_credentials_oauth2 import ClientCredentialsOAuth2 from benchling_sdk.benchling import Benchling from benchling_sdk.models.webhooks.v0 import WebhookEnvelopeV0 +from httpx import Client def init_app_from_webhook(webhook: WebhookEnvelopeV0) -> App: @@ -27,7 +28,8 @@ def app_definition_id() -> str: def _benchling_from_webhook(webhook: WebhookEnvelopeV0) -> Benchling: - return Benchling(webhook.base_url, _auth_method()) + client = Client(verify=False) # noqa: S501 + return Benchling(webhook.base_url, _auth_method(), httpx_client=client) @cache @@ -35,11 +37,12 @@ def _auth_method() -> ClientCredentialsOAuth2: client_id = os.environ.get("CLIENT_ID") assert client_id is not None, "Missing CLIENT_ID from environment" client_secret = _client_secret_from_file() - return ClientCredentialsOAuth2(client_id, client_secret) + client = Client(verify=False) # noqa: S501 + return ClientCredentialsOAuth2(client_id, client_secret, httpx_client=client) def _client_secret_from_file() -> str: file_path = os.environ.get("CLIENT_SECRET_FILE") assert file_path is not None, "Missing CLIENT_SECRET_FILE from environment" with Path(file_path).open() as f: - return f.read() + return f.read().strip() diff --git a/examples/chem-sync-local-flask/tests/unit/local_app/test_app.py b/examples/chem-sync-local-flask/tests/unit/local_app/test_app.py index a027d0c..37cce45 100644 --- a/examples/chem-sync-local-flask/tests/unit/local_app/test_app.py +++ b/examples/chem-sync-local-flask/tests/unit/local_app/test_app.py @@ -28,14 +28,8 @@ def client(app: Flask) -> FlaskClient: class TestApp: @patch("local_app.app._enqueue_work") - @patch("local_app.app.app_definition_id") - @patch("local_app.app.verify") - def test_app_receive_webhook( - self, mock_verify, mock_app_definition_id, mock_enqueue_work, client, - ) -> None: + def test_app_receive_webhook(self, mock_verify_app_installation, client) -> None: webhook = load_webhook_json(_TEST_FILES_PATH / "canvas_initialize_webhook.json") response = client.post("1/webhooks/canvas", json=webhook.to_dict()) assert response.status_code == 200 - mock_verify.assert_called_once() - mock_app_definition_id.assert_called_once() - mock_enqueue_work.assert_called_once() + mock_verify_app_installation.assert_called_once()