From 3a0017d2e50537463ab7392f5e5eb33c54e172b6 Mon Sep 17 00:00:00 2001 From: Ryan Northey Date: Mon, 4 Sep 2023 22:14:07 +0100 Subject: [PATCH] `aio.web`: Add app Signed-off-by: Ryan Northey --- README.md | 15 +++++++ aio.web/BUILD | 2 + aio.web/README.rst | 5 +++ aio.web/VERSION | 1 + aio.web/aio/web/BUILD | 18 +++++++++ aio.web/aio/web/__init__.py | 14 +++++++ aio.web/aio/web/abstract/__init__.py | 7 ++++ aio.web/aio/web/abstract/downloader.py | 43 ++++++++++++++++++++ aio.web/aio/web/downloader.py | 14 +++++++ aio.web/aio/web/exceptions.py | 7 ++++ aio.web/aio/web/interface.py | 20 ++++++++++ aio.web/aio/web/py.typed | 0 aio.web/aio/web/typing.py | 0 aio.web/setup.cfg | 55 ++++++++++++++++++++++++++ aio.web/setup.py | 5 +++ aio.web/tests/BUILD | 10 +++++ aio.web/tests/test_downloader.py | 36 +++++++++++++++++ aio.web/tests/test_interface.py | 12 ++++++ 18 files changed, 264 insertions(+) create mode 100644 aio.web/BUILD create mode 100644 aio.web/README.rst create mode 100644 aio.web/VERSION create mode 100644 aio.web/aio/web/BUILD create mode 100644 aio.web/aio/web/__init__.py create mode 100644 aio.web/aio/web/abstract/__init__.py create mode 100644 aio.web/aio/web/abstract/downloader.py create mode 100644 aio.web/aio/web/downloader.py create mode 100644 aio.web/aio/web/exceptions.py create mode 100644 aio.web/aio/web/interface.py create mode 100644 aio.web/aio/web/py.typed create mode 100644 aio.web/aio/web/typing.py create mode 100644 aio.web/setup.cfg create mode 100644 aio.web/setup.py create mode 100644 aio.web/tests/BUILD create mode 100644 aio.web/tests/test_downloader.py create mode 100644 aio.web/tests/test_interface.py diff --git a/README.md b/README.md index 496b5bf82..192371a1c 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,21 @@ pypi: https://pypi.org/project/aio.run.runner --- +#### [aio.web](aio.web) + +version: 0.1.0.dev0 + +pypi: https://pypi.org/project/aio.web + +##### requirements: + +- [abstracts](https://pypi.org/project/abstracts) >=0.0.12 +- [aiohttp](https://pypi.org/project/aiohttp) +- [pyyaml](https://pypi.org/project/pyyaml) + +--- + + #### [dependatool](dependatool) version: 0.2.3.dev0 diff --git a/aio.web/BUILD b/aio.web/BUILD new file mode 100644 index 000000000..b42c30a6c --- /dev/null +++ b/aio.web/BUILD @@ -0,0 +1,2 @@ + +toolshed_package("aio.web") diff --git a/aio.web/README.rst b/aio.web/README.rst new file mode 100644 index 000000000..3e6f2e53f --- /dev/null +++ b/aio.web/README.rst @@ -0,0 +1,5 @@ + +aio.web +======= + +Web utils for asyncio. diff --git a/aio.web/VERSION b/aio.web/VERSION new file mode 100644 index 000000000..0d4d12494 --- /dev/null +++ b/aio.web/VERSION @@ -0,0 +1 @@ +0.1.0-dev diff --git a/aio.web/aio/web/BUILD b/aio.web/aio/web/BUILD new file mode 100644 index 000000000..a454af073 --- /dev/null +++ b/aio.web/aio/web/BUILD @@ -0,0 +1,18 @@ + +toolshed_library( + "aio.web", + dependencies=[ + "//deps:reqs#abstracts", + "//deps:reqs#aiohttp", + "//deps:reqs#pyyaml", + ], + sources=[ + "__init__.py", + "abstract/__init__.py", + "abstract/downloader.py", + "downloader.py", + "exceptions.py", + "interface.py", + "typing.py", + ], +) diff --git a/aio.web/aio/web/__init__.py b/aio.web/aio/web/__init__.py new file mode 100644 index 000000000..e74b3e282 --- /dev/null +++ b/aio.web/aio/web/__init__.py @@ -0,0 +1,14 @@ + +from .abstract import ( + ADownloader, + AChecksumDownloader) +from .interface import ( + IDownloader, + IChecksumDownloader) + + +__all__ = ( + "ADownloader", + "AChecksumDownloader", + "IDownloader", + "IChecksumDownloader") diff --git a/aio.web/aio/web/abstract/__init__.py b/aio.web/aio/web/abstract/__init__.py new file mode 100644 index 000000000..190a733ba --- /dev/null +++ b/aio.web/aio/web/abstract/__init__.py @@ -0,0 +1,7 @@ +from .downloader import ADownloader, AChecksumDownloader + + +__all__ = ( + "ADownloader", + "AChecksumDownloader", +) diff --git a/aio.web/aio/web/abstract/downloader.py b/aio.web/aio/web/abstract/downloader.py new file mode 100644 index 000000000..7635a26f8 --- /dev/null +++ b/aio.web/aio/web/abstract/downloader.py @@ -0,0 +1,43 @@ + +import hashlib + +import aiohttp + +import abstracts + +from aio.web import exceptions, interface + + +@abstracts.implementer(interface.IDownloader) +class ADownloader(metaclass=abstracts.Abstraction): + + def __init__(self, url: str) -> None: + self.url = url + + async def download(self) -> bytes: + """Download content from the interwebs.""" + async with aiohttp.ClientSession() as session: + async with session.get(self.url) as resp: + return await resp.content.read() + + +@abstracts.implementer(interface.IChecksumDownloader) +class AChecksumDownloader(ADownloader, metaclass=abstracts.Abstraction): + + def __init__(self, url: str, sha: str) -> None: + super().__init__(url) + self.sha = sha + + async def checksum(self, content: bytes) -> None: + """Download content from the interwebs.""" + # do this in a thread + m = hashlib.sha256() + m.update(content) + if m.digest().hex() != self.sha: + raise ChecksumError( + f"Bad checksum, {m.digest().hex()}, expected {self.sha}") + + async def download(self) -> bytes: + content = await super().download() + await self.checksum(content) + return content diff --git a/aio.web/aio/web/downloader.py b/aio.web/aio/web/downloader.py new file mode 100644 index 000000000..4f65e0ccd --- /dev/null +++ b/aio.web/aio/web/downloader.py @@ -0,0 +1,14 @@ + +import abstracts + +from aio.web import abstract + + +@abstracts.implementer(abstract.ADownloader) +class Downloader: + pass + + +@abstracts.implementer(abstract.AChecksumDownloader) +class ChecksumDownloader: + pass diff --git a/aio.web/aio/web/exceptions.py b/aio.web/aio/web/exceptions.py new file mode 100644 index 000000000..527b5fa70 --- /dev/null +++ b/aio.web/aio/web/exceptions.py @@ -0,0 +1,7 @@ + +class ChecksumError(Exception): + pass + + +class DownloadError(Exception): + pass diff --git a/aio.web/aio/web/interface.py b/aio.web/aio/web/interface.py new file mode 100644 index 000000000..d6a7a471a --- /dev/null +++ b/aio.web/aio/web/interface.py @@ -0,0 +1,20 @@ + +from aiohttp import web + +import abstracts + + +class IDownloader(metaclass=abstracts.Interface): + + @abstracts.interfacemethod + async def download(self) -> web.Response: + """Download content from the interwebs.""" + raise NotImplementedError + + +class IChecksumDownloader(IDownloader, metaclass=abstracts.Interface): + + @abstracts.interfacemethod + async def checksum(self, content: bytes) -> bool: + """Checksum some content.""" + raise NotImplementedError diff --git a/aio.web/aio/web/py.typed b/aio.web/aio/web/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/aio.web/aio/web/typing.py b/aio.web/aio/web/typing.py new file mode 100644 index 000000000..e69de29bb diff --git a/aio.web/setup.cfg b/aio.web/setup.cfg new file mode 100644 index 000000000..f12a65423 --- /dev/null +++ b/aio.web/setup.cfg @@ -0,0 +1,55 @@ +[metadata] +name = aio.web +version = file: VERSION +author = Ryan Northey +author_email = ryan@synca.io +maintainer = Ryan Northey +maintainer_email = ryan@synca.io +license = Apache Software License 2.0 +url = https://github.com/envoyproxy/toolshed/tree/main/aio.web +description = A collection of functional utils for asyncio +long_description = file: README.rst +classifiers = + Development Status :: 4 - Beta + Framework :: Pytest + Intended Audience :: Developers + Topic :: Software Development :: Testing + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: Implementation :: CPython + Operating System :: OS Independent + License :: OSI Approved :: Apache Software License + +[options] +python_requires = >=3.8 +py_modules = aio.web +packages = find_namespace: +install_requires = + abstracts>=0.0.12 + aiohttp + pyyaml + +[options.extras_require] +test = + pytest + pytest-asyncio + pytest-coverage + pytest-iters + pytest-patches +lint = flake8 +types = + mypy +publish = wheel + +[options.package_data] +* = py.typed + +[options.packages.find] +include = aio.* +exclude = + build.* + tests.* + dist.* diff --git a/aio.web/setup.py b/aio.web/setup.py new file mode 100644 index 000000000..1f6a64b9c --- /dev/null +++ b/aio.web/setup.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +from setuptools import setup # type:ignore + +setup() diff --git a/aio.web/tests/BUILD b/aio.web/tests/BUILD new file mode 100644 index 000000000..74b0cb056 --- /dev/null +++ b/aio.web/tests/BUILD @@ -0,0 +1,10 @@ + +toolshed_tests( + "aio.web", + dependencies=[ + "//deps:reqs#abstracts", + "//deps:reqs#aiohttp", + "//deps:reqs#pyyaml", + "//deps:reqs#pytest-asyncio", + ], +) diff --git a/aio.web/tests/test_downloader.py b/aio.web/tests/test_downloader.py new file mode 100644 index 000000000..57d9e0c53 --- /dev/null +++ b/aio.web/tests/test_downloader.py @@ -0,0 +1,36 @@ + +from aio.web import abstract, downloader + + +def test_downloader_constructor(patches): + args = tuple(f"ARG{i}" for i in range(0, 3)) + kwargs = {f"K{i}": f"V{i}" for i in range(0, 3)} + patched = patches( + "abstract.ADownloader.__init__", + prefix="aio.web.downloader") + + with patched as (m_super, ): + m_super.return_value = None + dl = downloader.Downloader(*args, **kwargs) + + assert isinstance(dl, abstract.ADownloader) + assert ( + m_super.call_args + == [args, kwargs]) + + +def test_checksum_downloader_constructor(patches): + args = tuple(f"ARG{i}" for i in range(0, 3)) + kwargs = {f"K{i}": f"V{i}" for i in range(0, 3)} + patched = patches( + "abstract.AChecksumDownloader.__init__", + prefix="aio.web.downloader") + + with patched as (m_super, ): + m_super.return_value = None + dl = downloader.ChecksumDownloader(*args, **kwargs) + + assert isinstance(dl, abstract.AChecksumDownloader) + assert ( + m_super.call_args + == [args, kwargs]) diff --git a/aio.web/tests/test_interface.py b/aio.web/tests/test_interface.py new file mode 100644 index 000000000..8680581da --- /dev/null +++ b/aio.web/tests/test_interface.py @@ -0,0 +1,12 @@ + +import pytest + +from aio import web + + +@pytest.mark.parametrize( + "interface", + [web.IDownloader, + web.IChecksumDownloader]) +async def test_interfaces(iface, interface): + await iface(interface).check()