In Blacksheep, there is such a POST request (in the Login(Controller) class)
#post('/login')
async def login(self, request: Request) -> Response:
data = await request.form()
response = self.redirect('/')
response.set_cookie(Cookie('log', 'log'))
return response
and GET which accepts a redirect (in the Home(Controller) class)
#get('/')
def index(self, request: Request):
print(request.cookies.values())
return self view('forums')
Console outputs dict_values([]).
Why aren't cookies being sent? I also searched in the documentation, but found nothing.
Related
I have an issue in a personal project. I have developed a class to encapsulate an Http client which use aiohttp so it uses async code.
I have several classes (related to different REST services) that use this client by composition.
In every method of the service classes, I call the http client.
The code of the Http client looks like (I have simplified for the example):
class HttpClientSession:
"""
This class is used to interfacing Http server.
"""
def __init__(
self,
http_url: str,
auth: Optional[BasicAuth] = None,
):
"""
Constructs the Http client.
:param http_url: URL to connect to Http server.
:param auth: Authentication to connect to server.
:raises Exception: If URL is empty.
"""
logger.debug("Create Http client")
if not http_url:
raise Exception("Http URL is invalid")
self._url: str = http_url
#: Server authentication
self._auth: Optional[BasicAuth] = auth
#: HTTP session
self._session: Optional[ClientSession] = None
logger.debug("Http client created")
async def __aenter__(self):
"""
Create Http session to send requests.
"""
self._session = ClientSession(
auth=self._auth,
raise_for_status=True,
connector=TCPConnector(limit=50),
)
return self
async def __aexit__(self, *err):
"""
Close Http session.
"""
await self._session.close()
self._session = None
async def get(self, suffix_url: str, headers: dict, query: Any = None):
"""
Send a GET request.
:param suffix_url: Last part of the URL contains the request.
:param headers: Header for the request.
:param query: Query of the request.
:return: Response.
"""
async with self._session.get(
url=self._url + suffix_url,
headers=headers,
params=query,
) as response:
return await response.json()
class HttpClient:
"""
This class is used to interfacing Http server.
"""
def __init__(
self,
http_url: str,
auth: Optional[BasicAuth] = None,
):
"""
Constructs the Http client.
:param http_url: URL to connect to Http server.
:param auth: Authentication to connect to server.
:raises Exception: If URL is empty.
"""
logger.debug("Create Http client")
#: HTTP session
self._session: HttpClientSession = HttpClientSession(http_url, auth)
logger.debug("Http client created")
async def get(self, suffix_url: str, headers: dict, query: Any = None):
"""
Send a GET request.
:param suffix_url: Last part of the URL contains the request.
:param headers: Header for the request.
:param query: Query of the request.
:return: Response.
"""
async with self._session as session:
return await session.get(suffix_url,headers, query)
To be more efficient, I would like to be able to reuse an Http Session that's why I have created a session class which permit me to use the async with syntax by the caller but if it is not needed, the caller can call directly the HttpClient method which create a dedicate session.
So it is convenient because I can write:
http_client = HttpClient(...)
http_client.get(...)
or
http_client = HttpClient(...)
async with http_client as http_session:
http_session.get(...)
http_session.get(...)
So great it works but now my issue is I would like to do the same for user of services classes to be also able to reuse session or not. I'm a bit stuck but my intention is to have this syntax also :
client = ServiceClient(...)
client.do_something(...) # do_something will call http_client.get(...)
or
client = ServiceClient(...)
async with client as session:
session.do_something(...) # do_something will call http_session.get(...)
session.do_something_else(...) # do_something_else will reuse http_session
but I don't want to do every time:
client = ServiceClient(...)
async with client as session:
session.do_something(...) # session is not reuse so I don't want boilerplate of async with
I have tried to define __aenter__ method but I haven't found an elegant way to avoid duplication of code.
Have you some ideas ?
I have tried to add the same pattern used in HttpClient and HttpClientSession classes in services classes but haven't succeed to have ServiceClient call HttpClient and ServiceClientSession call HttpClientSession.
And in fact I'm even not sure it is the better pattern to do that.
I have found one solution.
For the HttpClient I have refactor to remove the HttpClientSession class and manage optional session:
class HttpClient:
"""
This class is used to interfacing Http server.
"""
def __init__(
self,
http_url: str,
auth: Optional[BasicAuth] = None,
):
"""
Constructs the Http client.
:param http_url: URL to connect to Http server.
:param auth: Authentication to connect to server.
:raises Exception: If URL is empty.
"""
logger.debug("Create Http client")
if not http_url:
raise Exception("Http URL is invalid")
self._url: str = http_url
#: Server authentication
self._auth: Optional[BasicAuth] = auth
#: HTTP session
self._session: Optional[ClientSession] = None
logger.debug("Http client created")
async def __aenter__(self):
"""
Create Http session to send requests.
"""
self._session = ClientSession(
auth=self._auth,
raise_for_status=True,
connector=TCPConnector(limit=50),
)
return self
async def __aexit__(self, *err):
"""
Close Http session.
"""
await self._session.close()
self._session = None
async def get(self, suffix_url: str, headers: dict, query: Any = None):
"""
Send a GET request.
:param suffix_url: Last part of the URL contains the request.
:param headers: Header for the request.
:param query: Query of the request.
:return: Response.
"""
if self._session:
async with self._session.get(
url=self._url + suffix_url,
headers=headers,
params=query,
) as response:
return await response.json()
else:
async with ClientSession(
auth=self._auth,
raise_for_status=True,
connector=TCPConnector(limit=50),
) as session:
async with session.get(
url=self._url + suffix_url,
headers=headers,
params=query,
) as response:
return await response.json()
And in service class, I have defined also the methods to support async with syntax:
async def __aenter__(self):
"""
Create session to send requests.
"""
await self._http_client.__aenter__()
return self
async def __aexit__(self, *err):
"""
Close session.
"""
await self._http_client.__aexit__(*err)
So it works but I don't know if there is a better way to do it because I'm not fan of the if-else statement.
Hopefully this is not a too stupid question, but I am having trouble with aiohttp cookie processing.
Aiohttp's CookieJar class mentions it implements cookie storage adhering to RFC 6265, which states that:
cookies for a given host are shared across all the ports on that host
Cookies do not provide isolation by port. If a cookie is readable by a service running on one port, the cookie is also readable by a service running on another port of the same server.
But if I create two aiohttp servers, one that makes you "login" and gives you a cookie back, and another one with an endpoint that expects you to have a cookie, both hosted on localhost (two different ports I guess), the cookie will not be processed.
Here's a set of 4 tests using aiohttp, pytest, pytest and pytest-aiohttp to explain:
import functools
import pytest
from aiohttp import web
pytestmark = pytest.mark.asyncio
def attach_session(f):
#functools.wraps(f)
async def wrapper(request: web.Request):
session_id = request.cookies.get("testcookie")
request["mysession"] = session_id
response = await f(request)
response.set_cookie("testcookie", session_id)
return response
return wrapper
def is_logged_in(f):
#functools.wraps(f)
#attach_session
async def wrapper(request: web.Request):
session = request["mysession"]
if not session:
raise web.HTTPUnauthorized
return await f(request)
return wrapper
async def login(_: web.Request):
response = web.Response()
response.set_cookie("testcookie", "somerandomstring")
return response
#is_logged_in
async def some_endpoint(request: web.Request):
return web.Response(text="sweet")
#pytest.fixture
def auth_client(event_loop, aiohttp_client):
app = web.Application()
app.router.add_post("/login", login)
return event_loop.run_until_complete(aiohttp_client(app))
#pytest.fixture
def core_client(event_loop, aiohttp_client):
app = web.Application()
app.router.add_get("/some_endpoint", some_endpoint)
return event_loop.run_until_complete(aiohttp_client(app))
async def test_login(auth_client):
resp = await auth_client.post("/login")
assert resp.status == 200
assert resp.cookies.get("testcookie").value == "somerandomstring"
async def test_some_endpoint_anonymous(core_client):
resp = await core_client.get("/some_endpoint")
assert resp.status == 401
async def test_some_endpoint_as_logged_in(auth_client, core_client):
resp1 = await auth_client.post("/login")
resp2 = await core_client.get("/some_endpoint", cookies=resp1.cookies)
assert resp2.status == 401
async def test_some_endpoint_as_logged_in_again(auth_client, core_client):
resp1 = await auth_client.post("/login")
_cookie = list(resp1.cookies.values())[0]
resp2 = await core_client.get(
"/some_endpoint", cookies={_cookie.key: _cookie.value}
)
assert resp2.status == 200
But from my understanding, the "test_some_endpoint_as_logged_in" test should work. Why is it returning 401, while the same thing but with sending the cookie as a dict returns 200?
I think the correct way of sharing the cookies between clients would be loading the SimpleCookie object of the resp1 to the core_client.session.cookie_jar.
Changing the code of the test_some_endpoint_as_logged_in to should fix it:
async def test_some_endpoint_as_logged_in(auth_client, core_client):
resp1 = await auth_client.post("/login")
core_client.session.cookie_jar.update_cookies(resp1.cookies)
resp2 = await core_client.get("/some_endpoint")
assert resp2.status == 401
Cookie data is kept in the session object as the auth_client and core_client are different sessions with there own data cookie data is not shared. It is comparable to using a different browser with each there own cookie_jar.
I am trying to implement a type of custom authentication by using aiohttp something like the example in this link but I also need request body. Here is an example for requests:
class CustomAuth(AuthBase):
def __init__(self, secretkey):
self.secretkey = secretkey
def get_hash(self, request):
if request.body:
data = request.body.decode('utf-8')
else:
data = "{}"
signature = hmac.new(
str.encode(self.secretkey),
msg=str.encode(data),
digestmod=hashlib.sha256
).hexdigest().upper()
return signature
def __call__(self, request):
request.headers["CUSTOM-AUTH"] = self.get_hash(request)
return request
I've looked into tracing and BasicAuth but they are useless in my situation. On on_request_start request body is not ready, on on_request_chunk_sent headers have already been sent. A solution like BasicAuth don't have access the request data at all.
Do you have any idea?
Thanks in advance.
I am new to pytest and working on to test the code. How ever the test result showing failed.
**main.py**
#app.post("/loginsuccess/", response_class=HTMLResponse)
async def login_success(request: Request, username: str = Form(...), password: str = Form(...)):
p = await User_Pydantic.from_tortoise_orm(await User.get(username=username, password=password))
json_compatible_item_data = jsonable_encoder(p)
if json_compatible_item_data is not None:
logger.info("Logged in Successfully")
return templates.TemplateResponse("homepage.html", {"request": request, "username":username})
else:
status_code:int
status_code = 500
logger.error("Invalid Credentials")
return templates.TemplateResponse("index.html", {"request":request, "status_code":status_code})
**test_main.py**
def test_login_success():
response = client.get('/loginsuccess/', json={'username': 'sheik', 'password':'abdullah'})
assert response.status_code==200
The test result below
================================================================== short test summary info ==================================================================
FAILED test_main.py::test_login_success - assert 405 == 200
actually, the response you are getting from the server is 405, not 200. That is why your assertion is failing.
HTTP 405 is Method Not Allowed response status code indicates that the request method is known by the server but is not supported by the target resource.
Just make sure that the server/backend handles /loginsuccess/ as a Post request
It looks that your request has invalid arguments or the backend is not configured the right way.
you also need to pass data in the form of json object,
import json
foo = {'username': 'sheik', 'password':'abdullah'}
json_data = json.dumps(foo)
then pass this data to client.post()
I am new to FastAPI framework, I want to print out the response. For example, in Django:
#api_view(['POST'])
def install_grandservice(req):
print(req.body)
And in FastAPI:
#app.post('/install/grandservice')
async def login():
//print out req
I tried to to like this
#app.post('/install/grandservice')
async def login(req):
print(req.body)
But I received this error: 127.0.0.1:52192 - "POST /install/login HTTP/1.1" 422 Unprocessable Entity
Please help me :(
Here is an example that will print the content of the Request for fastAPI.
It will print the body of the request as a json (if it is json parsable) otherwise print the raw byte array.
async def print_request(request):
print(f'request header : {dict(request.headers.items())}' )
print(f'request query params : {dict(request.query_params.items())}')
try :
print(f'request json : {await request.json()}')
except Exception as err:
# could not parse json
print(f'request body : {await request.body()}')
#app.post("/printREQUEST")
async def create_file(request: Request):
try:
await print_request(request)
return {"status": "OK"}
except Exception as err:
logging.error(f'could not print REQUEST: {err}')
return {"status": "ERR"}
You can define a parameter with a Request type in the router function, as
from fastapi import FastAPI, Request
app = FastAPI()
#app.post('/install/grandservice')
async def login(request: Request):
print(request)
return {"foo": "bar"}
This is also covered in the doc, under Use the Request object directly section