Skip to content

Commit

Permalink
aio.web: Add app
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Northey <ryan@synca.io>
  • Loading branch information
phlax committed Sep 5, 2023
1 parent 8a8f4f6 commit 94cc2d9
Show file tree
Hide file tree
Showing 20 changed files with 397 additions and 0 deletions.
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 = 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.*
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

0 comments on commit 94cc2d9

Please sign in to comment.