Skip to content

Commit 34821d1

Browse files
committed
Merge branch 'main' of github.com:NZX/extra
2 parents 32bf3d9 + 859b8b6 commit 34821d1

File tree

9 files changed

+156
-101
lines changed

9 files changed

+156
-101
lines changed

src/py/extra/client.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import NamedTuple, ClassVar, AsyncGenerator
1+
from typing import NamedTuple, ClassVar, AsyncGenerator, Self, Any, Iterator
22
from urllib.parse import quote_plus, urlparse
33
from contextvars import ContextVar
44
from contextlib import contextmanager
@@ -13,9 +13,10 @@
1313
HTTPRequestBody,
1414
HTTPBodyBlob,
1515
HTTPAtom,
16+
HTTPProcessingStatus,
1617
headername,
1718
)
18-
from .http.parser import HTTPParser, HTTPProcessingStatus
19+
from .http.parser import HTTPParser
1920

2021

2122
# --
@@ -105,7 +106,7 @@ class Connection:
105106
# A streaming connection won't be reused
106107
isStreaming: bool = False
107108

108-
def close(self):
109+
def close(self) -> Self:
109110
"""Closes the writer."""
110111
self.writer.close()
111112
self.until = None
@@ -116,7 +117,7 @@ def isValid(self) -> bool | None:
116117
"""Tells if the connection is still valid."""
117118
return (time.monotonic() <= self.until) if self.until else None
118119

