Pyodide, PyScript - Monkey patching requests

At PyCon US 2022, Anaconda announced PyScript: Python in the Browser. So far my understanding is that it builds on Pyodide and makes it magically easy to bridge the world of the Browser - the Document Object Model (DOM) and Python. It’s so magical that you can simply copy scripts that you were running using Python installed on a computer and they just run in the browser. Check out the blog post for some demos.

To explore it with some definite goal in mind, I started porting some programs from my book, “Doing Math with Python”, and things mostly worked as they were. This blog post aims to discuss a specific problem I came across while porting these programs and how I solved it.

Monkey patching requests

The requests package is widely used in the Python ecosystem whenever there is a need to make network requests. However, due to limitations of the networking stack in CPython’s WebAssembly, you cannot use it with Pyodide and hence, PyScript. This means that if you were trying to use a package which uses requests to make HTTP requests, you would not be able to get the functionality working in PyScript.

In my case, I was trying to use the pyowm package to fetch weather forecast data which was only making HTTP GET requests. Hence, the solution suggested by Hood in the Pyodide Gitter channel was to monkey patch the relevant code to use the pyodide.open_url() function.

First, I implement a just enough MyResponse class to encapsulate the response:

class MyResponse:
    def __init__(self, status_code, message, json_body):
        self.status_code = status_code
        self.text = message
        self.json_body = json_body

    def json(self):
       return self.json_body

The class contains just enough attributes and functions needed by pyowm and specifically, the functionality I am using.

Then, I create a JustEnoughRequests class to implement a get() method which will call the pyodide.open_url() function referred to earlier, essentially, intercepting the call to the requests.get() function and instead using the pyodide.open_url() function to make the HTTP GET call:

class JustEnoughRequests:
    def __init__(self):
        pass

    def get(self, uri, **kwargs):
        print("Sending request to:", uri)
        print("Got kwargs, igoring everyting other than params", kwargs)
        query_params = []
        for k, v in kwargs["params"].items():
            query_params.append(k + "=" + v) 
        query_string = "&".join(query_params)
        response = pyodide.open_url(uri + "?" + query_string)
        json_response = response.getvalue()
        d = json.loads(json_response)
        return MyResponse(int(d["cod"]), d["message"], json.loads(json_response))
 
just_enough_requests = JustEnoughRequests()

As you can see in the implementation of the get() method, it accepts one positional argument and one or more keyword arguments. From the keyword arguments it is called with, it ignores all, but the params keyword argument from which it constructs the query parameters, appends them to the uri (the target HTTP request host and path) and then invokes the pyodide.open_url() function. This function returns a io.StringIO() object, hence, we use the getvalue() method to get the JSON encoded data. The JSON response from the Open Weathter Map API contains a field, cod containing the HTTP status code, message containing any error message and other data relevant to the request made as key-value pairs. We encapsulate the result in a MyResponse object and return it.

Then, we create an object of type, JustEnoughRequests which will be what we replace the requests module with.

The patching is done as follows:

with mock.patch('pyowm.commons.http_client.requests', just_enough_requests):
    # Get a token from https://home.openweathermap.org/users/sign_up
    owm = OWM('your token')
    mgr = owm.weather_manager()
    three_h_forecast = mgr.forecast_at_place('new york, us', '3h').forecast

And that’s it. Here’s a complete HTML file which you can download. You will need an API key from open weather map. Once you have replaced the token in code, and open in your browser, you should see a graph.

Graph of the forecast temperature

If nothing seems to happen, open the Console to look for any error logs.

Summary

Now, of course, how you patch some other code which uses the requests package will vary. The key is to ensure that you are choosing the right namespace to patch in. Additionally, how much of the requests package you will need to implement also determines how simple or convoluted the patching gets.

Have a look at this github issue for PyScipt and the linked Pyodide issue to learn more and what’s happening in this space.