{"html_url": "https://github.com/simonw/datasette/issues/943#issuecomment-675747878", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675747878, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc0Nzg3OA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-18T22:18:46Z", "updated_at": "2020-08-18T22:19:12Z", "author_association": "OWNER", "body": "Could be as simple as `response = await datasette.get(\"/path/blah\")` - which could also be re-used by the implementation of the `datasette --get /` CLI option introduced in #927.\r\n\r\nBit weird calling it `.get()` since that clashes with Python's dictionary `.get()` method.", "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-675748573", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675748573, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc0ODU3Mw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-18T22:20:52Z", "updated_at": "2020-08-18T22:20:52Z", "author_association": "OWNER", "body": "Should it default to treating things as if they had the `.json` extension? There are use-cases for the non-JSON method, such as https://github.com/natbat/tidepools_near_me/commit/ec102c6da5a5d86f17628740d90b6365b671b5e1\r\n\r\nI think I'm OK with people having to add `.json` to their internal calls. Maybe they could use `format=\"json\"`) as an optional parameter which would automatically handle the very weird edge-cases where you need to use `?_format=json` instead of `.json` (due to table names existing with a `.json` suffix).", "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-675749076", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675749076, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc0OTA3Ng==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-18T22:22:21Z", "updated_at": "2020-08-18T22:22:21Z", "author_association": "OWNER", "body": "Alternative name possibilities:\r\n\r\n- `datasette.http_get(...)` - slightly misleading since it's not going over the HTTP protocol\r\n- `datasette.internal_get(...)` - the `internal_` might suggest its not an API for external use, which isn't true - it's for plugins\r\n- `datasette.get(...)` - clashes with `dict.get()` but I'm not at all sure that's a good reason not to use 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-675749319", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675749319, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc0OTMxOQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-18T22:23:01Z", "updated_at": "2020-08-18T22:23:01Z", "author_association": "OWNER", "body": "Actually no - `requests.get()` and `httpx.get()` prove that having a `.get()` method for an HTTP-related API isn't confusing to people at all.\r\n\r\n`datasette.get()` it is.\r\n\r\n(I'll probably add `datasette.post()` in the future too).", "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-675750382", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675750382, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc1MDM4Mg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-18T22:26:15Z", "updated_at": "2020-08-18T22:26:15Z", "author_association": "OWNER", "body": "Should internal requests executed in this way be handled by plugins that used the `asgi_wrapper()` hook?\r\n\r\nHard to be sure one way or the other. I'm worried about logging middleware triggering twice - but actually anyone doing serious logging of their Datasette instance is probably doing it in a different layer (uvicorn logs or nginx proxy or whatever) so they wouldn't be affected. There aren't any ASGI logging middlewares out there that I've seen.\r\n\r\nAlso: if you run into a situation where your stuff is breaking because `datasette.get()` is calling ASGI middleware twice you can fix it by running your ASGI middleware outside of the `asgi_wrapper` plugin hook mechanism.\r\n\r\nSo I think it DOES execute `asgi_wrapper()` middleware.", "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-675750845", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675750845, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc1MDg0NQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-18T22:27:43Z", "updated_at": "2020-08-18T22:27:43Z", "author_association": "OWNER", "body": "What about authentication checks etc? Won't they run twice?\r\n\r\nI think that's OK too, in fact it's desirable: think of the case of `datasette-graphql` where a bunch of different TableView calls are being made as part of the same GraphQL queries. Having those calls take advantage of finely grained per-table authentication and permission checks seems like a good feature.", "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-675751719", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675751719, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc1MTcxOQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-18T22:30:27Z", "updated_at": "2020-08-18T22:30:27Z", "author_association": "OWNER", "body": "Right now calling `datasette.app()` instantiates an ASGI application - complete with a bunch of routes and wrappers - and returns that application object. Calling it twice instantiates another ASGI application.\r\n\r\nI think a single `Datasette` instance should only ever create a single ASGI app - so the `.app()` method should cache the ASGI app that it returns the first time and return the same application again on future calls.", "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-675752436", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675752436, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc1MjQzNg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-18T22:32:44Z", "updated_at": "2020-08-18T22:32:44Z", "author_association": "OWNER", "body": "One thing to consider here: Datasette's table and database name escaping rules can be a little bit convoluted.\r\n\r\nIf a plugin wants to get back the first five rows of a table, it will need to construct a URL `/dbname/tablename?_size=5` - but it will need to know how to turn the database and table names into the correctly escaped `dbname` and `tablename` values.\r\n\r\nHere's how the `row.html` table handles that right now: https://github.com/simonw/datasette/blob/b21ed237ab940768574c834aa5a7130724bd3a2d/datasette/templates/row.html#L19-L23\r\n\r\nIt would be an improvement to have this logic abstracted out somewhere and documented so plugins can use 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-675753114", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675753114, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc1MzExNA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-18T22:34:55Z", "updated_at": "2020-08-18T22:34:55Z", "author_association": "OWNER", "body": "Maybe allow this:\r\n\r\n response = await datasette.get(\"/{database}/{table}.json\", database=database, table=table)\r\n\r\nThis could cause problems if users ever need to pass literal `{` in their paths. Maybe allow this too:\r\n\r\n response = await datasette.get(\"/{database}/{table}.json\", interpolate=False)\r\n\r\nNot convinced this is useful - it's a bit unintuitive.", "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-675787416", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675787416, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc4NzQxNg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-19T00:42:38Z", "updated_at": "2020-08-19T00:42:38Z", "author_association": "OWNER", "body": "I just realised that this mechanism is kind of like being able to use microservices - make API calls within your application - except that everything runs in the same process against SQLite databases so calls will be _lightning fast_.\r\n\r\nIt also means that a plugin can add a new internal API to Datasette that's accessible to other plugins by registering a new route with `register_routes`!", "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-675788203", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675788203, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTc4ODIwMw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-19T00:46:08Z", "updated_at": "2020-08-19T00:46:23Z", "author_association": "OWNER", "body": "Also fun: the inevitable plugin that exposes this to the template language - so Datasette templates can stitch together data from multiple other internal API calls. Fun way to take advantage of `async` support in Jinja.", "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-675884980", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675884980, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTg4NDk4MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-19T06:44:26Z", "updated_at": "2020-08-19T06:44:26Z", "author_association": "OWNER", "body": "Need to decide what to do about JSON responses.\r\n\r\nWhen called from a template it's likely the intent will be to further loop through the JSON data returned. It would be annoying to have to run `json.loads` here.\r\n\r\nMaybe a `.get_json()` method then? Or even return a response that has `.json()` and `.text` similar to `httpx` - or just return an `httpx` response.", "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-675889551", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675889551, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTg4OTU1MQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-19T06:56:06Z", "updated_at": "2020-08-19T06:56:17Z", "author_association": "OWNER", "body": "I'm leaning towards defaulting to JSON as the requested format - you can pass `format=\"html\"` if you want HTML.\r\n\r\nBut weird that it's different from the web UI.", "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-675889865", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 675889865, "node_id": "MDEyOklzc3VlQ29tbWVudDY3NTg4OTg2NQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-08-19T06:57:00Z", "updated_at": "2020-08-19T06:57:00Z", "author_association": "OWNER", "body": "Maybe `.get` vs `.get_html`?", "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} {"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-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-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-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-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-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-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-695133768", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 695133768, "node_id": "MDEyOklzc3VlQ29tbWVudDY5NTEzMzc2OA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-19T00:06:56Z", "updated_at": "2020-09-19T00:07:35Z", "author_association": "OWNER", "body": "[dogsheep-beta](https://github.com/dogsheep/dogsheep-beta) could do with this too. It currently [makes a call](https://github.com/dogsheep/dogsheep-beta/blob/ab36101bdae69b11af7c6bd7edee838d052e6ecf/dogsheep_beta/__init__.py#L216-L225) to `TableView` in a similar way to `datasette-graphql` in order to calculate facets.\r\n\r\n`dogsheep-beta` would benefit with a mechanism for changing the facet timeout setting during that call (as would `datasette-graphql`, see the [DatasetteSpecialConfig mechanism](https://github.com/simonw/datasette-graphql/blob/f9dc5c518b7cdc94b93873ef20069a7ea2882a95/datasette_graphql/utils.py#L516-L519) it uses).", "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-696769501", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 696769501, "node_id": "MDEyOklzc3VlQ29tbWVudDY5Njc2OTUwMQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-22T14:45:49Z", "updated_at": "2020-09-22T14:45:49Z", "author_association": "OWNER", "body": "I put together a minimal prototype of this and it feels pretty good:\r\n```diff\r\ndiff --git a/datasette/app.py b/datasette/app.py\r\nindex 20aae7d..fb3bdad 100644\r\n--- a/datasette/app.py\r\n+++ b/datasette/app.py\r\n@@ -4,6 +4,7 @@ import collections\r\n import datetime\r\n import glob\r\n import hashlib\r\n+import httpx\r\n import inspect\r\n import itertools\r\n from itsdangerous import BadSignature\r\n@@ -312,6 +313,7 @@ class Datasette:\r\n self._register_renderers()\r\n self._permission_checks = collections.deque(maxlen=200)\r\n self._root_token = secrets.token_hex(32)\r\n+ self.client = DatasetteClient(self)\r\n \r\n async def invoke_startup(self):\r\n for hook in pm.hook.startup(datasette=self):\r\n@@ -1209,3 +1211,25 @@ def route_pattern_from_filepath(filepath):\r\n \r\n class NotFoundExplicit(NotFound):\r\n pass\r\n+\r\n+\r\n+class DatasetteClient:\r\n+ def __init__(self, ds):\r\n+ self.app = ds.app()\r\n+\r\n+ def _fix(self, path):\r\n+ if path.startswith(\"/\"):\r\n+ path = \"http://localhost{}\".format(path)\r\n+ return path\r\n+\r\n+ async def get(self, path, **kwargs):\r\n+ async with httpx.AsyncClient(app=self.app) as client:\r\n+ return await client.get(self._fix(path), **kwargs)\r\n+\r\n+ async def post(self, path, **kwargs):\r\n+ async with httpx.AsyncClient(app=self.app) as client:\r\n+ return await client.post(self._fix(path), **kwargs)\r\n+\r\n+ async def options(self, path, **kwargs):\r\n+ async with httpx.AsyncClient(app=self.app) as client:\r\n+ return await client.options(self._fix(path), **kwargs)\r\n```\r\nUsed like this in `ipython`:\r\n```\r\nIn [1]: from datasette.app import Datasette\r\n\r\nIn [2]: ds = Datasette([\"fixtures.db\"])\r\n\r\nIn [3]: (await ds.client.get(\"/-/config.json\")).json()\r\nOut[3]: \r\n{'default_page_size': 100,\r\n 'max_returned_rows': 1000,\r\n 'num_sql_threads': 3,\r\n 'sql_time_limit_ms': 1000,\r\n 'default_facet_size': 30,\r\n 'facet_time_limit_ms': 200,\r\n 'facet_suggest_time_limit_ms': 50,\r\n 'hash_urls': False,\r\n 'allow_facet': True,\r\n 'allow_download': True,\r\n 'suggest_facets': True,\r\n 'default_cache_ttl': 5,\r\n 'default_cache_ttl_hashed': 31536000,\r\n 'cache_size_kb': 0,\r\n 'allow_csv_stream': True,\r\n 'max_csv_mb': 100,\r\n 'truncate_cells_html': 2048,\r\n 'force_https_urls': False,\r\n 'template_debug': False,\r\n 'base_url': '/'}\r\n\r\nIn [4]: (await ds.client.get(\"/fixtures/facetable.json?_shape=array\")).json()\r\nOut[4]: \r\n[{'pk': 1,\r\n 'created': '2019-01-14 08:00:00',\r\n 'planet_int': 1,\r\n 'on_earth': 1,\r\n 'state': 'CA',\r\n 'city_id': 1,\r\n 'neighborhood': 'Mission',\r\n 'tags': '[\"tag1\", \"tag2\"]',\r\n 'complex_array': '[{\"foo\": \"bar\"}]',\r\n 'distinct_some_null': 'one'},\r\n {'pk': 2,\r\n 'created': '2019-01-14 08:00:00',\r\n 'planet_int': 1,\r\n 'on_earth': 1,\r\n 'state': 'CA',\r\n 'city_id': 1,\r\n 'neighborhood': 'Dogpatch',\r\n 'tags': '[\"tag1\", \"tag3\"]',\r\n 'complex_array': '[]',\r\n 'distinct_some_null': 'two'},\r\n```", "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-696769853", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 696769853, "node_id": "MDEyOklzc3VlQ29tbWVudDY5Njc2OTg1Mw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-22T14:46:21Z", "updated_at": "2020-09-22T14:46:21Z", "author_association": "OWNER", "body": "This adds `httpx` as a dependency - I think I'm OK with that. I use it for testing in all of my plugins anyway.", "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-696774711", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 696774711, "node_id": "MDEyOklzc3VlQ29tbWVudDY5Njc3NDcxMQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-22T14:53:56Z", "updated_at": "2020-09-22T14:53:56Z", "author_association": "OWNER", "body": "How important is it to use `httpx.AsyncClient` with a context manager?\r\n\r\nhttps://www.python-httpx.org/async/#opening-and-closing-clients says:\r\n\r\n> Alternatively, use `await client.aclose()` if you want to close a client explicitly:\r\n> \r\n> ```\r\n> client = httpx.AsyncClient()\r\n> ...\r\n> await client.aclose()\r\n> ```\r\nThe `.aclose()` method has a comment saying \"Close transport and proxies\" - I'm not using proxies, so the relevant implementation seems to be a call to `await self._transport.aclose()` in https://github.com/encode/httpx/blob/f932af9172d15a803ad40061a4c2c0cd891645cf/httpx/_client.py#L1741-L1751\r\n\r\nThe transport I am using is a class called `ASGITransport` in https://github.com/encode/httpx/blob/master/httpx/_transports/asgi.py\r\n\r\nThe `aclose()` method on that class does nothing. So it looks like I can instantiate a client without bothering with the `async with httpx.AsyncClient` bit.", "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-696775516", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 696775516, "node_id": "MDEyOklzc3VlQ29tbWVudDY5Njc3NTUxNg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-22T14:55:10Z", "updated_at": "2020-09-22T14:55:10Z", "author_association": "OWNER", "body": "Even smaller `DatasetteClient` implementation:\r\n```python\r\nclass DatasetteClient:\r\n def __init__(self, ds):\r\n self._client = httpx.AsyncClient(app=ds.app())\r\n\r\n def _fix(self, path):\r\n if path.startswith(\"/\"):\r\n path = \"http://localhost{}\".format(path)\r\n return path\r\n\r\n async def get(self, path, **kwargs):\r\n return await self._client.get(self._fix(path), **kwargs)\r\n\r\n async def post(self, path, **kwargs):\r\n return await self._client.post(self._fix(path), **kwargs)\r\n\r\n async def options(self, path, **kwargs):\r\n return await self._client.options(self._fix(path), **kwargs)\r\n```", "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-696776828", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 696776828, "node_id": "MDEyOklzc3VlQ29tbWVudDY5Njc3NjgyOA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-22T14:57:13Z", "updated_at": "2020-09-22T14:57:13Z", "author_association": "OWNER", "body": "I may as well implement all of the HTTP methods supported by the `httpx` client:\r\n\r\n- get\r\n- options\r\n- head\r\n- post\r\n- put\r\n- patch\r\n- delete", "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-696777886", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 696777886, "node_id": "MDEyOklzc3VlQ29tbWVudDY5Njc3Nzg4Ng==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-22T14:58:54Z", "updated_at": "2020-09-22T14:58:54Z", "author_association": "OWNER", "body": "```python\r\nclass DatasetteClient:\r\n def __init__(self, ds):\r\n self._client = httpx.AsyncClient(app=ds.app())\r\n\r\n def _fix(self, path):\r\n if path.startswith(\"/\"):\r\n path = \"http://localhost{}\".format(path)\r\n return path\r\n\r\n async def get(self, path, **kwargs):\r\n return await self._client.get(self._fix(path), **kwargs)\r\n\r\n async def options(self, path, **kwargs):\r\n return await self._client.options(self._fix(path), **kwargs)\r\n\r\n async def head(self, path, **kwargs):\r\n return await self._client.head(self._fix(path), **kwargs)\r\n\r\n async def post(self, path, **kwargs):\r\n return await self._client.post(self._fix(path), **kwargs)\r\n\r\n async def put(self, path, **kwargs):\r\n return await self._client.put(self._fix(path), **kwargs)\r\n\r\n async def patch(self, path, **kwargs):\r\n return await self._client.patch(self._fix(path), **kwargs)\r\n\r\n async def delete(self, path, **kwargs):\r\n return await self._client.delete(self._fix(path), **kwargs)\r\n```", "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-696778735", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 696778735, "node_id": "MDEyOklzc3VlQ29tbWVudDY5Njc3ODczNQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-09-22T15:00:13Z", "updated_at": "2020-09-22T15:00:39Z", "author_association": "OWNER", "body": "Am I going to rewrite ALL of my tests to use this instead? It would clean up a lot of test code, at the cost of quite a bit of work.\r\n\r\nIt would make for much neater plugin tests too, and neater testing documentation: https://docs.datasette.io/en/stable/testing_plugins.html", "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-705887638", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 705887638, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTg4NzYzOA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-08T23:53:13Z", "updated_at": "2020-10-08T23:53:13Z", "author_association": "OWNER", "body": "I want this in Datasette 0.50, so I can use it in `datasette-graphql` and suchlike.", "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-706305784", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/943", "id": 706305784, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjMwNTc4NA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T17:22:55Z", "updated_at": "2020-10-09T17:22:55Z", "author_association": "OWNER", "body": "Documentation (from #1006): https://docs.datasette.io/en/latest/internals.html#client", "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}