Skip to content

Commit a232f83

Browse files
committed
aio.web: Add app
Signed-off-by: Ryan Northey <[email protected]>
1 parent 8a8f4f6 commit a232f83

File tree

20 files changed

+396
-0
lines changed

20 files changed

+396
-0
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@ pypi: https://pypi.org/project/aio.run.runner
120120
---
121121

122122

123+
#### [aio.web](aio.web)
124+
125+
version: 0.1.0.dev0
126+
127+
pypi: https://pypi.org/project/aio.web
128+
129+
##### requirements:
130+
131+
- [abstracts](https://pypi.org/project/abstracts) >=0.0.12
132+
- [aiohttp](https://pypi.org/project/aiohttp)
133+
- [pyyaml](https://pypi.org/project/pyyaml)
134+
135+
---
136+
137+
123138
#### [dependatool](dependatool)
124139

125140
version: 0.2.3.dev0

aio.web/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
toolshed_package("aio.web")

aio.web/README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
aio.web
3+
=======
4+
5+
Web utils for asyncio.

aio.web/VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.0-dev

aio.web/aio/web/BUILD

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
toolshed_library(
3+
"aio.web",
4+
dependencies=[
5+
"//deps:reqs#abstracts",
6+
"//deps:reqs#aiohttp",
7+
"//deps:reqs#pyyaml",
8+
],
9+
sources=[
10+
"__init__.py",
11+
"abstract/__init__.py",
12+
"abstract/downloader.py",
13+
"abstract/repository.py",
14+
"downloader.py",
15+
"exceptions.py",
16+
"interface.py",
17+
"repository.py",
18+
"typing.py",
19+
],
20+
)

aio.web/aio/web/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
from .abstract import (
3+
ADownloader,
4+
AChecksumDownloader,
5+
ARepositoryMirrors,
6+
ARepositoryRequest)
7+
from .interface import (
8+
IDownloader,
9+
IChecksumDownloader,
10+
IRepositoryMirrors,
11+
IRepositoryRequest)
12+
13+
14+
__all__ = (
15+
"ADownloader",
16+
"AChecksumDownloader",
17+
"ARepositoryRequest",
18+
"IDownloader",
19+
"IChecksumDownloader",
20+
"IRepositoryMirrors",
21+
"IRepositoryRequest")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .downloader import ADownloader, AChecksumDownloader
2+
from .repository import ARepositoryRequest, ARepositoryMirrors
3+
4+
5+
__all__ = (
6+
"ADownloader",
7+
"AChecksumDownloader",
8+
"ARepositoryMirrors",
9+
"ARepositoryRequest")
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
import hashlib
3+
4+
import aiohttp
5+
6+
import abstracts
7+
8+
from aio.web import exceptions, interface
9+
10+
11+
@abstracts.implementer(interface.IDownloader)
12+
class ADownloader(metaclass=abstracts.Abstraction):
13+
14+
def __init__(self, url: str) -> None:
15+
self.url = url
16+
17+
async def download(self) -> bytes:
18+
"""Download content from the interwebs."""
19+
async with aiohttp.ClientSession() as session:
20+
async with session.get(self.url) as resp:
21+
return await resp.content.read()
22+
23+
24+
@abstracts.implementer(interface.IChecksumDownloader)
25+
class AChecksumDownloader(ADownloader, metaclass=abstracts.Abstraction):
26+
27+
def __init__(self, url: str, sha: str) -> None:
28+
super().__init__(url)
29+
self.sha = sha
30+
31+
async def checksum(self, content: bytes) -> None:
32+
"""Download content from the interwebs."""
33+
# do this in a thread
34+
m = hashlib.sha256()
35+
m.update(content)
36+
if m.digest().hex() != self.sha:
37+
raise ChecksumError(
38+
f"Bad checksum, {m.digest().hex()}, expected {self.sha}")
39+
40+
async def download(self) -> bytes:
41+
content = await super().download()
42+
await self.checksum(content)
43+
return content
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
2+
import hashlib
3+
import pathlib
4+
import re
5+
from functools import cached_property
6+
7+
import yaml
8+
9+
import aiohttp
10+
from aiohttp import web
11+
12+
import abstracts
13+
14+
from aio.web import exceptions, interface
15+
16+
17+
@abstracts.implementer(interface.IRepositoryRequest)
18+
class ARepositoryRequest(metaclass=abstracts.Abstraction):
19+
20+
def __init__(self, url, config, request):
21+
self._url = url
22+
self.config = config
23+
self.request = request
24+
25+
@property
26+
def requested_repo(self):
27+
return f"{self.request.match_info['owner']}/{self.request.match_info['repo']}"
28+
29+
@property
30+
def url(self) -> str:
31+
return f"https://{self._url}/{self.requested_repo}/{self.path}"
32+
33+
@property
34+
def path(self):
35+
return self.matched["path"]
36+
37+
@property
38+
def sha(self):
39+
return self.matched["sha"]
40+
41+
@cached_property
42+
def matched(self) -> dict:
43+
for repo in self.config:
44+
if not re.match(repo, self.requested_repo):
45+
continue
46+
47+
for path, sha in self.config[repo].items():
48+
if path == self.request.match_info["extra"]:
49+
return dict(path=path, sha=sha)
50+
return {}
51+
52+
@property # type: ignore
53+
@abstracts.interfacemethod
54+
def downloader_class(self):
55+
raise NotImplementedError
56+
57+
async def fetch(self):
58+
content = await self.downloader_class(self.url, self.sha).download()
59+
response = web.Response(body=content)
60+
response.headers["cache-control"] = "max-age=31536000"
61+
return response
62+
63+
def match(self):
64+
if not self.matched:
65+
raise exceptions.MatchError()
66+
return self
67+
68+
69+
@abstracts.implementer(interface.IRepositoryMirrors)
70+
class ARepositoryMirrors(metaclass=abstracts.Abstraction):
71+
72+
def __init__(self, config_path):
73+
self.config_path = config_path
74+
75+
@cached_property
76+
def config(self):
77+
return yaml.safe_load(pathlib.Path(self.config_path).read_text())
78+
79+
@property # type: ignore
80+
@abstracts.interfacemethod
81+
def request_class(self):
82+
raise NotImplementedError
83+
84+
async def match(self, request):
85+
host = request.match_info['host']
86+
if host not in self.config:
87+
raise exceptions.MatchError()
88+
upstream_request = self.request_class(host, self.config[host], request)
89+
return upstream_request.match()

aio.web/aio/web/downloader.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
import abstracts
3+
4+
from aio.web import abstract
5+
6+
7+
@abstracts.implementer(abstract.ADownloader)
8+
class Downloader:
9+
pass
10+
11+
12+
@abstracts.implementer(abstract.AChecksumDownloader)
13+
class ChecksumDownloader:
14+
pass

0 commit comments

Comments
 (0)