{"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705890365", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705890365, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTg5MDM2NQ==", "user": {"value": 22429695, "label": "codecov[bot]"}, "created_at": "2020-10-09T00:03:29Z", "updated_at": "2020-10-09T16:07:03Z", "author_association": "NONE", "body": "# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1000?src=pr&el=h1) Report\n> Merging [#1000](https://codecov.io/gh/simonw/datasette/pull/1000?src=pr&el=desc) into [main](https://codecov.io/gh/simonw/datasette/commit/7249ac5ca04b5ddc6517750326ee7e522cc49145?el=desc) will **increase** coverage by `0.15%`.\n> The diff coverage is `100.00%`.\n\n[![Impacted file tree graph](https://codecov.io/gh/simonw/datasette/pull/1000/graphs/tree.svg?width=650&height=150&src=pr&token=eSahVY7kw1)](https://codecov.io/gh/simonw/datasette/pull/1000?src=pr&el=tree)\n\n```diff\n@@ Coverage Diff @@\n## main #1000 +/- ##\n==========================================\n+ Coverage 84.37% 84.52% +0.15% \n==========================================\n Files 28 28 \n Lines 3871 3878 +7 \n==========================================\n+ Hits 3266 3278 +12 \n+ Misses 605 600 -5 \n```\n\n\n| [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1000?src=pr&el=tree) | Coverage \u0394 | |\n|---|---|---|\n| [datasette/app.py](https://codecov.io/gh/simonw/datasette/pull/1000/diff?src=pr&el=tree#diff-ZGF0YXNldHRlL2FwcC5weQ==) | `96.34% <100.00%> (+0.02%)` | :arrow_up: |\n| [datasette/cli.py](https://codecov.io/gh/simonw/datasette/pull/1000/diff?src=pr&el=tree#diff-ZGF0YXNldHRlL2NsaS5weQ==) | `74.35% <100.00%> (\u00f8)` | |\n| [datasette/utils/testing.py](https://codecov.io/gh/simonw/datasette/pull/1000/diff?src=pr&el=tree#diff-ZGF0YXNldHRlL3V0aWxzL3Rlc3RpbmcucHk=) | `95.16% <100.00%> (-4.84%)` | :arrow_down: |\n| [datasette/views/base.py](https://codecov.io/gh/simonw/datasette/pull/1000/diff?src=pr&el=tree#diff-ZGF0YXNldHRlL3ZpZXdzL2Jhc2UucHk=) | `93.94% <100.00%> (+0.11%)` | :arrow_up: |\n| [datasette/utils/asgi.py](https://codecov.io/gh/simonw/datasette/pull/1000/diff?src=pr&el=tree#diff-ZGF0YXNldHRlL3V0aWxzL2FzZ2kucHk=) | `91.92% <0.00%> (\u00f8)` | |\n| [datasette/views/special.py](https://codecov.io/gh/simonw/datasette/pull/1000/diff?src=pr&el=tree#diff-ZGF0YXNldHRlL3ZpZXdzL3NwZWNpYWwucHk=) | `93.51% <0.00%> (+8.33%)` | :arrow_up: |\n\n------\n\n[Continue to review full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1000?src=pr&el=continue).\n> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)\n> `\u0394 = absolute (impact)`, `\u00f8 = not affected`, `? = missing data`\n> Powered by [Codecov](https://codecov.io/gh/simonw/datasette/pull/1000?src=pr&el=footer). Last update [7249ac5...8a80c79](https://codecov.io/gh/simonw/datasette/pull/1000?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705899629", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705899629, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTg5OTYyOQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T00:37:02Z", "updated_at": "2020-10-09T00:37:02Z", "author_association": "OWNER", "body": "I'm going to route the existing `TestClient` through this mechanism to exercise it during the tests.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705902902", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705902902, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkwMjkwMg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T00:50:49Z", "updated_at": "2020-10-09T00:50:49Z", "author_association": "OWNER", "body": "Almost all of the tests are passing:\r\n```\r\n=========================== short test summary info ============================\r\nFAILED tests/test_api.py::test_table_with_slashes_in_name - assert 404 == 200\r\nFAILED tests/test_api.py::test_row_strange_table_name - assert 404 == 200\r\nFAILED tests/test_html.py::test_row_strange_table_name_with_url_hash - assert...\r\nFAILED tests/test_html.py::test_css_classes_on_body[/fixtures/table%2Fwith%2Fslashes.csv-expected_classes5]\r\nFAILED tests/test_html.py::test_templates_considered[/fixtures/table%2Fwith%2Fslashes.csv-table-fixtures-tablewithslashescsv-fa7563.html, *table.html]\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/compound_three_primary_keys-https://example.com/]\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/compound_three_primary_keys/a,a,a-https://example.com/]\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/paginated_view-https://example.com/]\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/facetable-https://example.com/]\r\nFAILED tests/test_messages.py::test_messages_are_displayed_and_cleared - KeyE...\r\nFAILED tests/test_plugins.py::test_hook_register_magic_parameters - Assertion...\r\n============ 11 failed, 718 passed, 6 warnings in 225.77s (0:03:45) ============\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1001#issuecomment-705904566", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1001", "id": 705904566, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkwNDU2Ng==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T00:58:08Z", "updated_at": "2020-10-09T00:58:08Z", "author_association": "OWNER", "body": "To get a traceback:\r\n```\r\ndatasette . -p 8009 --pdb\r\n```\r\nAnd then:\r\n```\r\ncurl -XOPTIONS http://127.0.0.1:8009\r\n```\r\nThis causes the server to open a debugging prompt:\r\n```\r\nINFO: 127.0.0.1:59514 - \"OPTIONS / HTTP/1.1\" 500 Internal Server Error\r\n> /Users/simon/Dropbox/Development/datasette/datasette/views/base.py(115)dispatch_request()\r\n-> return await handler(request, *args, **kwargs)\r\n(Pdb) list\r\n110 \t def database_color(self, database):\r\n111 \t return \"ff0000\"\r\n112 \t\r\n113 \t async def dispatch_request(self, request, *args, **kwargs):\r\n114 \t handler = getattr(self, request.method.lower(), None)\r\n115 ->\t return await handler(request, *args, **kwargs)\r\n116 \t\r\n117 \t async def render(self, templates, request, context=None):\r\n118 \t context = context or {}\r\n119 \t template = self.ds.jinja_env.select_template(templates)\r\n120 \t template_context = {\r\n(Pdb) \r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717768441, "label": "OPTIONS requests return a 500 error"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1001#issuecomment-705904679", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1001", "id": 705904679, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkwNDY3OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T00:58:32Z", "updated_at": "2020-10-09T00:58:32Z", "author_association": "OWNER", "body": "So the bug is in this code here: https://github.com/simonw/datasette/blob/703439bdc37e724b01bc6d7a1fc1d955795132f2/datasette/views/base.py#L113-L115", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717768441, "label": "OPTIONS requests return a 500 error"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1001#issuecomment-705904759", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1001", "id": 705904759, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkwNDc1OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T00:58:47Z", "updated_at": "2020-10-09T00:58:47Z", "author_association": "OWNER", "body": "What should an OPTIONS request return, anyway?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717768441, "label": "OPTIONS requests return a 500 error"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1001#issuecomment-705904917", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1001", "id": 705904917, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkwNDkxNw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T00:59:25Z", "updated_at": "2020-10-09T00:59:25Z", "author_association": "OWNER", "body": "```\r\n~ % curl -XOPTIONS https://www.google.com/\r\n\r\n\r\n \r\n \r\n Error 405 (Method Not Allowed)!!1\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717768441, "label": "OPTIONS requests return a 500 error"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1001#issuecomment-705905121", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1001", "id": 705905121, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkwNTEyMQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T01:00:07Z", "updated_at": "2020-10-09T01:00:07Z", "author_association": "OWNER", "body": "The www.djangoproject.com site returns an empty page:\r\n```\r\n~ % curl -vv -XOPTIONS https://www.djangoproject.com/\r\n* Trying 151.101.42.217:443...\r\n* Connected to www.djangoproject.com (151.101.42.217) port 443 (#0)\r\n* ALPN, offering http/1.1\r\n* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\r\n* Server certificate: osff2.map.fastly.net\r\n* Server certificate: GlobalSign CloudSSL CA - SHA256 - G3\r\n* Server certificate: GlobalSign Root CA\r\n> OPTIONS / HTTP/1.1\r\n> Host: www.djangoproject.com\r\n> User-Agent: curl/7.70.0\r\n> Accept: */*\r\n> \r\n* Mark bundle as not supporting multiuse\r\n< HTTP/1.1 200 OK\r\n< Connection: keep-alive\r\n< Content-Length: 0\r\n< Server: nginx\r\n< Content-Type: text/html; charset=utf-8\r\n< Allow: GET, HEAD, OPTIONS\r\n< Content-Language: en\r\n< X-Frame-Options: SAMEORIGIN\r\n< X-Content-Type-Options: nosniff\r\n< X-XSS-Protection: 1; mode=block\r\n< Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\r\n< Access-Control-Allow-Origin: https://code.djangoproject.com\r\n< Accept-Ranges: bytes\r\n< Date: Fri, 09 Oct 2020 00:59:42 GMT\r\n< Via: 1.1 varnish\r\n< X-Served-By: cache-sjc10047-SJC\r\n< X-Cache: MISS\r\n< X-Cache-Hits: 0\r\n< X-Timer: S1602205182.833493,VS0,VE305\r\n< Vary: Accept-Language, Accept-Encoding\r\n< \r\n* Connection #0 to host www.djangoproject.com left intact\r\n~ % \r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717768441, "label": "OPTIONS requests return a 500 error"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1001#issuecomment-705905418", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1001", "id": 705905418, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkwNTQxOA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T01:00:54Z", "updated_at": "2020-10-09T01:00:54Z", "author_association": "OWNER", "body": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717768441, "label": "OPTIONS requests return a 500 error"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1001#issuecomment-705916614", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1001", "id": 705916614, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkxNjYxNA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T01:37:06Z", "updated_at": "2020-10-09T01:37:06Z", "author_association": "OWNER", "body": "I'm tempted to imitate Django Rest Framework here: https://github.com/encode/django-rest-framework/blob/2e721cdbc85a924d0b0f093b86fe1497b58fe287/rest_framework/views.py#L514-L521\r\n```python\r\n def options(self, request, *args, **kwargs):\r\n \"\"\"\r\n Handler method for HTTP 'OPTIONS' request.\r\n \"\"\"\r\n if self.metadata_class is None:\r\n return self.http_method_not_allowed(request, *args, **kwargs)\r\n data = self.metadata_class().determine_metadata(request, self)\r\n return Response(data, status=status.HTTP_200_OK)\r\n```\r\nThat `determine_metadata()` default method does this: https://github.com/encode/django-rest-framework/blob/335054a5d36b352a58286b303b608b6bf48152f8/rest_framework/metadata.py#L29\r\n\r\nNote the comment at the top:\r\n```\r\n There are not any formalized standards for `OPTIONS` responses\r\n for us to base this on.\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717768441, "label": "OPTIONS requests return a 500 error"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1001#issuecomment-705917015", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1001", "id": 705917015, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkxNzAxNQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T01:38:49Z", "updated_at": "2020-10-09T01:38:49Z", "author_association": "OWNER", "body": "I actually have a sensible `OPTIONS` implementation here:\r\n\r\nhttps://github.com/simonw/datasette/blob/a648bb82bac201c7658f6fdb499ff8ac17ebd2e8/datasette/views/base.py#L154-L165\r\n\r\nI'm going to set the default one to return a 405 (Method Not Allowed) like Google does.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717768441, "label": "OPTIONS requests return a 500 error"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705918844", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705918844, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkxODg0NA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T01:46:06Z", "updated_at": "2020-10-09T01:46:06Z", "author_association": "OWNER", "body": "For this failing test I'm suspicious that the AsyncClient may be persisting cookies in between requests:\r\n```\r\n def test_actor_cookie(app_client):\r\n \"A valid actor cookie sets request.scope['actor']\"\r\n cookie = app_client.actor_cookie({\"id\": \"test\"})\r\n response = app_client.get(\"/\", cookies={\"ds_actor\": cookie})\r\n> assert {\"id\": \"test\"} == app_client.ds._last_request.scope[\"actor\"]\r\nE AssertionError: assert {'id': 'test'} == {'id': 'root'}\r\nE Differing items:\r\nE {'id': 'test'} != {'id': 'root'}\r\nE Use -v to get the full diff\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705920055", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705920055, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkyMDA1NQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T01:51:05Z", "updated_at": "2020-10-09T01:51:05Z", "author_association": "OWNER", "body": "The topic of disabling cookie persistence is discussed a little here: https://github.com/encode/httpx/issues/422#issuecomment-537906693\r\n\r\n> We have a cookiejar abstraction, I think setting it to an always-empty jar like you describe is best. :)", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705920228", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705920228, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkyMDIyOA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T01:51:44Z", "updated_at": "2020-10-09T01:51:44Z", "author_association": "OWNER", "body": "I'm going to switch back to having each request run through a new client. I'm worried about the impact on test performance though. I'll run a microbenchmark before and after.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705921006", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705921006, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkyMTAwNg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T01:55:01Z", "updated_at": "2020-10-09T01:55:01Z", "author_association": "OWNER", "body": "With the single client that is reused for all tests:\r\n```\r\n% time pytest tests/test_api.py\r\n...\r\n6.73s user 9.91s system 81% cpu 20.365 total\r\n```\r\nAfter switching back to this class:\r\n```python\r\nclass 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 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\n async def head(self, path, **kwargs):\r\n async with httpx.AsyncClient(app=self.app) as client:\r\n return await client.head(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 put(self, path, **kwargs):\r\n async with httpx.AsyncClient(app=self.app) as client:\r\n return await client.put(self._fix(path), **kwargs)\r\n\r\n async def patch(self, path, **kwargs):\r\n async with httpx.AsyncClient(app=self.app) as client:\r\n return await client.patch(self._fix(path), **kwargs)\r\n\r\n async def delete(self, path, **kwargs):\r\n async with httpx.AsyncClient(app=self.app) as client:\r\n return await client.delete(self._fix(path), **kwargs)\r\n\r\n async def request(self, method, path, **kwargs):\r\n async with httpx.AsyncClient(app=self.app) as client:\r\n return await client.request(method, self._fix(path), **kwargs)\r\n```\r\nThe time taken is:\r\n```\r\n% time pytest tests/test_api.py\r\n...\r\n7.26s user 10.02s system 82% cpu 21.014 total\r\n```\r\nThat's close enough that I don't feel I need to investigate this further.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705926035", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705926035, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkyNjAzNQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T02:14:14Z", "updated_at": "2020-10-09T02:14:14Z", "author_association": "OWNER", "body": "Still need to handle these six failing tests:\r\n```\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/compound_three_primary_keys-https://example.com/] - AssertionError: {'base_url': 'https://example.com/', 'elemen...\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/compound_three_primary_keys/a,a,a-https://example.com/] - AssertionError: {'base_url': 'https://example.com/', '...\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/paginated_view-https://example.com/] - AssertionError: {'base_url': 'https://example.com/', 'element_parent': '<...\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/facetable-https://example.com/] - AssertionError: {'base_url': 'https://example.com/', 'element_parent': '

FAILED tests/test_messages.py::test_messages_are_displayed_and_cleared - KeyError: 'ds_messages'\r\n\r\nThat one is caused by `response.cookies` skipping cookies that were set to the empty string. Same fix as this: https://github.com/simonw/datasette/blob/a1687351fb75b01f737fda4ad07e0781029de05c/tests/test_auth.py#L90-L95", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705937696", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705937696, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTkzNzY5Ng==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T02:52:53Z", "updated_at": "2020-10-09T02:52:53Z", "author_association": "OWNER", "body": "These failures are giving me a severe \"how did this ever work in the first place?\" vibe:\r\n```\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/compound_three_primary_keys-https://example.com/]\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/compound_three_primary_keys/a,a,a-https://example.com/]\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/paginated_view-https://example.com/]\r\nFAILED tests/test_html.py::test_base_url_config[/fixtures/facetable-https://example.com/]\r\n```\r\nI have a fix for them, no idea why they weren't already failing though.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705940507", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705940507, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTk0MDUwNw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T03:04:15Z", "updated_at": "2020-10-09T03:04:15Z", "author_association": "OWNER", "body": "This is really weird: new set of test failures that I wasn't seeing before, and those tests aren't failing on my laptop:\r\n```\r\n=========================== short test summary info ============================\r\nFAILED tests/test_api.py::test_table_with_slashes_in_name - assert 404 == 200\r\nFAILED tests/test_api.py::test_row_strange_table_name - assert 404 == 200\r\nFAILED tests/test_html.py::test_row_strange_table_name_with_url_hash - assert...\r\nFAILED tests/test_html.py::test_css_classes_on_body[/fixtures/table%2Fwith%2Fslashes.csv-expected_classes5]\r\nFAILED tests/test_html.py::test_templates_considered[/fixtures/table%2Fwith%2Fslashes.csv-table-fixtures-tablewithslashescsv-fa7563.html, *table.html]\r\n================== 5 failed, 738 passed in 194.73s (0:03:14) ===================\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705941580", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705941580, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTk0MTU4MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T03:08:43Z", "updated_at": "2020-10-09T03:08:43Z", "author_association": "OWNER", "body": "Most likely reason for those failures is that `path` and `raw_path` are not being simulated correctly. I used to do those here:\r\n\r\nhttps://github.com/simonw/datasette/blob/402cf870b7d65f9b5fba9e23aa99433294bd4523/datasette/utils/testing.py#L116-L125\r\n\r\nBut now I'm delegating that to `httpx` to handle.\r\n\r\nWEIRD that it passes on my laptop but fails in GitHub Actions CI though.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705945591", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705945591, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTk0NTU5MQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T03:24:48Z", "updated_at": "2020-10-09T03:24:48Z", "author_association": "OWNER", "body": "I'm testing this with a `print(scope)` and `pytest -k test_table_with_slashes_in_name -s`.\r\n\r\nAgainst the `main` branch:\r\n\r\n`{'type': 'http', 'http_version': '1.0', 'method': 'GET', 'path': '/fixtures/table/with/slashes.csv', 'raw_path': b'/fixtures/table%2Fwith%2Fslashes.csv', 'query_string': b'_shape=objects&_format=json', 'headers': [[b'host', b'localhost']], 'csrftoken': ._asgi_csrf_decorator..app_wrapped_with_csrf..get_csrftoken at 0x10e2e6040>}`\r\n\r\nAgainst the broken branch:\r\n\r\n`tests/test_api.py {'type': 'http', 'asgi': {'version': '3.0'}, 'http_version': '1.1', 'method': 'GET', 'headers': [(b'host', b'localhost'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive'), (b'user-agent', b'python-httpx/0.15.0')], 'scheme': 'http', 'path': '/fixtures/table%2Fwith%2Fslashes.csv', 'query_string': b'_shape=objects&_format=json', 'server': ('localhost', None), 'client': ('127.0.0.1', 123), 'root_path': '', 'csrftoken': ._asgi_csrf_decorator..app_wrapped_with_csrf..get_csrftoken at 0x109e0eca0>}`\r\n\r\nThis is on my laptop though so both of those pass the tests.\r\n\r\nKey difference: the `httpx` version doesn't set a `raw_path` at all. BUT.. it does set `path` and sets it to `'/fixtures/table%2Fwith%2Fslashes.csv'`\r\n\r\nThe non-httpx version sets `raw_path` to `b'/fixtures/table%2Fwith%2Fslashes.csv'` and `path` to `'/fixtures/table/with/slashes.csv'`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705946120", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705946120, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTk0NjEyMA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T03:27:05Z", "updated_at": "2020-10-09T03:27:05Z", "author_association": "OWNER", "body": "I may need to fuss around with how the `httpx` client sends things to the ASGI app.\r\n\r\nhttps://github.com/encode/httpx/blob/92ca4d0cc654859fc2257c492e55d8752370d427/httpx/_transports/asgi.py#L26 is relevant:\r\n\r\n Alternatively, you can setup the transport instance explicitly.\r\n This allows you to include any additional configuration arguments specific\r\n to the ASGITransport class:\r\n ```\r\n transport = httpx.ASGITransport(\r\n app=app,\r\n root_path=\"/submount\",\r\n client=(\"1.2.3.4\", 123)\r\n )\r\n client = httpx.AsyncClient(transport=transport)\r\n ```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-705946360", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 705946360, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNTk0NjM2MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T03:28:08Z", "updated_at": "2020-10-09T03:28:08Z", "author_association": "OWNER", "body": "Here's where `httpx` sets up the ASGI scope: https://github.com/encode/httpx/blob/92ca4d0cc654859fc2257c492e55d8752370d427/httpx/_transports/asgi.py#L82-L97\r\n\r\n```python\r\n # ASGI scope.\r\n scheme, host, port, full_path = url\r\n path, _, query = full_path.partition(b\"?\")\r\n scope = {\r\n \"type\": \"http\",\r\n \"asgi\": {\"version\": \"3.0\"},\r\n \"http_version\": \"1.1\",\r\n \"method\": method.decode(),\r\n \"headers\": [(k.lower(), v) for (k, v) in headers],\r\n \"scheme\": scheme.decode(\"ascii\"),\r\n \"path\": unquote(path.decode(\"ascii\")),\r\n \"query_string\": query,\r\n \"server\": (host.decode(\"ascii\"), port),\r\n \"client\": self.client,\r\n \"root_path\": self.root_path,\r\n }\r\n```\r\nSure enough, it doesn't set the `raw_path`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-706263157", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 706263157, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjI2MzE1Nw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T15:57:15Z", "updated_at": "2020-10-09T15:57:15Z", "author_association": "OWNER", "body": "My `httpx` pull request adding `raw_path` support was just merged: https://github.com/encode/httpx/pull/1357 - but it's not in a release yet.\r\n\r\nI'm going to mark these tests as `xfail` so I can land this change - I'll remove that once an `httpx` release comes out that I can use to get the tests passing.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1000#issuecomment-706269271", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1000", "id": 706269271, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjI2OTI3MQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T16:08:49Z", "updated_at": "2020-10-09T16:08:49Z", "author_association": "OWNER", "body": "I'm going to document this in a separate issue.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717746043, "label": "datasette.client internal requests mechanism"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1006#issuecomment-706270877", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1006", "id": 706270877, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjI3MDg3Nw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T16:12:09Z", "updated_at": "2020-10-09T16:12:09Z", "author_association": "OWNER", "body": "This can become a section on https://docs.datasette.io/en/stable/internals.html", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 718264811, "label": "Documentation for datasette.client"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1003#issuecomment-706272322", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1003", "id": 706272322, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjI3MjMyMg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T16:14:56Z", "updated_at": "2020-10-09T16:14:56Z", "author_association": "OWNER", "body": "Yes I think that makes sense. I added `json` to the template context in Dogsheep Beta just a few days ago because I needed that: https://github.com/dogsheep/dogsheep-beta/blob/bed9df2b3ef68189e2e445427721a28f4e9b4887/dogsheep_beta/__init__.py#L176", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 718238967, "label": "from_json jinja2 filter"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1003#issuecomment-706273211", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1003", "id": 706273211, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjI3MzIxMQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T16:16:38Z", "updated_at": "2020-10-09T16:16:38Z", "author_association": "OWNER", "body": "I'm not a huge fan of `from_json` as the name for this. Some other options:\r\n\r\n- Expose `json` directly so templates can do `json.loads()` and `json.dumps()` - this allows for outputting JSON too, which is useful. But is there anything else on the `json` module that shouldn't be exposed in templates?\r\n- `json_dumps()` and `json_loads()` template functions. I quite like that.\r\n- Something else?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 718238967, "label": "from_json jinja2 filter"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1007#issuecomment-706276831", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1007", "id": 706276831, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjI3NjgzMQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T16:23:51Z", "updated_at": "2020-10-09T16:23:51Z", "author_association": "OWNER", "body": "I don't appear to be using these anywhere, not sure why I spotted a warning (which I now can't find).", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 718272593, "label": "set-env and add-path commands have been deprecated"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1003#issuecomment-706281451", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1003", "id": 706281451, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjI4MTQ1MQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T16:33:01Z", "updated_at": "2020-10-09T16:33:01Z", "author_association": "OWNER", "body": "I think `json_dumps()` and `json_loads()` as aliases for `json.dumps()` and `json.loads()` is the way to go here.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 718238967, "label": "from_json jinja2 filter"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1003#issuecomment-706302863", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1003", "id": 706302863, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjMwMjg2Mw==", "user": {"value": 649467, "label": "mhalle"}, "created_at": "2020-10-09T17:17:06Z", "updated_at": "2020-10-09T17:17:06Z", "author_association": "NONE", "body": "I agree on the descriptive and python-consistent naming. There is already a tojson, but frankly i find the \"to\" and \"from\" confusing in a text templating language where what's a string and what's data isn't 100% transparent.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 718238967, "label": "from_json jinja2 filter"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1006#issuecomment-706305601", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1006", "id": 706305601, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjMwNTYwMQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T17:22:31Z", "updated_at": "2020-10-09T17:22:31Z", "author_association": "OWNER", "body": "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": 718264811, "label": "Documentation for datasette.client"}, "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} {"html_url": "https://github.com/simonw/datasette/issues/1002#issuecomment-706306214", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1002", "id": 706306214, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjMwNjIxNA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-10-09T17:23:51Z", "updated_at": "2020-10-09T17:23:51Z", "author_association": "OWNER", "body": "I can start by combining the release notes for the 0.50 alphas.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 717783692, "label": "Release notes for Datasette 0.50"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1008#issuecomment-706383750", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1008", "id": 706383750, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjM4Mzc1MA==", "user": {"value": 22429695, "label": "codecov[bot]"}, "created_at": "2020-10-09T20:17:29Z", "updated_at": "2020-10-09T20:17:29Z", "author_association": "NONE", "body": "# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1008?src=pr&el=h1) Report\n> Merging [#1008](https://codecov.io/gh/simonw/datasette/pull/1008?src=pr&el=desc) into [main](https://codecov.io/gh/simonw/datasette/commit/1bdbc8aa7f4fd7a768d456146e44da86cb1b36d1?el=desc) will **increase** coverage by `0.00%`.\n> The diff coverage is `100.00%`.\n\n[![Impacted file tree graph](https://codecov.io/gh/simonw/datasette/pull/1008/graphs/tree.svg?width=650&height=150&src=pr&token=eSahVY7kw1)](https://codecov.io/gh/simonw/datasette/pull/1008?src=pr&el=tree)\n\n```diff\n@@ Coverage Diff @@\n## main #1008 +/- ##\n=======================================\n Coverage 84.55% 84.56% \n=======================================\n Files 28 28 \n Lines 3878 3880 +2 \n=======================================\n+ Hits 3279 3281 +2 \n Misses 599 599 \n```\n\n\n| [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1008?src=pr&el=tree) | Coverage \u0394 | |\n|---|---|---|\n| [datasette/app.py](https://codecov.io/gh/simonw/datasette/pull/1008/diff?src=pr&el=tree#diff-ZGF0YXNldHRlL2FwcC5weQ==) | `96.35% <100.00%> (+0.01%)` | :arrow_up: |\n\n------\n\n[Continue to review full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1008?src=pr&el=continue).\n> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)\n> `\u0394 = absolute (impact)`, `\u00f8 = not affected`, `? = missing data`\n> Powered by [Codecov](https://codecov.io/gh/simonw/datasette/pull/1008?src=pr&el=footer). Last update [1bdbc8a...4085898](https://codecov.io/gh/simonw/datasette/pull/1008?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 718395987, "label": "Add json_loads and json_dumps jinja2 filters"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-706413753", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 706413753, "node_id": "MDEyOklzc3VlQ29tbWVudDcwNjQxMzc1Mw==", "user": {"value": 173848, "label": "yozlet"}, "created_at": "2020-10-09T21:41:12Z", "updated_at": "2020-10-09T21:41:12Z", "author_association": "NONE", "body": "If you don't mind a somewhat bonkers idea: how about a JS client-side plugin capability that allows any user looking at a Datasette site to pull in external plugins for data manipulation, even if the Datasette owner hasn't added them? (Yes, this may be _much_ too ambitious. If you're remotely interested, maybe fork this discussion to a different issue.) \r\n\r\nThis is some fascinating reading about what JS sandboxing looks like these days:\r\nhttps://www.figma.com/blog/how-we-built-the-figma-plugin-system/", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null}