html_url,issue_url,id,node_id,user,created_at,updated_at,author_association,body,reactions,issue,performed_via_github_app
https://github.com/simonw/datasette/issues/943#issuecomment-696778735,https://api.github.com/repos/simonw/datasette/issues/943,696778735,MDEyOklzc3VlQ29tbWVudDY5Njc3ODczNQ==,9599,2020-09-22T15:00:13Z,2020-09-22T15:00:39Z,OWNER,"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.
It would make for much neater plugin tests too, and neater testing documentation: https://docs.datasette.io/en/stable/testing_plugins.html","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",681375466,
https://github.com/simonw/datasette/issues/943#issuecomment-696777886,https://api.github.com/repos/simonw/datasette/issues/943,696777886,MDEyOklzc3VlQ29tbWVudDY5Njc3Nzg4Ng==,9599,2020-09-22T14:58:54Z,2020-09-22T14:58:54Z,OWNER,"```python
class DatasetteClient:
def __init__(self, ds):
self._client = httpx.AsyncClient(app=ds.app())
def _fix(self, path):
if path.startswith(""/""):
path = ""http://localhost{}"".format(path)
return path
async def get(self, path, **kwargs):
return await self._client.get(self._fix(path), **kwargs)
async def options(self, path, **kwargs):
return await self._client.options(self._fix(path), **kwargs)
async def head(self, path, **kwargs):
return await self._client.head(self._fix(path), **kwargs)
async def post(self, path, **kwargs):
return await self._client.post(self._fix(path), **kwargs)
async def put(self, path, **kwargs):
return await self._client.put(self._fix(path), **kwargs)
async def patch(self, path, **kwargs):
return await self._client.patch(self._fix(path), **kwargs)
async def delete(self, path, **kwargs):
return await self._client.delete(self._fix(path), **kwargs)
```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",681375466,
https://github.com/simonw/datasette/issues/943#issuecomment-696776828,https://api.github.com/repos/simonw/datasette/issues/943,696776828,MDEyOklzc3VlQ29tbWVudDY5Njc3NjgyOA==,9599,2020-09-22T14:57:13Z,2020-09-22T14:57:13Z,OWNER,"I may as well implement all of the HTTP methods supported by the `httpx` client:
- get
- options
- head
- post
- put
- patch
- delete","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",681375466,
https://github.com/simonw/datasette/issues/943#issuecomment-696775516,https://api.github.com/repos/simonw/datasette/issues/943,696775516,MDEyOklzc3VlQ29tbWVudDY5Njc3NTUxNg==,9599,2020-09-22T14:55:10Z,2020-09-22T14:55:10Z,OWNER,"Even smaller `DatasetteClient` implementation:
```python
class DatasetteClient:
def __init__(self, ds):
self._client = httpx.AsyncClient(app=ds.app())
def _fix(self, path):
if path.startswith(""/""):
path = ""http://localhost{}"".format(path)
return path
async def get(self, path, **kwargs):
return await self._client.get(self._fix(path), **kwargs)
async def post(self, path, **kwargs):
return await self._client.post(self._fix(path), **kwargs)
async def options(self, path, **kwargs):
return await self._client.options(self._fix(path), **kwargs)
```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",681375466,
https://github.com/simonw/datasette/issues/943#issuecomment-696774711,https://api.github.com/repos/simonw/datasette/issues/943,696774711,MDEyOklzc3VlQ29tbWVudDY5Njc3NDcxMQ==,9599,2020-09-22T14:53:56Z,2020-09-22T14:53:56Z,OWNER,"How important is it to use `httpx.AsyncClient` with a context manager?
https://www.python-httpx.org/async/#opening-and-closing-clients says:
> Alternatively, use `await client.aclose()` if you want to close a client explicitly:
>
> ```
> client = httpx.AsyncClient()
> ...
> await client.aclose()
> ```
The `.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
The transport I am using is a class called `ASGITransport` in https://github.com/encode/httpx/blob/master/httpx/_transports/asgi.py
The `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.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",681375466,
https://github.com/simonw/datasette/issues/943#issuecomment-696769853,https://api.github.com/repos/simonw/datasette/issues/943,696769853,MDEyOklzc3VlQ29tbWVudDY5Njc2OTg1Mw==,9599,2020-09-22T14:46:21Z,2020-09-22T14:46:21Z,OWNER,This adds `httpx` as a dependency - I think I'm OK with that. I use it for testing in all of my plugins anyway.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",681375466,
https://github.com/simonw/datasette/issues/943#issuecomment-696769501,https://api.github.com/repos/simonw/datasette/issues/943,696769501,MDEyOklzc3VlQ29tbWVudDY5Njc2OTUwMQ==,9599,2020-09-22T14:45:49Z,2020-09-22T14:45:49Z,OWNER,"I put together a minimal prototype of this and it feels pretty good:
```diff
diff --git a/datasette/app.py b/datasette/app.py
index 20aae7d..fb3bdad 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -4,6 +4,7 @@ import collections
import datetime
import glob
import hashlib
+import httpx
import inspect
import itertools
from itsdangerous import BadSignature
@@ -312,6 +313,7 @@ class Datasette:
self._register_renderers()
self._permission_checks = collections.deque(maxlen=200)
self._root_token = secrets.token_hex(32)
+ self.client = DatasetteClient(self)
async def invoke_startup(self):
for hook in pm.hook.startup(datasette=self):
@@ -1209,3 +1211,25 @@ def route_pattern_from_filepath(filepath):
class NotFoundExplicit(NotFound):
pass
+
+
+class DatasetteClient:
+ def __init__(self, ds):
+ self.app = ds.app()
+
+ def _fix(self, path):
+ if path.startswith(""/""):
+ path = ""http://localhost{}"".format(path)
+ return path
+
+ async def get(self, path, **kwargs):
+ async with httpx.AsyncClient(app=self.app) as client:
+ return await client.get(self._fix(path), **kwargs)
+
+ async def post(self, path, **kwargs):
+ async with httpx.AsyncClient(app=self.app) as client:
+ return await client.post(self._fix(path), **kwargs)
+
+ async def options(self, path, **kwargs):
+ async with httpx.AsyncClient(app=self.app) as client:
+ return await client.options(self._fix(path), **kwargs)
```
Used like this in `ipython`:
```
In [1]: from datasette.app import Datasette
In [2]: ds = Datasette([""fixtures.db""])
In [3]: (await ds.client.get(""/-/config.json"")).json()
Out[3]:
{'default_page_size': 100,
'max_returned_rows': 1000,
'num_sql_threads': 3,
'sql_time_limit_ms': 1000,
'default_facet_size': 30,
'facet_time_limit_ms': 200,
'facet_suggest_time_limit_ms': 50,
'hash_urls': False,
'allow_facet': True,
'allow_download': True,
'suggest_facets': True,
'default_cache_ttl': 5,
'default_cache_ttl_hashed': 31536000,
'cache_size_kb': 0,
'allow_csv_stream': True,
'max_csv_mb': 100,
'truncate_cells_html': 2048,
'force_https_urls': False,
'template_debug': False,
'base_url': '/'}
In [4]: (await ds.client.get(""/fixtures/facetable.json?_shape=array"")).json()
Out[4]:
[{'pk': 1,
'created': '2019-01-14 08:00:00',
'planet_int': 1,
'on_earth': 1,
'state': 'CA',
'city_id': 1,
'neighborhood': 'Mission',
'tags': '[""tag1"", ""tag2""]',
'complex_array': '[{""foo"": ""bar""}]',
'distinct_some_null': 'one'},
{'pk': 2,
'created': '2019-01-14 08:00:00',
'planet_int': 1,
'on_earth': 1,
'state': 'CA',
'city_id': 1,
'neighborhood': 'Dogpatch',
'tags': '[""tag1"", ""tag3""]',
'complex_array': '[]',
'distinct_some_null': 'two'},
```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",681375466,
https://github.com/simonw/datasette/issues/943#issuecomment-693009048,https://api.github.com/repos/simonw/datasette/issues/943,693009048,MDEyOklzc3VlQ29tbWVudDY5MzAwOTA0OA==,9599,2020-09-15T22:17:30Z,2020-09-22T14:37:00Z,OWNER,"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.
```python
response = await datasette.client.get(""/"")
```
Or perhaps this should be a method in case I ever need to be able to `await` it:
```python
response = await (await datasette.client()).get(""/"")
```
This is a bit cosmetically ugly though, I'd rather avoid that if possible.
Maybe I could get this working by returning an object from `.client()` which provides a `await obj.get()` method:
```python
response = await datasette.client().get(""/"")
```
I don't think there's any benefit to that over `await datasette.client.get()` though.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",681375466,