Skip to content

Commit

Permalink
Mechanism for tracking Starlette gzip module, closes #1
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Apr 28, 2022
1 parent 5e0d6ef commit 87f78a8
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 0 deletions.
54 changes: 54 additions & 0 deletions .github/workflows/track.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Track the Starlette version of this

on:
push:
workflow_dispatch:
schedule:
- cron: '21 5 * * *'

permissions:
issues: write
contents: write

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/github-script@v6
env:
URL: https://raw.githubusercontent.com/encode/starlette/master/starlette/middleware/gzip.py
FILE_NAME: tracking/gzip.py
with:
script: |
const { URL, FILE_NAME } = process.env;
const util = require("util");
// Because 'exec' variable name is already used:
const exec_ = util.promisify(require("child_process").exec);
await exec_(`curl -o ${FILE_NAME} ${URL}`);
const { stdout } = await exec_(`git diff ${FILE_NAME}`);
if (stdout) {
// There was a diff to that file
const title = `${FILE_NAME} was updated`;
const body =
`${URL} changed:` +
"\n\n```diff\n" +
stdout +
"\n```\n\n" +
"Close this issue once those changes have been integrated here";
const issue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
});
const issueNumber = issue.data.number;
// Now commit and reference that issue
const commitMessage = `${FILE_NAME} updated, refs #${issueNumber}`;
await exec_(`git config user.name "Automated"`);
await exec_(`git config user.email "actions@users.noreply.github.com"`);
await exec_(`git add -A`);
await exec_(`git commit -m "${commitMessage}" || exit 0`);
await exec_(`git pull --rebase`);
await exec_(`git push`);
}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ Install this library using `pip`:

Usage instructions go here.

## Tracking Starlette

Since this code is extracted from Starlette, it's important to keep watch for changes and bug fixes to the Starlette implementation that should be replicated here.

The GitHub repository for this library uses [Git scraping](https://simonwillison.net/2020/Oct/9/git-scraping/) to track changes to a copy of the Starlette `gzip.py` module, which is kept in the `tracking/` folder.

Any time a change to that file is detected, an issue will be automatically created in the repository. This issue should be closed once the change to Starlette has been applied here, if necessary.

## Development

To contribute to this library, first checkout the code. Then create a new virtual environment:
Expand Down
105 changes: 105 additions & 0 deletions tracking/gzip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import gzip
import io
import typing

from starlette.datastructures import Headers, MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send


class GZipMiddleware:
def __init__(
self, app: ASGIApp, minimum_size: int = 500, compresslevel: int = 9
) -> None:
self.app = app
self.minimum_size = minimum_size
self.compresslevel = compresslevel

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
headers = Headers(scope=scope)
if "gzip" in headers.get("Accept-Encoding", ""):
responder = GZipResponder(
self.app, self.minimum_size, compresslevel=self.compresslevel
)
await responder(scope, receive, send)
return
await self.app(scope, receive, send)


class GZipResponder:
def __init__(self, app: ASGIApp, minimum_size: int, compresslevel: int = 9) -> None:
self.app = app
self.minimum_size = minimum_size
self.send: Send = unattached_send
self.initial_message: Message = {}
self.started = False
self.gzip_buffer = io.BytesIO()
self.gzip_file = gzip.GzipFile(
mode="wb", fileobj=self.gzip_buffer, compresslevel=compresslevel
)

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
self.send = send
await self.app(scope, receive, self.send_with_gzip)

async def send_with_gzip(self, message: Message) -> None:
message_type = message["type"]
if message_type == "http.response.start":
# Don't send the initial message until we've determined how to
# modify the outgoing headers correctly.
self.initial_message = message
elif message_type == "http.response.body" and not self.started:
self.started = True
body = message.get("body", b"")
more_body = message.get("more_body", False)
if len(body) < self.minimum_size and not more_body:
# Don't apply GZip to small outgoing responses.
await self.send(self.initial_message)
await self.send(message)
elif not more_body:
# Standard GZip response.
self.gzip_file.write(body)
self.gzip_file.close()
body = self.gzip_buffer.getvalue()

headers = MutableHeaders(raw=self.initial_message["headers"])
headers["Content-Encoding"] = "gzip"
headers["Content-Length"] = str(len(body))
headers.add_vary_header("Accept-Encoding")
message["body"] = body

await self.send(self.initial_message)
await self.send(message)
else:
# Initial body in streaming GZip response.
headers = MutableHeaders(raw=self.initial_message["headers"])
headers["Content-Encoding"] = "gzip"
headers.add_vary_header("Accept-Encoding")
del headers["Content-Length"]

self.gzip_file.write(body)
message["body"] = self.gzip_buffer.getvalue()
self.gzip_buffer.seek(0)
self.gzip_buffer.truncate()

await self.send(self.initial_message)
await self.send(message)

elif message_type == "http.response.body":
# Remaining body in streaming GZip response.
body = message.get("body", b"")
more_body = message.get("more_body", False)

self.gzip_file.write(body)
if not more_body:
self.gzip_file.close()

message["body"] = self.gzip_buffer.getvalue()
self.gzip_buffer.seek(0)
self.gzip_buffer.truncate()

await self.send(message)


async def unattached_send(message: Message) -> typing.NoReturn:
raise RuntimeError("send awaitable not set") # pragma: no cover

0 comments on commit 87f78a8

Please sign in to comment.