commit 3c2d665bfd4ceef056c183e0ad13f3899bb77a3f
Author: Michael Pilosov 
Date:   Fri May 31 11:25:40 2024 -0600
    new repo
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..424d620
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+out/
+*.png
+*.gif
+*.mp4
+__pycache__/
+*.db
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..f984865
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,51 @@
+# Use Python 3.10.11 slim image as the base image
+FROM python:3.10.11-slim
+
+# Set environment variables to avoid writing .pyc files and buffering stdout and stderr
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV PYTHONUNBUFFERED 1
+
+# Create a new user 'user' with UID and GID of 1000
+RUN groupadd -g 1000 user && \
+    useradd -m -s /bin/bash -u 1000 -g user user
+
+# Set environment variables for the user install
+ENV PATH=/home/user/.local/bin:$PATH
+
+# Install system dependencies as root
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends make ffmpeg dumb-init && \
+    rm -rf /var/lib/apt/lists/*
+
+# Set the home directory
+WORKDIR /home/user/
+RUN chown -R user:user /home/user
+
+# Switch to non-root user before copying files and installing Python packages
+USER user
+
+# Copy the requirements file to /tmp and install Python dependencies with user flag
+COPY --chown=user:user requirements.txt /tmp/requirements.txt
+RUN python -m pip install --upgrade pip
+RUN pip install --no-cache-dir --user -r /tmp/requirements.txt
+
+# APPLICATION SETUP
+
+# Copy the default profiles file and set the appropriate permissions
+COPY --chown=user:user profiles.default.toml /home/user/.prefect/profiles.toml
+
+# Copy the application files
+COPY --chown=user:user app ./app
+COPY --chown=user:user noaa_animate.py .
+COPY --chown=user:user start.sh .
+COPY --chown=user:user init_db.py .
+RUN chmod +x start.sh
+RUN mkdir -p out
+RUN python init_db.py /home/user/.prefect/prefect.db
+
+# Set the correct ownership (recursively) for /app
+# Already owned by user due to --chown in COPY commands
+
+# Define the entrypoint and the commands to execute
+ENTRYPOINT ["dumb-init", "--"]
+CMD ["./start.sh"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..17d748d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# NOAA Animation Web App 
+
+
+Animate images from a website full of urls by way of a proxy server and some Python functions wrapped with the `prefect` decorator.
+Uses https://services.swpc.noaa.gov as an example.
+
+
+## Instructions
+If you have `docker` and `make` installed, just run:
+
+```bash
+make
+```
+
+and visit `localhost:4200` to see Prefect, and `localhost:9021/iframe` to view the UI.
+
+An example Flow of about 100 images:
+
+
diff --git a/app/app.py b/app/app.py
new file mode 100644
index 0000000..1fc7303
--- /dev/null
+++ b/app/app.py
@@ -0,0 +1,150 @@
+import logging
+import os
+import re
+import time
+from datetime import datetime
+
+import requests
+from flask import Flask, Response, render_template, request, send_from_directory
+from prefect.deployments import run_deployment
+
+PORT = 9021
+app = Flask(__name__)
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+def deploy_name():
+    return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") + "Z"
+
+
+def get_host():
+    host = os.environ.get("LIGHTNING_CLOUDSPACE_HOST")
+    if host is None:
+        default_host = os.environ.get("HOST_NAME", "0.0.0.0")
+        return f"{default_host}:{PORT}"
+    else:
+        return f"{PORT}-{host}"
+
+
+@app.route("/iframe")
+@app.route("/iframe/")
+@app.route("/iframe/")
+def home(subpath="images/animations/"):
+    host = get_host()
+    initial_url = f"http://{host}/{subpath}"
+    api_url = f"http://{host}/api"
+    return render_template(
+        "index.html", initial_url=initial_url, host=f"http://{host}", api_url=api_url
+    )
+
+
+@app.route("/api", methods=["POST"])
+def handle_api():
+    data = request.json  # This assumes you're sending JSON data.
+    url = data.get("url")
+    if not url.endswith("/"):
+        url += "/"
+
+    logging.debug(f"Received URL: {url}")
+    params = {"url": url, "limit": 24 * 60, "ext": None}
+    response = run_deployment(
+        name="create-animations/noaa-animate",
+        parameters=params,
+        flow_run_name=f"{deploy_name()}.webapp.{url}",
+    )
+    # response is a FlowRun - need to get what we want from it.
+
+    # Process the data as needed.
+    return {
+        "status": "success",
+        "message": f"{url} processed successfully",
+        # "response": response,
+    }, 200
+
+
+@app.route("/videos/")
+def custom_static(filename):
+    return send_from_directory("../out", filename)
+
+
+@app.route("/", methods=["GET"])
+@app.route("/", methods=["GET"])
+def proxy(url=""):
+    original_base_url = "https://services.swpc.noaa.gov"
+    host = get_host()
+    proxy_base_url = f"http://{host}/"
+
+    target_url = f"{original_base_url}/{url}"
+    logging.debug(f"Fetching URL: {target_url}")
+
+    try:
+        headers = {
+            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
+        }
+        response = requests.get(target_url, headers=headers, stream=True)
+        excluded_headers = [
+            "content-encoding",
+            "content-length",
+            "transfer-encoding",
+            "connection",
+        ]
+        headers = [
+            (name, value)
+            for (name, value) in response.raw.headers.items()
+            if name.lower() not in excluded_headers
+        ]
+
+        if "text/html" in response.headers.get("Content-Type", ""):
+            content = response.content.decode("utf-8")
+            content = re.sub(r"'http://", "'https://", content)
+            content = re.sub(
+                r"https?://services.swpc.noaa.gov", proxy_base_url, content
+            )
+
+            content = content.replace(
+                "
+    Animate a Folder of Images
+    Navigate to a folder of 60+ images.
+    
+     
+        
+        
+    
+    
+    
+
+    
+
+    
+    
+    
+
+    Source: Loading...
+",
+                f"""
+                
+                """,
+            )
+            content = content.encode("utf-8")
+            return Response(content, status=response.status_code, headers=headers)
+        else:
+            return Response(
+                response.content, status=response.status_code, headers=headers
+            )
+
+    except Exception as e:
+        logging.error(f"Error fetching URL: {e}")
+        return Response(f"Error fetching URL: {e}", status=500)
+
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", port=9021, debug=True)
diff --git a/app/makefile b/app/makefile
new file mode 100644
index 0000000..8ea0bd4
--- /dev/null
+++ b/app/makefile
@@ -0,0 +1,5 @@
+start:
+	gunicorn --worker-class gevent --bind 0.0.0.0:9021 app:app
+
+dev:
+	python app.py
diff --git a/app/templates/index.html b/app/templates/index.html
new file mode 100644
index 0000000..bd1bbbe
--- /dev/null
+++ b/app/templates/index.html
@@ -0,0 +1,271 @@
+
+
+
+