From 26470de563bbe3bc65a73718aee1988ba64e8601 Mon Sep 17 00:00:00 2001
From: gsingh58 <gurmail-singh@wisc.edu>
Date: Tue, 5 Mar 2024 05:22:26 -0600
Subject: [PATCH] lec12 and 13: ipynb files added

---
 lecture_material/12-web-2/solution.ipynb      | 101 +++++++++++
 lecture_material/12-web-2/solution.py         |  54 ++++++
 .../12-web-2/template_lec_001.ipynb           |  84 ++++++++++
 lecture_material/12-web-2/template_lec_001.py |  36 ++++
 .../12-web-2/template_lec_002.ipynb           |  84 ++++++++++
 lecture_material/12-web-2/template_lec_002.py |  36 ++++
 lecture_material/13-web-3/solution.ipynb      | 158 ++++++++++++++++++
 lecture_material/13-web-3/solution.py         |  91 ++++++++++
 .../13-web-3/template_lec_001.ipynb           | 110 ++++++++++++
 lecture_material/13-web-3/template_lec_001.py |  47 ++++++
 .../13-web-3/template_lec_002.ipynb           | 110 ++++++++++++
 lecture_material/13-web-3/template_lec_002.py |  47 ++++++
 12 files changed, 958 insertions(+)
 create mode 100644 lecture_material/12-web-2/solution.ipynb
 create mode 100644 lecture_material/12-web-2/solution.py
 create mode 100644 lecture_material/12-web-2/template_lec_001.ipynb
 create mode 100644 lecture_material/12-web-2/template_lec_001.py
 create mode 100644 lecture_material/12-web-2/template_lec_002.ipynb
 create mode 100644 lecture_material/12-web-2/template_lec_002.py
 create mode 100644 lecture_material/13-web-3/solution.ipynb
 create mode 100644 lecture_material/13-web-3/solution.py
 create mode 100644 lecture_material/13-web-3/template_lec_001.ipynb
 create mode 100644 lecture_material/13-web-3/template_lec_001.py
 create mode 100644 lecture_material/13-web-3/template_lec_002.ipynb
 create mode 100644 lecture_material/13-web-3/template_lec_002.py

