xy-signalrcore


Namexy-signalrcore JSON
Version 1.0.1 PyPI version JSON
download
home_pagehttps://github.com/colin-chang/signalrcore
SummaryA Python SignalR Core client(json and messagepack), with invocation auth and two way streaming. Compatible with azure / serverless functions. Also with automatic reconnect and manually reconnect.
upload_time2024-04-10 10:14:56
maintainerNone
docs_urlNone
authorColin Chang
requires_pythonNone
licenseNone
keywords signalr core client
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI
coveralls test coverage No coveralls.
            # SignalR core client
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?logo=paypal&style=flat-square)](https://www.paypal.me/mandrewcito/1)
![Pypi](https://img.shields.io/pypi/v/signalrcore.svg)
[![Downloads](https://pepy.tech/badge/signalrcore/month)](https://pepy.tech/project/signalrcore/month)
[![Downloads](https://pepy.tech/badge/signalrcore)](https://pepy.tech/project/signalrcore)
![Issues](https://img.shields.io/github/issues/mandrewcito/signalrcore.svg)
![Open issues](https://img.shields.io/github/issues-raw/mandrewcito/signalrcore.svg)
![codecov.io](https://codecov.io/github/mandrewcito/signalrcore/coverage.svg?branch=master)

![logo alt](https://raw.githubusercontent.com/mandrewcito/signalrcore/master/docs/img/logo_temp.128.svg.png)

# 改进
在[源库](https://pypi.org/project/signalrcore/)基础上修正了如下问题以兼容新版`signalR(.NET 6.0)`:
1. 补充了`negotiateVersion=1` 参数
2. 使用`signalR`链接`connectToken`参数替换`connectionId`
3. `hubConnection`扩展`state`属性表示连接状态,扩展`ready`属性表示是否已连接

# Links 

* [Dev to posts with library examples and implementation](https://dev.to/mandrewcito/singlar-core-python-client-58e7)

* [Pypi](https://pypi.org/project/signalrcore/)

* [Wiki - This Doc](https://mandrewcito.github.io/signalrcore/)

# Develop

Test server will be avaiable in [here](https://github.com/mandrewcito/signalrcore-containertestservers) and docker compose is required.

```bash
git clone https://github.com/mandrewcito/signalrcore-containertestservers
cd signalrcore-containertestservers
docker-compose up
cd ../signalrcore
make tests
```

## Known Issues

Issues related with closing sockets are inherited from the websocket-client library. Due to these problems i can't update the library to versions higher than websocket-client 0.54.0. 
I'm working to solve it but for now its patched (Error number 1. Raises an exception, and then exception is treated for prevent errors). 
If I update the websocket library I fall into error number 2, on local machine I can't reproduce it but travis builds fail (sometimes and randomly :()
* [1. Closing socket error](https://github.com/slackapi/python-slackclient/issues/171)
* [2. Random errors closing socket](https://github.com/websocket-client/websocket-client/issues/449)

# A Tiny How To

## Connect to a server without auth

```python
hub_connection = HubConnectionBuilder()\
    .with_url(server_url)\
    .configure_logging(logging.DEBUG)\
    .with_automatic_reconnect({
        "type": "raw",
        "keep_alive_interval": 10,
        "reconnect_interval": 5,
        "max_attempts": 5
    }).build()
```
## Connect to a server with auth

login_function must provide auth token

```python
hub_connection = HubConnectionBuilder()\
            .with_url(server_url,
            options={
                "access_token_factory": login_function,
                "headers": {
                    "mycustomheader": "mycustomheadervalue"
                }
            })\
            .configure_logging(logging.DEBUG)\
            .with_automatic_reconnect({
                "type": "raw",
                "keep_alive_interval": 10,
                "reconnect_interval": 5,
                "max_attempts": 5
            }).build()
```
### Unauthorized errors
A login function must provide an error controller if authorization fails. When connection starts, if authorization fails exception will be propagated.

```python
    def login(self):
        response = requests.post(
            self.login_url,
            json={
                "username": self.email,
                "password": self.password
                },verify=False)
        if response.status_code == 200:
            return response.json()["token"]
        raise requests.exceptions.ConnectionError()

    hub_connection.start()   # this code will raise  requests.exceptions.ConnectionError() if auth fails
```
## Configure logging

```python
HubConnectionBuilder()\
    .with_url(server_url,
    .configure_logging(logging.DEBUG)
    ...
```
## Configure socket trace
```python 
HubConnectionBuilder()\
    .with_url(server_url,
    .configure_logging(logging.DEBUG, socket_trace=True) 
    ... 
 ```
## Configure your own handler
```python
 import logging
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
hub_connection = HubConnectionBuilder()\
    .with_url(server_url, options={"verify_ssl": False}) \
    .configure_logging(logging.DEBUG, socket_trace=True, handler=handler)
    ...
 ```
## Configuring reconnection
After reaching max_attempts an exeption will be thrown and on_disconnect event will be fired.
```python
hub_connection = HubConnectionBuilder()\
    .with_url(server_url)\
    ...
    .build()
```
## Configuring additional headers
```python
hub_connection = HubConnectionBuilder()\
            .with_url(server_url,
            options={
                "headers": {
                    "mycustomheader": "mycustomheadervalue"
                }
            })
            ...
            .build()
```
## Configuring additional querystring parameters
```python
server_url ="http.... /?myquerystringparam=134&foo=bar"
connection = HubConnectionBuilder()\
            .with_url(server_url,
            options={
            })\
            .build()
```
## Congfiguring skip negotiation
```python
hub_connection = HubConnectionBuilder() \
        .with_url("ws://"+server_url, options={
            "verify_ssl": False,
            "skip_negotiation": False,
            "headers": {
            }
        }) \
        .configure_logging(logging.DEBUG, socket_trace=True, handler=handler) \
        .build()

```
## Configuring ping(keep alive)

keep_alive_interval sets the seconds of ping message

```python
hub_connection = HubConnectionBuilder()\
    .with_url(server_url)\
    .configure_logging(logging.DEBUG)\
    .with_automatic_reconnect({
        "type": "raw",
        "keep_alive_interval": 10,
        "reconnect_interval": 5,
        "max_attempts": 5
    }).build()
```
## Configuring logging
```python
hub_connection = HubConnectionBuilder()\
    .with_url(server_url)\
    .configure_logging(logging.DEBUG)\
    .with_automatic_reconnect({
        "type": "raw",
        "keep_alive_interval": 10,
        "reconnect_interval": 5,
        "max_attempts": 5
    }).build()
```

## Configure messagepack

```python
from signalrcore.protocol.messagepack_protocol import MessagePackHubProtocol

HubConnectionBuilder()\
            .with_url(self.server_url, options={"verify_ssl":False})\
                ... 
            .with_hub_protocol(MessagePackHubProtocol())\
                ...
            .build()
```
## Events

### On Connect / On Disconnect
on_open - fires when connection is opened and ready to send messages
on_close - fires when connection is closed
```python
hub_connection.on_open(lambda: print("connection opened and handshake received ready to send messages"))
hub_connection.on_close(lambda: print("connection closed"))

```
### On Hub Error (Hub Exceptions ...)
```
hub_connection.on_error(lambda data: print(f"An exception was thrown closed{data.error}"))
```
### Register an operation 
ReceiveMessage - signalr method
print - function that has as parameters args of signalr method
```python
hub_connection.on("ReceiveMessage", print)
```
## Sending messages
SendMessage - signalr method
username, message - parameters of signalrmethod
```python
    hub_connection.send("SendMessage", [username, message])
```

## Sending messages with callback
SendMessage - signalr method
username, message - parameters of signalrmethod
```python
    send_callback_received = threading.Lock()
    send_callback_received.acquire()
    self.connection.send(
        "SendMessage", # Method
        [self.username, self.message], # Params
        lambda m: send_callback_received.release()) # Callback
    if not send_callback_received.acquire(timeout=1):
        raise ValueError("CALLBACK NOT RECEIVED")
```

## Requesting streaming (Server to client)
```python
hub_connection.stream(
            "Counter",
            [len(self.items), 500]).subscribe({
                "next": self.on_next,
                "complete": self.on_complete,
                "error": self.on_error
            })
```
## Client side Streaming
```python
from signalrcore.subject import  Subject

subject = Subject()

# Start Streaming
hub_connection.send("UploadStream", subject)

# Each iteration
subject.next(str(iteration))

# End streaming
subject.complete()
```

# Full Examples

Examples will be avaiable [here](https://github.com/mandrewcito/signalrcore/tree/master/test/examples)
It were developed using package from [aspnet core - SignalRChat](https://codeload.github.com/aspnet/Docs/zip/master) 

## Chat example
A mini example could be something like this:

```python
import logging
import sys
from signalrcore.hub_connection_builder import HubConnectionBuilder


def input_with_default(input_text, default_value):
    value = input(input_text.format(default_value))
    return default_value if value is None or value.strip() == "" else value


server_url = input_with_default('Enter your server url(default: {0}): ', "wss://localhost:44376/chatHub")
username = input_with_default('Enter your username (default: {0}): ', "mandrewcito")
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
hub_connection = HubConnectionBuilder()\
    .with_url(server_url, options={"verify_ssl": False}) \
    .configure_logging(logging.DEBUG, socket_trace=True, handler=handler) \
    .with_automatic_reconnect({
            "type": "interval",
            "keep_alive_interval": 10,
            "intervals": [1, 3, 5, 6, 7, 87, 3]
        }).build()

hub_connection.on_open(lambda: print("connection opened and handshake received ready to send messages"))
hub_connection.on_close(lambda: print("connection closed"))

hub_connection.on("ReceiveMessage", print)
hub_connection.start()
message = None

# Do login

while message != "exit()":
    message = input(">> ")
    if message is not None and message != "" and message != "exit()":
        hub_connection.send("SendMessage", [username, message])

hub_connection.stop()

sys.exit(0)

```



            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/colin-chang/signalrcore",
    "name": "xy-signalrcore",
    "maintainer": null,
    "docs_url": null,
    "requires_python": null,
    "maintainer_email": null,
    "keywords": "signalr core client",
    "author": "Colin Chang",
    "author_email": "zhangcheng5468@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/06/65/b09d26af3222dacd13dc9f94c20f5eaa825aceb2e8e904d630cbaf9bbc58/xy-signalrcore-1.0.1.tar.gz",
    "platform": null,
    "description": "# SignalR core client\n[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?logo=paypal&style=flat-square)](https://www.paypal.me/mandrewcito/1)\n![Pypi](https://img.shields.io/pypi/v/signalrcore.svg)\n[![Downloads](https://pepy.tech/badge/signalrcore/month)](https://pepy.tech/project/signalrcore/month)\n[![Downloads](https://pepy.tech/badge/signalrcore)](https://pepy.tech/project/signalrcore)\n![Issues](https://img.shields.io/github/issues/mandrewcito/signalrcore.svg)\n![Open issues](https://img.shields.io/github/issues-raw/mandrewcito/signalrcore.svg)\n![codecov.io](https://codecov.io/github/mandrewcito/signalrcore/coverage.svg?branch=master)\n\n![logo alt](https://raw.githubusercontent.com/mandrewcito/signalrcore/master/docs/img/logo_temp.128.svg.png)\n\n# \u6539\u8fdb\n\u5728[\u6e90\u5e93](https://pypi.org/project/signalrcore/)\u57fa\u7840\u4e0a\u4fee\u6b63\u4e86\u5982\u4e0b\u95ee\u9898\u4ee5\u517c\u5bb9\u65b0\u7248`signalR(.NET 6.0)`\uff1a\n1. \u8865\u5145\u4e86`negotiateVersion=1` \u53c2\u6570\n2. \u4f7f\u7528`signalR`\u94fe\u63a5`connectToken`\u53c2\u6570\u66ff\u6362`connectionId`\n3. `hubConnection`\u6269\u5c55`state`\u5c5e\u6027\u8868\u793a\u8fde\u63a5\u72b6\u6001\uff0c\u6269\u5c55`ready`\u5c5e\u6027\u8868\u793a\u662f\u5426\u5df2\u8fde\u63a5\n\n# Links \n\n* [Dev to posts with library examples and implementation](https://dev.to/mandrewcito/singlar-core-python-client-58e7)\n\n* [Pypi](https://pypi.org/project/signalrcore/)\n\n* [Wiki - This Doc](https://mandrewcito.github.io/signalrcore/)\n\n# Develop\n\nTest server will be avaiable in [here](https://github.com/mandrewcito/signalrcore-containertestservers) and docker compose is required.\n\n```bash\ngit clone https://github.com/mandrewcito/signalrcore-containertestservers\ncd signalrcore-containertestservers\ndocker-compose up\ncd ../signalrcore\nmake tests\n```\n\n## Known Issues\n\nIssues related with closing sockets are inherited from the websocket-client library. Due to these problems i can't update the library to versions higher than websocket-client 0.54.0. \nI'm working to solve it but for now its patched (Error number 1. Raises an exception, and then exception is treated for prevent errors). \nIf I update the websocket library I fall into error number 2, on local machine I can't reproduce it but travis builds fail (sometimes and randomly :()\n* [1. Closing socket error](https://github.com/slackapi/python-slackclient/issues/171)\n* [2. Random errors closing socket](https://github.com/websocket-client/websocket-client/issues/449)\n\n# A Tiny How To\n\n## Connect to a server without auth\n\n```python\nhub_connection = HubConnectionBuilder()\\\n    .with_url(server_url)\\\n    .configure_logging(logging.DEBUG)\\\n    .with_automatic_reconnect({\n        \"type\": \"raw\",\n        \"keep_alive_interval\": 10,\n        \"reconnect_interval\": 5,\n        \"max_attempts\": 5\n    }).build()\n```\n## Connect to a server with auth\n\nlogin_function must provide auth token\n\n```python\nhub_connection = HubConnectionBuilder()\\\n            .with_url(server_url,\n            options={\n                \"access_token_factory\": login_function,\n                \"headers\": {\n                    \"mycustomheader\": \"mycustomheadervalue\"\n                }\n            })\\\n            .configure_logging(logging.DEBUG)\\\n            .with_automatic_reconnect({\n                \"type\": \"raw\",\n                \"keep_alive_interval\": 10,\n                \"reconnect_interval\": 5,\n                \"max_attempts\": 5\n            }).build()\n```\n### Unauthorized errors\nA login function must provide an error controller if authorization fails. When connection starts, if authorization fails exception will be propagated.\n\n```python\n    def login(self):\n        response = requests.post(\n            self.login_url,\n            json={\n                \"username\": self.email,\n                \"password\": self.password\n                },verify=False)\n        if response.status_code == 200:\n            return response.json()[\"token\"]\n        raise requests.exceptions.ConnectionError()\n\n    hub_connection.start()   # this code will raise  requests.exceptions.ConnectionError() if auth fails\n```\n## Configure logging\n\n```python\nHubConnectionBuilder()\\\n    .with_url(server_url,\n    .configure_logging(logging.DEBUG)\n    ...\n```\n## Configure socket trace\n```python \nHubConnectionBuilder()\\\n    .with_url(server_url,\n    .configure_logging(logging.DEBUG, socket_trace=True) \n    ... \n ```\n## Configure your own handler\n```python\n import logging\nhandler = logging.StreamHandler()\nhandler.setLevel(logging.DEBUG)\nhub_connection = HubConnectionBuilder()\\\n    .with_url(server_url, options={\"verify_ssl\": False}) \\\n    .configure_logging(logging.DEBUG, socket_trace=True, handler=handler)\n    ...\n ```\n## Configuring reconnection\nAfter reaching max_attempts an exeption will be thrown and on_disconnect event will be fired.\n```python\nhub_connection = HubConnectionBuilder()\\\n    .with_url(server_url)\\\n    ...\n    .build()\n```\n## Configuring additional headers\n```python\nhub_connection = HubConnectionBuilder()\\\n            .with_url(server_url,\n            options={\n                \"headers\": {\n                    \"mycustomheader\": \"mycustomheadervalue\"\n                }\n            })\n            ...\n            .build()\n```\n## Configuring additional querystring parameters\n```python\nserver_url =\"http.... /?myquerystringparam=134&foo=bar\"\nconnection = HubConnectionBuilder()\\\n            .with_url(server_url,\n            options={\n            })\\\n            .build()\n```\n## Congfiguring skip negotiation\n```python\nhub_connection = HubConnectionBuilder() \\\n        .with_url(\"ws://\"+server_url, options={\n            \"verify_ssl\": False,\n            \"skip_negotiation\": False,\n            \"headers\": {\n            }\n        }) \\\n        .configure_logging(logging.DEBUG, socket_trace=True, handler=handler) \\\n        .build()\n\n```\n## Configuring ping(keep alive)\n\nkeep_alive_interval sets the seconds of ping message\n\n```python\nhub_connection = HubConnectionBuilder()\\\n    .with_url(server_url)\\\n    .configure_logging(logging.DEBUG)\\\n    .with_automatic_reconnect({\n        \"type\": \"raw\",\n        \"keep_alive_interval\": 10,\n        \"reconnect_interval\": 5,\n        \"max_attempts\": 5\n    }).build()\n```\n## Configuring logging\n```python\nhub_connection = HubConnectionBuilder()\\\n    .with_url(server_url)\\\n    .configure_logging(logging.DEBUG)\\\n    .with_automatic_reconnect({\n        \"type\": \"raw\",\n        \"keep_alive_interval\": 10,\n        \"reconnect_interval\": 5,\n        \"max_attempts\": 5\n    }).build()\n```\n\n## Configure messagepack\n\n```python\nfrom signalrcore.protocol.messagepack_protocol import MessagePackHubProtocol\n\nHubConnectionBuilder()\\\n            .with_url(self.server_url, options={\"verify_ssl\":False})\\\n                ... \n            .with_hub_protocol(MessagePackHubProtocol())\\\n                ...\n            .build()\n```\n## Events\n\n### On Connect / On Disconnect\non_open - fires when connection is opened and ready to send messages\non_close - fires when connection is closed\n```python\nhub_connection.on_open(lambda: print(\"connection opened and handshake received ready to send messages\"))\nhub_connection.on_close(lambda: print(\"connection closed\"))\n\n```\n### On Hub Error (Hub Exceptions ...)\n```\nhub_connection.on_error(lambda data: print(f\"An exception was thrown closed{data.error}\"))\n```\n### Register an operation \nReceiveMessage - signalr method\nprint - function that has as parameters args of signalr method\n```python\nhub_connection.on(\"ReceiveMessage\", print)\n```\n## Sending messages\nSendMessage - signalr method\nusername, message - parameters of signalrmethod\n```python\n    hub_connection.send(\"SendMessage\", [username, message])\n```\n\n## Sending messages with callback\nSendMessage - signalr method\nusername, message - parameters of signalrmethod\n```python\n    send_callback_received = threading.Lock()\n    send_callback_received.acquire()\n    self.connection.send(\n        \"SendMessage\", # Method\n        [self.username, self.message], # Params\n        lambda m: send_callback_received.release()) # Callback\n    if not send_callback_received.acquire(timeout=1):\n        raise ValueError(\"CALLBACK NOT RECEIVED\")\n```\n\n## Requesting streaming (Server to client)\n```python\nhub_connection.stream(\n            \"Counter\",\n            [len(self.items), 500]).subscribe({\n                \"next\": self.on_next,\n                \"complete\": self.on_complete,\n                \"error\": self.on_error\n            })\n```\n## Client side Streaming\n```python\nfrom signalrcore.subject import  Subject\n\nsubject = Subject()\n\n# Start Streaming\nhub_connection.send(\"UploadStream\", subject)\n\n# Each iteration\nsubject.next(str(iteration))\n\n# End streaming\nsubject.complete()\n```\n\n# Full Examples\n\nExamples will be avaiable [here](https://github.com/mandrewcito/signalrcore/tree/master/test/examples)\nIt were developed using package from [aspnet core - SignalRChat](https://codeload.github.com/aspnet/Docs/zip/master) \n\n## Chat example\nA mini example could be something like this:\n\n```python\nimport logging\nimport sys\nfrom signalrcore.hub_connection_builder import HubConnectionBuilder\n\n\ndef input_with_default(input_text, default_value):\n    value = input(input_text.format(default_value))\n    return default_value if value is None or value.strip() == \"\" else value\n\n\nserver_url = input_with_default('Enter your server url(default: {0}): ', \"wss://localhost:44376/chatHub\")\nusername = input_with_default('Enter your username (default: {0}): ', \"mandrewcito\")\nhandler = logging.StreamHandler()\nhandler.setLevel(logging.DEBUG)\nhub_connection = HubConnectionBuilder()\\\n    .with_url(server_url, options={\"verify_ssl\": False}) \\\n    .configure_logging(logging.DEBUG, socket_trace=True, handler=handler) \\\n    .with_automatic_reconnect({\n            \"type\": \"interval\",\n            \"keep_alive_interval\": 10,\n            \"intervals\": [1, 3, 5, 6, 7, 87, 3]\n        }).build()\n\nhub_connection.on_open(lambda: print(\"connection opened and handshake received ready to send messages\"))\nhub_connection.on_close(lambda: print(\"connection closed\"))\n\nhub_connection.on(\"ReceiveMessage\", print)\nhub_connection.start()\nmessage = None\n\n# Do login\n\nwhile message != \"exit()\":\n    message = input(\">> \")\n    if message is not None and message != \"\" and message != \"exit()\":\n        hub_connection.send(\"SendMessage\", [username, message])\n\nhub_connection.stop()\n\nsys.exit(0)\n\n```\n\n\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "A Python SignalR Core client(json and messagepack), with invocation auth and two way streaming. Compatible with azure / serverless functions. Also with automatic reconnect and manually reconnect.",
    "version": "1.0.1",
    "project_urls": {
        "Homepage": "https://github.com/colin-chang/signalrcore"
    },
    "split_keywords": [
        "signalr",
        "core",
        "client"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "585ccf6cefaa4349a918655558296389325fbc5c07302db14f73288c0f93cc9c",
                "md5": "6eb94590d1f8e64c8c06035d30aeda18",
                "sha256": "c38fc16fb5cecdb7e35eb003050afd1014de054ab68a647213d0bff074cccf40"
            },
            "downloads": -1,
            "filename": "xy_signalrcore-1.0.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "6eb94590d1f8e64c8c06035d30aeda18",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 35535,
            "upload_time": "2024-04-10T10:14:54",
            "upload_time_iso_8601": "2024-04-10T10:14:54.099898Z",
            "url": "https://files.pythonhosted.org/packages/58/5c/cf6cefaa4349a918655558296389325fbc5c07302db14f73288c0f93cc9c/xy_signalrcore-1.0.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "0665b09d26af3222dacd13dc9f94c20f5eaa825aceb2e8e904d630cbaf9bbc58",
                "md5": "1cdab3db69ea9b8a4ef7099aa1973b95",
                "sha256": "fd911de1496a3505edcb6130036a7036e7a29f9c24b98fefda1f24123d722de6"
            },
            "downloads": -1,
            "filename": "xy-signalrcore-1.0.1.tar.gz",
            "has_sig": false,
            "md5_digest": "1cdab3db69ea9b8a4ef7099aa1973b95",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 25839,
            "upload_time": "2024-04-10T10:14:56",
            "upload_time_iso_8601": "2024-04-10T10:14:56.587459Z",
            "url": "https://files.pythonhosted.org/packages/06/65/b09d26af3222dacd13dc9f94c20f5eaa825aceb2e8e904d630cbaf9bbc58/xy-signalrcore-1.0.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-04-10 10:14:56",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "colin-chang",
    "github_project": "signalrcore",
    "travis_ci": true,
    "coveralls": false,
    "github_actions": true,
    "requirements": [],
    "lcname": "xy-signalrcore"
}
        
Elapsed time: 3.87319s