119-
def touch(self):
120+
def touch(self) -> Self:
120121
"""Touches the connection, bumping its `until` time."""
121122
self.until = time.monotonic() + self.idle
122123
return self
@@ -166,7 +167,9 @@ async def Make(
166167
class ConnectionPool:
167168
"""Context-aware pool of connections."""
168169

169-
All: ClassVar[ContextVar] = ContextVar("httpConnectionsPool")
170+
All: ClassVar[ContextVar[list["ConnectionPool"]]] = ContextVar(
171+
"httpConnectionsPool"
172+
)
170173

171174
@classmethod
172175
def Get(cls, *, idle: float | None = None) -> "ConnectionPool":
@@ -232,10 +235,9 @@ def Push(cls, *, idle: float | None = None) -> "ConnectionPool":
232235
return pool
233236

234237
@classmethod
235-
def Pop(cls):
238+
def Pop(cls) -> "ConnectionPool|None":
236239
pools = cls.All.get(None)
237-
if pools:
238-
pools.pop().release()
240+
return pools.pop().release() if pools else None
239241

240242
def __init__(self, idle: float | None = None):
241243
self.connections: dict[ConnectionTarget, list[Connection]] = {}
@@ -263,7 +265,7 @@ def put(self, connection: Connection) -> None:
263265
as long as it is valid."""
264266
self.connections.setdefault(connection.target, []).append(connection)
265267

266-
def clean(self):
268+
def clean(self) -> Self:
267269
"""Cleans idle connections by closing them and removing them
268270
from available connections."""
269271
to_remove = []
@@ -279,14 +281,15 @@ def clean(self):
279281
del self.connections[k]
280282
return self
281283

282-
def release(self):
284+
def release(self) -> Self:
283285
"""Releases all the connections registered"""
284286
for l in self.connections.values():
285287
while l:
286288
l.pop().close()
287289
self.connections.clear()
290+
return self
288291

289-
def pop(self):
292+
def pop(self) -> Self:
290293
"""Pops this pool from the connection pool context and release
291294
all its connections."""
292295
pools = ConnectionPool.All.get(None)
@@ -295,10 +298,10 @@ def pop(self):
295298
self.release()
296299
return self
297300

298-
def __enter__(self):
301+
def __enter__(self) -> Self:
299302
return self
300303

301-
def __exit__(self, type, value, traceback):
304+
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
302305
"""The connection pool automatically cleans when used
303306
as a context manager."""
304307
self.clean()
@@ -518,7 +521,7 @@ async def Request(
518521

519522

520523
@contextmanager
521-
def pooling(idle: float | None = None):
524+
def pooling(idle: float | None = None) -> Iterator[ConnectionPool]:
522525
"""Creates a context in which connections will be pooled."""
523526
pool = ConnectionPool().Push(idle=idle)
524527
try:
@@ -529,7 +532,7 @@ def pooling(idle: float | None = None):
529532

530533
if __name__ == "__main__":
531534

532-
async def main():
535+
async def main() -> None:
533536
async for atom in HTTPClient.Request(
534537
host="google.com",
535538
method="GET",

src/py/extra/decorators.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from typing import ClassVar, Union, Callable, NamedTuple, TypeVar, Any
1+
from typing import ClassVar, Union, Callable, NamedTuple, TypeVar, Any, cast
22

33
T = TypeVar("T")
44

55

66
class Transform(NamedTuple):
77
"""Represents a transformation to be applied to a request handler"""
88

9-
transform: Callable
9+
transform: Callable[..., Any]
1010
args: tuple[Any, ...]
1111
kwargs: dict[str, Any]
1212

@@ -29,12 +29,12 @@ class Extra:
2929
Annotations: ClassVar[dict[int, dict[str, Any]]] = {}
3030

3131
@staticmethod
32-
def Meta(scope: Any, *, strict: bool = False) -> dict:
32+
def Meta(scope: Any, *, strict: bool = False) -> dict[str, Any]:
3333
"""Returns the dictionary of meta attributes for the given value."""
3434
if isinstance(scope, type):
3535
if not hasattr(scope, "__extra__"):
3636
setattr(scope, "__extra__", {})
37-
return getattr(scope, "__extra__")
37+
return cast(dict[str, Any], getattr(scope, "__extra__"))
3838
else:
3939
if hasattr(scope, "__dict__"):
4040
return scope.__dict__
@@ -48,7 +48,7 @@ def Meta(scope: Any, *, strict: bool = False) -> dict:
4848

4949

5050
def on(
51-
priority=0, **methods: Union[str, list[str], tuple[str, ...]]
51+
priority: int = 0, **methods: Union[str, list[str], tuple[str, ...]]
5252
) -> Callable[[T], T]:
5353
"""The @on decorator is one of the main important things you will use within
5454
Retro. This decorator allows to wrap an existing method and indicate that
@@ -104,7 +104,11 @@ class Expose(NamedTuple):
104104

105105

106106
def expose(
107-
priority=0, compress=False, contentType=None, raw=False, **methods
107+
priority: int = 0,
108+
compress: bool = False,
109+
contentType: str | None = None,
110+
raw: bool = False,
111+
**methods: str,
108112
) -> Callable[[T], T]:
109113
"""The @expose decorator is a variation of the @on decorator. The @expose
110114
decorator allows you to _expose_ an existing Python function as a JavaScript
@@ -145,36 +149,36 @@ def decorator(function: T) -> T:
145149
return decorator
146150

147151

148-
def when(*predicates):
152+
def when(*predicates: Callable[..., bool]) -> Callable[..., T]:
149153
"""The @when(...) decorate allows to specify that the wrapped method will
150154
only be executed when the given predicate (decorated with `@on`)
151155
succeeds."""
152156

153-
def decorator(function):
157+
def decorator(function: T, *args: Any, **kwargs: Any) -> T:
154158
v = Extra.Meta(function).setdefault(Extra.WHEN, [])
155159
v.extend(predicates)
156160
return function
157161

158162
return decorator
159163

160164

161-
def pre(transform: Callable) -> Callable[[T], T]:
165+
def pre(transform: Callable[..., bool]) -> Callable[..., T]:
162166
"""Registers the given `transform` as a pre-processing step of the
163167
decorated function."""
164168

165-
def decorator(function: T, *args, **kwargs) -> T:
169+
def decorator(function: T, *args: Any, **kwargs: Any) -> T:
166170
v = Extra.Meta(function).setdefault(Extra.PRE, [])
167171
v.append(Transform(transform, args, kwargs))
168172
return function
169173

170174
return decorator
171175

172176

173-
def post(transform) -> Callable[[T], T]:
177+
def post(transform: Callable[..., bool]) -> Callable[[T], T]:
174178
"""Registers the given `transform` as a post-processing step of the
175179
decorated function."""
176180

177-
def decorator(function: T, *args, **kwargs) -> T:
181+
def decorator(function: T, *args: Any, **kwargs: Any) -> T:
178182
v = Extra.Meta(function).setdefault(Extra.POST, [])
179183
v.append(Transform(transform, args, kwargs))
180184
return function

src/py/extra/handler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def Create(
108108
return payload
109109

110110
@staticmethod
111-
def AsRequest(event: dict) -> HTTPRequest:
111+
def AsRequest(event: dict[str, Any]) -> HTTPRequest:
112112
body: bytes = (
113113
(
114114
b64decode(event["body"].encode())
@@ -237,13 +237,13 @@ def event(
237237
uri: str,
238238
headers: dict[str, str] | None = None,
239239
body: str | bytes | None = None,
240-
) -> dict:
240+
) -> dict[str, Any]:
241241
return AWSLambdaEvent.Create(method=method, uri=uri, headers=headers, body=body)
242242

243243

244244
def awslambda(
245245
handler: Callable[[HTTPRequest], HTTPResponse | Coroutine[Any, HTTPResponse, Any]]
246-
):
246+
) -> Callable[[dict[str, Any], dict[str, Any] | None], dict[str, Any]]:
247247
def wrapper(
248248
event: dict[str, Any], context: dict[str, Any] | None = None
249249
) -> dict[str, Any]:

src/py/extra/http/api.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pathlib import Path
44

55
from base64 import b64encode
6+
67
from .status import HTTP_STATUS
78
from ..utils.json import json
89
from ..utils.files import contentType
@@ -41,7 +42,7 @@ def error(
4142
content: str | None = None,
4243
contentType: str = "text/plain",
4344
headers: dict[str, str] | None = None,
44-
):
45+
) -> T:
4546
message = HTTP_STATUS.get(status, "Server Error")
4647
return self.respond(
4748
content=message if content is None else content,
@@ -54,32 +55,36 @@ def error(
5455
def notAuthorized(
5556
self,
5657
content: str = "Unauthorized",
57-
contentType="text/plain",
58+
contentType: str = "text/plain",
5859
*,
5960
status: int = 403,
60-
):
61+
) -> T:
6162
return self.error(status, content=content, contentType=contentType)
6263

6364
def notFound(
64-
self, content: str = "Not Found", contentType="text/plain", *, status: int = 404
65-
):
65+
self,
66+
content: str = "Not Found",
67+
contentType: str = "text/plain",
68+
*,
69+
status: int = 404,
70+
) -> T:
6671
return self.error(status, content=content, contentType=contentType)
6772

68-
def notModified(self):
69-
pass
73+
def notModified(self) -> None:
74+
raise NotImplementedError
7075

7176
def fail(
7277
self,
7378
content: str | None = None,
7479
*,
7580
status: int = 500,
7681
contentType: str = "text/plain",
77-
):
82+
) -> T:
7883
return self.respondError(
7984
content=content, status=status, contentType=contentType
8085
)
8186

82-
def redirect(self, url: str, permanent: bool = False):
87+
def redirect(self, url: str, permanent: bool = False) -> T:
8388
# SEE: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
8489
return self.respondEmpty(
8590
status=301 if permanent else 302, headers={"Location": str(url)}
@@ -92,7 +97,7 @@ def returns(
9297
*,
9398
status: int = 200,
9499
contentType: str = "application/json",
95-
):
100+
) -> T:
96101
if isinstance(value, bytes):
97102
try:
98103
value = value.decode("ascii")
@@ -110,20 +115,22 @@ def returns(
110115
def respondText(
111116
self,
112117
content: str | bytes | Iterator[str | bytes],
113-
contentType="text/plain",
118+
contentType: str = "text/plain",
114119
status: int = 200,
115-
):
120+
) -> T:
116121
return self.respond(content=content, contentType=contentType, status=status)
117122

118-
def respondHTML(self, html: str | bytes | Iterator[str | bytes], status: int = 200):
123+
def respondHTML(
124+
self, html: str | bytes | Iterator[str | bytes], status: int = 200
125+
) -> T:
119126
return self.respond(content=html, contentType="text/html", status=status)
120127

121128
def respondFile(
122129
self,
123130
path: Path | str,
124131
headers: dict[str, str] | None = None,
125132
status: int = 200,
126-
):
133+
) -> T:
127134
# TODO: We should have a much more detailed file handling, supporting ranges, etags, etc.
128135
p: Path = path if isinstance(path, Path) else Path(path)
129136
content_type: str = contentType(p)
@@ -141,10 +148,10 @@ def respondError(
141148
contentType: str = "text/plain",
142149
*,
143150
status: int = 500,
144-
):
151+
) -> T:
145152
return self.error(status, content, contentType)
146153

147-
def respondEmpty(self, status, headers: dict[str, str] | None = None):
154+
def respondEmpty(self, status: int, headers: dict[str, str] | None = None) -> T:
148155
return self.respond(content=None, status=status, headers=headers)
149156

150157

src/py/extra/model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def process(
151151
else:
152152
return self.onRouteNotFound(request)
153153

154-
def mount(self, service: Service, prefix: str | None = None):
154+
def mount(self, service: Service, prefix: str | None = None) -> Service:
155155
if service.isMounted:
156156
raise RuntimeError(
157157
f"Cannot mount service, it is already mounted: {service}"
@@ -160,7 +160,7 @@ def mount(self, service: Service, prefix: str | None = None):
160160
self.dispatcher.register(handler, prefix or service.prefix)
161161
return service
162162

163-
def unmount(self, service: Service):
163+
def unmount(self, service: Service) -> Service:
164164
if not service.isMounted:
165165
raise RuntimeError(
166166
f"Cannot unmount service, it is not already mounted: {service}"
@@ -172,7 +172,7 @@ def unmount(self, service: Service):
172172
service.app = self
173173
return service
174174

175-
def onRouteNotFound(self, request: HTTPRequest):
175+
def onRouteNotFound(self, request: HTTPRequest) -> HTTPResponse:
176176
return request.notFound()
177177

178178

0 commit comments

Comments
 (0)