{"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-693009048", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 693009048, "node_id": "MDEyOklzc3VlQ29tbWVudDY5MzAwOTA0OA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-15T22:17:30Z", "updated_at": "2020-09-22T14:37:00Z", "author_association": "OWNER", "body": "Maybe instead of implementing `datasette.get()` and `datasette.post()` and `datasette.request()` and `datasette.stream()` I could instead have a nested object called `datasette.client` which is a preconfigured `AsyncClient` instance.\r\n\r\n```python\r\nresponse = await datasette.client.get(\"/\")\r\n```\r\nOr perhaps this should be a method in case I ever need to be able to `await` it:\r\n```python\r\nresponse = await (await datasette.client()).get(\"/\")\r\n```\r\nThis is a bit cosmetically ugly though, I'd rather avoid that if possible.\r\n\r\nMaybe I could get this working by returning an object from `.client()` which provides a `await obj.get()` method:\r\n```python\r\nresponse = await datasette.client().get(\"/\")\r\n```\r\nI don't think there's any benefit to that over `await datasette.client.get()` though.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 681375466, "label": "await datasette.client.get(path) mechanism for executing internal requests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-693010291", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 693010291, "node_id": "MDEyOklzc3VlQ29tbWVudDY5MzAxMDI5MQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-15T22:20:55Z", "updated_at": "2020-09-15T22:20:55Z", "author_association": "OWNER", "body": "Should I instantiate a single `Client` and reuse it for all internal requests, or can I instantiate a new `Client` for each request?\r\n\r\nhttps://www.python-httpx.org/advanced/#why-use-a-client says that the main benefit of a Client instance is HTTP connection pooling - which isn't an issue for these internal requests since they won't be using the HTTP protocol at all, they'll be calling the ASGI application directly.\r\n\r\nSo I'm leaning towards instantiating a fresh client for every internal request. I'll run a microbenchmark to check that this doesn't have any unpleasant performance implications.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 681375466, "label": "await datasette.client.get(path) mechanism for executing internal requests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-693008540", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 693008540, "node_id": "MDEyOklzc3VlQ29tbWVudDY5MzAwODU0MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-15T22:16:07Z", "updated_at": "2020-09-15T22:16:07Z", "author_association": "OWNER", "body": "I think I can use `async with httpx.AsyncClient(base_url=\"http://localhost/\") as client:` to ensure I don't need to use `http://localhost/` on every call.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 681375466, "label": "await datasette.client.get(path) mechanism for executing internal requests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-693007512", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 693007512, "node_id": "MDEyOklzc3VlQ29tbWVudDY5MzAwNzUxMg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-15T22:13:30Z", "updated_at": "2020-09-15T22:13:30Z", "author_association": "OWNER", "body": "I could solve streaming using something like this:\r\n```python\r\nasync with datasette.stream(\"GET\", \"/fixtures/compound_three_primary_keys.csv?_stream=on&_size=max\") as response:\r\n async for chunk in response.aiter_bytes():\r\n print(chunk)\r\n```\r\nWhich would be a wrapper around `AsyncClient.stream(method, url, ...)` from https://www.python-httpx.org/async/#streaming-responses", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 681375466, "label": "await datasette.client.get(path) mechanism for executing internal requests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-693005033", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 693005033, "node_id": "MDEyOklzc3VlQ29tbWVudDY5MzAwNTAzMw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-15T22:06:58Z", "updated_at": "2020-09-15T22:10:58Z", "author_association": "OWNER", "body": "What if `datasette.get()` was an alias for `httpx.get()`, pre-configured to route to the correct application? And with some sugar that added `http://localhost/` to the beginning of the path if it was missing?\r\n\r\nThis would make `httpx` a dependency of core Datasette, which I think is OK.\r\n\r\nIt would also solve the return type problem: I would return whatever `httpx` returns.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 681375466, "label": "await datasette.client.get(path) mechanism for executing internal requests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-693004770", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 693004770, "node_id": "MDEyOklzc3VlQ29tbWVudDY5MzAwNDc3MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-15T22:06:13Z", "updated_at": "2020-09-15T22:06:13Z", "author_association": "OWNER", "body": "I'm tempted to create a `await datasette.request()` method which can take any HTTP verb - then have `datasette.get()` and `datasette.post()` as thin wrappers around it.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 681375466, "label": "await datasette.client.get(path) mechanism for executing internal requests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-693004572", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 693004572, "node_id": "MDEyOklzc3VlQ29tbWVudDY5MzAwNDU3Mg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-15T22:05:39Z", "updated_at": "2020-09-15T22:05:39Z", "author_association": "OWNER", "body": "Maybe these methods become the way most Datasette tests are written, replacing the existing `TestClient` mechanism?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 681375466, "label": "await datasette.client.get(path) mechanism for executing internal requests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-693004296", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 693004296, "node_id": "MDEyOklzc3VlQ29tbWVudDY5MzAwNDI5Ng==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-15T22:04:54Z", "updated_at": "2020-09-15T22:04:54Z", "author_association": "OWNER", "body": "So what should I do about streaming responses?\r\n\r\nI could deliberately ignore them - through an exception if you attempt to run `await datasette.get(...)` against a streaming URL.\r\n\r\nI could load the entire response into memory and return it as a wrapped object.\r\n\r\nI could support some kind of asynchronous iterator mechanism. This would be pretty elegant if I could decide the right syntax for it - it would allow plugins to take advantage of other internal URLs that return streaming content without needing to load that content entirely into memory in order to process it.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 681375466, "label": "await datasette.client.get(path) mechanism for executing internal requests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-693003652", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 693003652, "node_id": "MDEyOklzc3VlQ29tbWVudDY5MzAwMzY1Mg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-15T22:03:08Z", "updated_at": "2020-09-15T22:03:08Z", "author_association": "OWNER", "body": "I'm not going to mess around with formats - you'll get back the exact response that a web client would receive.\r\n\r\nQuestion: what should the response object look like? e.g. if you do:\r\n\r\n response = await datasette.get(\"/db/table.json\")\r\n\r\nWhat should `response` be?\r\n\r\nI could reuse the Datasette `Response` class from `datasette.utils.asgi`. This would work well for regular responses which just have a status code, some headers and a response body. It wouldn't be great for streaming responses though such as you get back from `?_stream=1` CSV exports.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 681375466, "label": "await datasette.client.get(path) mechanism for executing internal requests"}, "performed_via_github_app": null}