From 5471002ea9799666a2de0282e74a92bc03574885 Mon Sep 17 00:00:00 2001 From: Taylor Troesh Date: Sat, 10 May 2025 16:53:21 -0700 Subject: [PATCH 01/13] Add server command functionality and corresponding tests Implemented a new `server_command` function to serve `.scrap` files over HTTP, allowing retrieval by path or hash. Added error handling for missing files. Included unit tests for server functionality in `scrapscript_tests.py` to ensure proper operation and error responses. --- scrapscript.py | 68 ++++++++++++++++++++++++++++++++++++++++++++ scrapscript_tests.py | 42 +++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/scrapscript.py b/scrapscript.py index 667e0edf..6b54d032 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2482,6 +2482,64 @@ def flat_command(args: argparse.Namespace) -> None: sys.stdout.buffer.write(serializer.output) +def server_command(args: argparse.Namespace) -> None: + import http.server + import socketserver + import hashlib + import os.path + + dir = os.path.abspath(args.directory) + if not os.path.isdir(dir): + print(f"Error: {dir} is not a valid directory") + sys.exit(1) + + scraps = {} + for root, _, files in os.walk(dir): + for file in files: + if file.endswith(".scrap"): + file_path = os.path.join(root, file) + rel_path = os.path.relpath(file_path, dir) + rel_path_without_ext = os.path.splitext(rel_path)[0] + with open(file_path, "r") as f: + try: + program = parse(tokenize(f.read())) + serializer = Serializer() + serializer.serialize(program) + serialized = bytes(serializer.output) + scraps[rel_path_without_ext] = serialized + logger.debug(f"Loaded {rel_path_without_ext}") + file_hash = hashlib.md5(serialized).hexdigest() + scraps[f"${file_hash}"] = serialized + logger.debug(f"Loaded {rel_path_without_ext} as ${file_hash}") + except Exception as e: + logger.error(f"Error processing {file_path}: {e}") + + class ScrapHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self) -> None: + path = self.path.lstrip("/") + if path in scraps: + self.send_response(200) + self.send_header("Content-Type", "application/scrap; charset=binary") + self.send_header("Content-Disposition", f'attachment; filename="{path}.scrap"') + self.send_header("Content-Length", str(len(scraps[path]))) + self.end_headers() + self.wfile.write(scraps[path]) + else: + self.send_response(404) + self.send_header("Content-Type", "text/plain") + self.end_headers() + self.wfile.write(b"File not found") + + os.chdir(dir) + handler = ScrapHTTPRequestHandler + with socketserver.TCPServer((args.host, args.port), handler) as httpd: + logger.info(f"Serving {dir} at http://{args.host}:{args.port}") + try: + httpd.serve_forever() + except KeyboardInterrupt: + httpd.server_close() + + def main() -> None: parser = argparse.ArgumentParser(prog="scrapscript") subparsers = parser.add_subparsers(dest="command") @@ -2521,6 +2579,16 @@ def main() -> None: flat = subparsers.add_parser("flat") flat.set_defaults(func=flat_command) + yard = subparsers.add_parser("yard") + yard_subparsers = yard.add_subparsers(dest="yard_command") + + yard_server = yard_subparsers.add_parser("server") + yard_server.set_defaults(func=server_command) + yard_server.add_argument("directory", type=str, help="Directory to serve") + yard_server.add_argument("--host", type=str, default="127.0.0.1", help="Host to bind to") + yard_server.add_argument("--port", type=int, default=8080, help="Port to listen on") + yard_server.add_argument("--debug", action="store_true") + args = parser.parse_args() if not args.command: args.debug = False diff --git a/scrapscript_tests.py b/scrapscript_tests.py index e1626d0e..69f8d6f8 100644 --- a/scrapscript_tests.py +++ b/scrapscript_tests.py @@ -4051,5 +4051,47 @@ def test_pretty_print_variant(self) -> None: self.assertEqual(pretty(obj), "#x (a -> b)") +class ServerCommandTests(unittest.TestCase): + def setUp(self) -> None: + import threading, time, os, socket, argparse + from scrapscript import server_command + + # Find a random available port + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + self.port = s.getsockname()[1] + + args = argparse.Namespace( + directory=os.path.abspath(os.path.join(os.path.dirname(__file__), "examples")), + host="127.0.0.1", + port=self.port, + ) + + self.server_thread = threading.Thread(target=server_command, args=(args,)) + self.server_thread.daemon = True + self.server_thread.start() + + time.sleep(0.1) + + def test_server_serves_scrap_by_path(self) -> None: + import urllib.request + + response = urllib.request.urlopen(f"http://localhost:{self.port}/0_home/factorial") + self.assertEqual(response.status, 200) + + def test_server_serves_scrap_by_hash(self) -> None: + import urllib.request + + response = urllib.request.urlopen(f"http://localhost:{self.port}/$781cdc5a4f9855a2a60321d2eea56685") + self.assertEqual(response.status, 200) + + def test_server_fails_missing_scrap(self) -> None: + import urllib.request + + with self.assertRaises(urllib.error.HTTPError) as cm: + urllib.request.urlopen(f"http://localhost:{self.port}/foo") + self.assertEqual(cm.exception.code, 404) + + if __name__ == "__main__": unittest.main() From a9db5a626cda2345e5cc1ee2e35661cad11ed591 Mon Sep 17 00:00:00 2001 From: Taylor Troesh Date: Mon, 12 May 2025 17:40:32 -0700 Subject: [PATCH 02/13] Update scrapscript.py Co-authored-by: Max Bernstein --- scrapscript.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scrapscript.py b/scrapscript.py index 6b54d032..c5b1ecad 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2486,7 +2486,6 @@ def server_command(args: argparse.Namespace) -> None: import http.server import socketserver import hashlib - import os.path dir = os.path.abspath(args.directory) if not os.path.isdir(dir): From 5e6c85fffcf5ee2eecdb934082fd74d077976a58 Mon Sep 17 00:00:00 2001 From: Taylor Troesh Date: Mon, 12 May 2025 17:40:46 -0700 Subject: [PATCH 03/13] Update scrapscript.py Co-authored-by: Max Bernstein --- scrapscript.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index c5b1ecad..cc076503 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2516,13 +2516,14 @@ def server_command(args: argparse.Namespace) -> None: class ScrapHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self) -> None: path = self.path.lstrip("/") - if path in scraps: + scrap = scraps.get(path) + if scrap is not None: self.send_response(200) self.send_header("Content-Type", "application/scrap; charset=binary") self.send_header("Content-Disposition", f'attachment; filename="{path}.scrap"') - self.send_header("Content-Length", str(len(scraps[path]))) + self.send_header("Content-Length", str(len(scrap))) self.end_headers() - self.wfile.write(scraps[path]) + self.wfile.write(scrap) else: self.send_response(404) self.send_header("Content-Type", "text/plain") From ba4165b142d8acb6ce1edba6eb60002c67e0f2c3 Mon Sep 17 00:00:00 2001 From: Taylor Troesh Date: Mon, 12 May 2025 18:16:42 -0700 Subject: [PATCH 04/13] Refactor server command tests and improve scrap file handling Updated `scrapscript_tests.py` to dynamically determine the host and port for server tests, enhancing reliability. Modified `scrapscript.py` to validate scrap file names using regex and adjusted content disposition header formatting. Defaulted the directory argument in the server command to improve usability. --- scrapscript.py | 7 ++++--- scrapscript_tests.py | 26 ++++++++++++++------------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index cc076503..c5f876db 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2486,6 +2486,7 @@ def server_command(args: argparse.Namespace) -> None: import http.server import socketserver import hashlib + import re dir = os.path.abspath(args.directory) if not os.path.isdir(dir): @@ -2495,7 +2496,7 @@ def server_command(args: argparse.Namespace) -> None: scraps = {} for root, _, files in os.walk(dir): for file in files: - if file.endswith(".scrap"): + if re.match(r"^[a-zA-Z0-9-]+\.scrap$", file): file_path = os.path.join(root, file) rel_path = os.path.relpath(file_path, dir) rel_path_without_ext = os.path.splitext(rel_path)[0] @@ -2520,7 +2521,7 @@ def do_GET(self) -> None: if scrap is not None: self.send_response(200) self.send_header("Content-Type", "application/scrap; charset=binary") - self.send_header("Content-Disposition", f'attachment; filename="{path}.scrap"') + self.send_header("Content-Disposition", f'attachment; filename={json.dumps(f"{path}.scrap")}') self.send_header("Content-Length", str(len(scrap))) self.end_headers() self.wfile.write(scrap) @@ -2584,7 +2585,7 @@ def main() -> None: yard_server = yard_subparsers.add_parser("server") yard_server.set_defaults(func=server_command) - yard_server.add_argument("directory", type=str, help="Directory to serve") + yard_server.add_argument("directory", type=str, default=".", help="Directory to serve") yard_server.add_argument("--host", type=str, default="127.0.0.1", help="Host to bind to") yard_server.add_argument("--port", type=int, default=8080, help="Port to listen on") yard_server.add_argument("--debug", action="store_true") diff --git a/scrapscript_tests.py b/scrapscript_tests.py index 69f8d6f8..49db9f5c 100644 --- a/scrapscript_tests.py +++ b/scrapscript_tests.py @@ -4052,6 +4052,8 @@ def test_pretty_print_variant(self) -> None: class ServerCommandTests(unittest.TestCase): + import urllib.request + def setUp(self) -> None: import threading, time, os, socket, argparse from scrapscript import server_command @@ -4059,11 +4061,11 @@ def setUp(self) -> None: # Find a random available port with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("127.0.0.1", 0)) - self.port = s.getsockname()[1] + self.host, self.port = s.getsockname() args = argparse.Namespace( directory=os.path.abspath(os.path.join(os.path.dirname(__file__), "examples")), - host="127.0.0.1", + host=self.host, port=self.port, ) @@ -4071,25 +4073,25 @@ def setUp(self) -> None: self.server_thread.daemon = True self.server_thread.start() - time.sleep(0.1) + # Wait for the server to start + while True: + try: + with socket.create_connection((self.host, self.port), timeout=0.1) as s: + break + except Exception: + time.sleep(0.01) def test_server_serves_scrap_by_path(self) -> None: - import urllib.request - - response = urllib.request.urlopen(f"http://localhost:{self.port}/0_home/factorial") + response = urllib.request.urlopen(f"http://{self.host}:{self.port}/0_home/factorial") self.assertEqual(response.status, 200) def test_server_serves_scrap_by_hash(self) -> None: - import urllib.request - - response = urllib.request.urlopen(f"http://localhost:{self.port}/$781cdc5a4f9855a2a60321d2eea56685") + response = urllib.request.urlopen(f"http://{self.host}:{self.port}/$781cdc5a4f9855a2a60321d2eea56685") self.assertEqual(response.status, 200) def test_server_fails_missing_scrap(self) -> None: - import urllib.request - with self.assertRaises(urllib.error.HTTPError) as cm: - urllib.request.urlopen(f"http://localhost:{self.port}/foo") + urllib.request.urlopen(f"http://{self.host}:{self.port}/foo") self.assertEqual(cm.exception.code, 404) From c64b6be87983f1776868507de5049cd77f5ed0be Mon Sep 17 00:00:00 2001 From: Taylor Troesh Date: Mon, 12 May 2025 18:21:32 -0700 Subject: [PATCH 05/13] Refactor imports and improve code formatting in tests Updated `scrapscript_tests.py` to streamline import statements and enhance code readability. Added `urllib.request` import at the top and organized other imports in the `setUp` method for better clarity. --- scrapscript_tests.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scrapscript_tests.py b/scrapscript_tests.py index 49db9f5c..0180a52c 100644 --- a/scrapscript_tests.py +++ b/scrapscript_tests.py @@ -1,6 +1,7 @@ import unittest import re from typing import Optional +import urllib.request # ruff: noqa: F405 # ruff: noqa: F403 @@ -4052,10 +4053,12 @@ def test_pretty_print_variant(self) -> None: class ServerCommandTests(unittest.TestCase): - import urllib.request - def setUp(self) -> None: - import threading, time, os, socket, argparse + import threading + import time + import os + import socket + import argparse from scrapscript import server_command # Find a random available port @@ -4076,7 +4079,7 @@ def setUp(self) -> None: # Wait for the server to start while True: try: - with socket.create_connection((self.host, self.port), timeout=0.1) as s: + with socket.create_connection((self.host, self.port), timeout=0.1) as s: break except Exception: time.sleep(0.01) From 9a5425abe4ad52f22338b9f839752fc5d31a10e2 Mon Sep 17 00:00:00 2001 From: Taylor Troesh Date: Tue, 13 May 2025 17:51:31 -0700 Subject: [PATCH 06/13] Refactor error handling in server connection tests and remove unused debug argument Updated `scrapscript_tests.py` to specifically catch `ConnectionRefusedError` and `socket.timeout` exceptions for improved error handling during server connection tests. Removed the unused `--debug` argument from the server command in `scrapscript.py` to streamline argument parsing. --- scrapscript.py | 1 - scrapscript_tests.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index c5f876db..7e7317be 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2588,7 +2588,6 @@ def main() -> None: yard_server.add_argument("directory", type=str, default=".", help="Directory to serve") yard_server.add_argument("--host", type=str, default="127.0.0.1", help="Host to bind to") yard_server.add_argument("--port", type=int, default=8080, help="Port to listen on") - yard_server.add_argument("--debug", action="store_true") args = parser.parse_args() if not args.command: diff --git a/scrapscript_tests.py b/scrapscript_tests.py index 0180a52c..680af0a1 100644 --- a/scrapscript_tests.py +++ b/scrapscript_tests.py @@ -4081,7 +4081,7 @@ def setUp(self) -> None: try: with socket.create_connection((self.host, self.port), timeout=0.1) as s: break - except Exception: + except (ConnectionRefusedError, socket.timeout): time.sleep(0.01) def test_server_serves_scrap_by_path(self) -> None: From 717da18328e9f59afddb71f8807b03ce65e1bec2 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 13 May 2025 21:58:28 -0400 Subject: [PATCH 07/13] Don't use re --- scrapscript.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index 7e7317be..5a80149d 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2486,7 +2486,6 @@ def server_command(args: argparse.Namespace) -> None: import http.server import socketserver import hashlib - import re dir = os.path.abspath(args.directory) if not os.path.isdir(dir): @@ -2496,23 +2495,25 @@ def server_command(args: argparse.Namespace) -> None: scraps = {} for root, _, files in os.walk(dir): for file in files: - if re.match(r"^[a-zA-Z0-9-]+\.scrap$", file): - file_path = os.path.join(root, file) - rel_path = os.path.relpath(file_path, dir) - rel_path_without_ext = os.path.splitext(rel_path)[0] - with open(file_path, "r") as f: - try: - program = parse(tokenize(f.read())) - serializer = Serializer() - serializer.serialize(program) - serialized = bytes(serializer.output) - scraps[rel_path_without_ext] = serialized - logger.debug(f"Loaded {rel_path_without_ext}") - file_hash = hashlib.md5(serialized).hexdigest() - scraps[f"${file_hash}"] = serialized - logger.debug(f"Loaded {rel_path_without_ext} as ${file_hash}") - except Exception as e: - logger.error(f"Error processing {file_path}: {e}") + file_path = os.path.join(root, file) + rel_path = os.path.relpath(file_path, dir) + if file.startswith("$"): + logger.debug(f"Skipping {rel_path}") + continue + rel_path_without_ext = os.path.splitext(rel_path)[0] + with open(file_path, "r") as f: + try: + program = parse(tokenize(f.read())) + serializer = Serializer() + serializer.serialize(program) + serialized = bytes(serializer.output) + scraps[rel_path_without_ext] = serialized + logger.debug(f"Loaded {rel_path_without_ext}") + file_hash = hashlib.md5(serialized).hexdigest() + scraps[f"${file_hash}"] = serialized + logger.debug(f"Loaded {rel_path_without_ext} as ${file_hash}") + except Exception as e: + logger.error(f"Error processing {file_path}: {e}") class ScrapHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self) -> None: From 83879c8cb779a2b88bbe2a793100ea947d295b0a Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 13 May 2025 22:09:45 -0400 Subject: [PATCH 08/13] Explicitly shut down scrapyard server in tests --- scrapscript.py | 15 +++++++++++---- scrapscript_tests.py | 4 ++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index 5a80149d..d832c0a6 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2515,7 +2515,16 @@ def server_command(args: argparse.Namespace) -> None: except Exception as e: logger.error(f"Error processing {file_path}: {e}") + keep_serving = True + class ScrapHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def do_QUIT(self) -> None: + self.send_response(200) + self.end_headers() + self.wfile.write(b"Quitting") + nonlocal keep_serving + keep_serving = False + def do_GET(self) -> None: path = self.path.lstrip("/") scrap = scraps.get(path) @@ -2536,10 +2545,8 @@ def do_GET(self) -> None: handler = ScrapHTTPRequestHandler with socketserver.TCPServer((args.host, args.port), handler) as httpd: logger.info(f"Serving {dir} at http://{args.host}:{args.port}") - try: - httpd.serve_forever() - except KeyboardInterrupt: - httpd.server_close() + while keep_serving: + httpd.handle_request() def main() -> None: diff --git a/scrapscript_tests.py b/scrapscript_tests.py index 680af0a1..344dcac1 100644 --- a/scrapscript_tests.py +++ b/scrapscript_tests.py @@ -4084,6 +4084,10 @@ def setUp(self) -> None: except (ConnectionRefusedError, socket.timeout): time.sleep(0.01) + def tearDown(self) -> None: + quit_request = urllib.request.Request(f"http://{self.host}:{self.port}/", method="QUIT") + urllib.request.urlopen(quit_request) + def test_server_serves_scrap_by_path(self) -> None: response = urllib.request.urlopen(f"http://{self.host}:{self.port}/0_home/factorial") self.assertEqual(response.status, 200) From 690e1ff90c74a6e6c827b000b71b949b56493706 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 13 May 2025 22:16:13 -0400 Subject: [PATCH 09/13] Print help for bare yard argument --- scrapscript.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scrapscript.py b/scrapscript.py index d832c0a6..5ace20da 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2589,6 +2589,7 @@ def main() -> None: flat.set_defaults(func=flat_command) yard = subparsers.add_parser("yard") + yard.set_defaults(func=lambda _: yard.print_help()) yard_subparsers = yard.add_subparsers(dest="yard_command") yard_server = yard_subparsers.add_parser("server") From a9860c65bb95e7d519effe5b52be483917e62f95 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 13 May 2025 22:18:14 -0400 Subject: [PATCH 10/13] Make yard server directory optional --- scrapscript.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapscript.py b/scrapscript.py index 5ace20da..4416f8a8 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2594,7 +2594,7 @@ def main() -> None: yard_server = yard_subparsers.add_parser("server") yard_server.set_defaults(func=server_command) - yard_server.add_argument("directory", type=str, default=".", help="Directory to serve") + yard_server.add_argument("directory", type=str, nargs="?", default=".", help="Directory to serve") yard_server.add_argument("--host", type=str, default="127.0.0.1", help="Host to bind to") yard_server.add_argument("--port", type=int, default=8080, help="Port to listen on") From a20f5c2ae8fe82acb3269644c992c5cb3981534d Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 13 May 2025 22:25:56 -0400 Subject: [PATCH 11/13] Don't chdir --- scrapscript.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scrapscript.py b/scrapscript.py index 4416f8a8..f992c2e7 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2541,7 +2541,6 @@ def do_GET(self) -> None: self.end_headers() self.wfile.write(b"File not found") - os.chdir(dir) handler = ScrapHTTPRequestHandler with socketserver.TCPServer((args.host, args.port), handler) as httpd: logger.info(f"Serving {dir} at http://{args.host}:{args.port}") From d707a92f49d74a90e641ca386ac1ead516575308 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 13 May 2025 22:26:18 -0400 Subject: [PATCH 12/13] Don't bother passing absolute path server_command uses abspath internally. --- scrapscript_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapscript_tests.py b/scrapscript_tests.py index 344dcac1..2042af64 100644 --- a/scrapscript_tests.py +++ b/scrapscript_tests.py @@ -4067,7 +4067,7 @@ def setUp(self) -> None: self.host, self.port = s.getsockname() args = argparse.Namespace( - directory=os.path.abspath(os.path.join(os.path.dirname(__file__), "examples")), + directory=os.path.join(os.path.dirname(__file__), "examples"), host=self.host, port=self.port, ) From 5b98e1893f259f15f2a763a70ac19d9ea8cd38f6 Mon Sep 17 00:00:00 2001 From: Taylor Troesh Date: Tue, 13 May 2025 20:13:39 -0700 Subject: [PATCH 13/13] md5->sha256 --- scrapscript.py | 2 +- scrapscript_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scrapscript.py b/scrapscript.py index f992c2e7..805f8d1f 100755 --- a/scrapscript.py +++ b/scrapscript.py @@ -2509,7 +2509,7 @@ def server_command(args: argparse.Namespace) -> None: serialized = bytes(serializer.output) scraps[rel_path_without_ext] = serialized logger.debug(f"Loaded {rel_path_without_ext}") - file_hash = hashlib.md5(serialized).hexdigest() + file_hash = hashlib.sha256(serialized).hexdigest() scraps[f"${file_hash}"] = serialized logger.debug(f"Loaded {rel_path_without_ext} as ${file_hash}") except Exception as e: diff --git a/scrapscript_tests.py b/scrapscript_tests.py index 2042af64..ceecf58b 100644 --- a/scrapscript_tests.py +++ b/scrapscript_tests.py @@ -4093,7 +4093,7 @@ def test_server_serves_scrap_by_path(self) -> None: self.assertEqual(response.status, 200) def test_server_serves_scrap_by_hash(self) -> None: - response = urllib.request.urlopen(f"http://{self.host}:{self.port}/$781cdc5a4f9855a2a60321d2eea56685") + response = urllib.request.urlopen(f"http://{self.host}:{self.port}/$09242a8dfec0ed32eb9ddd5452f0082998712d35306fec2042bad8ac5b6e9580") self.assertEqual(response.status, 200) def test_server_fails_missing_scrap(self) -> None: