### Example Workflow for Asset Compression Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/02-compress-module.md This workflow demonstrates the steps to build your project's assets, pre-compress them using BlackNoise, and then start your development server. Ensure you have run `npm run build` before executing the compression command. ```bash # 1. Build your assets (webpack, vite, etc.) npm run build # 2. Pre-compress assets python -m blacknoise.compress ./dist # 3. Start server python manage.py runserver ``` -------------------------------- ### ASGI Server Startup Pattern with BlackNoise Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/08-asgi-integration.md This pattern demonstrates the recommended way to create and configure an ASGI application with BlackNoise, ensuring all setup is done before the server starts. It wraps the base application and adds static directories. ```python from blacknoise import BlackNoise from pathlib import Path def get_app(): """Create and configure the ASGI application.""" # 1. Create the base application from myapp import app # 2. Wrap with BlackNoise (should be outermost) app = BlackNoise(app) # 3. Add static directories base_dir = Path(__file__).parent app.add(base_dir / "static", "/static/") app.add(base_dir / "public", "/public/") return app # Create app instance at module level application = get_app() # Uvicorn will call: # uvicorn myapp:application ``` -------------------------------- ### Install and Run Uvicorn Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/08-asgi-integration.md Install Uvicorn and run your ASGI application. Use the `--workers` flag for multi-threaded production environments. ```bash pip install uvicorn # Single-threaded uvicorn myapp:application # Multi-threaded (production) uvicorn myapp:application --workers 4 # With reload uvicorn myapp:application --reload ``` -------------------------------- ### Install and Run Daphne Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/08-asgi-integration.md Install Daphne and run your ASGI application. Daphne is a popular ASGI server. ```bash pip install daphne daphne -b 0.0.0.0 -p 8000 myapp:application ``` -------------------------------- ### Install and Run Hypercorn Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/08-asgi-integration.md Install Hypercorn and run your ASGI application. Hypercorn supports HTTP/2 and WebSockets. ```bash pip install hypercorn hypercorn myapp:application --bind 0.0.0.0:8000 ``` -------------------------------- ### Example BlackNoise Registry Initialization Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md Demonstrates how to initialize BlackNoise and add static file directories, showing the resulting file registry. ```python bn = BlackNoise(app) bn.add(Path("/var/www/static"), "/static/") bn.add(Path("/var/www/public"), "/public/") print(bn._files) # Output: # { # '/static/app.js': '/var/www/static/app.js', # '/static/app.css': '/var/www/static/app.css', # '/static/images/logo.png': '/var/www/static/images/logo.png', # '/public/robots.txt': '/var/www/public/robots.txt', # '/public/sitemap.xml': '/var/www/public/sitemap.xml', # } ``` -------------------------------- ### Installation Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/README.md Install the blacknoise package using pip. Optionally, install with brotli support. ```APIDOC ## Install ```bash pip install blacknoise pip install blacknoise brotli # with brotli support ``` ``` -------------------------------- ### Basic Blacknoise Setup Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/README.md Integrate Blacknoise into your ASGI application by wrapping it and adding a static file directory. ```python from blacknoise import BlackNoise from pathlib import Path app = BlackNoise(wrapped_app) app.add(Path("./static"), "/static/") ``` -------------------------------- ### Access Blacknoise Version Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md Demonstrates how to import and print the installed version of the blacknoise library. ```python from blacknoise import __version__ print(__version__) # "1.2.0" ``` ```python import blacknoise print(blacknoise.__version__) ``` -------------------------------- ### Install and Run Gunicorn with Uvicorn Worker Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/08-asgi-integration.md Install Gunicorn and Uvicorn, then run your application using Gunicorn with the Uvicorn worker class for production deployments. ```bash pip install gunicorn uvicorn gunicorn \ --workers 4 \ --worker-class uvicorn.workers.UvicornWorker \ myapp:application ``` -------------------------------- ### Install Blacknoise Source: https://github.com/matthiask/blacknoise/blob/main/README.md Install the blacknoise package using pip. This is the first step to using blacknoise in your project. ```console pip install blacknoise ``` -------------------------------- ### Example Path Computation Logic Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md Illustrates the step-by-step computation of a registry key for a file within a directory structure, given a root path and a URL prefix. ```python base = "/var/www/static/css" path = Path("/var/www/static") # Relative path: /css relative = base[len("/var/www/static"): ].strip("/") # "css" # Registry key: /static/css/style.css path_prefix = os.path.join("/static/", "css") # "/static/css" registry_key = os.path.join("/static/css", "style.css") # "/static/css/style.css" ``` -------------------------------- ### Valid Range Request Example Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md Demonstrates a valid HTTP GET request with a Range header for partial content, resulting in a 206 Partial Content response. ```http GET /static/file.bin HTTP/1.1 Range: bytes=0-99 Response: 206 Partial Content Content-Range: bytes 0-99/1000 Content-Length: 100 ``` -------------------------------- ### Install Brotli Package Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/02-compress-module.md Optional package installation for Brotli compression. If not installed, Brotli compression will not be attempted. ```bash pip install brotli ``` -------------------------------- ### Path Normalization Example Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/07-internal-functions.md Illustrates path normalization within BlackNoise.__call__(). Shows how root_path is removed and paths are standardized. ```python path = os.path.normpath(scope["path"].removeprefix(scope["root_path"])) ``` -------------------------------- ### Install Blacknoise Package Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/README.md Install the Blacknoise package using pip. For brotli support, install with the brotli extra. ```bash pip install blacknoise ``` ```bash pip install blacknoise brotli # with brotli support ``` -------------------------------- ### Nginx Configuration for Blacknoise Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md Example Nginx configuration to serve static files directly and proxy other requests to a Blacknoise-backed application. This setup leverages Nginx for caching static assets. ```nginx upstream app { server 127.0.0.1:8000; } server { listen 80; server_name example.com; # Let nginx serve static files directly location /static/ { alias /var/www/myapp/static/; expires 10y; add_header Cache-Control "public, immutable"; } # Everything else via the app (including dynamic content) location / { proxy_pass http://app; } } ``` -------------------------------- ### BlackNoise Example with Custom Immutable File Test Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/01-blacknoise-class.md Demonstrates how to initialize BlackNoise with a custom `immutable_file_test` function to serve specific files with long cache expiry headers. ```python from blacknoise import BlackNoise from starlette.applications import Starlette from pathlib import Path def immutable_file_test(path): # Mark files containing a hash as immutable return any(hash_pattern in path for hash_pattern in ['.12345.', '.abcdef.']) app = Starlette(routes=[]) middleware = BlackNoise(app, immutable_file_test=immutable_file_test) ``` -------------------------------- ### Setup BlackNoise Middleware Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md Instantiate BlackNoise middleware and add static file directories. The `immutable_file_test` function determines if files are immutable and can be cached aggressively. ```python from blacknoise import BlackNoise from pathlib import Path # Create middleware def create_app(): from myapp import get_app app = get_app() app = BlackNoise(app, immutable_file_test=my_test) app.add(Path("./static"), "/static/") return app def my_test(path: str) -> bool: return "hash" in path ``` -------------------------------- ### 405 Method Not Allowed Response Example Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md Shows the HTTP 405 Method Not Allowed response returned when a request to a registered static file path uses an unsupported HTTP method. BlackNoise only supports GET and HEAD for static files. ```text HTTP/1.1 405 Method Not Allowed Content-Type: text/plain; charset=utf-8 Content-Length: 18 Method Not Allowed ``` -------------------------------- ### Wrap FastAPI with BlackNoise for Static Files Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/08-asgi-integration.md Wrap a FastAPI application with BlackNoise to serve static files. This example adds a `/static/` prefix for serving files from a `./static` directory. ```python from fastapi import FastAPI from blacknoise import BlackNoise from pathlib import Path api_app = FastAPI() @api_app.get("/api/data") async def get_data(): return {"message": "Hello"} # Wrap with static file serving static_app = BlackNoise(api_app) static_app.add(Path("./static"), "/static/") ``` -------------------------------- ### BlackNoise File Registry Example Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/04-architecture-and-design.md Shows the internal dictionary structure used by BlackNoise to map request paths to filesystem paths. Compressed variants are not registered if the uncompressed file exists. ```python _files: Dict[str, str] = {} # Example: # { # "/static/app.js": "/home/user/project/static/app.js", # "/static/app.js.gz": (not registered if app.js exists), # "/static/style.css": "/home/user/project/static/style.css", # "/static/style.css.br": (not registered if style.css exists), # } ``` -------------------------------- ### Full Starlette Stack with BlackNoise Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/08-asgi-integration.md Demonstrates integrating BlackNoise into a Starlette application stack. This example shows how BlackNoise can serve files from './static' and './public' directories under '/static/' and '/public/' paths respectively, while other requests are processed by Starlette's middleware and routing. ```python from blacknoise import BlackNoise from starlette.applications import Starlette from starlette.middleware import Middleware from starlette.middleware.cors import CORSMiddleware from starlette.responses import JSONResponse from starlette.routing import Route from pathlib import Path async def api_endpoint(request): return JSONResponse({"hello": "world"}) # Create app with routes and middleware routes = [ Route("/api/hello", api_endpoint), ] middleware = [ Middleware(CORSMiddleware, allow_origins=["*"]), ] app = Starlette(routes=routes, middleware=middleware) # Wrap with static file serving app = BlackNoise(app) app.add(Path("./static"), "/static/") app.add(Path("./public"), "/public/") # Now the stack is: # Client Request # ↓ # BlackNoise (intercepts /static/*, /public/*) # ↓ # CORSMiddleware # ↓ # Starlette Router # ↓ # Route Handlers / 404 ``` -------------------------------- ### Prefix Checking Logic Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/07-internal-functions.md Demonstrates how BlackNoise checks if a path starts with any of the registered prefixes using tuple unpacking. ```python # path.startswith(tuple) checks all items "/static/file.js".startswith(("/static", "/vendor")) # True "/api/endpoint".startswith(("/static", "/vendor")) # False ``` -------------------------------- ### Request Path Normalization - os.path.normpath() Examples Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md Demonstrates how os.path.normpath() handles various path formats, including double slashes, current directory references, and parent directory references. ```python os.path.normpath("/static//file.js") # "/static/file.js" ``` ```python os.path.normpath("/static/./file.js") # "/static/file.js" ``` ```python os.path.normpath("/static/../file.js") # "/file.js" ``` -------------------------------- ### Optional Dependency Declaration Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/03-types-and-constants.md Lists an optional dependency. Brotli compression will be skipped if this is not installed. ```toml brotli (optional) ``` -------------------------------- ### Dictionary File Lookup Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md This demonstrates the O(1) lookup time for files within the registry using a dictionary get operation. This is the core mechanism for request matching. ```python file = self._files.get(path) # Dictionary lookup, instant ``` -------------------------------- ### Example Immutable File Test Function Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/03-types-and-constants.md An example implementation of an ImmutableFileTest callable. This function checks if the file path contains a content hash to determine caching behavior. ```python def immutable_file_test(path: str) -> bool: # Return True for files with content hashes (e.g., app.12abc34.js) import re return bool(re.search(r'\.[a-f0-9]{6,}\.', path)) ``` -------------------------------- ### Wrap ASGI Application with Blacknoise Source: https://github.com/matthiask/blacknoise/blob/main/README.md Integrate blacknoise by wrapping your ASGI application. This example shows integration with Django's get_asgi_application. ```python from blacknoise import BlackNoise from django.core.asgi import get_asgi_application from pathlib import Path BASE_DIR = Path(__file__).parent application = BlackNoise(get_asgi_application()) application.add(BASE_DIR / "static", "/static") ``` -------------------------------- ### Core Dependency Declaration Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/03-types-and-constants.md Lists the core dependency for the project. Ensure this version or higher is installed. ```toml starlette >= 0.47 ``` -------------------------------- ### 404 Not Found Response Example Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md Illustrates the HTTP 404 Not Found response generated when a requested static file does not exist in the registry. This can occur due to typos, case mismatches, or files not being added after initialization. ```text HTTP/1.1 404 Not Found Content-Type: text/plain; charset=utf-8 Content-Length: 9 Not Found ``` -------------------------------- ### Check if Path Starts with Registered Prefixes Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md Use `str.startswith()` with a tuple to efficiently check if a given path begins with any of the registered prefixes. This is a core part of BlackNoise's routing logic. ```python if not path.startswith(self._prefixes): # Delegate to wrapped app response = self._application ``` -------------------------------- ### ASGI Response Start Message Structure Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/08-asgi-integration.md Illustrates the structure of the 'http.response.start' message sent by an ASGI application to initiate a response. This includes the status code and headers. ```python { "type": "http.response.start", "status": 200, "headers": [[b"content-type", b"text/plain"], ...], } ``` -------------------------------- ### Try Brotli Function Signature Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/11-module-structure.md Compresses a file using brotli if the package is installed. Returns a status message or an empty string if brotli is unavailable. ```python def try_brotli(path: Path, orig_bytes: bytes) -> str: ``` -------------------------------- ### Path Normalization Logic Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md Illustrates the normalization process in Python, where the path is normalized after removing the root path. It then checks if the normalized path starts with registered prefixes before proceeding. ```python path = os.path.normpath(scope["path"].removeprefix(scope["root_path"])) # If request path after normalization doesn't start with registered prefix: if not path.startswith(self._prefixes): # Delegate to app (safe) ``` -------------------------------- ### Try Brotli Compression Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/02-compress-module.md Compresses a file using brotli. This function requires the `brotli` package to be installed. The compressed file is saved as `{path}.br` only if it is smaller than 90% of the original size. Returns an empty string if brotli is unavailable. ```python from pathlib import Path from blacknoise.compress import try_brotli path = Path("app.js") orig_bytes = path.read_bytes() status = try_brotli(path, orig_bytes) print(status) # Output: app.js: Brotli compressed 12345 to 3456 bytes (-72%) # or empty string if brotli is not installed ``` -------------------------------- ### Range Not Satisfiable Example Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md Shows an HTTP GET request with a Range header that exceeds the file's bounds, leading to a 416 Range Not Satisfiable response. ```http GET /static/file.bin HTTP/1.1 Range: bytes=5000-6000 (file is 1000 bytes) Response: 416 Range Not Satisfiable Content-Range: bytes */1000 ``` -------------------------------- ### Dockerfile for Blacknoise Application Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md A Dockerfile to build an image for a Blacknoise application. It installs dependencies, copies project files, pre-compresses assets using Blacknoise, and sets the command to run the application with Gunicorn. ```dockerfile FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . # Pre-compress assets at build time RUN python -m blacknoise.compress static/ EXPOSE 8000 CMD ["gunicorn", "--worker-class", "uvicorn.workers.UvicornWorker", "myproject.asgi:application"] ``` -------------------------------- ### Invalid Range Syntax Example Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md Illustrates an HTTP GET request with an invalid Range header syntax, which BlackNoise (via Starlette) should reject with a 400 Bad Request response. ```http GET /static/file.bin HTTP/1.1 Range: words=1-10 Response: 400 Bad Request ``` -------------------------------- ### Import Blacknoise Compression Utilities Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md Import utility functions for compression. `compress` and `parse_args` are for general compression tasks, while `try_gzip` and `try_brotli` are internal helpers. ```python from blacknoise.compress import compress, parse_args ``` ```python from blacknoise.compress import try_gzip, try_brotli ``` -------------------------------- ### add Method Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/01-blacknoise-class.md Registers a filesystem directory to be served under a URL prefix. Files are discovered by walking the directory tree at initialization time. ```APIDOC ## `add` Method ### Description Registers a filesystem directory to be served under a URL prefix. Files are discovered by walking the directory tree at initialization time. ### Signature ```python def add(self, path, prefix) ``` ### Parameters #### Path Parameters - **path** (str or Path) - Required - Filesystem path to the directory containing static files. Can be relative or absolute. - **prefix** (str) - Required - URL path prefix under which files will be served. Should start with `/`. Trailing slashes are recommended for clarity. ### Returns `None` ### Behavior - Recursively walks the directory tree starting at `path` - Registers all files found, computing their served path as `prefix + relative_path` - Compressed variants (`.gz`, `.br` suffixes) are only registered if the uncompressed file does not exist - If a directory contains both `file.txt` and `file.txt.gz`, only `file.txt` is registered as a serving entry, but `.gz` will be served if the client accepts gzip encoding ### Example ```python from pathlib import Path middleware = BlackNoise(app) # Serve static files static_dir = Path(__file__).parent / "static" middleware.add(static_dir, "/static/") # Serve vendor libraries from node_modules middleware.add(Path(__file__).parent / "node_modules", "/vendor/") ``` ``` -------------------------------- ### Basic Usage Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/README.md Initialize BlackNoise with your ASGI application and add static file directories. ```APIDOC ## Basic Usage ```python from blacknoise import BlackNoise from pathlib import Path app = BlackNoise(wrapped_app) app.add(Path("./static"), "/static/") ``` ``` -------------------------------- ### Test Static File Serving with httpx Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md Demonstrates how to test serving static files using httpx and Blacknoise. Ensure the static file is correctly served with a 200 status code and appropriate cache-control headers. ```python from blacknoise import BlackNoise from starlette.applications import Starlette from starlette.responses import PlainTextResponse import httpx from pathlib import Path async def hello(request): return PlainTextResponse("Hello") app = Starlette() app = BlackNoise(app) app.add(Path("./static"), "/static/") @pytest.mark.asyncio async def test_static_file(): transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get("/static/style.css") assert response.status_code == 200 assert response.headers["cache-control"] == "max-age=60, public" ``` -------------------------------- ### Cache Control Logic in Python Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/04-architecture-and-design.md Determines cache control headers based on whether a file is considered immutable. Immutable files get a 10-year cache, while others get a 1-minute cache. ```python if immutable_file_test(request_path): cache_control = "max-age=315360000, public, immutable" # 10 years else: cache_control = "max-age=60, public" # 1 minute ``` -------------------------------- ### Import Blacknoise Compression Utilities Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/00-index.md Import functions and constants for file compression from the blacknoise.compress module. ```python from blacknoise.compress import compress, parse_args, try_gzip, try_brotli ``` -------------------------------- ### try_brotli Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md Compress a file using brotli, if the brotli library is installed. This function attempts brotli compression. ```APIDOC ## try_brotli ### Description Compress a file using brotli (if installed). ### Parameters - `path` (Path object) - Required - File path - `orig_bytes` (bytes) - Required - File contents ### Returns - `str` - Status message, empty if brotli unavailable ``` -------------------------------- ### BlackNoise.add Method Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md Registers a directory to serve static files under a specified URL prefix. ```APIDOC ## BlackNoise.add Method ### Description Registers a directory to serve under a URL prefix. ### Parameters - `path` – Directory path (str or Path) - `prefix` – URL prefix (str) ### Example ```python bn = BlackNoise(app) bn.add("./static", "/static/") ``` ``` -------------------------------- ### Access Blacknoise Version Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/11-module-structure.md Retrieve the installed version of the blacknoise package. This is typically used for dependency checking or logging. ```python import blacknoise print(blacknoise.__version__) # "1.2.0" ``` -------------------------------- ### Pre-compress Assets for Serving Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md Compress assets using npm build and Python's blacknoise.compress module before deploying and serving them with a Python server. ```bash # Build assets npm run build # Compress them python -m blacknoise.compress ./dist # Deploy and serve python runserver.py ``` -------------------------------- ### Python Tuple `startswith()` for Multiple Prefixes Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md Demonstrates how Python's `str.startswith()` method can check against multiple prefixes when provided as a tuple. This is useful for BlackNoise's prefix matching. ```python # Registry has prefixes: ("/static", "/vendor") "/static/app.js".startswith(("/static", "/vendor")) # True → Handle "/vendor/lib.js".startswith(("/static", "/vendor")) # True → Handle "/api/data".startswith(("/static", "/vendor")) # False → Delegate ``` -------------------------------- ### Handle ArgumentError with parse_args Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md This example shows how `argparse` raises a `SystemExit` with code 2 when required arguments are missing for `blacknoise.compress.parse_args`. ```python from blacknoise.compress import parse_args try: args = parse_args([]) # No arguments except SystemExit as e: print(f"Exit code: {e.code}") # 2 ``` -------------------------------- ### BlackNoise Path Computation Algorithm in add() Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md The `add` method walks a directory, computes registry keys using a prefix and relative path, and registers each file. It skips compressed variants if uncompressed versions exist. ```python def add(self, path, prefix): self._prefixes = (*self._prefixes, prefix) for base, _dirs, files in os.walk(path): # Compute prefix for this directory level path_prefix = os.path.join(prefix, base[len(str(path)):].strip("/")) # Register each file self._files |= { os.path.join(path_prefix, file): os.path.join(base, file) for file in files if all( # Skip compressed variants if uncompressed exists not file.endswith(suffix) or file.removesuffix(suffix) not in files for suffix in SUFFIX_ENCODINGS ) } ``` -------------------------------- ### try_brotli Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/11-module-structure.md Compresses a given file using the brotli algorithm, if the brotli package is installed. It returns a status message indicating the result. ```APIDOC ## Function: try_brotli ### Description Compress a file using brotli (if brotli package installed). ### Signature ```python def try_brotli(path: Path, orig_bytes: bytes) -> str ``` ### Parameters - **path** (Path) - File path (for status messages) - **orig_bytes** (bytes) - Original uncompressed file contents ### Returns - str - Status message string (empty if brotli unavailable) ``` -------------------------------- ### Windows Path Handling with os.path.join Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md Illustrates platform-aware path joining on Windows. Note that registry keys should use forward slashes, but os.path.join might use backslashes on Windows. ```python # File path uses backslashes os.path.join("C:\\static", "app.js") # "C:\static\app.js" # Registry key uses forward slashes (HTTP convention) os.path.join("/static", "app.js") # "/static\app.js" (Windows) or "/static/app.js" (Unix) ``` -------------------------------- ### Build Assets with npm Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md Execute the npm build script to generate optimized frontend assets such as JavaScript and CSS files. ```bash npm run build # Creates: dist/app.js, dist/style.css, etc. ``` -------------------------------- ### Handle Missing Brotli Dependency Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md The `try_brotli` function checks for the `brotli` module and returns an empty string if it's not installed, preventing a `ModuleNotFoundError`. ```python def try_brotli(path, orig_bytes): if not brotli: return "" # No error, just skip # ... ``` -------------------------------- ### Register Static Files Directory Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/01-blacknoise-class.md Use the `add` method to register a local filesystem directory to be served under a specific URL prefix. Compressed variants are only registered if the uncompressed file does not exist. ```python from pathlib import Path middleware = BlackNoise(app) # Serve static files static_dir = Path(__file__).parent / "static" middleware.add(static_dir, "/static/") # Serve vendor libraries from node_modules middleware.add(Path(__file__).parent / "node_modules", "/vendor/") ``` -------------------------------- ### Thread-Safe File Registration Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/04-architecture-and-design.md Register all static files before the server starts handling requests to ensure thread safety. Do not call `add()` after requests are being processed. ```python # Safe: add all files before server starts app = BlackNoise(wrapped_app) app.add("./static", "/static/") app.add("./vendor", "/vendor/") # Then server starts handling requests # Do not call add() after requests are being processed ``` -------------------------------- ### Full Import Paths for BlackNoise Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/11-module-structure.md Illustrates various import paths available within the BlackNoise library, including main classes, compression utilities, version information, and internal constants. ```python # Main class from blacknoise import BlackNoise from blacknoise._impl import BlackNoise # Internal; use above instead # Compression utilities from blacknoise.compress import compress from blacknoise.compress import try_gzip from blacknoise.compress import try_brotli from blacknoise.compress import parse_args from blacknoise.compress import SKIP_COMPRESS_EXTENSIONS # Version from blacknoise.__about__ import __version__ import blacknoise blacknoise.__version__ # Constants (if importing directly from _impl) from blacknoise._impl import FOREVER, A_LITTE_WHILE, SUFFIX_ENCODINGS, never ``` -------------------------------- ### Test Static File Compression (Gzip) Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/00-index.md Verify that static files are served with gzip compression when the client accepts it. ```python @pytest.mark.asyncio async def test_gzip(): transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport) as client: response = await client.get( "/static/app.js", headers={"accept-encoding": "gzip"} ) assert response.headers.get("content-encoding") == "gzip" ``` -------------------------------- ### Uvicorn Timeout Configuration Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md Example of configuring a keep-alive timeout for the Uvicorn ASGI server. This setting affects how long the server waits for a client connection before closing it. ```bash uvicorn app:app --timeout-keep-alive 5 ``` -------------------------------- ### Configure Blacknoise with Django ASGI Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md Wrap your Django ASGI application with Blacknoise and add static and media directories for Blacknoise to serve. ```python # myproject/asgi.py from blacknoise import BlackNoise from django.core.asgi import get_asgi_application from pathlib import Path BASE_DIR = Path(__file__).parent.parent # Get the Django ASGI application django_app = get_asgi_application() # Wrap with BlackNoise application = BlackNoise(django_app) application.add(BASE_DIR / "static", "/static/") application.add(BASE_DIR / "media", "/media/") ``` -------------------------------- ### Pre-compress Static Assets Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md Use the Blacknoise compression utility to create .gz and .br versions of your static assets after building them. This reduces bandwidth and improves performance. ```bash # After building your static assets npm run build # Compress them python -m blacknoise.compress dist/ # Your dist directory now contains: # dist/app.js # dist/app.js.gz (if gzip compressed well) # dist/app.js.br (if brotli compressed well) ``` -------------------------------- ### Serving Multiple Directories Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/00-index.md Add multiple directories to Blacknoise for serving static, vendor, and media files from different locations. ```python app = BlackNoise(app) app.add("./static", "/static/") app.add("./vendor", "/vendor/") app.add("./media", "/media/") ``` -------------------------------- ### Directory Traversal Protection Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md Demonstrates how normalization prevents directory traversal by ensuring normalized paths start with the registered prefix. If not, the request is delegated to the wrapped application. ```text Request: /static/../../../etc/passwd Normalized: /etc/passwd Result: Path doesn't start with "/static", delegated to wrapped app ``` -------------------------------- ### Compress File with Brotli Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md This function attempts to compress a file using the Brotli algorithm, provided it is installed. It returns a status message or an empty string if Brotli is unavailable. ```python def try_brotli(path, orig_bytes) -> str ``` -------------------------------- ### BlackNoise Error Conversion to HTTP Responses Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/08-asgi-integration.md BlackNoise converts common errors into standard HTTP responses instead of raising exceptions. This list shows examples of such conversions. ```python # These are safe (return HTTP responses, don't raise) - 404 Not Found - 405 Method Not Allowed - 400 Bad Request (from Starlette for invalid ranges) - 206 Partial Content (for range requests) ``` -------------------------------- ### Add Multiple Static Directories with BlackNoise Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md Configure BlackNoise to serve static files from multiple directories under different URL prefixes. Each directory is registered independently, and in case of path conflicts, the last added directory takes precedence. ```python from blacknoise import BlackNoise from pathlib import Path app = BlackNoise(django_app) # Application static files app.add(Path("./static"), "/static/") # Vendor libraries app.add(Path("./node_modules"), "/vendor/") # User-uploaded media (pre-compressed) app.add(Path("./media"), "/media/") # Third-party assets app.add(Path("./assets/icons"), "/icons/") ``` -------------------------------- ### Pre-compress Static Files Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/04-architecture-and-design.md Use the `blacknoise.compress` module to generate compressed variants of static files ahead of time. This is the only supported method for pre-compression. ```bash python -m blacknoise.compress ./static ``` -------------------------------- ### Configure Blacknoise for Pre-compressed Assets Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md Configure Blacknoise to serve pre-compressed assets. Blacknoise automatically detects and serves `.gz` and `.br` variants based on `Accept-Encoding` headers. ```python from blacknoise import BlackNoise from pathlib import Path app = BlackNoise(django_app) app.add(Path("./dist"), "/static/") ``` -------------------------------- ### BlackNoise Instance Methods Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/11-module-structure.md Defines methods for adding paths to be served and for handling incoming ASGI requests. ```python def add(self, path, prefix) -> None ``` ```python async def __call__(self, scope, receive, send) -> None ``` -------------------------------- ### Pre-compress Assets Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/README.md Use the blacknoise.compress module to pre-compress static assets. ```APIDOC ## Pre-compress Assets ```bash python -m blacknoise.compress ./static ``` ``` -------------------------------- ### BlackNoise Registration Algorithm Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/04-architecture-and-design.md Outlines the logic used by the `add()` method to register files, specifically how it handles compressed variants to avoid duplicate entries. ```python For each file in the directory tree: If the file ends with .gz or .br: Check if the uncompressed variant (without suffix) exists in the same directory If it exists: Skip this compressed file (don't register) Otherwise: Register as a serving entry Otherwise: Register as a serving entry ``` -------------------------------- ### 404 Not Found Scenario and Client Handling Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md Demonstrates a scenario where a request for a non-existent file results in a 404 response. Includes client-side Python code using httpx to check for and handle this status code. ```python app = BlackNoise(django_app) app.add(Path("./static"), "/static/") # Request to /static/nonexistent.js # Response: 404 Not Found ``` ```python async with httpx.AsyncClient(transport=transport) as client: response = await client.get("/static/missing.js") if response.status_code == 404: # Handle missing file print("Asset not found") ``` -------------------------------- ### Integrate BlackNoise into ASGI Middleware Chain Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/04-architecture-and-design.md Place BlackNoise early in the middleware chain to ensure it intercepts static file requests before they reach the main application logic. This setup ensures BlackNoise handles static files directly. ```python app = Starlette(routes=[...]) app = SomeMiddleware(app) app = BlackNoise(app) # Last so it wraps everything ``` -------------------------------- ### Default Immutable File Test Function Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/07-internal-functions.md Provides a default immutable file test that always returns False. It's used as the default for `BlackNoise.__init__` to ensure all files get short-lived cache headers unless explicitly configured otherwise. ```python def never(_path): return False ``` -------------------------------- ### Case-Sensitive Path Matching on Unix Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/10-file-registry-and-matching.md Demonstrates case-sensitive path matching on Unix-like systems. A request for '/static/app.js' will not match a registered '/static/App.js' if the filesystem is case-sensitive. ```python # On Unix (case-sensitive filesystem): _files = { "/static/App.js": "/var/www/static/App.js", } "/static/app.js".startswith(...) # True _files.get("/static/app.js") # None (not registered, KeyError avoided) ``` -------------------------------- ### Development vs. Production Immutable Files Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/00-index.md Configure immutable file serving based on the environment variable 'ENV'. Static files are served immutably only in production. ```python import os def immutable_test(path): if os.environ.get('ENV') == 'production': return 'static' in path return False app = BlackNoise(app, immutable_file_test=immutable_test) ``` -------------------------------- ### 405 Method Not Allowed Scenario and Client Handling Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/06-error-handling.md Illustrates scenarios where POST, PUT, or DELETE requests to static files result in a 405 error. Provides client-side Python code using httpx to attempt a GET request after receiving a 405 status code. ```python # Request: POST /static/file.js # Response: 405 Method Not Allowed # Request: PUT /static/file.js # Response: 405 Method Not Allowed # Request: DELETE /static/file.js # Response: 405 Method Not Allowed ``` ```python async with httpx.AsyncClient(transport=transport) as client: response = await client.post("/static/file.js") if response.status_code == 405: # Method not allowed, use GET instead response = await client.get("/static/file.js") ``` -------------------------------- ### Compress Static Assets Source: https://github.com/matthiask/blacknoise/blob/main/README.md Use the blacknoise.compress module to pre-compress static assets. This can improve performance by serving smaller files. ```console python -m blacknoise.compress static/ ``` -------------------------------- ### BlackNoise Constructor Initialization Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/01-blacknoise-class.md Initializes the BlackNoise middleware, wrapping an ASGI application and optionally configuring a custom test for immutable files to control cache headers. ```python def __init__(self, application, *, immutable_file_test=never) ``` -------------------------------- ### Import BlackNoise Class Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md Import the main BlackNoise class from the library. This is the primary class for integrating static file serving into an ASGI application. ```python from blacknoise import BlackNoise ``` -------------------------------- ### Try Gzip Function Signature Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/11-module-structure.md Compresses a given file using gzip with maximum compression level. Returns a status message. ```python def try_gzip(path: Path, orig_bytes: bytes) -> str: ``` -------------------------------- ### Instantiate BlackNoise Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md Instantiate the BlackNoise class, wrapping an existing ASGI application. The `immutable_file_test` parameter can be used to specify a custom test for immutable files. ```python bn = BlackNoise(app) ``` -------------------------------- ### Try Gzip Compression Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/02-compress-module.md Compresses a file using gzip with maximum compression level (9). The compressed file is saved as `{path}.gz` only if it is smaller than 90% of the original size. Requires the original file contents as bytes. ```python from pathlib import Path from blacknoise.compress import try_gzip path = Path("app.js") orig_bytes = path.read_bytes() status = try_gzip(path, orig_bytes) print(status) # Output: app.js: Gzip compressed 12345 to 4567 bytes (-63%) ``` -------------------------------- ### BlackNoise Constructor Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/09-api-quick-reference.md Initializes a BlackNoise instance, which acts as an ASGI application callable to serve static files. ```APIDOC ## BlackNoise Constructor ### Description Initializes a BlackNoise instance. ### Parameters - `application` – ASGI application callable - `immutable_file_test` – `Callable[[str], bool]` (optional, default: `never`) ### Returns `BlackNoise` instance (ASGI callable) ``` -------------------------------- ### Configure BlackNoise for Compressed Assets Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md After compression, configure BlackNoise to serve the original and compressed asset variants. BlackNoise will automatically serve the most appropriate compressed version based on the client's `Accept-Encoding` header. ```python from blacknoise import BlackNoise app = BlackNoise(django_app) app.add("./dist", "/static/") ``` -------------------------------- ### Compression Utilities Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/00-index.md The `blacknoise.compress` module provides functions for compressing files and parsing command-line arguments related to compression. ```APIDOC ## Compression Module ### Description This module contains utilities for compressing static files, primarily used for pre-compressing files before deployment. It includes functions to walk directories, compress individual files, and parse command-line arguments. ### Functions - `compress(root)`: Walks the specified directory (`root`) and compresses all eligible files, creating `.gz` and `.br` variants if compression offers a significant saving. - `try_gzip(path, orig_bytes)`: Attempts to gzip compress a single file. Returns the compressed bytes if successful, otherwise `None`. - `try_brotli(path, orig_bytes)`: Attempts to brotli compress a single file. Returns the compressed bytes if successful and brotli is available, otherwise `None`. - `parse_args(args)`: Parses command-line arguments for the compression utility. ### Constants - `SKIP_COMPRESS_EXTENSIONS`: A tuple of file extensions that should be skipped during compression. ``` -------------------------------- ### Serve SPA with Blacknoise and Starlette Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md Configure Blacknoise with Starlette to serve static assets from a 'dist' directory and fallback to 'index.html' for all other routes. This is ideal for single-page applications. ```python from blacknoise import BlackNoise from starlette.applications import Starlette from starlette.responses import FileResponse from pathlib import Path async def index(request): # Serve index.html for all non-static routes (SPA fallback) return FileResponse(Path("./dist/index.html")) routes = [Route("/{path:path}", index)] app = Starlette(routes=routes) # Serve static assets first app = BlackNoise(app) app.add(Path("./dist"), "/") ``` -------------------------------- ### BlackNoise Constructor Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/01-blacknoise-class.md Initializes the BlackNoise middleware wrapper. It takes the ASGI application to wrap and an optional function to determine if files should be treated as immutable for caching purposes. ```APIDOC ## BlackNoise.__init__ ### Description Initializes the BlackNoise middleware wrapper. ### Method Signature `def __init__(self, application, *, immutable_file_test=never)` ### Parameters #### Positional Parameters - **application** (ASGI application callable) - Required - The wrapped ASGI application. Requests that don't match static prefixes are delegated to this application. #### Keyword-Only Parameters - **immutable_file_test** (`Callable[[str], bool]`) - Optional - A function that receives a normalized request path and returns `True` if the file should be served with far-future expiry headers (max-age 10 years, immutable), or `False` for default short-lived cache headers. Defaults to `never` (returns `False`). ### Returns - `BlackNoise` instance ### Example ```python from blacknoise import BlackNoise from starlette.applications import Starlette from pathlib import Path def immutable_file_test(path): # Mark files containing a hash as immutable return any(hash_pattern in path for hash_pattern in ['.12345.', '.abcdef.']) app = Starlette(routes=[]) middleware = BlackNoise(app, immutable_file_test=immutable_file_test) ``` ``` -------------------------------- ### Compress Assets with BlackNoise Source: https://github.com/matthiask/blacknoise/blob/main/_autodocs/05-usage-patterns.md Use the BlackNoise compression utility to create Brotli and Gzip versions of your static assets. This step optimizes file sizes for faster delivery. ```bash python -m blacknoise.compress dist/ ```