diff --git a/lecture_material/12-web-2/solution.ipynb b/lecture_material/12-web-2/solution.ipynb
new file mode 100644
index 0000000..8ca8f48
--- /dev/null
+++ b/lecture_material/12-web-2/solution.ipynb
@@ -0,0 +1,101 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "cf313adf",
+   "metadata": {},
+   "source": [
+    "# Web 2: Flask"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "d55e4bb4-9f29-4f4f-bba6-05054718259b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import requests\n",
+    "import time\n",
+    "import urllib.robotparser"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "527600aa",
+   "metadata": {},
+   "source": [
+    "### Rate-limited webpage parsing\n",
+    "\n",
+    "- `requests` module:\n",
+    "    - `resp = requests.get(<URL>)` method: enables us to send HTTP GET request\n",
+    "    - `resp.status_code`: status code of the response\n",
+    "    - `resp.text`: `str` text content of the response\n",
+    "    - `resp.headers`: `dict` content of response headers"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "8241e51c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "base_url = \"http://34.123.132.20:5000/\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "6cc81b85",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'welcome!'"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "def friendly_get(url):\n",
+    "    while True:\n",
+    "        resp = requests.get(url)\n",
+    "        if resp.status_code == 429:\n",
+    "            seconds = int(resp.headers.get(\"Retry-After\", 1))\n",
+    "            print(f\"sleep {seconds}\")\n",
+    "            time.sleep(seconds)\n",
+    "            continue\n",
+    "        resp.raise_for_status() # raise exception if not 200\n",
+    "        return resp\n",
+    "    \n",
+    "friendly_get(base_url + \"/slow\").text"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/lecture_material/12-web-2/solution.py b/lecture_material/12-web-2/solution.py
new file mode 100644
index 0000000..941b19f
--- /dev/null
+++ b/lecture_material/12-web-2/solution.py
@@ -0,0 +1,54 @@
+import flask # requires installation if not already installed - pip3 install flask
+import time
+import json
+
+app = flask.Flask("my application") # name of the web application can be anything
+
+major_counts = {}
+last_visit = 0 # TODO: dict of visit times, for each IP
+
+# TODO: create a slow page
+# GOAL: don't let people visit this more often than once per 3s
+# flask.request.remote_addr: enables us to take action based on the IP address from
+#                            which we receive the request
+@app.route("/slow")
+def slow():
+    global last_visit
+    print("VISITOR", flask.request.remote_addr)
+    if time.time() - last_visit > 3:
+        last_visit = time.time()
+        return "welcome!"
+    else:
+        return flask.Response("<b>go away</b>",
+                              status=429,
+                              headers={"Retry-After": "3"})
+
+# TODO: write code for creating a page for time.html
+# TEMPLATE semi-static / semi-dynamic
+@app.route("/time.html")
+def clock():
+    with open("time.html") as f:
+        s = f.read()
+    s = s.replace("REPLACE_ME", str(time.time()))
+    return s
+
+# TODO: create a dynamic page ha.html
+# DYNAMIC
+@app.route("/ha.html")
+def laugh():
+    return "ha "*1000
+
+# STATIC
+# @ operator is called a "decorator"
+@app.route("/")
+def home():
+    with open("index.html") as f:
+        html = f.read()
+        
+    return html
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", debug=True, threaded=False)
+
+# app.run never returns, so don't define functions
+# after this (the def lines will never be reached)
diff --git a/lecture_material/12-web-2/template_lec_001.ipynb b/lecture_material/12-web-2/template_lec_001.ipynb
new file mode 100644
index 0000000..d4f7589
--- /dev/null
+++ b/lecture_material/12-web-2/template_lec_001.ipynb
@@ -0,0 +1,84 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "cf313adf",
+   "metadata": {},
+   "source": [
+    "# Web 2: Flask"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d55e4bb4-9f29-4f4f-bba6-05054718259b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import requests\n",
+    "import time\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "527600aa",
+   "metadata": {},
+   "source": [
+    "### Rate-limited webpage parsing\n",
+    "\n",
+    "- `requests` module:\n",
+    "    - `resp = requests.get(<URL>)` method: enables us to send HTTP GET request\n",
+    "    - `resp.status_code`: status code of the response\n",
+    "    - `resp.text`: `str` text content of the response\n",
+    "    - `resp.headers`: `dict` content of response headers"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8241e51c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "base_url = \"http://34.123.132.20:5000/\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6cc81b85",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def friendly_get(url):\n",
+    "    while True:\n",
+    "        resp = requests.get(url)\n",
+    "        resp.raise_for_status() # raise exception if not 200\n",
+    "        return resp\n",
+    "    \n",
+    "friendly_get(base_url + \"/slow\").text"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/lecture_material/12-web-2/template_lec_001.py b/lecture_material/12-web-2/template_lec_001.py
new file mode 100644
index 0000000..94e9273
--- /dev/null
+++ b/lecture_material/12-web-2/template_lec_001.py
@@ -0,0 +1,36 @@
+import flask # requires installation if not already installed - pip3 install flask
+import time
+import json
+
+app = flask.Flask("my application") # name of the web application can be anything
+
+major_counts = {}
+last_visit = 0 # TODO: dict of visit times, for each IP
+
+# TODO: create a slow page
+# GOAL: don't let people visit this more often than once per 3s
+# flask.request.remote_addr: enables us to take action based on the IP address from
+#                            which we receive the request
+
+# TODO: write code for creating a page for time.html
+# TEMPLATE semi-static / semi-dynamic
+
+# TODO: create a dynamic page ha.html
+# DYNAMIC
+
+# STATIC
+# @ operator is called a "decorator"
+# STATIC
+# @ operator is called a "decorator"
+@app.route("/")
+def home():
+    with open("index.html") as f:
+        html = f.read()
+        
+    return html
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", debug=True, threaded=False)
+
+# app.run never returns, so don't define functions
+# after this (the def lines will never be reached)
diff --git a/lecture_material/12-web-2/template_lec_002.ipynb b/lecture_material/12-web-2/template_lec_002.ipynb
new file mode 100644
index 0000000..d4f7589
--- /dev/null
+++ b/lecture_material/12-web-2/template_lec_002.ipynb
@@ -0,0 +1,84 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "cf313adf",
+   "metadata": {},
+   "source": [
+    "# Web 2: Flask"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d55e4bb4-9f29-4f4f-bba6-05054718259b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import requests\n",
+    "import time\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "527600aa",
+   "metadata": {},
+   "source": [
+    "### Rate-limited webpage parsing\n",
+    "\n",
+    "- `requests` module:\n",
+    "    - `resp = requests.get(<URL>)` method: enables us to send HTTP GET request\n",
+    "    - `resp.status_code`: status code of the response\n",
+    "    - `resp.text`: `str` text content of the response\n",
+    "    - `resp.headers`: `dict` content of response headers"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8241e51c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "base_url = \"http://34.123.132.20:5000/\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6cc81b85",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def friendly_get(url):\n",
+    "    while True:\n",
+    "        resp = requests.get(url)\n",
+    "        resp.raise_for_status() # raise exception if not 200\n",
+    "        return resp\n",
+    "    \n",
+    "friendly_get(base_url + \"/slow\").text"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/lecture_material/12-web-2/template_lec_002.py b/lecture_material/12-web-2/template_lec_002.py
new file mode 100644
index 0000000..94e9273
--- /dev/null
+++ b/lecture_material/12-web-2/template_lec_002.py
@@ -0,0 +1,36 @@
+import flask # requires installation if not already installed - pip3 install flask
+import time
+import json
+
+app = flask.Flask("my application") # name of the web application can be anything
+
+major_counts = {}
+last_visit = 0 # TODO: dict of visit times, for each IP
+
+# TODO: create a slow page
+# GOAL: don't let people visit this more often than once per 3s
+# flask.request.remote_addr: enables us to take action based on the IP address from
+#                            which we receive the request
+
+# TODO: write code for creating a page for time.html
+# TEMPLATE semi-static / semi-dynamic
+
+# TODO: create a dynamic page ha.html
+# DYNAMIC
+
+# STATIC
+# @ operator is called a "decorator"
+# STATIC
+# @ operator is called a "decorator"
+@app.route("/")
+def home():
+    with open("index.html") as f:
+        html = f.read()
+        
+    return html
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", debug=True, threaded=False)
+
+# app.run never returns, so don't define functions
+# after this (the def lines will never be reached)
diff --git a/lecture_material/13-web-3/solution.ipynb b/lecture_material/13-web-3/solution.ipynb
new file mode 100644
index 0000000..6e99ca0
--- /dev/null
+++ b/lecture_material/13-web-3/solution.ipynb
@@ -0,0 +1,158 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "cf313adf",
+   "metadata": {},
+   "source": [
+    "# Web 3: More Flask"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "d55e4bb4-9f29-4f4f-bba6-05054718259b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import requests\n",
+    "import time\n",
+    "import urllib.robotparser"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "527600aa",
+   "metadata": {},
+   "source": [
+    "### Rate-limited webpage parsing\n",
+    "\n",
+    "- `requests` module:\n",
+    "    - `resp = requests.get(<URL>)` method: enables us to send HTTP GET request\n",
+    "    - `resp.status_code`: status code of the response\n",
+    "    - `resp.text`: `str` text content of the response\n",
+    "    - `resp.headers`: `dict` content of response headers"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "8241e51c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "base_url = \"http://34.123.132.20:5000/\""
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "23ba100b",
+   "metadata": {},
+   "source": [
+    "### `urllib.robotparser`\n",
+    "\n",
+    "- Documentation: https://docs.python.org/3/library/urllib.robotparser.html"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "379c3ae5-7344-45b1-88c3-b35f0bd8eb5b",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "rp = urllib.robotparser.RobotFileParser()\n",
+    "rp.set_url(base_url + \"/robots.txt\")\n",
+    "rp.read()\n",
+    "rp.can_fetch(\"cs320bot\", base_url + \"/slow\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "2e3fb01c-4281-4cbf-8828-98e04d27d09a",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "rp.can_fetch(\"cs320bot\", base_url + \"/never\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "6cc81b85",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'welcome!'"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "def friendly_get(url):\n",
+    "    if not rp.can_fetch(\"cs320bot\", url):\n",
+    "        raise Exception(\"you're not supposed to visit that page\")\n",
+    "    while True:\n",
+    "        resp = requests.get(url)\n",
+    "        if resp.status_code == 429:\n",
+    "            seconds = int(resp.headers.get(\"Retry-After\", 1))\n",
+    "            print(f\"sleep {seconds}\")\n",
+    "            time.sleep(seconds)\n",
+    "            continue\n",
+    "        resp.raise_for_status() # raise exception if not 200\n",
+    "        return resp\n",
+    "    \n",
+    "friendly_get(base_url + \"/slow\").text"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/lecture_material/13-web-3/solution.py b/lecture_material/13-web-3/solution.py
new file mode 100644
index 0000000..3253c47
--- /dev/null
+++ b/lecture_material/13-web-3/solution.py
@@ -0,0 +1,91 @@
+import flask # requires installation if not already installed - pip3 install flask
+import time
+import json
+
+app = flask.Flask("my application") # name of the web application can be anything
+
+major_counts = {}
+last_visit = 0 # TODO: dict of visit times, for each IP
+
+# TODO: create a survey page
+# flask.request.args: enables us to get the arguments passed as part of the URL
+@app.route("/survey")
+def survey():
+    major = flask.request.args.get("major", "unkown")
+    if not major in major_counts:
+        major_counts[major] = 0
+    major_counts[major] += 1
+    
+    return "MAJORS: \n" + json.dumps(major_counts)
+
+# TODO: create an add page
+@app.route("/add")
+def adder():
+    args = dict(flask.request.args)
+    try:
+        x = float(args["x"])
+        y = float(args["y"])
+    except KeyError:
+        return "Please specify x and y."
+    return f"{x} + {y} = {x+y}"
+
+# TODO: create a never page
+@app.route("/never")
+def never():
+    return "humans only, no bots allowed!"
+
+# TODO: create a robots.txt page
+# flask.Response: enables us to create a response object instance
+# 		  Arguments: str representing reponse, headers dict representing metadata
+@app.route("/robots.txt")
+def bot_rules():
+    return flask.Response("""\
+    User-Agent: *
+    Disallow: /never
+    """, headers={"Content-Type": "text/plain"})
+
+# TODO: create a slow page
+# GOAL: don't let people visit this more often than once per 3s
+# flask.request.remote_addr: enables us to take action based on the IP address from
+#                            which we receive the request
+@app.route("/slow")
+def slow():
+    global last_visit
+    print("VISITOR", flask.request.remote_addr)
+    if time.time() - last_visit > 3:
+        last_visit = time.time()
+        return "welcome!"
+    else:
+        return flask.Response("<b>go away</b>",
+                              status=429,
+                              headers={"Retry-After": "3"})
+
+# TODO: write code for creating a page for time.html
+# TEMPLATE semi-static / semi-dynamic
+@app.route("/time.html")
+def clock():
+    with open("time.html") as f:
+        s = f.read()
+    s = s.replace("REPLACE_ME", str(time.time()))
+    return s
+
+# TODO: create a dynamic page ha.html
+# DYNAMIC
+@app.route("/ha.html")
+def laugh():
+    return "ha "*1000
+
+# STATIC
+# @ operator is called a "decorator"
+@app.route("/")
+def home():
+    with open("index.html") as f:
+        html = f.read()
+        
+    return html
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", debug=True, threaded=False)
+
+# app.run never returns, so don't define functions
+# after this (the def lines will never be reached)
diff --git a/lecture_material/13-web-3/template_lec_001.ipynb b/lecture_material/13-web-3/template_lec_001.ipynb
new file mode 100644
index 0000000..66eb333
--- /dev/null
+++ b/lecture_material/13-web-3/template_lec_001.ipynb
@@ -0,0 +1,110 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "cf313adf",
+   "metadata": {},
+   "source": [
+    "# Web 3: More Flask"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d55e4bb4-9f29-4f4f-bba6-05054718259b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import requests\n",
+    "import time\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "527600aa",
+   "metadata": {},
+   "source": [
+    "### Rate-limited webpage parsing\n",
+    "\n",
+    "- `requests` module:\n",
+    "    - `resp = requests.get(<URL>)` method: enables us to send HTTP GET request\n",
+    "    - `resp.status_code`: status code of the response\n",
+    "    - `resp.text`: `str` text content of the response\n",
+    "    - `resp.headers`: `dict` content of response headers"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8241e51c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "base_url = \"http://34.123.132.20:5000/\""
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "23ba100b",
+   "metadata": {},
+   "source": [
+    "### `urllib.robotparser`\n",
+    "\n",
+    "- Documentation: https://docs.python.org/3/library/urllib.robotparser.html"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "379c3ae5-7344-45b1-88c3-b35f0bd8eb5b",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "2e3fb01c-4281-4cbf-8828-98e04d27d09a",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6cc81b85",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def friendly_get(url):\n",
+    "    while True:\n",
+    "        resp = requests.get(url)\n",
+    "        resp.raise_for_status() # raise exception if not 200\n",
+    "        return resp\n",
+    "    \n",
+    "friendly_get(base_url + \"/slow\").text"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/lecture_material/13-web-3/template_lec_001.py b/lecture_material/13-web-3/template_lec_001.py
new file mode 100644
index 0000000..b939f28
--- /dev/null
+++ b/lecture_material/13-web-3/template_lec_001.py
@@ -0,0 +1,47 @@
+import flask # requires installation if not already installed - pip3 install flask
+import time
+import json
+
+app = flask.Flask("my application") # name of the web application can be anything
+
+major_counts = {}
+last_visit = 0 # TODO: dict of visit times, for each IP
+
+# TODO: create a survey page
+# flask.request.args: enables us to get the arguments passed as part of the URL
+    
+# TODO: create an add page
+
+# TODO: create a never page
+
+# TODO: create a robots.txt page
+# flask.Response: enables us to create a response object instance
+# 		  Arguments: str representing reponse, headers dict representing metadata
+
+# TODO: create a slow page
+# GOAL: don't let people visit this more often than once per 3s
+# flask.request.remote_addr: enables us to take action based on the IP address from
+#                            which we receive the request
+
+# TODO: write code for creating a page for time.html
+# TEMPLATE semi-static / semi-dynamic
+
+# TODO: create a dynamic page ha.html
+# DYNAMIC
+
+# STATIC
+# @ operator is called a "decorator"
+# STATIC
+# @ operator is called a "decorator"
+@app.route("/")
+def home():
+    with open("index.html") as f:
+        html = f.read()
+        
+    return html
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", debug=True, threaded=False)
+
+# app.run never returns, so don't define functions
+# after this (the def lines will never be reached)
diff --git a/lecture_material/13-web-3/template_lec_002.ipynb b/lecture_material/13-web-3/template_lec_002.ipynb
new file mode 100644
index 0000000..66eb333
--- /dev/null
+++ b/lecture_material/13-web-3/template_lec_002.ipynb
@@ -0,0 +1,110 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "cf313adf",
+   "metadata": {},
+   "source": [
+    "# Web 3: More Flask"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d55e4bb4-9f29-4f4f-bba6-05054718259b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import requests\n",
+    "import time\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "527600aa",
+   "metadata": {},
+   "source": [
+    "### Rate-limited webpage parsing\n",
+    "\n",
+    "- `requests` module:\n",
+    "    - `resp = requests.get(<URL>)` method: enables us to send HTTP GET request\n",
+    "    - `resp.status_code`: status code of the response\n",
+    "    - `resp.text`: `str` text content of the response\n",
+    "    - `resp.headers`: `dict` content of response headers"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8241e51c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "base_url = \"http://34.123.132.20:5000/\""
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "23ba100b",
+   "metadata": {},
+   "source": [
+    "### `urllib.robotparser`\n",
+    "\n",
+    "- Documentation: https://docs.python.org/3/library/urllib.robotparser.html"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "379c3ae5-7344-45b1-88c3-b35f0bd8eb5b",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "2e3fb01c-4281-4cbf-8828-98e04d27d09a",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6cc81b85",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def friendly_get(url):\n",
+    "    while True:\n",
+    "        resp = requests.get(url)\n",
+    "        resp.raise_for_status() # raise exception if not 200\n",
+    "        return resp\n",
+    "    \n",
+    "friendly_get(base_url + \"/slow\").text"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/lecture_material/13-web-3/template_lec_002.py b/lecture_material/13-web-3/template_lec_002.py
new file mode 100644
index 0000000..b939f28
--- /dev/null
+++ b/lecture_material/13-web-3/template_lec_002.py
@@ -0,0 +1,47 @@
+import flask # requires installation if not already installed - pip3 install flask
+import time
+import json
+
+app = flask.Flask("my application") # name of the web application can be anything
+
+major_counts = {}
+last_visit = 0 # TODO: dict of visit times, for each IP
+
+# TODO: create a survey page
+# flask.request.args: enables us to get the arguments passed as part of the URL
+    
+# TODO: create an add page
+
+# TODO: create a never page
+
+# TODO: create a robots.txt page
+# flask.Response: enables us to create a response object instance
+# 		  Arguments: str representing reponse, headers dict representing metadata
+
+# TODO: create a slow page
+# GOAL: don't let people visit this more often than once per 3s
+# flask.request.remote_addr: enables us to take action based on the IP address from
+#                            which we receive the request
+
+# TODO: write code for creating a page for time.html
+# TEMPLATE semi-static / semi-dynamic
+
+# TODO: create a dynamic page ha.html
+# DYNAMIC
+
+# STATIC
+# @ operator is called a "decorator"
+# STATIC
+# @ operator is called a "decorator"
+@app.route("/")
+def home():
+    with open("index.html") as f:
+        html = f.read()
+        
+    return html
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", debug=True, threaded=False)
+
+# app.run never returns, so don't define functions
+# after this (the def lines will never be reached)
-- 
GitLab