Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] aio.web: Add app #810

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions aio.web/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

toolshed_package("aio.web")
5 changes: 5 additions & 0 deletions aio.web/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

aio.web
=======

Web utils for asyncio.
1 change: 1 addition & 0 deletions aio.web/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0-dev
20 changes: 20 additions & 0 deletions aio.web/aio/web/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

toolshed_library(
"aio.web",
dependencies=[
"//deps:reqs#abstracts",
"//deps:reqs#aiohttp",
"//deps:reqs#pyyaml",
],
sources=[
"__init__.py",
"abstract/__init__.py",
"abstract/downloader.py",
"abstract/repository.py",
"downloader.py",
"exceptions.py",
"interface.py",
"repository.py",
"typing.py",
],
)
22 changes: 22 additions & 0 deletions aio.web/aio/web/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

from .abstract import (
ADownloader,
AChecksumDownloader,
ARepositoryMirrors,
ARepositoryRequest)
from .interface import (
IDownloader,
IChecksumDownloader,
IRepositoryMirrors,
IRepositoryRequest)


__all__ = (
"ADownloader",
"AChecksumDownloader",
"ARepositoryMirrors",
"ARepositoryRequest",
"IDownloader",
"IChecksumDownloader",
"IRepositoryMirrors",
"IRepositoryRequest")
9 changes: 9 additions & 0 deletions aio.web/aio/web/abstract/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .downloader import ADownloader, AChecksumDownloader
from .repository import ARepositoryRequest, ARepositoryMirrors


__all__ = (
"ADownloader",
"AChecksumDownloader",
"ARepositoryMirrors",
"ARepositoryRequest")
43 changes: 43 additions & 0 deletions aio.web/aio/web/abstract/downloader.py
Original file line number Diff line number Diff line change
@@ -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 exceptions.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
89 changes: 89 additions & 0 deletions aio.web/aio/web/abstract/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@

import pathlib
import re
from functools import cached_property

import yaml

from aiohttp import web

import abstracts

from aio.web import exceptions, interface


@abstracts.implementer(interface.IRepositoryRequest)
class ARepositoryRequest(metaclass=abstracts.Abstraction):

def __init__(self, url, config, request):
self._url = url
self.config = config
self.request = request

@property
def requested_repo(self):
return (
f"{self.request.match_info['owner']}"
f"/{self.request.match_info['repo']}")

@property
def url(self) -> str:
return f"https://{self._url}/{self.requested_repo}/{self.path}"

@property
def path(self):
return self.matched["path"]

@property
def sha(self):
return self.matched["sha"]

@cached_property
def matched(self) -> dict:
for repo in self.config:
if not re.match(repo, self.requested_repo):
continue

for path, sha in self.config[repo].items():
if path == self.request.match_info["extra"]:
return dict(path=path, sha=sha)
return {}

@property # type: ignore
@abstracts.interfacemethod
def downloader_class(self):
raise NotImplementedError

async def fetch(self):
content = await self.downloader_class(self.url, self.sha).download()
response = web.Response(body=content)
response.headers["cache-control"] = "max-age=31536000"
return response

def match(self):
if not self.matched:
raise exceptions.MatchError()
return self


@abstracts.implementer(interface.IRepositoryMirrors)
class ARepositoryMirrors(metaclass=abstracts.Abstraction):

def __init__(self, config_path):
self.config_path = config_path

@cached_property
def config(self):
return yaml.safe_load(pathlib.Path(self.config_path).read_text())

@property # type: ignore
@abstracts.interfacemethod
def request_class(self):
raise NotImplementedError

async def match(self, request):
host = request.match_info['host']
if host not in self.config:
raise exceptions.MatchError()
upstream_request = self.request_class(host, self.config[host], request)
return upstream_request.match()
14 changes: 14 additions & 0 deletions aio.web/aio/web/downloader.py
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions aio.web/aio/web/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

class ChecksumError(Exception):
pass


class DownloadError(Exception):
pass


class MatchError(Exception):
pass
28 changes: 28 additions & 0 deletions aio.web/aio/web/interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

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


class IRepositoryRequest(metaclass=abstracts.Interface):
pass


class IRepositoryMirrors(metaclass=abstracts.Interface):
pass
Empty file added aio.web/aio/web/py.typed
Empty file.
20 changes: 20 additions & 0 deletions aio.web/aio/web/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

import abstracts

from aio.web import abstract, downloader


@abstracts.implementer(abstract.ARepositoryRequest)
class RepositoryRequest:

@property
def downloader_class(self):
return downloader.ChecksumDownloader


@abstracts.implementer(abstract.ARepositoryMirrors)
class RepositoryMirrors:

@property
def request_class(self):
return RepositoryRequest
Empty file added aio.web/aio/web/typing.py
Empty file.
55 changes: 55 additions & 0 deletions aio.web/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[metadata]
name = aio.web
version = file: VERSION
author = Ryan Northey
author_email = [email protected]
maintainer = Ryan Northey
maintainer_email = [email protected]
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.*
5 changes: 5 additions & 0 deletions aio.web/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python

from setuptools import setup # type:ignore

setup()
10 changes: 10 additions & 0 deletions aio.web/tests/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

toolshed_tests(
"aio.web",
dependencies=[
"//deps:reqs#abstracts",
"//deps:reqs#aiohttp",
"//deps:reqs#pyyaml",
"//deps:reqs#pytest-asyncio",
],
)
Loading