Skip to content

Commit

Permalink
rewrite of request events (#286)
Browse files Browse the repository at this point in the history
* rewrite of "request" events

needed to simplify fix of #280.

RestApiUser uses locust.contrib.fasthttp.FastHttpSession for synchronous requests as well.

RequestLogger and RequestHandler is not inherited, but always added for users inheriting GrizzlyUser, and added as event listeners on the dedicated event hook.

A new GrizzlyEventHook, so let through all exceptions.

ResponseContextManager is only used with in RestApiUser._request, all events are fired from GrizzlyUser.request (final), so it's done the same for all users.

* fixed tests

* move the remains of grizzly.users.base to grizzly.users

remove SftpUser, never used and does not work as intended as it was.
remove unused interfaces, HttpRequests and FileRequests.

* make sure that load users always have the "global" metadata dictionary

and that any request tasks executed by a load user has a metadata dictionary that is the merged version of "global" metadata and any request specific metadata.

where the request specific metadata has precedence over the global one.

---------

Co-authored-by: boffman <[email protected]>
  • Loading branch information
mgor and boffman authored Nov 24, 2023
1 parent 05f6324 commit ab8b7e2
Show file tree
Hide file tree
Showing 62 changed files with 1,995 additions and 3,452 deletions.
1 change: 0 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
],
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": [
"-o testpaths=tests",
"--cov=.",
"--cov-report=xml:coverage.xml",
"tests/"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<img align="right" src="https://raw.githubusercontent.com/Biometria-se/grizzly/main/docs/content/assets/logo/grizzly_grasshopper_brown_256px.png" alt="grizzly logo">
<span>

###### Framework

![PyPI - License](https://img.shields.io/pypi/l/grizzly-loadtester?style=for-the-badge)
Expand Down Expand Up @@ -69,7 +70,6 @@ They are useful when history of test runs is needed, or when wanting to correlat
* `RestApiUser`: send requests to REST API endpoinds, supports authentication with username+password or client secret
* `ServiceBusUser`: send to and receive from Azure Service Bus queues and topics
* `MessageQueueUser`: send and receive from IBM MQ queues
* `SftpUser`: send and receive files from an SFTP-server
* `BlobStorageUser`: send and receive files to Azure Blob Storage
* `IotHubUser`: send/put files to Azure IoT Hub

Expand Down
24 changes: 19 additions & 5 deletions grizzly/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Generic, Literal, Optional, Tuple, Type, TypeVar, Union, cast
from urllib.parse import urlparse

from grizzly.tasks import RequestTask
from grizzly.types import GrizzlyResponse
from grizzly.utils import merge_dicts, safe_del

Expand Down Expand Up @@ -42,7 +43,7 @@ class AuthType(Enum):
class GrizzlyHttpAuthClient(Generic[P], metaclass=ABCMeta):
host: str
environment: Environment
headers: Dict[str, str]
metadata: Dict[str, Any]
cookies: Dict[str, str]
__context__: ClassVar[Dict[str, Any]] = {
'verify_certificates': True,
Expand Down Expand Up @@ -150,21 +151,34 @@ def refresh_token(client: GrizzlyHttpAuthClient, *args: P.args, **kwargs: P.kwar
auth_method = AuthMethod.NONE

if auth_method is not AuthMethod.NONE and client.session_started is not None:
request: Optional[RequestTask] = None
# look for RequestTask in args, we need to set metadata on it
for arg in args:
if isinstance(arg, RequestTask):
request = arg
break

session_now = time()
session_duration = session_now - client.session_started

# refresh token if session has been alive for at least refresh_time
if session_duration >= auth_context.get('refresh_time', 3000) or (client.headers.get('Authorization', None) is None and client.cookies == {}):
authorization_token = client.metadata.get('Authorization', None)
if session_duration >= auth_context.get('refresh_time', 3000) or (authorization_token is None and client.cookies == {}):
auth_type, secret = self.impl.get_token(client, auth_method)
client.session_started = time()
if auth_type == AuthType.HEADER:
client.headers.update({'Authorization': f'Bearer {secret}'})
header = {'Authorization': f'Bearer {secret}'}
client.metadata.update(header)
if request is not None:
request.metadata.update(header)
else:
name, value = secret.split('=', 1)
client.cookies.update({name: value})
elif authorization_token is not None and request is not None: # update current request with active authorization token
request.metadata.update({'Authorization': authorization_token})
else:
safe_del(client.headers, 'Authorization')
safe_del(client.headers, 'Cookie')
safe_del(client.metadata, 'Authorization')
safe_del(client.metadata, 'Cookie')

bound = func.__get__(client, client.__class__)

Expand Down
2 changes: 1 addition & 1 deletion grizzly/auth/aad.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ def get_oauth_token(cls, client: GrizzlyHttpAuthClient, pkcs: Optional[Tuple[str

# build generic header values, but remove stuff that shouldn't be part
# of authentication flow
headers = {**client.headers}
headers = {**client.metadata}
safe_del(headers, 'Authorization')
safe_del(headers, 'Content-Type')
safe_del(headers, 'Ocp-Apim-Subscription-Key')
Expand Down
139 changes: 0 additions & 139 deletions grizzly/clients.py

This file was deleted.

61 changes: 61 additions & 0 deletions grizzly/events/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Logic for grizzly specific events."""
from __future__ import annotations

from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Any, List, Optional

from locust.event import EventHook

if TYPE_CHECKING: # pragma: no cover
from grizzly.tasks import RequestTask
from grizzly.types import GrizzlyResponse
from grizzly.users import GrizzlyUser

class GrizzlyEventHook(EventHook):
"""Override locust.events.EventHook to get types, and not to catch any exceptions."""

_handlers: List[GrizzlyEventHandler]

def __init__(self) -> None:
self._handlers = []

def add_listener(self, handler: GrizzlyEventHandler) -> None:
super().add_listener(handler)

def remove_listener(self, handler: GrizzlyEventHandler) -> None:
super().remove_listener(handler)

def fire(self, *, reverse: bool = False, **kwargs: Any) -> None:
handlers = reversed(self._handlers) if reverse else self._handlers

for handler in handlers:
handler(**kwargs)


class GrizzlyEventHandler(metaclass=ABCMeta):
user: GrizzlyUser
event_hook: EventHook

def __init__(self, user: GrizzlyUser) -> None:
self.user = user
self.event_hook = user.event_hook

@abstractmethod
def __call__(
self,
name: str,
context: GrizzlyResponse,
request: RequestTask,
exception: Optional[Exception] = None,
**kwargs: Any,
) -> None:
...


from .request_logger import RequestLogger
from .response_handler import ResponseHandler

__all__ = [
'RequestLogger',
'ResponseHandler',
]
Loading

0 comments on commit ab8b7e2

Please sign in to comment.