: 1 add pages service 8 files changed, 443 insertions(+), 4 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~gotmax23/sourcehutx/patches/44010/mbox | git am -3Learn more about email & git
--- .reuse/dep5 | 2 +- fixtures/cassettes/test_pages_publish | 157 ++++++++++++++++++++++ fixtures/test_site.tar.gz | Bin 0 -> 449 bytes fixtures/test_site/index.html | 19 +++ noxfile.py | 9 +- src/sourcehut/client.py | 4 +- src/sourcehut/services/pages.py | 179 ++++++++++++++++++++++++++ tests/integration/test_pages.py | 77 +++++++++++ 8 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 fixtures/cassettes/test_pages_publish create mode 100644 fixtures/test_site.tar.gz create mode 100644 fixtures/test_site/index.html create mode 100644 src/sourcehut/services/pages.py create mode 100644 tests/integration/test_pages.py diff --git a/.reuse/dep5 b/.reuse/dep5 index 75c6734..dc65ca2 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,5 +1,5 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Files: fixtures/cassettes/* +Files: fixtures/* Copyright: None License: Unlicense diff --git a/fixtures/cassettes/test_pages_publish b/fixtures/cassettes/test_pages_publish new file mode 100644 index 0000000..8f0c81c --- /dev/null +++ b/fixtures/cassettes/test_pages_publish @@ -0,0 +1,157 @@ +interactions: +- request: + body: '{"query": "\n query {\n me {\n username\n }\n }\n ", + "variables": {}}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '119' + content-type: + - application/json + host: + - pages.sr.ht + user-agent: + - python-httpx/0.24.1 + method: POST + uri: https://pages.sr.ht/query + response: + content: '{"data":{"me":{"username":"gotmax23-test"}}}' + headers: + Content-Length: + - '44' + Content-Type: + - application/json + Date: + - Sun, 13 Aug 2023 01:58:42 GMT + http_version: HTTP/1.1 + status_code: 200 +- request: + body: !!binary | + LS0wNjYwMTFkMjkxNjI5OWFiNGQ3OTg3ZjdkZjYzZTg4Yw0KQ29udGVudC1EaXNwb3NpdGlvbjog + Zm9ybS1kYXRhOyBuYW1lPSJvcGVyYXRpb25zIg0KDQp7InF1ZXJ5IjogIlxuICAgICAgICBtdXRh + dGlvbiBwdWJsaXNoKFxuICAgICAgICAgICAgJGRvbWFpbjogU3RyaW5nIVxuICAgICAgICAgICAg + JGZpbGU6IFVwbG9hZCFcbiAgICAgICAgICAgICRwcm90b2NvbDogUHJvdG9jb2whXG4gICAgICAg + ICAgICAkc3ViZGlyZWN0b3J5OiBTdHJpbmdcbiAgICAgICAgICAgICRzaXRlQ29uZmlnOiBTaXRl + Q29uZmlnXG4gICAgICAgICkge1xuICAgICAgICAgICAgcHVibGlzaChcbiAgICAgICAgICAgICAg + ICBkb21haW46ICRkb21haW5cbiAgICAgICAgICAgICAgICBjb250ZW50OiAkZmlsZVxuICAgICAg + ICAgICAgICAgIHByb3RvY29sOiAkcHJvdG9jb2xcbiAgICAgICAgICAgICAgICBzdWJkaXJlY3Rv + cnk6ICRzdWJkaXJlY3RvcnlcbiAgICAgICAgICAgICAgICBzaXRlQ29uZmlnOiAkc2l0ZUNvbmZp + Z1xuICAgICAgICAgICAgKSB7XG4gICAgICAgICAgICAgICAgaWRcbmRvbWFpblxucHJvdG9jb2xc + bmNyZWF0ZWRcbnVwZGF0ZWRcbnZlcnNpb25cbm5vdEZvdW5kXG5cbiAgICAgICAgICAgIH1cbiAg + ICAgICAgfVxuXG4gICAgICAgICIsICJ2YXJpYWJsZXMiOiB7ImRvbWFpbiI6ICJnb3RtYXgyMy10 + ZXN0LnNyaHQuc2l0ZSIsICJwcm90b2NvbCI6ICJIVFRQUyIsICJzdWJkaXJlY3RvcnkiOiAicHl0 + aG9uLXNvdXJjZWh1dC10ZXN0IiwgInNpdGVDb25maWciOiB7Im5vdEZvdW5kIjogbnVsbCwgImZp + bGVDb25maWdzIjogbnVsbH19fQ0KLS0wNjYwMTFkMjkxNjI5OWFiNGQ3OTg3ZjdkZjYzZTg4Yw0K + Q29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJtYXAiDQoNCnsiMCI6IFsidmFy + aWFibGVzLmZpbGUiXX0NCi0tMDY2MDExZDI5MTYyOTlhYjRkNzk4N2Y3ZGY2M2U4OGMNCkNvbnRl + bnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0iMCI7IGZpbGVuYW1lPSJ1cGxvYWQiDQpD + b250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL29jdGV0LXN0cmVhbQ0KDQofiwgIDC7YZAL/c2l0ZS50 + YXIA7dVba9swFABgP/tXqHnZky3bsZ00KKbQCy2MLdAUtkc1PokEvgTphCYv++2Tq972sO1htLD2 + fBhknSMs+XLkmMf8ZCH3lyBrMMGrSLzftUkyzp/Ph3iaZGkWsH3wBnYWpXHTBx9TNmUt6hbmaXmc + TqdFMSnirEzzcnwcBuTd010N+1hh27zeHENRl/l9jaeTInnZ3iuTIkjzvCyTzPUmrv6zfDIOWPKW + 9b/psZV/2HH+lv9PiaMoCq8XZ9+iC93Aab89GL1RuIQ9ztiXvgOf/KxX0FmIrmroUK81mBm76Rof + DaOoCkNxdPb1dPl9cc6Gr6kKhXK/lCpkjmgBJVspaSzgfHSzvIimo4cUamyguoSm6dldb5pacB8K + BfdXELd9fXgYrdJfh7q+T2x9O1gqbZk7JEOwyO7g1mqE+Cl/hUN2Z6Fm6948hYVkysB6PlKIWzvj + 3BpXF/yHf/HZmNt+Z1agdshHFSpg1499tjig6ju2arR7Op+s4PJ5NbpD2BiJ2g0Y1mP9QgTfDnfo + b422IUIIIYQQQgghhBBCCCGEEEIIIYT8g5//ep60ACgAAA0KLS0wNjYwMTFkMjkxNjI5OWFiNGQ3 + OTg3ZjdkZjYzZTg4Yy0tDQo= + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '1556' + content-type: + - multipart/form-data; boundary=066011d2916299ab4d7987f7df63e88c + host: + - pages.sr.ht + user-agent: + - python-httpx/0.24.1 + method: POST + uri: https://pages.sr.ht/query + response: + content: '{"data":{"publish":{"id":171768,"domain":"gotmax23-test.srht.site","protocol":"https","created":"2023-08-13T01:58:15.991942Z","updated":"2023-08-13T01:58:15.991942Z","version":"cd1ecccdff2e4c1497d7996091d76cf1","notFound":null}}}' + headers: + Content-Length: + - '229' + Content-Type: + - application/json + Date: + - Sun, 13 Aug 2023 01:58:42 GMT + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"query": "\n query listSites(\n $cursor: Cursor\n ) + {\n sites(cursor: $cursor) {\n cursor\n results + {\n id\ndomain\nprotocol\ncreated\nupdated\nversion\nnotFound\n\n }\n }\n }\n ", + "variables": {"cursor": null}}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '337' + content-type: + - application/json + host: + - pages.sr.ht + user-agent: + - python-httpx/0.24.1 + method: POST + uri: https://pages.sr.ht/query + response: + content: '{"data":{"sites":{"cursor":null,"results":[{"id":171768,"domain":"gotmax23-test.srht.site","protocol":"https","created":"2023-08-13T01:58:15.991942Z","updated":"2023-08-13T01:58:15.991942Z","version":"cd1ecccdff2e4c1497d7996091d76cf1","notFound":null}]}}}' + headers: + Content-Length: + - '255' + Content-Type: + - application/json + Date: + - Sun, 13 Aug 2023 01:58:42 GMT + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"query": "\n mutation unpublish(\n $domain: String!\n $protocol: + Protocol!\n ) {\n unpublish(\n domain: $domain\n protocol: + $protocol\n ) {\n id\ndomain\nprotocol\ncreated\nupdated\nversion\nnotFound\n\n }\n }\n ", + "variables": {"domain": "gotmax23-test.srht.site", "protocol": "HTTPS"}}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '416' + content-type: + - application/json + host: + - pages.sr.ht + user-agent: + - python-httpx/0.24.1 + method: POST + uri: https://pages.sr.ht/query + response: + content: '{"data":{"unpublish":{"id":171768,"domain":"gotmax23-test.srht.site","protocol":"","created":"2023-08-13T01:58:15.991942Z","updated":"2023-08-13T01:58:15.991942Z","version":"cd1ecccdff2e4c1497d7996091d76cf1","notFound":null}}}' + headers: + Content-Length: + - '226' + Content-Type: + - application/json + Date: + - Sun, 13 Aug 2023 01:58:42 GMT + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/fixtures/test_site.tar.gz b/fixtures/test_site.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c45da8545c022dce9ac218d07d911748bd1338cb GIT binary patch literal 449 zcmV;y0Y3g8iwFn|F4$xO|8r?{WiE7KaschsTWi}e6aZj9`&X!W*^@2Xv7Iz1rqBy5 zj4jX<wnsHSi3Gj`>4YXP`|Xpf-uAHVVYIOJd>CY%BP{uH<e10&WFhyLAZswet4O@x zwMjD0pU;PQnq+Afuzm0bb{3^^(+3|@Hf1g97UpSroKB}jF)8A#O!IPl90ugw(_Ibw zSYg||-W)?wm-&5-r;{Ri+1@LYB1rSRER)RjCa(Uod@>GL@|N|#>9Li6*m3@r|4)gd zC<?C^XWya=-N0$TJs7=FxP(2<*q6QopZu&>&{>GCYUog}HJBN@=^7smqo@i)adh_e zboqU8&W5k43WajNlqzJ7XaOZ#DQPU=d~~zCh^C{56l!dsx`d|b*<Ej%T5ul<1@~7F zw|#wh7_HJ@hq}H`Y<)XeDs7oNBtx*s?%>vHg!n0cg+q2{p=RseJcWg1%D{R)QixkS z<J=lo7yO5R@!6Q$erHyocF0E+Dqz=-eYQBD>N~b-w0k<Atl;u_HM&FC7>U{q7}jI` rLImFqcj#}8wjn|YA%qY@2qA<JLI@#*5JCtcg#3e_|9YOZ04M+e1i9QJ literal 0 HcmV?d00001 diff --git a/fixtures/test_site/index.html b/fixtures/test_site/index.html new file mode 100644 index 0000000..6937791 --- /dev/null +++ b/fixtures/test_site/index.html @@ -0,0 +1,19 @@ +<!-- +SPDX-FileCopyrightText: None +SPDX-License-Identifier: Unlicense +--> + +<!DOCTYPE html> +<head> + <meta charset="UTF-8"> + <title>Hello world</title> +</head> +<body> + <h1>Hello world</h1> + <p> + This is a test website. + It is used for + <a href="https://sr.ht/~gotmax23/sourcehut/">the Sourcehut Python client's</a> + integration tests. + </p> +</body> diff --git a/noxfile.py b/noxfile.py index 48c0d6a..825400c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -45,7 +45,14 @@ def git(session: nox.Session, *args, **kwargs): @nox.session(python=["3.8", "3.9", "3.10", "3.11"], tags=["test"]) def test(session: nox.Session): - install(session, ".[test]", editable=True) + install( + session, + ".[test]", + # httpx: use surrogateescape to allow for binary data + # https://github.com/kevin1024/vcrpy/pull/760 + "git+https://github.com/gotmax23/vcrpy.git@binary_httpx", + editable=True, + ) args = list(session.posargs) tmp = Path(session.create_tmp()) diff --git a/src/sourcehut/client.py b/src/sourcehut/client.py index 2621c07..c86bdb0 100644 --- a/src/sourcehut/client.py +++ b/src/sourcehut/client.py @@ -127,7 +127,7 @@ class SrhtClient: service: SRHT_SERVICE, query: str, variables: dict[str, Any], - filename: StrPath, + filename: StrPath | None, content: str | bytes | IO[bytes], *, extra_params: dict[str, Any] | None = None, @@ -143,7 +143,7 @@ class SrhtClient: resp = await self.http_client.post( endpoint, data=data, - files={"0": (str(filename), content)}, + files={"0": (str(filename), content) if filename is not None else content}, headers=headers, follow_redirects=True, **extra_params, diff --git a/src/sourcehut/services/pages.py b/src/sourcehut/services/pages.py new file mode 100644 index 0000000..0aa06fa --- /dev/null +++ b/src/sourcehut/services/pages.py @@ -0,0 +1,179 @@ +# Copyright (C) 2023 Maxwell G <maxwell@gtmx.me> +# SPDX-License-Identifier: MIT +# ruff: noqa: ARG002 + +""" +pages.sr.ht API +""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from dataclasses import dataclass +from datetime import datetime as DT +from enum import Enum +from typing import IO, Any, Dict, List, Optional, Union + +from pydantic import Field, validator + +from .._utils import check_found as _cf +from .._utils import get_locals +from ..client import SRHT_SERVICE +from ._base import _Resource, _ServiceClient + +_SITE_REF_MEMBERS = """\ +id +domain +protocol +""" + +_SITE_MEMBERS = ( + _SITE_REF_MEMBERS + + """\ +created +updated +version +notFound +""" +) + + +class SITE_PROTOCOL(str, Enum): + HTTPS = "https" + GEMINI = "gemini" + + def __str__(self) -> str: + return self.value + + +@dataclass() +class FileConfig: + glob: str + cache_control: Optional[str] = None + + def get_dict(self) -> Dict[str, Any]: + return {"glob": self.glob, "options": {"cacheControl": self.cache_control}} + + +@dataclass() +class _SiteConfig: + not_found: Optional[str] = None + file_configs: Optional[List[FileConfig]] = None + + def get_dict(self) -> Dict[str, Any]: + return { + "notFound": self.not_found, + "fileConfigs": [config.get_dict() for config in self.file_configs] + if self.file_configs is not None + else None, + } + + +class PagesSrhtClient(_ServiceClient): + SERVICE = SRHT_SERVICE.PAGES + + async def publish( + self, + domain: str, + content: Union[bytes, IO[bytes]], + protocol: SITE_PROTOCOL = SITE_PROTOCOL.HTTPS, + subdirectory: Optional[str] = None, + not_found: Optional[str] = None, + file_configs: Optional[List[FileConfig]] = None, + ) -> Site: + inp = get_locals(**locals()) + inp["siteConfig"] = _SiteConfig( + inp.pop("not_found"), inp.pop("file_configs") + ).get_dict() + inp["protocol"] = str(inp["protocol"]).upper() + del inp["content"] + query = ( + """ + mutation publish( + $domain: String! + $file: Upload! + $protocol: Protocol! + $subdirectory: String + $siteConfig: SiteConfig + ) { + publish( + domain: $domain + content: $file + protocol: $protocol + subdirectory: $subdirectory + siteConfig: $siteConfig + ) { + %s + } + } + + """ + % _SITE_MEMBERS + ) + data = await self.client._query_with_upload( + self.SERVICE, query, inp, None, content + ) + return Site(**_cf(data["publish"]), client=self) + + async def unpublish( + self, domain: str, protocol: SITE_PROTOCOL = SITE_PROTOCOL.HTTPS + ) -> Site: + inp = get_locals(**locals()) + inp["protocol"] = str(inp["protocol"]).upper() + query = ( + """ + mutation unpublish( + $domain: String! + $protocol: Protocol! + ) { + unpublish( + domain: $domain + protocol: $protocol + ) { + %s + } + } + """ + % _SITE_MEMBERS + ) + data = await self.query(query, inp) + return Site(**_cf(data["unpublish"]), client=self) + + def list_sites(self, *, max_pages: Optional[int] = 1) -> AsyncIterator[Site]: + query = ( + """ + query listSites( + $cursor: Cursor + ) { + sites(cursor: $cursor) { + cursor + results { + %s + } + } + } + """ + % _SITE_MEMBERS + ) + return self._cursorit("sites", Site, query, max_pages) + + +class SiteRef(_Resource[PagesSrhtClient]): + domain: str + protocol: SITE_PROTOCOL + + @validator("protocol", pre=True) + def _v_site_ref(cls, value: Any) -> Any: + if value == "": # noqa: PLC1901 # Other falsy values should cause an error + value = SITE_PROTOCOL.HTTPS + return value + + +class Site(SiteRef): + created: DT + updated: DT + version: str + not_found: Optional[str] = Field(None, alias="notFound") + + async def unpublish(self) -> Site: + return await self._client.unpublish(self.domain, self.protocol) diff --git a/tests/integration/test_pages.py b/tests/integration/test_pages.py new file mode 100644 index 0000000..adbcc7a --- /dev/null +++ b/tests/integration/test_pages.py @@ -0,0 +1,77 @@ +# Copyright (C) 2023 Maxwell G <maxwell@gtmx.me> +# SPDX-License-Identifier: MIT + +""" +pages.sr.ht API +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + +from sourcehut.client import SrhtClient +from sourcehut.services import pages + +from .. import ROOT, vcr + + +@pytest.mark.asyncio +@pytest.mark.xfail( + reason="The cassettes contain binary data" + " and require a patched vcrpy to handle them" +) +@vcr.use_cassette +async def test_pages_publish(authed_client: SrhtClient, tmp_path: Path): + async with authed_client: + client = pages.PagesSrhtClient(authed_client) + content = (ROOT / "fixtures/test_site.tar.gz").read_bytes() + whoami = await client.whoami() + domain = f"{whoami}.srht.site" + site = await client.publish( + f"{whoami}.srht.site", + content, + subdirectory="python-sourcehut-test", + ) + try: + expected = pages.Site( + id=site.id, + client=client, + domain=domain, + protocol=pages.SITE_PROTOCOL.HTTPS, + created=site.created, + updated=site.updated, + version=site.version, + notFound=None, + ) + assert site == expected + + gotten = [s async for s in client.list_sites()] + assert site in gotten + finally: + await site.unpublish() + + +@pytest.mark.parametrize( + "config, expected", + [ + pytest.param( + pages._SiteConfig(None, None), {"notFound": None, "fileConfigs": None} + ), + pytest.param( + pages._SiteConfig("abc.html", []), + {"notFound": "abc.html", "fileConfigs": []}, + ), + pytest.param( + pages._SiteConfig("abc.html", [pages.FileConfig("*", "no-cache")]), + { + "notFound": "abc.html", + "fileConfigs": [{"glob": "*", "options": {"cacheControl": "no-cache"}}], + }, + ), + ], +) +def test_pages_site_config(config: pages._SiteConfig, expected: dict[str, Any]): + assert config.get_dict() == expected -- 2.41.0
Applied.
builds.sr.ht <builds@sr.ht>sourcehut/patches: FAILED in 12m19s [add pages service][0] from [Maxwell G][1] [0]: https://lists.sr.ht/~gotmax23/sourcehut/patches/43547 [1]: mailto:maxwell@gtmx.me ✗ #1040546 FAILED sourcehut/patches/mockbuild-epel9.yml.disabled https://builds.sr.ht/~gotmax23/job/1040546 ✓ #1040545 SUCCESS sourcehut/patches/main.yml https://builds.sr.ht/~gotmax23/job/1040545 ✓ #1040547 SUCCESS sourcehut/patches/mockbuild.yml https://builds.sr.ht/~gotmax23/job/1040547