{"id":2150,"date":"2022-08-11T12:38:46","date_gmt":"2022-08-11T19:38:46","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/azure-sdk\/?p=2150"},"modified":"2022-08-11T12:38:46","modified_gmt":"2022-08-11T19:38:46","slug":"using-the-azure-sdk-for-python-in-pyodide-and-pyscript","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/azure-sdk\/using-the-azure-sdk-for-python-in-pyodide-and-pyscript\/","title":{"rendered":"Using the Azure SDK for Python in Pyodide and PyScript"},"content":{"rendered":"<h2>Using the Azure SDK for Python in Pyodide and PyScript<\/h2>\n<p><a href=\"https:\/\/pyodide.org\/en\/stable\/usage\/index.html\">Pyodide<\/a> is a Python runtime in WebAssembly. It holds immense promise because it:<\/p>\n<ul>\n<li>Is a JavaScript alternative.<\/li>\n<li>Brings the power of Python&#8217;s scientific computing libraries (for example, NumPy, SciPy) to the browser without the hassle of a backend. No need to worry about scaling, cost overruns, DDOS attacks, and user data laws.<\/li>\n<li>Can provide a standardized Python environment for beginners, thus removing barriers to entry. For example, juggling Python versions and executables.<\/li>\n<\/ul>\n<p>Finally, Pyodide is the engine for <a href=\"https:\/\/pyscript.net\/\">PyScript<\/a>\u2014a thin but convenient Pyodide wrapper that allows for the embedding of Python code into HTML. Supporting the Azure SDK for Python in Pyodide\/PyScript would allow for users to easily interact with Azure in each of the aforementioned use cases.<\/p>\n<p>The challenge with running the Azure SDK for Python in Pyodide is networking. The main job of the SDK is to communicate with Azure via the internet. Traditional implementations of Python, such as CPython, give developers near full access to a computer&#8217;s networking functions. However, the browser&#8217;s built-in security features limit a program&#8217;s networking abilities. As such, we can&#8217;t use traditional networking libraries (think <code>requests<\/code>, <code>aiohttp<\/code>) because they violate the browser&#8217;s rules (for example, preflight requests, forbidden headers).<\/p>\n<h2>Implementation<\/h2>\n<p>Given the browser&#8217;s networking limitations and Pyodide&#8217;s powerful <a href=\"https:\/\/pyodide.org\/en\/stable\/usage\/api\/python-api.html\">JavaScript interfacing<\/a>, the best networking tool to make network requests is JavaScript&#8217;s <a href=\"https:\/\/developer.mozilla.org\/docs\/Web\/API\/Fetch_API\/Using_Fetch\"><code>fetch<\/code> API<\/a>. The Azure SDK for Python is architected such that non-<code>core<\/code> libraries, like the <a href=\"https:\/\/docs.microsoft.com\/python\/api\/overview\/azure\/ai-textanalytics-readme?view=azure-python\">Text Analytics library<\/a>, rely on abstract networking pipelines to handle retries, authentication headers, and most importantly, the actual networking calls. See <a href=\"https:\/\/devblogs.microsoft.com\/azure-sdk\/custom-transport-in-python-sdk-an-httpx-experiment\/\">this post<\/a> for an in-depth explanation of the architecture. The part that performs the network calls is called a transport, and we can implement it as such using the <code>fetch<\/code> API:<\/p>\n<pre><code class=\"language-python\">from collections.abc import AsyncIterator\r\nfrom io import BytesIO\r\nfrom typing import Any, MutableMapping, Optional\r\n\r\nimport js\r\nfrom azure.core.configuration import ConnectionConfiguration\r\nfrom azure.core.exceptions import HttpResponseError, ResponseNotReadError\r\nfrom azure.core.pipeline.transport import AsyncHttpTransport\r\nfrom azure.core.rest import AsyncHttpResponse, HttpRequest\r\nfrom requests.structures import CaseInsensitiveDict\r\n\r\nfrom pyodide import JsException, JsProxy\r\nfrom pyodide.http import FetchResponse, pyfetch\r\n\r\nclass PyodideTransport(AsyncHttpTransport):\r\n    \"\"\"Implements a basic HTTP sender using the Pyodide JavaScript fetch API.\"\"\"\r\n\r\n    def __init__(self, **kwargs):\r\n        self.connection_config = ConnectionConfiguration(**kwargs)\r\n\r\n    async def send(self, request: HttpRequest, **kwargs) -&gt; \"PyodideTransportResponse\":\r\n        \"\"\"Send request object according to configuration.\"\"\"\r\n        stream_response = kwargs.pop(\"stream_response\", False)\r\n        endpoint = request.url\r\n        init = {\r\n            \"method\": request.method,\r\n            \"headers\": dict(request_headers),\r\n            \"body\": request.data,\r\n            \"files\": request.files,\r\n            \"verify\": kwargs.pop(\"connection_verify\", self.connection_config.verify),\r\n            \"cert\": kwargs.pop(\"connection_cert\", self.connection_config.cert),\r\n            \"allow_redirects\": False,\r\n            **kwargs,\r\n        }\r\n\r\n        try:\r\n            response = await pyfetch(endpoint, **init)\r\n        except JsException as error:\r\n            raise HttpResponseError(error, error=error) from error\r\n\r\n        headers = CaseInsensitiveDict(response.js_response.headers)\r\n        transport_response = PyodideTransportResponse(\r\n            request=request,\r\n            internal_response=response,\r\n            block_size=self.connection_config.data_block_size,\r\n            reason=response.status_text,\r\n            headers=headers,\r\n        )\r\n        if not stream_response:\r\n            await transport_response.read()\r\n\r\n        return transport_response<\/code><\/pre>\n<p>The <code>send<\/code> method accepts a generic <code>HttpRequest<\/code> object and maps its attributes to a <code>pyfetch<\/code> call. <code>pyfetch<\/code> is Pyodide&#8217;s built-in <code>fetch<\/code> wrapper. It also raises <code>pyfetch<\/code> exceptions as <code>azure.core<\/code> exceptions. This way, other libraries only have to handle <code>azure.core<\/code> exceptions and not worry about the implementation details of the transport. Finally, the <code>send<\/code> method maps <code>pyfetch<\/code> response fields to a <code>PyodideTransportResponse<\/code> object.<\/p>\n<p>Next, we need to implement a <code>PyodideTransportResponse<\/code> class that acts as an interface between the data of the <code>pyfetch<\/code> response and the rest of the SDK. We also need to implement a download generator class to stream the response, for which we use JavaScript&#8217;s <a href=\"https:\/\/developer.mozilla.org\/docs\/Web\/API\/ReadableStreamDefaultReader\"><code>ReadableStreamDefaultReader<\/code> API<\/a>.<\/p>\n<pre><code class=\"language-python\">class PyodideTransportResponse(AsyncHttpResponse):\r\n    \"\"\"Async response object for the `PyodideTransport`.\"\"\"\r\n\r\n    def __init__(\r\n        self,\r\n        request: HttpRequest,\r\n        internal_response: FetchResponse,\r\n        headers: MutableMapping[str, str],\r\n        block_size: int,\r\n        **__\r\n    ):\r\n        self._block_size = block_size\r\n        self._content = None\r\n        self._encoding: str = \"utf-8\"\r\n        self._headers = headers\r\n        self._internal_response = internal_response\r\n        self._is_closed = False\r\n        self._request = request\r\n\r\n    @property\r\n    def _js_stream(self):\r\n        \"\"\"Use a fresh stream every time.\"\"\"\r\n        return self._internal_response.js_response.clone().body\r\n\r\n    async def read(self) -&gt; bytes:\r\n        if self._content is None:\r\n            parts = []\r\n            async for part in self.iter_bytes():\r\n                parts.append(part)\r\n            self._content = b\"\".join(parts)\r\n        return self._content\r\n\r\n    async def iter_raw(self, **__) -&gt; AsyncIterator[bytes]:\r\n        \"\"\"Asynchronously iterates over the response's bytes. Will not decompress in the process.\"\"\"\r\n        if self._content is not None:\r\n            for i in range(0, len(self.content), self._block_size):\r\n                yield self.content[i : i + self._block_size]\r\n        else:\r\n            async for part in PyodideStreamDownloadGenerator(\r\n                response=self,\r\n                decompress=False,\r\n            ):\r\n                yield part\r\n\r\n    async def iter_bytes(self, **__) -&gt; AsyncIterator[bytes]:\r\n        \"\"\"Asynchronously iterates over the response's bytes. Will decompress in the process.\"\"\"\r\n        if self._content is not None:\r\n            for i in range(0, len(self.content), self._block_size):\r\n                yield self.content[i : i + self._block_size]\r\n        else:\r\n            async for part in PyodideStreamDownloadGenerator(\r\n                response=self,\r\n                decompress=True,\r\n            ):\r\n                yield part\r\n\r\nclass PyodideStreamDownloadGenerator(AsyncIterator[bytes]):\r\n    \"\"\"Simple stream download generator using the JavaScript reader API.\"\"\"\r\n\r\n    def __init__(self, response: PyodideTransportResponse, **kwargs):\r\n        self._decompress = kwargs.get(\"decompress\", False)\r\n        self._block_size = response.block_size\r\n        self.response = response\r\n        self._stream = BytesIO()\r\n        self._closed = False\r\n        self._buffer_left = 0\r\n        self._done = False\r\n        if self._decompress and self.response.headers.get(\"enc\", None) in (\"gzip\", \"deflate\"):\r\n            self._reader = response._js_stream.pipeThrough(js.DecompressionStream.new(\"gzip\")).getReader()\r\n        else:\r\n            self._reader = response._js_stream.getReader()\r\n\r\n    async def __anext__(self) -&gt; bytes:\r\n        if self._closed:\r\n            raise StopAsyncIteration()\r\n        start_pos = self._stream.tell()\r\n        self._stream.read()\r\n        while self._buffer_left &lt; self._block_size:\r\n            read = await self._reader.read()\r\n            if read.done:\r\n                self._closed = True\r\n                break\r\n            self._buffer_left += self._stream.write(bytes(read.value))\r\n        self._stream.seek(start_pos)\r\n        self._buffer_left -= self._block_size\r\n        return self._stream.read(self._block_size)<\/code><\/pre>\n<blockquote><p>Safari and Internet Explorer don&#8217;t support <code>ReadableStreamDefaultReader<\/code>. For more information, see the <a href=\"https:\/\/developer.mozilla.org\/docs\/Web\/API\/ReadableStreamDefaultReader#browser_compatibility\">MDN docs<\/a>.<\/p><\/blockquote>\n<p>Some code was redacted for brevity. For the full implementation, see <a href=\"https:\/\/aka.ms\/pyodide-demo-blog-code\">this gist<\/a>.<\/p>\n<h2>Usage<\/h2>\n<p>Now, we can use our transport directly in the browser. First, add the <a href=\"https:\/\/pyodide.org\/en\/stable\/usage\/downloading-and-deploying.html\">Pyodide CDN link<\/a> to your HTML file. Next, run the following JavaScript:<\/p>\n<pre><code class=\"language-js\">async function main() {\r\n    pyodide = await loadPyodide();\r\n    await pyodide.loadPackage('micropip');\r\n    pyodide.runPythonAsync(`\r\n        import micropip\r\n        await micropip.install(\"azure-ai-textanalytics\")   \r\n    `);\r\n    pyodide.runPython(`&lt;Code for PyodideTransport, PyodideTransportResponse, PyodideStreamDownloadGenerator&gt;`);\r\n    await pyodide.runPythonAsync(`\r\n        from azure.ai.textanalytics.aio import TextAnalyticsClient\r\n        from azure.core.credentials import AzureKeyCredential\r\n        import js\r\n        client = TextAnalyticsClient(\r\n            endpoint=\"https:\/\/my-endpoint.azure.com\", \r\n            # We don't recommend hardcoding keys into HTML pages. Consider some other way to input your key.\r\n            credential=AzureKeyCredential(MY_KEY),\r\n            transport=PyodideTransport(),\r\n        )\r\n        documents = [\"Bonjour mon ami.\"]\r\n        response = (await client.detect_language(documents=documents))[0]\r\n        js.alert(response.primary_language.name)  # French`);\r\n}\r\nmain();<\/code><\/pre>\n<p>Or with PyScript and a little less boilerplate:<\/p>\n<pre><code class=\"language-html\">&lt;head&gt;\r\n&lt;!-- See https:\/\/pyscript.net\/ for instructions to add PyScript to your page --&gt;\r\n&lt;\/head&gt;\r\n&lt;body&gt;\r\n    &lt;py-env&gt;\r\n        - azure-ai-textanalytics\r\n    &lt;\/py-env&gt;\r\n    &lt;py-script&gt;\r\n        &lt;!-- code for PyodideTransport, PyodideTransportResponse, PyodideStreamDownloadGenerator --&gt;\r\n    &lt;\/py-script&gt;\r\n    &lt;py-script&gt;\r\n        # async\r\n        # Need the above comment to have top-level await.\r\n        from azure.ai.textanalytics.aio import TextAnalyticsClient\r\n        from azure.core.credentials import AzureKeyCredential\r\n        client = TextAnalyticsClient(\r\n            endpoint=\"https:\/\/my-endpoint.azure.com\", \r\n            # We don't recommend hardcoding keys into HTML pages. Consider some other way to input your key.\r\n            credential=AzureKeyCredential(MY_KEY),\r\n            transport=PyodideTransport(),\r\n        )\r\n        documents = [\"Bonjour\"]\r\n        response = (await client.detect_language(documents=documents, country_hint=\"us\"))[0]\r\n        print(response.primary_language.name)  # French\r\n    &lt;\/py-script&gt;\r\n&lt;\/body&gt;<\/code><\/pre>\n<p>And you&#8217;ll see &#8220;French&#8221; displayed on your page. One last note is that we have only made an asynchronous client because there&#8217;s no synchronous version of <code>pyfetch<\/code>.<\/p>\n<h2>What now?<\/h2>\n<p>There&#8217;s a <a href=\"https:\/\/github.com\/Azure\/azure-sdk-for-python\/pull\/25250\">pull request with a Pyodide-compatible transport<\/a> in the works for out-of-the-box compatibility. If you have any questions or comments, or want the pull request to be merged sooner, open an issue in the <a href=\"https:\/\/github.com\/Azure\/azure-sdk-for-python\">Azure SDK for Python GitHub repository<\/a>.<\/p>\n<h2>Conclusion<\/h2>\n<p>The Azure SDK for Python is architected to be modular and extensible. We can apply this architecture and extend it into Pyodide and PyScript\u2014a browser-based Python runtime, opening new doors for Azure.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to run the Azure SDK for Python natively in the browser using Pyodide and PyScript.<\/p>\n","protected":false},"author":95870,"featured_media":2158,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[868,706,869,866,867,162],"class_list":["post-2150","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure-sdk","tag-azure-core","tag-azuresdk","tag-networking","tag-pyodide","tag-pyscript","tag-python"],"acf":[],"blog_post_summary":"<p>Learn how to run the Azure SDK for Python natively in the browser using Pyodide and PyScript.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/2150","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/users\/95870"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/comments?post=2150"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/2150\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/media\/2158"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/media?parent=2150"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/categories?post=2150"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/tags?post=2150"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}