diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..d73d5053 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**IMPORTANT**: If you have a question, or you are not sure if you have found a bug in this package, then you are in the wrong place. Hit back in your web browser, and then open a GitHub Discussion instead. Likewise, if you are unable to provide the information requested below, open a discussion to troubleshoot your issue. + +**Describe the bug** +A clear and concise description of what the bug is. If you are getting errors, please include the complete error message, including the stack trace. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/v4/logging-and-debugging/) for how to enable logs. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3657c972 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: GitHub Discussions + url: https://github.com/miguelgrinberg/python-socketio/discussions + about: Ask questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..968c279f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Logs** +Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/v4/logging-and-debugging/) for how to enable logs. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..99ad78e2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,43 @@ +name: build +on: [push, pull_request, workflow_dispatch] +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - run: python -m pip install --upgrade pip wheel + - run: pip install tox tox-gh-actions + - run: tox -eflake8 + - run: tox -edocs + tests: + name: tests + strategy: + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + python: ['3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11'] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - run: python -m pip install --upgrade pip wheel + - run: pip install tox tox-gh-actions + - run: tox + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - run: python -m pip install --upgrade pip wheel + - run: pip install tox tox-gh-actions + - run: tox + - uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 30526d0a..01d8b9ad 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ dist build eggs parts -bin var sdist develop-eggs @@ -44,3 +43,5 @@ venv* tags htmlcov *.swp + +node_modules diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..4358e806 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3" + +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 187660b0..00000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,3 +0,0 @@ -python: - version: 3.6 - setup_py_install: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cd3a253b..00000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: python -matrix: - include: - - python: 3.6 - env: TOXENV=flake8 - - python: 2.7 - env: TOXENV=py27 - - python: 3.4 - env: TOXENV=py34 - - python: 3.5 - env: TOXENV=py35 - - python: 3.6 - env: TOXENV=py36 - - python: pypy - env: TOXENV=pypy - - python: 3.6 - env: TOXENV=docs -install: - - pip install tox -script: - - tox diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..7e97e423 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,736 @@ +# python-socketio change log + +**Release 5.16.0** - 2025-12-24 + +- Address deprecation warnings ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b235699d9b06564753c570b76055997e9d62a938)) +- Drop Python 3.8 and 3.9 from CI builds ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d0728d2f74538762dd551fa9cd0cd1fd5aedfa37)) + +**Release 5.15.1** - 2025-12-16 + +- Restore support multiple arguments via pubsub emits [#1540](https://github.com/miguelgrinberg/python-socketio/issues/1540) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c279f26bb8c9887c4ca99d4d81ad331c4844438c)) + +**Release 5.15.0** - 2025-11-22 + +- Retry initial Redis connection [#1534](https://github.com/miguelgrinberg/python-socketio/issues/1536) ([commit #1](https://github.com/miguelgrinberg/python-socketio/commit/1e903e173a2d7b04599c4f7f9630c1abbb531fad) [commit #2](https://github.com/miguelgrinberg/python-socketio/commit/5e898a9b93526e6e667767e54c60f4c84589989d)) +- Correctly regenerate RabbitMQ binding after a connection failure [#1516](https://github.com/miguelgrinberg/python-socketio/issues/1516) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c52e93b4a328d98a968bfbdec0cfd598b73ee913)) (thanks **Gritty_dev**!) +- Support `ext_type` in the `MsgPackPacket` class [#1521](https://github.com/miguelgrinberg/python-socketio/issues/1521) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/208925344a48485d2cd56e40eb74266c3bcb5311)) +- Support sending `bytesarray`s when using pub/sub managers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6c9b9974f72e2efdf62407ecab24ee6995448098)) +- Fix typos in documentation [#1520](https://github.com/miguelgrinberg/python-socketio/issues/1520) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/db3f1c2a0105c30cb833ddfca8f05fe4320468fd)) (thanks **Lê Nam Khánh**!) +- Improvements to the logging documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b423d0e38eef559b7e81acb7e32059de305f982c)) + +**Release 5.14.3** - 2025-10-29 + +- Support Python's native `ConnectionRefusedError` exception to reject a connection [#1515](https://github.com/miguelgrinberg/python-socketio/issues/1515) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f3b18bde3f16437b223491d4c3e440ea37105fe3)) +- Push binary data to the aiopika client manager [#1514](https://github.com/miguelgrinberg/python-socketio/issues/1514) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/194e1b7f277b5f72e1de78d3f614e7b8b6c788ac)) + +**Release 5.14.2** - 2025-10-15 + +- Restore binary message support in message queue setups [#1509](https://github.com/miguelgrinberg/python-socketio/issues/1509) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bab4a10f48aaae11d7f832ebe5c30ad3f85d31b3)) +- Fix formatting of client connection error [#1507](https://github.com/miguelgrinberg/python-socketio/issues/1507) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f298c9b54d76ab09ff72935937e1b9575bc45ffd)) +- Add 3.14 and pypy-3.11 CI tasks ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1f4cd3b025c294f25208ec3c05b5f8df6209e403)) +- Improve documentation of the `BaseManager.get_participants()` method ([commit](https://github.com/miguelgrinberg/python-socketio/commit/33722a0d96036f005188b07b8b46a5ef091fe65f)) + +**Release 5.14.1** - 2025-10-02 + +- Restore support for `rediss://` URLs, and add support for `valkeys://` as well ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6e2d0de12bb4e4a99fdfc30bed0706ded620822c)) +- Add support for Redis connections using unix sockets [#1503](https://github.com/miguelgrinberg/python-socketio/issues/1503) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a8deb3a8f3ee51d75c124157efa7fc9289fd592b)) (thanks **Darren Chang**!) + +**Release 5.14.0** - 2025-09-30 + +- Replace pickle with json in message queue communications [#1502](https://github.com/miguelgrinberg/python-socketio/issues/1502) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/53f6be094257ed81476b0e212c8cddd6d06ca39a)) +- Add support for Valkey in the Redis client managers [#1488](https://github.com/miguelgrinberg/python-socketio/issues/1488) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/36a89226a2fb18f876dcba48125a8c51904586ec)) (thanks **phi-friday**!) +- Keep track of which namespaces failed to connect [#1496](https://github.com/miguelgrinberg/python-socketio/issues/1496) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5dc2aea077469ad318e47b28a84845c5efb6bdcf)) +- Fixed transport property of the simple clients to be a string as documented [#1499](https://github.com/miguelgrinberg/python-socketio/issues/1499) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/23556fb3dcb37074020494df40fb4495d47e7efe)) +- SimpleClient.call does not raise `TimeoutError` on timeout [#1501](https://github.com/miguelgrinberg/python-socketio/issues/1501) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a59c6f520059eb095ab4472e5192ce3e486875d9)) (thanks **James Thistlewood**!) +- Wait for client to end background tasks on disconnect [#1500](https://github.com/miguelgrinberg/python-socketio/issues/1500) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f61e0bec3750a83de619db9b779f9d93e449eade)) +- Better error logging for the Redis managers [#1479](https://github.com/miguelgrinberg/python-socketio/issues/1479) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b01b197df1ea5914fdaa2e12758e916ebd60162a)) (thanks **Eugnee**!) +- Channel was not properly initialized in several pubsub client managers [#1476](https://github.com/miguelgrinberg/python-socketio/issues/1476) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/efd1247ed9ed61a61d6840dc2c8c92ab02031afb)) (thanks **Eugnee**!) +- Add message queue deployment recommendations for security ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b3da354ed9eb46c0fb847c628b379ccae475a970)) +- Add missing `async` on session examples for the async server [#1465](https://github.com/miguelgrinberg/python-socketio/issues/1465) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5e04003dad0140fb1c6acff328e4215e62fbc58a)) (thanks **Func**!) +- Add SPDX license identifier [#1453](https://github.com/miguelgrinberg/python-socketio/issues/1453) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/81c80cddde06dd9561687c74e5769e25613282d7)) (thanks **Marc Mueller**!) + +**Release 5.13.0** - 2025-04-12 + +- Eliminate race conditions on disconnect [#1441](https://github.com/miguelgrinberg/python-socketio/issues/1441) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/288ebb189d799a05bbc5979a834433034ea2939f)) +- Preserve exception context in `Client.connect` and `AsyncClient.connect` [#1450](https://github.com/miguelgrinberg/python-socketio/issues/1450) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5c93c59648358862514f317838f61498a101ba54)) (thanks **Tim Van Baak**!) +- Allow custom client subclasses to be used in SimpleClient and AsyncSimpleClient [#1432](https://github.com/miguelgrinberg/python-socketio/issues/1432) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7605630bb236b4baf98574ca2a8f0cdba2696ef4)) +- Add support for Redis Sentinel URLs in `RedisManager` and `AsyncRedisManager` [#1448](https://github.com/miguelgrinberg/python-socketio/issues/1448) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6a52e8b50274a7524fadcd2633eb819811a63734)) +- Remove incorrect reference to an `asyncio` installation extra in documentation [#1449](https://github.com/miguelgrinberg/python-socketio/issues/1449) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/537630b983245cc137f609c3e6247d6d68ebdea5)) + +**Release 5.12.1** - 2024-12-29 + +- Fix admin instrumentation support of disconnect reasons [#1423](https://github.com/miguelgrinberg/python-socketio/issues/1423) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b75fd31625cfea0d8c67d776070e4f8de99c1e45)) +- Stop using deprecated datetime functions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8fe012abbb350107b742ab2cf9aa44d328bc23e9)) +- Enable admin instrumentation by default in WSGI and ASGI examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/269332da8041df115e3a1e2ca04808c3179a72e1)) +- Fixed broken gevent URL in documentation [#1427](https://github.com/miguelgrinberg/python-socketio/issues/1427) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8964dab9d545333646fafad9aae0becd761a1045)) (thanks **Carlos Guerrero**!) + +**Release 5.12.0** - 2024-12-18 + +- Added a `reason` argument to the disconnect handler [#1422](https://github.com/miguelgrinberg/python-socketio/issues/1422) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bd8555da8523d1a73432685a00eb5acb4d2261f5)) +- Prevented starting multiple tasks for reconnection [#1369](https://github.com/miguelgrinberg/python-socketio/issues/1369) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b6ee33e56cf2679664c1b894bf7e5d33a30976db)) (thanks **humayunsr**!) +- Fixed `AsyncClient::wait()` unexpected return after success reconnect [#1407](https://github.com/miguelgrinberg/python-socketio/issues/1407) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/78d1124c50cff149051fb89bf6f08000bf184da5)) (thanks **Arseny**!) +- Removed old constructs from old and unsupported Python versions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/db642bb2bd9794eeceddd54abc47665c69e85406)) +- Removed dependency on unittest.TestCase base class in unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/abf336e108b01f44afb473eb86c1dece6360195c)) +- Adopted pyenv-asyncio for async unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0b5c4638e5e4bff06fcf46476d218ae5ad4ada14)) +- Adopted unittest.mock.AsyncMock in async unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8f0e66c1cd1cd63dcef703576cc9cb9c99104df7)) +- Fix typo with `AsyncClient.connect` example [#1403](https://github.com/miguelgrinberg/python-socketio/issues/1403) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/72d37ea79f4cf6076591782e4781fd4868a7e0d6)) (thanks **Peter Bierma**!) +- Documentation typo [#1421](https://github.com/miguelgrinberg/python-socketio/issues/1421) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bf5a05ae9bf94b2fbd1367a04884ef5a39cd2671)) (thanks **Masen Furer**!) +- Renamed flask-socketio references to python-socketio in documentation [#1377](https://github.com/miguelgrinberg/python-socketio/issues/1377) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5f83cd0f7b2911705eaf1a8cb9060afbee6eb456)) +- Added Python 3.13 CI builds ([commit](https://github.com/miguelgrinberg/python-socketio/commit/42da5d2f5426e812fd37d4cabcb9277810cae9c1)) + +**Release 5.11.4** - 2024-09-02 + +- Prevent crash when client sends empty event ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1b901de0077322eedb3509318ccba939f5f0bf10)) +- Add missing `to` argument in manager's `emit()` method [#1374](https://github.com/miguelgrinberg/python-socketio/issues/1374) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f1476041e5bb0857a99024c9a38203edfc974bdf)) (thanks **Pavieł Michalkievič**!) +- Reorganization of server documentation [#1350](https://github.com/miguelgrinberg/python-socketio/issues/1350) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3a618c67ef60736d5619b9ac52c47d96a6acf3c3)) +- Update documentation note about Sanic issues [#1365](https://github.com/miguelgrinberg/python-socketio/issues/1365) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/287d6ed551090eed822f924bb548ba93923dd4d1)) + +**Release 5.11.3** - 2024-06-19 + +- New `shutdown()` method added to the client [#1333](https://github.com/miguelgrinberg/python-socketio/issues/1333) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/811e044a46b7d6e4d94bf870e59d0cd8187850d3)) +- Ignore catch-all namespace in client connections [#1351](https://github.com/miguelgrinberg/python-socketio/issues/1351) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/469b7c0dd51eea97979f784d0c94359ad18a96ac)) +- Accept 0 as a callback id [#1329](https://github.com/miguelgrinberg/python-socketio/issues/1329) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/e59351969241c3d7f44560027cf1e44206e17d6c)) (thanks **Ruslan Bel'kov**!) +- Minor updates to the server and client documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5e78ecbc343d9c7252a6449029263b4a5cb967c8)) +- Remove outdated information in intro section of documentation [#1337](https://github.com/miguelgrinberg/python-socketio/issues/1337) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/82ceaf7a23c51ed1911e550aaf71d6449584eaa0)) +- Fixed typos in server documentation [#1331](https://github.com/miguelgrinberg/python-socketio/issues/1331) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/547449bc70248b1a2c68e362a026d42db083222c)) (thanks **John Sigg**!) + +**Release 5.11.2** - 2024-03-24 + +- Improved routing to catch-all namespace handlers [#1316](https://github.com/miguelgrinberg/python-socketio/issues/1316) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bd39b8f2156f600ea6de558af03a6be205d6e892)) (thanks **asuka**!) +- Option to disable routing in ASGIApp ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7cc84bd13dfa0a86b0326446fb6954949ef948c0)) + +**Release 5.11.1** - 2024-02-05 + +- Connection retry option in the client [#1306](https://github.com/miguelgrinberg/python-socketio/issues/1306) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b4f36148d816b2fbf2a81237dff70b18d909aebf)) +- use Socket.IO sid in transport[#1299](https://github.com/miguelgrinberg/python-socketio/issues/1299) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0e1f23229dc5a478df474e5bb6e5c91e5e30aa04)) +- Add support for Python 3.12 and drop 3.7 [#1297](https://github.com/miguelgrinberg/python-socketio/issues/1297) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/417785293f6c1351be6ea10b6b9f57abff03fdfc)) (thanks **Hugo van Kemenade**!) + +**Release 5.11.0** - 2024-01-06 + +- Support catch-all namespaces [#1288](https://github.com/miguelgrinberg/python-socketio/issues/1288) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/801241378ec6cbbaef6490399220b00473f66d34)) (thanks **mooomooo**!) +- Prevent pubsub managers from ever crashing [#1262](https://github.com/miguelgrinberg/python-socketio/issues/1262) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/12134bd5c50851a1c1cd9ac319beeb7b12a9398b)) +- Hold references to background tasks to avoid garbage collection [#1191](https://github.com/miguelgrinberg/python-socketio/issues/1191) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2f07824347190026e3dc40c66cffbb5604d09799)) +- Clearer documentation for the `max_http_buffer_size` argument [#1272](https://github.com/miguelgrinberg/python-socketio/issues/1272) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1f488b03b669ee8b4435e342ff00d8b88f43d5fc)) +- Improved catch-all handler documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0a54ec6ae46848b336849d6c198c11a6e1dcf51f)) +- Documentation typos ([commit](https://github.com/miguelgrinberg/python-socketio/commit/29c794333ec284bd8f968db68d24738ef2b43923)) +- Remove documentation references to gevent-websocket, which is not needed anymore ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0aa8683a3b13f5175fea70e52f889297d605b2bc)) +- Remove documentation reference to outdated aioredis package [#1268](https://github.com/miguelgrinberg/python-socketio/issues/1268) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/344e5c1d6b3927464cf31069cea198569c6540b3)) +- Update requirements for Django example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f72139b89e4de9eb38aa90d62ca0fa205db196d9)) +- Unit test fixes for the new simple clients [#1265](https://github.com/miguelgrinberg/python-socketio/issues/1265) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/66b9586a65564d82ae3589c65e541a0e593d90d3)) + +**Release 5.10.0** - 2023-10-15 + +- New `SimpleClient` and `AsyncSimpleClient` classes [#1237](https://github.com/miguelgrinberg/python-socketio/issues/1237) ([commit #1](https://github.com/miguelgrinberg/python-socketio/commit/55d6310eb3e194b1e67e40942a953bc4413b98b7)) ([commit #2](https://github.com/miguelgrinberg/python-socketio/commit/699ee9c47adcb44f1a8150d8c92a9555d07f7b5b)) +- Reporting to Socket.IO Admin UI (https://admin.socket.io) [#1164](https://github.com/miguelgrinberg/python-socketio/issues/1164) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4bf48776ca08acba255bf660f5ca61810202826a)) +- Support entering and leaving rooms through pubsub client managers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d222f4c3deb7a12f3a8c584777dbfc18448a206d)) +- Async versions of enter_room and leave_room should be coroutines (**breaking change**) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8e3460c5fcc351ca0cff83c690eedcde0e8a6e7e)) +- Add a `shutdown()` function for the server ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c419fc5481846ab026ca847234f5ebe5420e510a)) +- Message queue optimizations [#1240](https://github.com/miguelgrinberg/python-socketio/issues/1240) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/dc6e4f516f38d1125cb44ccae11b120ffa8fe7f3)) +- Remove unneeded arguments from super() ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8da3c617a66877ec0fc5ab25256ead41ae611351)) +- Internal code restructure (no functional changes) ([commit #1](https://github.com/miguelgrinberg/python-socketio/commit/ef0f88f6cf5687dddfd0e3be9a90af2865d5871f)) ([commit #2](https://github.com/miguelgrinberg/python-socketio/commit/58b57068ab770d099bdd54d4eb8d22ce8a4f6751)) +- Add FastAPI and Litestar server examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2e1f0262ad9a037858b04b9dcec7509ee5008f8c)) +- Update Socket.IO JavaScript client to version 4.7.2 in all examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3f78af283171d23b7f1dbfd055e21afe00877399)) +- Update `ping_timeout` documented default to accord with current Engine.IO behavior [#1255](https://github.com/miguelgrinberg/python-socketio/issues/1255) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d40b3a33fff5c6b896559fc534ccd611ab9cf1f4)) (thanks **[object Object]**!) +- Refactor common testing helpers into a separate module ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7208ec09e17466758cebb0732801ec84b25f1299)) +- Migrate Python package metadata to pyproject.toml ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6bdc498d423ca793b3db2d158fba4a461610de6a)) + +**Release 5.9.0** - 2023-09-03 + +- Optimized performance and memory usage for broadcasts [#1233](https://github.com/miguelgrinberg/python-socketio/issues/1233) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bf11ad36aebeebbc0b7a74d88d4b9a8fad113456)) +- Improved documentation on horizontal scaling [#1099](https://github.com/miguelgrinberg/python-socketio/issues/1099) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9d02247bc5914798a965cf423e978747a5535e33)) +- Corrected user session documentation example [#1170](https://github.com/miguelgrinberg/python-socketio/issues/1170) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/72dfdc6c8aff7f4f61c409e771df6a19299ad36f)) +- Improved grammar in documentation [#1175](https://github.com/miguelgrinberg/python-socketio/issues/1175) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c66d7a9f1ade0f728840b8ee9079599dd6a3de3f)) (thanks **zykron1**!) +- Fix docstring typo: client/server mixup [#1163](https://github.com/miguelgrinberg/python-socketio/issues/1163) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/598dd7e258c0c1d050456bbad2cd867dfca3df0f)) (thanks **Sasja**!) +- Fix typos in the documentation [#1230](https://github.com/miguelgrinberg/python-socketio/issues/1230) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1b7ed682e06d7c8a758a2f89391ddc941f70ee98)) (thanks **Alex Kuhrt**!) +- Upgrade dependencies in Django server example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/00b31663c29968bd50282d22fca9b95c09e4becf)) +- Update reference JavaScript examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6df96fb1c598c00ddf8f8995cbc095f0383d38ef)) +- Upgrade to pypy-3.9 in unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f49d65a0c3ff55f92a817994898c4182efa5ba69)) + +**Release 5.8.0** - 2023-03-16 + +- Made kombu client manager more robust and efficient ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8293dc3f8fa90f3d92192a702b28c23d8c516110)) +- Made aio_pika client manager more robust and efficient [#1142](https://github.com/miguelgrinberg/python-socketio/issues/1142) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cd7f781c022dd1d1ec3c6695a0fd6ab3ce864fd5)) +- Correctly handle emits to multiple rooms in the async server [#1081](https://github.com/miguelgrinberg/python-socketio/issues/1081) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/232cef1f86ff19878190c44caf991e017c8480a4)) +- Expose the `ignore_queue` option in namespaces [#1103](https://github.com/miguelgrinberg/python-socketio/issues/1103) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1cadada02dd7dc1eb96f45e88cbec67e1a393db3)) +- Do not automatically import zmq ([commit](https://github.com/miguelgrinberg/python-socketio/commit/de4d5b51e5fc8ba0d0f904851f23f8cced16d7f6)) +- TLS/SSL client documentation [#1040](https://github.com/miguelgrinberg/python-socketio/issues/1040) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/54aecfda917ec100e0b5e2c0e955ef719e0eb645)) +- Removed incorrect reference to multiple callback invocations in documentation [#1152](https://github.com/miguelgrinberg/python-socketio/issues/1152) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c4117fd6514374042127ab05f1f2d8d221fcf60d)) +- Fix documentation typo [#1155](https://github.com/miguelgrinberg/python-socketio/issues/1155) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/270eb372cc83778a897d32e95453eb385328d9de)) (thanks **Onwuka Gideon**!) +- Fix documentation typos ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8c747ab67b3e5c9f31db540a13c3da1b7784617c)) +- Fix documentation typo in asyncio_server.py [#1150](https://github.com/miguelgrinberg/python-socketio/issues/1150) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b2cc86cfb2502691d70447b0002179602a798e77)) (thanks **riz-j**!) +- Fix documentationi type [#1091](https://github.com/miguelgrinberg/python-socketio/issues/1091) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/55db7458900a179a9363294cc4fc91eb9c775f54)) (thanks **mostlycryptic**!) +- Add Python 3.11 to builds ([commit](https://github.com/miguelgrinberg/python-socketio/commit/60fe63b098af8c035891ae62a4336538ea419184)) + +**Release 5.7.2** - 2022-10-17 + +- Fixed disconnect implementation when using a message queue [#1002](https://github.com/miguelgrinberg/python-socketio/issues/1002) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f56ef6f0401b273107fc483c4ae1b5512209ac48)) +- Fixed remote async disconnects via message queue [#1003](https://github.com/miguelgrinberg/python-socketio/issues/1003) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/104d6569a0480ed0adb04e7d41f156762f9ebe9b)) +- Support optional payloads in msgpack implementation [#981](https://github.com/miguelgrinberg/python-socketio/issues/981) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ce1afd79e69e35b81a6f0d02fbd7ae04af59f9d6)) (thanks **Cromfel**!) +- Recommend ASGI integration for Sanic in Documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2c3e360ae8b151bc0bfedbde50248cd0dc8d1ff9)) + +**Release 5.7.1** - 2022-07-15 + +- Add `namespaces` argument to `Server` and `AsyncServer` [#822](https://github.com/miguelgrinberg/python-socketio/issues/822) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/efe87d867a205493654107d381bdb8b619b8ab2d)) +- Add missing `await` in asyncio server [#952](https://github.com/miguelgrinberg/python-socketio/issues/952) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d4e69fb7ceecdb98584f36e085a186eb4da23b07)) (thanks **sjrodahl**!) + +**Release 5.7.0** - 2022-07-04 + +- Server refuses connections on unknown namespaces [#822](https://github.com/miguelgrinberg/python-socketio/issues/822) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/44715012dc0d578b067a9a389c8aef2ce39f65c1)) +- Do not send ACK packet for unknown events [#824](https://github.com/miguelgrinberg/python-socketio/issues/824) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/268fe12ffa68f7af8881c75695c287c09490cef9)) +- Fix Python 3.11 deprecation warning [#941](https://github.com/miguelgrinberg/python-socketio/issues/941) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4b697815c3da4574fca8b759d21a4e0800dafc50)) (thanks **Jérôme Boulmier**!) +- Correct handling of RedisError exception [#919](https://github.com/miguelgrinberg/python-socketio/issues/919) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/98318fbdde2c4dcfba15d1b0aaf266b599e81e0c)) +- Update Django example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/dc7ac74c1d2c97544056541736d644060837a080)) +- Documentation fix for async client ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5b9134617759a1b64adb2f9aba0974c732576cc4)) +- Update documentation of asyncio server ([commit](https://github.com/miguelgrinberg/python-socketio/commit/98f3cb4664ff10c0bb17826b11564644bed99fd6)) +- Fix documentation typo [#948](https://github.com/miguelgrinberg/python-socketio/issues/948) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f888446b330146484325b568cdc2303c9b35c095)) (thanks **mostlycryptic**!) + +**Release 5.6.0** - 2022-04-24 + +- Catch and log errors in pubsub listening thread [#889](https://github.com/miguelgrinberg/python-socketio/issues/889) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f2ae136dcd724d56353b783092c448d6e638635f)) +- Use new asyncio support in redis package [#911](https://github.com/miguelgrinberg/python-socketio/issues/911) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0e7691b77605de00dedcbf87e415bb79fcd7f2aa)) +- Add support for aiopiko version 7 and higher [#897](https://github.com/miguelgrinberg/python-socketio/issues/897) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b4b55e1fb5e9d837b3301b5b873d9a7fe2a12023)) (thanks **Dmitriy**!) +- Fixed documentation typo [#910](https://github.com/miguelgrinberg/python-socketio/issues/910) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/16243e7c5f95f73f1f17b6febbf23c6483c17c62)) (thanks **Omar Costa Hamido**!) +- Fix aiohttp example's background task [#881](https://github.com/miguelgrinberg/python-socketio/issues/881) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/fb9648575ef5bd529579f854f141df52d9e000ed)) +- Bump sanic from 0.8 to 20.12.6 in /examples/server/sanic [#875](https://github.com/miguelgrinberg/python-socketio/issues/875) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/342f2e3fe9a3f973a491326b19fa4f96705b9b7e)) (thanks **dependabot[bot]**!) +- Add application name to Sanic example [#892](https://github.com/miguelgrinberg/python-socketio/issues/892) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4d5e36d36f865bf0527cfeb486a4e7f917b28717)) (thanks **Florian Metzger-Noel**!) + +**Release 5.5.2** - 2022-02-15 + +- Connect with an empty auth object instead of `None` [#861](https://github.com/miguelgrinberg/python-socketio/issues/861) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1fb7a76575426dd58a5e9c0e01646302ccc96188)) +- Fix indentation in the "Rooms" docs example. [#872](https://github.com/miguelgrinberg/python-socketio/issues/872) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3336181f9ce5fe737d675f8343f18a885c651ebd)) (thanks **Ezio Melotti**!) +- Remove 3.6 and pypy-3.6 builds, add 3.10 and pypy-3.8 ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ed5679a7cb01963b44a5004e1236b7e8b485aa0b)) + +**Release 5.5.1** - 2022-01-11 + +- Support multiple Kafka servers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4ee3649514b98c50cc0bf70d3f269389da52772d)) (thanks **sparkingdark**!) +- Include example code in flake8 pass ([commit](https://github.com/miguelgrinberg/python-socketio/commit/273a4b0439c84560d403662d8eba4122c28ba0d8)) + +**Release 5.5.0** - 2021-11-14 + +- Option to disable the SIGINT handler in the client [#792](https://github.com/miguelgrinberg/python-socketio/issues/792) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ea84b9b1c714b02eaf1081f4e37fd130a3159d8c)) +- Do not invoke reserved events on a catch-all handler [#814](https://github.com/miguelgrinberg/python-socketio/issues/814) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/34f34e53d650dde605f5f4a98d7a70936524a1b8)) +- Use correct binary packet types in the msgpack packet encoder [#811](https://github.com/miguelgrinberg/python-socketio/issues/811) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/60735dd4c2fc87ed863d7dbf7de361500d963dd3)) +- Add missing `call()` method to namespace classes [#800](https://github.com/miguelgrinberg/python-socketio/issues/800) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/32db48d12ceb44d7a02fd9f05047b47c7ed3f4a5)) +- Add missing `to` argument to namespace `emit()` and `send()` methods [#810](https://github.com/miguelgrinberg/python-socketio/issues/810) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ed08a01e65635160923f3d6d5755df74d53274e1)) +- Configure Redis pubsub to skip subscription messages ([commit](https://github.com/miguelgrinberg/python-socketio/commit/e8fff07b367929794e5e30cecbf252b72d307c16)) +- Migrate async Redis client manager to aioredis 2 [#771](https://github.com/miguelgrinberg/python-socketio/issues/771) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f245191d86722244a2d3d0529d9f5ff15dfd817a)) (thanks **Sam Mosleh**!) +- Update Python supported versions in docs ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a54152f2466bad4869d9cfdad6be3a5547e0b6bc)) +- Document how to get the connection state in the client [#799](https://github.com/miguelgrinberg/python-socketio/issues/799) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/47c5f45c765ae207f58ba2675f91eaf8c79f8500)) +- Improved documentation of `start_background_task()` function ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4f5bf1e9898154aa1a9896a7016ba22bfb73cdf2)) +- Improved documentation of `call()` method [#813](https://github.com/miguelgrinberg/python-socketio/issues/813) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8c2a6ac86972bf94acafe687d2e86bdf65119960)) +- Fixed intermittent test failures [#572](https://github.com/miguelgrinberg/python-socketio/issues/572) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/db0565ada6c8891be3230bcc415e5465bd409c09)) + +**Release 5.4.1** - 2021-10-14 + +- Catch-all event handlers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/28569d48ad74d5414a0d2a8f69d7540dbdddf066)) +- Implement disconnect method for external processes [#684](https://github.com/miguelgrinberg/python-socketio/issues/684) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a830c9f7887df715227f4284f30e8d62680e58ce)) + +**Release 5.4.0** - 2021-08-02 + +- Support msgpack and custom packet serializers [#749](https://github.com/miguelgrinberg/python-socketio/issues/749) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5159e84c49daaf2da0579bfc6ee954a9c738a076)) +- Return error packet if client connects to an already connected namespace ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cb1b8ec74bee3b5247200a6fc6e3f6aab3a3f941)) +- Handle CancelledError in async pubsub managers [#750](https://github.com/miguelgrinberg/python-socketio/issues/750) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a813bde0626c16fe693db74cbc2ea7c331a177d3)) +- More robust handling of emit's "to" argument [#689](https://github.com/miguelgrinberg/python-socketio/issues/689) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2f0d8bbd8c4de43fe26e0f2edcd05aef3c8c71f9)) +- Remove executable permissions from setup.py, which has no shebang [#748](https://github.com/miguelgrinberg/python-socketio/issues/748) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/fb3ac958dfa2d3b04f8ec1b0221c3f4734c9f756)) (thanks **Ben Beasley**!) +- Improved project structure ([commit](https://github.com/miguelgrinberg/python-socketio/commit/98c7ac23f231b64cc8b8c51104b792d0cd5cf361)) + +**Release 5.3.0** - 2021-05-15 + +- Document WebSocket support for threading mode ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2f085b3338acc56a5c4625783d50ad53f5ad0c1a)) +- Allow functions to be used for URL, headers and auth data in client connection [#588](https://github.com/miguelgrinberg/python-socketio/issues/588) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7d2e7f7eb3eb34860c2b28df1807a932ed632b54)) +- Emit events to multiple rooms [#605](https://github.com/miguelgrinberg/python-socketio/issues/605) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2538df8bcf0e44881a7653cc684f985252c7fce0)) +- More descriptive error when joining a room on a bad namespace [#650](https://github.com/miguelgrinberg/python-socketio/issues/650) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/66068d9e968942343c840609e6e062162256111b)) +- Document the use of arguments in the `connect_error` handler [#554](https://github.com/miguelgrinberg/python-socketio/issues/554) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d5308504d304b1bd26398309d31d0ce5fbd76e74)) +- Document that callbacks cannot be used in external processes [#1533](https://github.com/miguelgrinberg/Flask-SocketIO/issues/1533) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/703843b42b8fb7fb6a9d0152610e714e9c6fb75e)) +- Improve `start_background_task()` example in the documentation [#647](https://github.com/miguelgrinberg/python-socketio/issues/647) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ef4ae900c5245439921e706fa46008fe5ca102d6)) +- Added Open Collective funding option ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6572ad683ac87981493f69307d5e01cb15a0dacb)) +- Remove Python 2 from PyPI classifiers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a37ab00b344e035adbdc532f2760c223db118c0f)) + +**Release 5.2.1** - 2021-04-18 + +- Fixed incorrect handling of dashes inside the JSON payload of a packet [#675](https://github.com/miguelgrinberg/python-socketio/issues/675) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f4e101079fdc49c673ab9433aca8346480cc9e4f)) + +**Release 5.2.0** - 2021-04-17 + +- Pass custom authentication data with client connection [#661](https://github.com/miguelgrinberg/python-socketio/issues/661) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a07eedf54e535843401cd8b280c1bb24de3dfd27)) +- Configure the JSON decoder for safer parsing ([commit](https://github.com/miguelgrinberg/python-socketio/commit/81b0b849bd7329c7fef2f6a9491aeae279d7b6e5)) (thanks **Onno Kortmann**!) +- Made parsing of id field of Socket.IO packet faster and more robust ([commit](https://github.com/miguelgrinberg/python-socketio/commit/09cb411776b9035343d7349650bc4b84715f00fd)) (thanks **Onno Kortmann**!) +- Correct use of a trailing comma in Socket.IO packets with no id or data [#671](https://github.com/miguelgrinberg/python-socketio/issues/671) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8d1aeb2e401713758a0a2576d0d1ea8eaf14896e)) +- Updated Socket.IO JavaScript client versions in documentation example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9ff8bf354110e860c73f3298278c8be6ba44cb64)) + +**Release 5.1.0** - 2021-03-10 + +- Added `wait` argument to client's connect method [#634](https://github.com/miguelgrinberg/python-socketio/issues/634) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4da6d74f56a58e68b0aef08212347097dd73cda9)) +- Invoke the disconnect handler when the client initiates a disconnection [#594](https://github.com/miguelgrinberg/python-socketio/issues/594) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3349b024d59a78a6a7282257941b4623af72d7c9)) +- Pass auth information sent by client to the connect handler ([commit](https://github.com/miguelgrinberg/python-socketio/commit/11b6f1a08d4840cc2f20a644bd9db7d5d95496bf)) +- Catch all possible Redis errors [#635](https://github.com/miguelgrinberg/python-socketio/issues/635) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6f812ef8e4b86db8d15fcc9a4d79b721fe7ca068)) +- Reset message queue sleep timer upon reconnect ([commit](https://github.com/miguelgrinberg/python-socketio/commit/54180987cd429c6d48c6fdad5c97b1ca894d2b09)) (thanks **Ed Serzo**!) +- Fixed bad event object used by asyncio client reconnect logic [#622](https://github.com/miguelgrinberg/python-socketio/issues/622) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bff76c432c12e678cc1043d8b046bd4a2fe22c28)) +- Adding missing example of async client implementation to documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f341abe88ec19b20cd115dd246dd4bc2b3ad61fe)) (thanks **manuel**!) +- Add scrolling to documentation sidebar [#610](https://github.com/miguelgrinberg/python-socketio/issues/610) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/eabcc4679bc283acdb9f87022ef1e0e82c48497e)) (thanks **Mohammed Abdul Raheem**!) +- Typo fix in documentation [#602](https://github.com/miguelgrinberg/python-socketio/issues/602) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b418af4c53619305c0b91a040e40fc7f3b907a74)) (thanks **Tim Gates**!) + +**Release 5.0.4** - 2020-12-25 + +- Include error message and arguments in CONNECT_ERROR packet [#590](https://github.com/miguelgrinberg/python-socketio/issues/590) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/314971c8a0ba327acd12b0ecfef84f0a5dd63bed)) +- Fix typos in the documentation [#599](https://github.com/miguelgrinberg/python-socketio/issues/599) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c809774c3ec0921306e7274987076b3ce51a4e95)) (thanks **Arpit Jain**!) +- Updated connection options in the documentation [#597](https://github.com/miguelgrinberg/python-socketio/issues/597) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/251fee1763df168fd070b89d83f24698ba0b3bb8)) + +**Release 5.0.3** - 2020-12-14 + +- Correct handling of user session [#585](https://github.com/miguelgrinberg/python-socketio/issues/585) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a61d59c02aa08483f86bf45accf6620b69b06a41)) +- Performace tuning ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bcdf9bb009ef32dac156ea518c1b113c0773877c)) +- Updated ASGI examples to latest release ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8c5b762d724c0cfc45f982ffc3303202985ab812)) +- Updated Django example to latest release ([commit](https://github.com/miguelgrinberg/python-socketio/commit/94afcf643a66d85b64d76f4c219afe8881e8dbe6)) + +**Release 5.0.2** - 2020-12-12 + +- Return `environ`` dictionary for a client ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2e71c22c6db41b82d863b3189c122ea9f7916bf0)) + +**Release 5.0.1** - 2020-12-08 + +- Fix Engine.IO dependency version ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d2bb2b12e536036e34d94a1ad87e0d48a1a504a8)) +- Conversion from Socket.IO sid to Engine.IO sid ([commit](https://github.com/miguelgrinberg/python-socketio/commit/805b33fa7d2e38be4ada60e382c989433fd5af03)) + +**Release 5.0.0** - 2020-12-07 + +- Update to match the JavaScript Socket.IO 3.x releases (Socket.IO v5 protocol revision) + - v5 protocol: do not connect the default namespace unless requested explicitly ([commit](https://github.com/miguelgrinberg/python-socketio/commit/49822e6919d3de9d52f6dde32c7f04ad62d73990)) + - v5 protocol: handle per-namespace sids in base manager ([commit](https://github.com/miguelgrinberg/python-socketio/commit/308b0c8eeb71e1fead35d19088a3291a15ccd50a)) + - v5 protocol: rename ERROR packet to CONNECT_ERROR ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4940fc1e1e2ddc86c28cd3b626dad75d6845243f)) + - v5 protocol: use Engine.IO 4.x +- Remove unnecessary binary argument ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9270a5bcf85785935520ef816d314a5e197ed227)) +- Remove dependency on the six package ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f6eeedb767614fb68b41927c8fd620c95cafcc6c)) +- Added version compatibility chart to README ([commit](https://github.com/miguelgrinberg/python-socketio/commit/342ca0bb9da8b9ea6c63aa3bd05a37903416d301)) + +**Release 4.6.1** - 2020-11-28 + +- Added troubleshooting section to the documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/48906071307d79df356379fe6e05a73b0c65d9d4)) +- Document the use of tuples when emitting ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3ac3437af781d54a343b4430d1f3546c580677e3)) +- Handle the case of not having a previous signal handler [#518](https://github.com/miguelgrinberg/python-socketio/issues/518) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8107216848672792e420b3254b5217e51bcd4b32)) (thanks **David Brooks**!) +- Simplify asserts in unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a4f9992d34a49350a853d9f1a0c3d63034785ab3)) +- Fixed route path for tornado server [#494](https://github.com/miguelgrinberg/python-socketio/issues/494) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4385057c34bb468b9741edcd918e926a17c9af7c)) (thanks **someApprentice**!) +- Use pytest as test runner ([commit](https://github.com/miguelgrinberg/python-socketio/commit/280d132d284699cc1ae641b1e41403ceab6c66f5)) +- Move builds to GitHub actions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/104d5dd97b207cf59f47f1c72b408ed4b1e6f770)) +- Fixed typo in documentation [#524](https://github.com/miguelgrinberg/python-socketio/issues/524) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1fbf95940cfceca13a2033fb3d455810a3b34e83)) (thanks **Fover**!) + +**Release 4.6.0** - 2020-05-23 + +- Improved handling of rejected connections [#391](https://github.com/miguelgrinberg/python-socketio/issues/391) [#487](https://github.com/miguelgrinberg/python-socketio/issues/487) [#447](https://github.com/miguelgrinberg/python-socketio/issues/447) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8d08096dc442e817b5e4ce53321ccf196daafcd1)) +- Fix multi-namespace disconnect logic [#456](https://github.com/miguelgrinberg/python-socketio/issues/456) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/76a9860abc11b5a18fa05e6a8fef815390642d09)) +- `AsyncPubSubManager` does not await for `can_disconnect()` [#488](https://github.com/miguelgrinberg/python-socketio/issues/488) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b0518936b949f759f2be8da0cc7b9c30af440371)) (thanks **Andrei Neagu**!) +- Require a recipient in `call()` function in the server [#476](https://github.com/miguelgrinberg/python-socketio/issues/476) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bbb9a7f0b630b6932960ae2545c62d6e5aee4f09)) (thanks **tt2468**!) +- ASGI startup and shutdown lifespan handlers [#468](https://github.com/miguelgrinberg/python-socketio/issues/468) Co-authored-by: avi ([commit](https://github.com/miguelgrinberg/python-socketio/commit/87d51dd1e6cde94adf4f8fb23828e1537c4f1301)) (thanks **databasedav**!) +- Remove references to Python 2.7 in the documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/02a7ce32c00ed5e64b0fae62d2d5ef93f25367df)) +- Fix server example in docstring [#449](https://github.com/miguelgrinberg/python-socketio/issues/449) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8caebfd524d77d118243878c4bd9d396c420e0a3)) (thanks **wangjiancn**!) +- Fix documentation typo [#450](https://github.com/miguelgrinberg/python-socketio/issues/450) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cdde846e575c98d1316b1857b933bfeabf159580)) (thanks **kizError**!) + +**Release 4.5.1** - 2020-03-22 + +- Fix endless loop when disconnecting on multi-server deployments [#441](https://github.com/miguelgrinberg/python-socketio/issues/441) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/16e873dbc7e100780c83a907a11299bd8269e5e3)) + +**Release 4.5.0** - 2020-03-14 + +- Add support for client disconnects in multi-server configurations [#1174](https://github.com/miguelgrinberg/Flask-SocketIO/issues/1174) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/01378ef1efca73330327006be467270462d504e0)) +- Initialize the client's SIGINT signal handler only if a client is created [#424](https://github.com/miguelgrinberg/python-socketio/issues/424) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/dc89963e328920a3756cefb508213c310ffa730c)) +- Fix for `Server` and `AsyncServer` when emitting no data [#420](https://github.com/miguelgrinberg/python-socketio/issues/420) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/e2242ce40e65c682e031d245db50fdd7956c3b2d)) (thanks **Aaron**!) +- More accurate logging documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d745477abf606f56f566f9d5b1b7bf9ffdb4fbc6)) +- `AsyncClient` documentation fixes [#389](https://github.com/miguelgrinberg/python-socketio/issues/389) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/aa2882cb3e2f3cda3a9d8c94b1c5db1bd0dbbf99)) (thanks **Dmitry Volodin**!) +- Document concurrency problems with emits [#403](https://github.com/miguelgrinberg/python-socketio/issues/403) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d972ca3a5476f5e4e9a114913bdd5f528e558a9f)) +- Minor documentation fixes [#386](https://github.com/miguelgrinberg/python-socketio/issues/386) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d4b403431152cbbbf31ff723562b069a36c330c4)) (thanks **Rotzbua**!) + +**Release 4.4.0** - 2019-11-24 + +- Last version to support Python 2 +- Support the `connect_error` event in the client [#344](https://github.com/miguelgrinberg/python-socketio/issues/344) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/805d5f37413a1e3bbad22012237412803217b4b9)) +- Do not dispatch events for disconnected namespaces [#333](https://github.com/miguelgrinberg/python-socketio/issues/333) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a839a36fa0fa7f0e5d8976ff47b217f6b1e8a44b)) +- Fix documentation typos [#374](https://github.com/miguelgrinberg/python-socketio/issues/374) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b60bbc0307edd0bef2c8b11197ef5a04b2d11b71)) (thanks **Dmitry Volodin**!) +- Updated documentation with new Engine.IO client options ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7c32b379aeeafdb4d6e24e8695734c985753a9d7)) + +**Release 4.3.1** - 2019-08-05 + +- New asyncio based RabbitMQ manager [#320](https://github.com/miguelgrinberg/python-socketio/issues/320) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d984f32e76cc3c204fb6099cbb3c3cd91bfe6e3a)) (thanks **salimaboubacar**!) +- New Apache Kafka manager ([commit](https://github.com/miguelgrinberg/python-socketio/commit/36d17856f43c6ce750e6318e8994b4e2426f480a)) +- Pass additional options to Redis and Kombu managers [#307](https://github.com/miguelgrinberg/python-socketio/issues/307) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7dbc47049a605f78d795b6f59d458522d67c6fe6)) +- Do not allow emits on a namespace that is not connected [#325](https://github.com/miguelgrinberg/python-socketio/issues/325) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f2c1cf7f04cefa57182eeb646c4f3fe246e69b0c)) +- Disconnect Engine.IO connection when server disconnects a client [#1017](https://github.com/miguelgrinberg/Flask-SocketIO/issues/1017) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/516a2958f4e87041aeeea0a0a8e3622d3d636184)) +- Update CORS documentation [#327](https://github.com/miguelgrinberg/python-socketio/issues/327) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6848bed1d74219eeb7f2ead40a77e775f48c68ee)) +- Updated documentation on message queue manager classes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f36fa88d9e4cba33e1c008b5535448326ea3a461)) +- Documentation fixes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d23581e657413043afa378d7be6020940ebf4af8)) + +**Release 4.3.0** - 2019-07-29 + +- Address potential websocket cross-origin attacks [#128](https://github.com/miguelgrinberg/python-engineio/issues/128) ([commit](https://github.com/miguelgrinberg/python-engineio/commit/7548f704a0a3000b7ac8a6c88796c4ae58aa9c37)) +- Documentation for the Same Origin security policy ([commit](https://github.com/miguelgrinberg/python-socketio/commit/045188c63dffeec82539354fd0498fca969e444e)) + +**Release 4.2.1** - 2019-07-27 + +- Added rediss:// URL scheme to AsyncRedisManager [#319](https://github.com/miguelgrinberg/python-socketio/issues/319) * Added rediss:// URL scheme to AsyncRedisManager * Obeyed flake8 ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0b25ff42b8927ac881be7c8ebe1785819bc4c35e)) (thanks **Dylan Anthony**!) + +**Release 4.2.0** - 2019-06-29 + +- Handle keyboard interrupt during reconnect [#301](https://github.com/miguelgrinberg/python-socketio/issues/301) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/fa53e3869ce27af3d497d6e021aa2e3d5c808ece)) +- Added "to" parameter as an alias to "room" ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8a4e5ffa5ceb03b63156b9520e79a4c7414ac214)) +- Correctly autodetect asgi async mode ([commit](https://github.com/miguelgrinberg/python-socketio/commit/eecd3676c15bb7c4c0e165eb02c814cba53a6bb4)) +- Improved documentation on user session behavior on disconnections [#308](https://github.com/miguelgrinberg/python-socketio/issues/308) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2aa8636223d714b1c87323f625645a433ac7e010)) + +**Release 4.1.0** - 2019-06-03 + +- New @event decorator for handler registration ([commit](https://github.com/miguelgrinberg/python-socketio/commit/70ebfdbfa1ad40e471b93dc9b0d3a9c2e7025ce0)) +- Much more flexible support for static files in the server ([commit](https://github.com/miguelgrinberg/python-socketio/commit/aaa87a82779c5dbd5f2cac19991a6ca93bde90ae)) +- Move python-engineio dependency to versions 3.8 and up ([commit](https://github.com/miguelgrinberg/python-socketio/commit/814ff84ec310ce208522c8fd00302fdc1a689f44)) +- Various simplifications to examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0f42c181da7c56ca270814442bdebeecb0e38bf6)) +- Expose the sid for the connection as `sio.sid` ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3b32dbde8d59d0df7c958534732f5d5437f8decf)) + +**Release 4.0.3** - 2019-05-25 + +- skip_sid parameter can also be a list [#202](https://github.com/miguelgrinberg/python-socketio/issues/202) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/00d39ca6985e94e283f5c766c18cacb760e0658d)) +- Fixed Sanic documentation [#193](https://github.com/miguelgrinberg/python-socketio/issues/193) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0249f05a2fb94c29f3be5cb33248b30d6c748eba)) +- Added note on CORS support for sanic [#205](https://github.com/miguelgrinberg/python-socketio/issues/205) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d3e19b7e52debe0db759f56e25eb2706ad2434b6)) +- flake8 fixes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/18fa5286c7505813aca2a8c99606bdcce7cadf31)) +- added python 3.7 build ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7348d923e3af0331706c51053120e672fa0dabc1)) +- added change log ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7803a7bce0fca52f8b40668e97395018be339c16)) +- auto-generate change log during release ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b46dc0fa1a1435dcbd43d21b2c6d0bc33a4da6ea)) + +**Release 4.0.2** - 2019-05-19 + +- properly handle disconnects from ios client ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c0c1bf8d21e3597389b18938550a0724dd9676b7)) +- updated asgi examples to the latest uvicorn ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f86999ff9c3ac5202b017afb9b50036f1f7903a2)) +- helper release script ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4632a3522e7e855ca006f797411f02e56291e07d)) +- fixed requirements file ([commit](https://github.com/miguelgrinberg/python-socketio/commit/dd3608c79e751940491a2841f8a1b1d63de841dc)) +- updated some requirements ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ac7fa5cb5efc750b7d8199fb16557d332f4229d3)) + +**Release 4.0.1** - 2019-04-26 + +- remove unused wait and timeout arguments from send method ([commit](https://github.com/miguelgrinberg/python-socketio/commit/fd91e36799b3ba66c0f5ff22504f54b50951833f)) +- Add missing import statement for socketio in docs [#289](https://github.com/miguelgrinberg/python-socketio/issues/289) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cacb621ad75b64b7bd740e695446cef5e4f335a2)) (thanks **Syed Faheel Ahmad**!) +- change a typo from `client` to `server` [#280](https://github.com/miguelgrinberg/python-socketio/issues/280) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/55e3a6cb02471adff9fa6cd45774003f778b6db4)) (thanks **Almog Cohen**!) +- add link to stack overflow for questions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/65c4675bab7f40a2c446428f1f7ba4faa85c659a)) +- Merge branch 'Keylekan-master' ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3af9a003aa63259bfe4dd7ae25b1bcefc627507a)) +- Add namespaces parameter to the self.connect call in the reconnection process ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c4e4b0b226390a48e4ff9e73a60ff85f68cb9c4c)) (thanks **quentin**!) + +**Release 4.0.0** - 2019-03-09 + +- change async_handlers default to true, and add call() method ([commit](https://github.com/miguelgrinberg/python-socketio/commit/88dcf7d414baec32d6f9f5571d1c03cb09e75c65)) +- Add ConnectionRefusedError and handling for it ([commit](https://github.com/miguelgrinberg/python-socketio/commit/38edd2c93950d85d97258c104af24792b01dc1e3)) (thanks **Andrey Rusanov**!) +- fix python 2 unit test ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9ddc860391ba1b0ea8a43d16d2865e1422346eda)) +- client disconnect does not take namespace as argument [#259](https://github.com/miguelgrinberg/python-socketio/issues/259) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c35451421d2754c09c101c56590152d13cc8b0ea)) +- Avoid double calls to client disconnect handlers [#261](https://github.com/miguelgrinberg/python-socketio/issues/261) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f752312884332e86946df0c4c60ee52dd0590164)) +- readme fixes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1fb1f6d6e5a25159e9c1f35b005098e05a4a18e4)) +- Update sanic version in requirements.txt [#255](https://github.com/miguelgrinberg/python-socketio/issues/255) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6e4079ac64c1c69843fd5b2a9c735bb46671b5bf)) (thanks **maxDzh**!) +- simplify client dependencies ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d28eff7013c084c4f3c491a146bbec35acb18560)) + +**Release 3.1.2** - 2019-01-30 + +- Type in documentation [#241](https://github.com/miguelgrinberg/python-socketio/issues/241) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3c08dda918dd1985c2f10174e7b54f031908099b)) (thanks **Emeka Icha**!) +- fix typo with quotes in doc [#238](https://github.com/miguelgrinberg/python-socketio/issues/238) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a5a67120fec2211002915e8bbcff1c034a5eb93f)) (thanks **Julien Deniau**!) + +**Release 3.1.1** - 2019-01-09 + +- do not use six in setup.py ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b4512b13176b6c04e29803759fff346ca78eb76f)) + +**Release 3.1.0** - 2019-01-03 + +- unit test reorganization ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b0a8b1f31bce4305c00ab0937ddfec1120a2d49d)) +- user sessions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9f2186725adba33b634f7287e3518086e2bbc3ea)) + +**Release 3.0.1** - 2019-01-01 + +- correct handling of disconnects [#227](https://github.com/miguelgrinberg/python-socketio/issues/227) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/45d880cf905f05e1c9966fbe46ed53236bda2851)) + +**Release 3.0.0** - 2018-12-22 + +- SocketIO client +- reorganization of examples, plus some new ones for the client ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4822d8125df11bbf2ef3061c0622164f37a261c9)) +- minor example code fix ([commit](https://github.com/miguelgrinberg/python-socketio/commit/fc5fbcf005f750a67d6aeb7bbd190125ea7970fd)) +- resolve wrong variable and css element [#177](https://github.com/miguelgrinberg/python-socketio/issues/177) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/251b5be9dfc5d88a3bbf146ff52197863d5ce109)) (thanks **iepngs**!) + +**Release 2.1.2** - 2018-12-10 + +- update dependencies ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f642a7f2078fc639ed1302b5b3f7133f19c629c6)) + +**Release 2.1.1** - 2018-12-05 + +- fix backwards compatible problems with python-engineio 3.0 ([commit](https://github.com/miguelgrinberg/python-socketio/commit/fce2006eeef2528e9b59ce9097ff215e6117d787)) + +**Release 2.1.0** - 2018-11-26 + +- ASGI support ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b214380d056dbbfb08273ac482633254176cb847)) +- Fix synchronization issue in SocketIO [#213](https://github.com/miguelgrinberg/python-socketio/issues/213) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4a030d2f9c812b2dd0237883b072189feccac9b7)) (thanks **Anthony Zhang**!) +- Logging improvements for write-only connections [#197](https://github.com/miguelgrinberg/python-socketio/issues/197) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/da7cb863301524aead8c4c422491e84c5a1c10bc)) (thanks **Kelly Truesdale**!) +- improved documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/87fb830bd803809a6577332a838285b4bc22dce5)) +- updated dependencies and fixed linter error ([commit](https://github.com/miguelgrinberg/python-socketio/commit/015ea2037f8fb75c35f8478d14628bf91c54e603)) +- better handling of packets with no data ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f2d28ad62caa4ea6374bf9330b4ad7c79eaf2e05)) +- add python 3.7 to tox ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1777c527781f788571e4f8a1f8e1f0d813ea9b8b)) +- Tornado examples readme ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2a8befdbc9e4fa47a355bf2919319df47b30c973)) +- Tornado docs ([commit](https://github.com/miguelgrinberg/python-socketio/commit/415af129b756d5e955180af314589bdc0c5930ff)) + +**Release 2.0.0** - 2018-06-28 + +- Tornado 5 support ([commit](https://github.com/miguelgrinberg/python-socketio/commit/555b69e80754ceb90d7c7aab1825cc37724b2e90)) +- Fix typo in docs [#187](https://github.com/miguelgrinberg/python-socketio/issues/187) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/58c8a6f4301758ff27fb95d28b939bf953a67119)) (thanks **Grey Li**!) +- Update documentation link [#185](https://github.com/miguelgrinberg/python-socketio/issues/185) Pythonhosted.org was deprecated. ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4131e539f8b480ecd11f0c03239a5359a3012e34)) (thanks **Grey Li**!) +- typo fix [#180](https://github.com/miguelgrinberg/python-socketio/issues/180) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c24c91a72521eeb19eb5ee60e06006da5e8c7578)) (thanks **Toon Knapen**!) +- add pypy3 target to travis builds ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ad37d0db930edf773221cfad40582618c821cb48)) + +**Release 1.9.0** - 2018-03-07 + +- assigning local ret variable to none if the asyncio task is canceled [#164](https://github.com/miguelgrinberg/python-socketio/issues/164) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/598568c7152dc970dc740de040697e97cf374ec1)) (thanks **rettier**!) +- Recoonect to redis when connection is lost [#143](https://github.com/miguelgrinberg/python-socketio/issues/143) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/af13ef067c0f5b357e80d0e7f718bb725ed55fd6)) + +**Release 1.8.4** - 2017-12-11 + +- properly handle callbacks in multi-host configurations for asyncio ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b3fc842c76d53c2632192c5fda3db42a0bb764cd)) +- Properly handle callbacks in multi-host configurations [#150](https://github.com/miguelgrinberg/python-socketio/issues/150) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8d7059a1a22e2d5e3092623251ab357046595a33)) +- aiohttp instead of flask in example HTML [#91](https://github.com/miguelgrinberg/python-socketio/issues/91) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/06e7cece74b01707247d28537537eafd73d96f02)) (thanks **Jacopo Farina**!) +- Fix backquotes usage in docs. [#146](https://github.com/miguelgrinberg/python-socketio/issues/146) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ef7088d67a5b26fa57f921797057622f6525f768)) (thanks **Kane Blueriver**!) +- updated socketio js client to v2.0.4 in all examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8415a7a33c324ee70002bf8893bb388768fd5cb1)) + +**Release 1.8.3** - 2017-11-13 + +- fixed memory leak on rejected connections for asyncio ([commit](https://github.com/miguelgrinberg/python-socketio/commit/935077563490f890f1b1d596be9fc38c8d65588b)) + +**Release 1.8.2** - 2017-11-13 + +- fixed memory leak on rejected connections [#574](https://github.com/miguelgrinberg/Flask-SocketIO/issues/574) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/66e17fb387510522bc9a1718ae9da8b0f6ace005)) + +**Release 1.8.1** - 2017-10-02 + +- JavaScript client sends query string attached to namespace [#124](https://github.com/miguelgrinberg/python-socketio/issues/124) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7f02f7aaa92c18c043466cb4721356221361f481)) +- Update README.rst ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8bba811408b2e3a570701b34113a53662915f97c)) +- Documented protocol defaults ([commit](https://github.com/miguelgrinberg/python-socketio/commit/187c52582ba4a3c08a368e6b5f4033a417a432f1)) + +**Release 1.8.0** - 2017-07-26 + +- Made queues non-durable. Always retry after disconnect. [#120](https://github.com/miguelgrinberg/python-socketio/issues/120) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/358a8e118fe73920fb7e0b72811f14c0daedeea2)) (thanks **Alex Plugaru**!) + +**Release 1.7.7** - 2017-07-20 + +- pass redis password in the URL ([commit](https://github.com/miguelgrinberg/python-socketio/commit/af811326103abf90338987a326190df210c421af)) +- redis pub/sub support set password [#116](https://github.com/miguelgrinberg/python-socketio/issues/116) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8cf035fb3a13651060f7241c3460e3339891e1e5)) (thanks **larry.liu**!) + +**Release 1.7.6** - 2017-07-02 + +- async_handlers option for asyncio servers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/93f70dc9e1e9bc4cb13f631edc4c12cf110356b7)) +- fixed requirements file for python 2 ([commit](https://github.com/miguelgrinberg/python-socketio/commit/690be4fd194747593ab44acd0a1ea4de5dbcbfa4)) +- minor improvements to django example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1ecbf5bef0c16d5665f72fad205af3ad45679ae4)) +- Django example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3531b513a845a5bcac1b7ae511aeb71e19de7256)) + +**Release 1.7.5** - 2017-05-30 + +- validate namespace in disconnect call [#427](https://github.com/miguelgrinberg/Flask-SocketIO/issues/427) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3ae013b90e1915a99abe8ca0b7ed8611cc4203d2)) +- The usage of class-based namespace There should be an additional `self` argument passed to each member function of custom Namespaces. ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8e734573a9f792d8dbfb143cba1f10056930f0e9)) (thanks **Kaiyu Shi**!) +- fix misleading error message ([commit](https://github.com/miguelgrinberg/python-socketio/commit/36f8d35d08a29298b8e4acefb7bb7ba67bddf54c)) + +**Release 1.7.4** - 2017-03-28 + +- Handle broadcasts to zero clients [#88](https://github.com/miguelgrinberg/python-socketio/issues/88) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/af0004d78edda2da1d9bbcab9742ca75976d4373)) + +**Release 2.7.3** - 2017-03-22 + +- Fix variable referenced before assignment [#85](https://github.com/miguelgrinberg/python-socketio/issues/85) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5b9b17c4a0654811aeda4184d159dafb5e0e4041)) +- use Python 3.6 for docs build ([commit](https://github.com/miguelgrinberg/python-socketio/commit/142e4787375782c74d6d0b992115269dc290069b)) + +**Release 1.7.2** - 2017-03-08 + +- websocket support for sanic ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2582f286f592c881fefeed0b639edecd41d81e4c)) + +**Release 1.7.1** - 2017-02-15 + +- Fixed initialize() method in PubSubManager subclasses [#406](https://github.com/miguelgrinberg/python-socketio/issues/406) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5e1391d80ee2e6b215819ea00df4d166f0ae3f0b)) +- Update README.rst Spelling correction ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d6839e9ae833bbb5aa8bbaff1e907c1869fdfc59)) (thanks **Ken W. Alger**!) +- sanic examples (long-polling only) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/38af61ee1f3fa2b76c87e12e83077a9b20156db9)) +- fixed link in readme file ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ea94110559c3ada0b1a78e57f72fd3e6a8f0c34e)) + +**Release 1.7.0** - 2017-02-12 + +- redis message queue for asyncio ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ce44133acd66d53d607937d04261668fbc2aa328)) +- updated documentation logo ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2e55d7f0208fe0679fd87081c9598640a14c2fdb)) +- asyncio documentation and various fixes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/43788db7a77421ec9f84f1eb02b60f9bc09d29d3)) +- readme file updates ([commit](https://github.com/miguelgrinberg/python-socketio/commit/83379732f9a597d248c9f191304fcfbf1622b7b6)) +- updated public symbols ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3556c3e7ce547e0d49255e883db3128fdf9974b6)) +- updated package requirements ([commit](https://github.com/miguelgrinberg/python-socketio/commit/561065141a8121f544a718530ed87bced46ed529)) +- readme files and requirements for all examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5c882acd884271a9fa05ab3397e872ddaa1235a5)) +- async namespaces, and more unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6f41206f7dfda01a8ad3d75110eb1022b6b768f3)) +- a few asyncio related fixes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/763583226a91505051889ac3b23bf1183aa6c421)) +- asyncio support ([commit](https://github.com/miguelgrinberg/python-socketio/commit/53d10d9f3204a86b8c48ffca347857044215d009)) +- fixed documentation typo [#19](https://github.com/miguelgrinberg/python-socketio/issues/19) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6c93f7fb15e5848de48daefe5998139734333dd9)) +- minor fixes to zeromq support ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f0f6b18f42c7b9a2d4f9806b854849ae2202d547)) +- use non-blocking eventlet zmq wrapper in listen method ([commit](https://github.com/miguelgrinberg/python-socketio/commit/10d273b3feee216ea711689004454e87c3b237bc)) (thanks **Eric Seidler**!) + +**Release 1.6.3** - 2017-01-23 + +- allow event names with hyphens [#51](https://github.com/miguelgrinberg/python-socketio/issues/51) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/940d262a1ed9b45969895b93b7184f93bdab71b9)) +- Merge branch 'Kurlov-fix-namespace-hyphens' ([commit](https://github.com/miguelgrinberg/python-socketio/commit/923ded03b2fba18ee84491d11b5059eefa81cc99)) +- Fixed hyphens namcespace bug ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4f35e67e42fad8b75d9f755a00141d5ef86eb76d)) (thanks **Aleksandr Kurlov**!) +- removed py33 from tests, added py36 ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3c868255a481ef17deb53f022e668a60957a2f17)) +- handle failed pickled.loads ([commit](https://github.com/miguelgrinberg/python-socketio/commit/42ad98e750afd91847f65a7221de665da6585495)) (thanks **Eric Seidler**!) +- check message type before yielding message data ([commit](https://github.com/miguelgrinberg/python-socketio/commit/09d8d5d0d40d71e246d5c1a1ba8d2375b591f833)) (thanks **Eric Seidler**!) +- add zmq prefix to default value for url ([commit](https://github.com/miguelgrinberg/python-socketio/commit/fab0683bebbc550d9e9fef418baa07fc510f71d0)) (thanks **Eric Seidler**!) +- add zmq manager ([commit](https://github.com/miguelgrinberg/python-socketio/commit/88f3b87efa9f91d1e6eb23a110962d8d664ff1c3)) (thanks **Eric Seidler**!) +- add ZmqManager to dunder init ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d6f703f1bd36b1a68e0e555212ef128fef3aacad)) (thanks **Eric Seidler**!) + +**Release 1.6.2** - 2017-01-03 + +- prevent binary attachments from getting mixed up Flask-SocketIO issue [#385](https://github.com/miguelgrinberg/python-socketio/issues/385) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2c98906aafc53ae84e6fe7492ca6f1f48115ce58)) +- Merge branch 'AndrewPashkin-clarify-level-of-separation-of-namespaces' ([commit](https://github.com/miguelgrinberg/python-socketio/commit/652ced75c010877e5b2566f61e9ce2098869dc0d)) +- Add mentioning of SIDs to the list of what each namespace has separate from other namespaces. ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8a7ea67f99f23d5b83274bda62d6345108d022be)) (thanks **Andrew Pashkin**!) + +**Release 1.6.1** - 2016-11-26 + +- Added "ignore_queue" option to bypass message queue in emits ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3eac53261b9d602da8d27c1f9b31f92f86a3b395)) +- Use a statically allocated logger by default ([commit](https://github.com/miguelgrinberg/python-socketio/commit/749f8663c48cf100440ba51724542577e46c1b58)) +- Warn when message queues are used without monkey patching ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6ba131af5cf6238fe4a4a702b38dfec2bb1292f9)) +- Added clarification regarding class based namespace being singletons [#59](https://github.com/miguelgrinberg/python-socketio/issues/59) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d09627faff80e78b92a27ec8f8c46a846002e873)) +- Fix typo Sokcet -> Socket [#56](https://github.com/miguelgrinberg/python-socketio/issues/56) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/415a0eb9c7d4105d75f569e455b8f293dc6540c4)) (thanks **Lenno Nagel**!) + +**Release 1.6.0** - 2016-09-25 + +- some improvements and optimizations to KombuManager class ([commit](https://github.com/miguelgrinberg/python-socketio/commit/052fd937453dc098761dba10c7240c39bdb5d750)) +- add a TTL option to Kombu queues when RabbitMQ is used ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cc9027586f045b6aa96e832bb287971762fb339d)) +- put clients in a pre-disconnect state while their disconnect handler runs This avoids potential endless recursion. [#312](https://github.com/miguelgrinberg/Flask-SocketIO/issues/312) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/da2d141e8d68fd698ddb2dd5d54c4b1a7622d4df)) +- do not disconnect an already disconnected client [#312](https://github.com/miguelgrinberg/Flask-SocketIO/issues/312) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a58c184b0ffe76d8b00912b0084b12c26bd85631)) + +**Release 1.5.1** - 2016-09-04 + +- add __version__ to package ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b436d60a9ffe787a25e284046d32f5d9f8c18f58)) +- document the use of the new gevent_uwsgi async mode ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5529fbb2bd13654ed34094629bf77250ccf60167)) + +**Release 1.5.0** - 2016-08-26 + +- add async_handlers option to server ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2d39058b7c693bf74c8e3169d72c21a5597d73d8)) +- minor class-based namespace fixes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/214abc8d31927673d9b853cf6a901cec1f3988ea)) +- minor documentation fixes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5cc91993369c33ea090782dba1256cce90140295)) +- class-based namespaces ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7bc329a12d9f9b4ad4953dc74505167ee4dd5f57)) +- minor correction in the readme file example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6abd86fd868ae4bd5e23b02401f638ec1d0bad2c)) +- Update README.rst Add the code highlight to python ([commit](https://github.com/miguelgrinberg/python-socketio/commit/44b1acb6973882be9c89e48e6f187de7aecef8e9)) (thanks **Rodolfo Silva**!) +- add explicit eventlet.wsgi import ([commit](https://github.com/miguelgrinberg/python-socketio/commit/33bd933d8b6c5926c751d6a83f38a2ab7e524b36)) +- Handle empty argument list for callbacks [#26](https://github.com/miguelgrinberg/python-socketio/issues/26) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/544b01ad3df74cd92199c251292d6a8fdb09f47b)) + +**Release 1.4.4** - 2016-08-05 + +- minor documentation improvement ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c52f4f6a594f91fc69473d492c0e3117b32de21e)) +- Fix race condition in handling of binary attachments [#37](https://github.com/miguelgrinberg/python-socketio/issues/37) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/024609e10e570ccd2e932a0584c5a1784c4bbf75)) +- release 1.4.3 ([commit](https://github.com/miguelgrinberg/python-socketio/commit/047712da097b44812d09a88537c6877757b25eba)) +- Do not allow event names with hyphens in them [#36](https://github.com/miguelgrinberg/python-socketio/issues/36) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a6838a233d051b406baeb9a4c09e50f62bc9f14a)) +- add unit test for sleep function ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a3ae2a938472f103d543ad46ff6d923c0f2415ee)) + +**Release 1.4.2** - 2016-07-12 + +- fix the order of triggered disconnect event [#33](https://github.com/miguelgrinberg/python-socketio/issues/33) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ea1a52ac84cf2cd3d5cb6d1c0f6a64932159d835)) (thanks **Hanzawa Ye**!) +- Correct sio.emit() call in readme [#32](https://github.com/miguelgrinberg/python-socketio/issues/32) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a1ca2c421c4f2f05e83372e4dac0b3826be66d16)) (thanks **winek**!) + +**Release 1.4.1** - 2016-06-28 + +- pass return value of start_background_task to the caller ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cd9e1a7c6427eb6e13abea15d84dfc692f843df2)) +- a few improvements to examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0810f0b039753d9c98f83eebf5a1b183aabd6d49)) + +**Release 1.4** - 2016-06-28 + +- a few improvements to examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/20185c9a95bcd1ab4d4457b483cf6172eaf020c2)) +- expose async_mode and sleep from engineio ([commit](https://github.com/miguelgrinberg/python-socketio/commit/065cc4b2219d22f7b470600073f21c47c09d5578)) + +**Release 1.3** - 2016-05-15 + +- delay start of message queue listener thread until first request comes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b6df63d8492c9a18e7d4a7ea5e8378e076b7a723)) +- Avoid KeyError when no room exists [#27](https://github.com/miguelgrinberg/python-socketio/issues/27) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/626499cd4596c13a41a5ecbe3e350b243c5fe6be)) (thanks **Patrick Decat**!) + +**Release 1.2** - 2016-03-21 + +- Avoid KeyError in is_connected. [#22](https://github.com/miguelgrinberg/python-socketio/issues/22) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cba6c3ed8b8b2c777538a5a35e73f98f4ec9d4bb)) (thanks **Chris Doehring**!) + +**Release v1.1** - 2016-03-06 + +- remove disconnected client before invoking disconnect handler ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d34f49b3b19c39f0beaed9d0e762f070b71f7104)) +- Eliminate problematic _clean_rooms method ([commit](https://github.com/miguelgrinberg/python-socketio/commit/370db6488bfee5db9d90d675fd0ed8fae9baab0d)) +- handle leaving a room and entering again right after [#17](https://github.com/miguelgrinberg/python-socketio/issues/17) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/74e9ab11dcaef919cba198aef6b8124dcfc3007c)) +- document the need to monkey patch with eventlet and gevent ([commit](https://github.com/miguelgrinberg/python-socketio/commit/dabe33f1a1d332649f42c9adfd9e5ec15501e64c)) + +**Release 1.0** - 2016-01-17 + +- Expand tuples to multiple arguments, but not lists ([commit](https://github.com/miguelgrinberg/python-socketio/commit/259f98d3cdc7274007c7ad4781979cc108b667f2)) + +**Release 0.9.2** - 2016-01-16 + +- silence a flak8 error on imports not at top ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ba31975d65108c106d9193277441e8acad18baf9)) +- Use separate read and write Kombu connections Eventlet does not like file handles to be shared among greenlets. Using an independent connection in the listening thread addresses this problem. [#13](https://github.com/miguelgrinberg/python-socketio/issues/13) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0c357573599e687137ba2e8648b2cac49c8b7bbb)) + +**Release 0.9.1** - 2016-01-10 + +- Pass result of connect handler up to engineio ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d8982dba81fe56a16e10f80b01e5f262e4b3b0ae)) + +**Release 0.9.0** - 2016-01-10 + +- Add write_only argument to Kombu and Redis manager classes ([commit](https://github.com/miguelgrinberg/python-socketio/commit/57756e3bdcc025e1bc31f67f8a44c0b38fc3747c)) +- minor message queue documentation improvements ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0e47a75a3a937beed271a797cc8dbda1eb6c9de1)) + +**Release 0.8.2** - 2015-12-30 + +- correct kombu implementation of a fanout queue ([commit](https://github.com/miguelgrinberg/python-socketio/commit/73fd49903387725b47498625005a6a1b13ff6948)) + +**Release 0.8.1** - 2015-12-14 + + +**Release 0.8.0** - 2015-12-14 + +- documentation updates ([commit](https://github.com/miguelgrinberg/python-socketio/commit/e56ff807f4faabb4e7ef339fda79de5b8a201b26)) +- do not allow callbacks outside of a server context ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6ae89688d72ec7f570a0b04a1bbf0483865bb5b5)) +- message queue documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8ee4cf7e7557aeb02079c8402029eec5b68c79b6)) +- pubsub unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/71142aa2db1434ab799edebeaa97020c6ec21089)) +- README typo fix Important typo fix in README example code. ([commit](https://github.com/miguelgrinberg/python-socketio/commit/71e99d9a6b0804942bb9e507b3ce984143bee3ba)) (thanks **Davis Miculis**!) +- Support for callbacks across servers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/63f5ed3429f96afd8f3a7cd82f281c2e5db93de1)) +- initial implementation of inter-process communication ([commit](https://github.com/miguelgrinberg/python-socketio/commit/47620bbebd92a1b388df72a88c0ca35cdb530073)) + +**Release 0.7.0** - 2015-11-22 + +- Correctly handle payloads that are empty lists or dictionaries [#5](https://github.com/miguelgrinberg/python-socketio/issues/5) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a72f2dfb052d1f60a81edf1bb783c7aa4562f124)) +- Added python 3.5 to the tox build ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3cb904cfd83f7407b2e6317b349bea925c7333a4)) + +**Release 0.6.1** - 2015-10-31 + +- support packets with empty payload given as an integer ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d31d167c78203732c74a97f5a98788462231e01f)) + +**Release 0.6.0** - 2015-10-17 + + +**Release 0.5.2** - 2015-09-22 + +- fixed regression introduced in latest release with ack ids ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4a4ba41d0c7c6c5547fc73065ab2279ca7fce76b)) + +**Release 0.5.1** - 2015-09-16 + +- Cleaned up the interface to provide a custom client manager ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5bb6c9da7df017318975d35e1221056ea5670d2d)) +- Move ack functionality into BaseManager class ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ad12b837be360d4d9dbe1a1e5e1afdb573ab335d)) + +**Release 0.5.0** - 2015-09-02 + +- Added a latency check example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/798c126d76247d9b41d07bf30aaa41d5daea63bb)) +- Fix executable bits in several files ([commit](https://github.com/miguelgrinberg/python-socketio/commit/171008023e3662ba5375c3c76cc7629a4620eaa1)) +- Added transport method to server class ([commit](https://github.com/miguelgrinberg/python-socketio/commit/37cd746a8eac8a23c6b3e396a2ae48510fcdb974)) +- Updated example requirements ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a1e232f8c77f0599fd53dc2b683b133436dda5a9)) +- Allow application to provide a custom JSON encoder/decoder. ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f3967320883f8e5d41df0cc69475fa89eb6357aa)) + +**Release 0.4.2** - 2015-08-23 + +- Improved handling of logging ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4cb515136336864de0012595963341f81f262dc7)) +- Updated example app to use gevent websocket if available ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7b2e4ab88789a52f1e91be538a1b5da6fb8dff02)) + +**Release 0.4.1** - 2015-08-20 + +- Added docs on websocket support with gevent ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b59cefcfdb42e13646b4f1bee50e483935b8e57c)) +- Fixed executable bit on several files ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9cee03859c69cfb5aac37dac4163e852bec7e8e0)) + +**Release 0.4.0** - 2015-08-08 + +- Added support for gevent and standard threads besides eventlet ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8e570789aabb90fd494d6468a5512d631e56d60f)) +- added server disconnect support ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5633917201825366771011e10d7a3df3d2dc8a75)) + +**Release 0.3.0** - 2015-07-27 + +- allow events to be sent from the connect handler ([commit](https://github.com/miguelgrinberg/python-socketio/commit/19042e157061345938450bcd9d098ffbef2acecd)) + +**Release 0.2.0** - 2015-07-20 + +- Return the rooms a client is in ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d4cd9de799726a8ed6f0d30e15c9bf80047d7de6)) +- Added build files ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7195217b3bb087530dc1cd080b69e4cc7f5c6914)) +- Initial commit ([commit](https://github.com/miguelgrinberg/python-socketio/commit/aa2e146a60aa9b2257ce748690104c796a09b551)) diff --git a/MANIFEST.in b/MANIFEST.in index 64ad321d..10dd5123 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,5 @@ -include README.md LICENSE +include README.md LICENSE tox.ini +recursive-include docs * +recursive-exclude docs/_build * +recursive-include tests * +exclude **/*.pyc diff --git a/README.md b/README.md new file mode 100644 index 00000000..780465f4 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +python-socketio +=============== + +[![Build status](https://github.com/miguelgrinberg/python-socketio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/python-socketio/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/python-socketio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/python-socketio) + +Python implementation of the `Socket.IO` realtime client and server. + +Sponsors +-------- + +The following organizations are funding this project: + +![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| +-|- + +Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? + +Version compatibility +--------------------- + +The Socket.IO protocol has been through a number of revisions, and some of these +introduced backward incompatible changes, which means that the client and the +server must use compatible versions for everything to work. + +If you are using the Python client and server, the easiest way to ensure compatibility +is to use the same version of this package for the client and the server. If you are +using this package with a different client or server, then you must ensure the +versions are compatible. + +The version compatibility chart below maps versions of this package to versions +of the JavaScript reference implementation and the versions of the Socket.IO and +Engine.IO protocols. + +JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol revision | python-socketio version +-|-|-|- +0.9.x | 1, 2 | 1, 2 | Not supported +1.x and 2.x | 3, 4 | 3 | 4.x +3.x and 4.x | 5 | 4 | 5.x + +Resources +--------- + +- [Documentation](http://python-socketio.readthedocs.io/) +- [PyPI](https://pypi.python.org/pypi/python-socketio) +- [Change Log](https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md) +- Questions? See the [questions](https://stackoverflow.com/questions/tagged/python-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+python-socketio) your own question. diff --git a/README.rst b/README.rst deleted file mode 100644 index ce09f4b9..00000000 --- a/README.rst +++ /dev/null @@ -1,127 +0,0 @@ -python-socketio -=============== - -.. image:: https://travis-ci.org/miguelgrinberg/python-socketio.svg?branch=master - :target: https://travis-ci.org/miguelgrinberg/python-socketio - -Python implementation of the `Socket.IO `_ -realtime server. - -Features --------- - -- Fully compatible with the - `Javascript `_, - `Swift `_, - `C++ `_ and - `Java `_ official - Socket.IO clients, plus any third party clients that comply with the - Socket.IO specification. -- Compatible with Python 2.7 and Python 3.3+. -- Supports large number of clients even on modest hardware when used with an - asynchronous server based on `asyncio `_ - (`sanic `_ and `aiohttp `_), - `eventlet `_ or `gevent `_. For - development and testing, any WSGI compliant multi-threaded server can also be - used. -- Includes a WSGI middleware that integrates Socket.IO traffic with standard - WSGI applications. -- Broadcasting of messages to all connected clients, or to subsets of them - assigned to "rooms". -- Optional support for multiple servers, connected through a messaging queue - such as Redis or RabbitMQ. -- Send messages to clients from external processes, such as Celery workers or - auxiliary scripts. -- Event-based architecture implemented with decorators that hides the details - of the protocol. -- Support for HTTP long-polling and WebSocket transports. -- Support for XHR2 and XHR browsers. -- Support for text and binary messages. -- Support for gzip and deflate HTTP compression. -- Configurable CORS responses, to avoid cross-origin problems with browsers. - -Example -------- - -The following example application uses the `aiohttp `_ -framework for asyncio: - -.. code:: python - - from aiohttp import web - import socketio - - sio = socketio.AsyncServer() - app = web.Application() - sio.attach(app) - - async def index(request): - """Serve the client-side application.""" - with open('index.html') as f: - return web.Response(text=f.read(), content_type='text/html') - - @sio.on('connect', namespace='/chat') - def connect(sid, environ): - print("connect ", sid) - - @sio.on('chat message', namespace='/chat') - async def message(sid, data): - print("message ", data) - await sio.emit('reply', room=sid) - - @sio.on('disconnect', namespace='/chat') - def disconnect(sid): - print('disconnect ', sid) - - app.router.add_static('/static', 'static') - app.router.add_get('/', index) - - if __name__ == '__main__': - web.run_app(app) - -And below is a similar example, using Flask to serve the client application. -This example is compatible with Python 2.7 and Python 3.3+: - -.. code:: python - - import socketio - import eventlet - import eventlet.wsgi - from flask import Flask, render_template - - sio = socketio.Server() - app = Flask(__name__) - - @app.route('/') - def index(): - """Serve the client-side application.""" - return render_template('index.html') - - @sio.on('connect', namespace='/chat') - def connect(sid, environ): - print("connect ", sid) - - @sio.on('chat message', namespace='/chat') - def message(sid, data): - print("message ", data) - sio.emit('reply', room=sid) - - @sio.on('disconnect', namespace='/chat') - def disconnect(sid): - print('disconnect ', sid) - - if __name__ == '__main__': - # wrap Flask application with engineio's middleware - app = socketio.Middleware(sio, app) - - # deploy as an eventlet WSGI server - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) - -Resources ---------- - -- `Documentation`_ -- `PyPI`_ - -.. _Documentation: http://pythonhosted.org/python-socketio -.. _PyPI: https://pypi.python.org/pypi/python-socketio diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..bfea1de3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +If you think you've found a vulnerability on this project, please send me (Miguel Grinberg) an email at mailto:miguel.grinberg@gmail.com with a description of the problem. I will personally review the issue and respond to you with next steps. + +If the issue is highly sensitive, you are welcome to encrypt your message. Here is my [PGP key](http://pgp.mit.edu/pks/lookup?search=miguel.grinberg%40gmail.com&op=index). + +Please do not disclose vulnerabilities publicly before discussing how to proceed with me. diff --git a/docs/Makefile b/docs/Makefile index 68ceda87..298ea9e2 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,192 +1,19 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = +SOURCEDIR = . BUILDDIR = _build -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext - +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/socketio.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/socketio.qhc" - -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/socketio" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/socketio" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." +.PHONY: help Makefile -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/_static/README.md b/docs/_static/README.md new file mode 100644 index 00000000..0d94aa7d --- /dev/null +++ b/docs/_static/README.md @@ -0,0 +1 @@ +Place static files used by the documentation here. diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 00000000..9a862943 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,5 @@ +div.sphinxsidebar { + max-height: calc(100% - 30px); + overflow-y: auto; + overflow-x: hidden; +} diff --git a/docs/_static/index.html b/docs/_static/index.html deleted file mode 100644 index d2f10fe5..00000000 --- a/docs/_static/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - python-socketio documentation - - - - - The python-socketio documentation is available at Read the Docs. - If your browser does not automatically redirect you, please click here. - - diff --git a/docs/_static/logo.png b/docs/_static/logo.png deleted file mode 100644 index bd27187c..00000000 Binary files a/docs/_static/logo.png and /dev/null differ diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE deleted file mode 100644 index 8daab7ee..00000000 --- a/docs/_themes/LICENSE +++ /dev/null @@ -1,37 +0,0 @@ -Copyright (c) 2010 by Armin Ronacher. - -Some rights reserved. - -Redistribution and use in source and binary forms of the theme, with or -without modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -We kindly ask you to only use these themes in an unmodified manner just -for Flask and Flask-related products, not for unrelated projects. If you -like the visual style and want to use it for your own projects, please -consider making some larger changes to the themes (such as changing -font faces, sizes, colors or margins). - -THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/_themes/README b/docs/_themes/README deleted file mode 100644 index b3292bdf..00000000 --- a/docs/_themes/README +++ /dev/null @@ -1,31 +0,0 @@ -Flask Sphinx Styles -=================== - -This repository contains sphinx styles for Flask and Flask related -projects. To use this style in your Sphinx documentation, follow -this guide: - -1. put this folder as _themes into your docs folder. Alternatively - you can also use git submodules to check out the contents there. -2. add this to your conf.py: - - sys.path.append(os.path.abspath('_themes')) - html_theme_path = ['_themes'] - html_theme = 'flask' - -The following themes exist: - -- 'flask' - the standard flask documentation theme for large - projects -- 'flask_small' - small one-page theme. Intended to be used by - very small addon libraries for flask. - -The following options exist for the flask_small theme: - - [options] - index_logo = '' filename of a picture in _static - to be used as replacement for the - h1 in the index.rst file. - index_logo_height = 120px height of the index logo - github_fork = '' repository name on github for the - "fork me" badge diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html deleted file mode 100644 index 19c43fbb..00000000 --- a/docs/_themes/flask/layout.html +++ /dev/null @@ -1,24 +0,0 @@ -{%- extends "basic/layout.html" %} -{%- block extrahead %} - {{ super() }} - {% if theme_touch_icon %} - - {% endif %} - -{% endblock %} -{%- block relbar2 %}{% endblock %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{%- block footer %} - - {% if pagename == 'index' %} -
- {% endif %} -{%- endblock %} diff --git a/docs/_themes/flask/relations.html b/docs/_themes/flask/relations.html deleted file mode 100644 index 3bbcde85..00000000 --- a/docs/_themes/flask/relations.html +++ /dev/null @@ -1,19 +0,0 @@ -

Related Topics

- diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t deleted file mode 100644 index 5906e751..00000000 --- a/docs/_themes/flask/static/flasky.css_t +++ /dev/null @@ -1,577 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -{% set page_width = '940px' %} -{% set sidebar_width = '220px' %} - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - background-color: white; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - width: {{ page_width }}; - margin: 30px auto 0 auto; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ sidebar_width }}; -} - -div.sphinxsidebar { - width: {{ sidebar_width }}; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 0 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - width: {{ page_width }}; - margin: 20px auto 30px auto; - font-size: 14px; - color: #888; - text-align: right; -} - -div.footer a { - color: #888; -} - -div.related { - display: none; -} - -div.sphinxsidebar a { - color: #444; - text-decoration: none; - border-bottom: 1px dotted #999; -} - -div.sphinxsidebar a:hover { - border-bottom: 1px solid #999; -} - -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 18px 10px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 0 0 20px 0; - margin: 0; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: 'Garamond', 'Georgia', serif; - color: #444; - font-size: 24px; - font-weight: normal; - margin: 0 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 20px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p.logo a, -div.sphinxsidebar h3 a, -div.sphinxsidebar p.logo a:hover, -div.sphinxsidebar h3 a:hover { - border: none; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: 'Georgia', serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} -div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #ddd; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition tt.xref, div.admonition a tt { - border-bottom: 1px solid #fafafa; -} - -dd div.admonition { - margin-left: -60px; - padding-left: 60px; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight { - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; - background: #fdfdfd; - font-size: 0.9em; -} - -table.footnote + table.footnote { - margin-top: -15px; - border-top: none; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td.label { - width: 0px; - padding: 0.3em 0 0.3em 0.5em; -} - -table.footnote td { - padding: 0.3em 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -blockquote { - margin: 0 0 0 30px; - padding: 0; -} - -ul, ol { - margin: 10px 0 10px 30px; - padding: 0; -} - -pre { - background: #eee; - padding: 7px 30px; - margin: 15px -30px; - line-height: 1.3em; -} - -dl pre, blockquote pre, li pre { - margin-left: -60px; - padding-left: 60px; -} - -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; - border-bottom: 1px solid white; -} - -a.reference { - text-decoration: none; - border-bottom: 1px dotted #004B6B; -} - -a.reference:hover { - border-bottom: 1px solid #6D4100; -} - -a.footnote-reference { - text-decoration: none; - font-size: 0.7em; - vertical-align: top; - border-bottom: 1px dotted #004B6B; -} - -a.footnote-reference:hover { - border-bottom: 1px solid #6D4100; -} - -a:hover tt { - background: #EEE; -} - - -@media screen and (max-width: 870px) { - - div.sphinxsidebar { - display: none; - } - - div.document { - width: 100%; - - } - - div.documentwrapper { - margin-left: 0; - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - } - - div.bodywrapper { - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - margin-left: 0; - } - - ul { - margin-left: 0; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .bodywrapper { - margin: 0; - } - - .footer { - width: auto; - } - - .github { - display: none; - } - - - -} - - - -@media screen and (max-width: 875px) { - - body { - margin: 0; - padding: 20px 30px; - } - - div.documentwrapper { - float: none; - background: white; - } - - div.sphinxsidebar { - display: block; - float: none; - width: 102.5%; - margin: 50px -30px -20px -30px; - padding: 10px 20px; - background: #333; - color: white; - } - - div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, - div.sphinxsidebar h3 a { - color: white; - } - - div.sphinxsidebar a { - color: #aaa; - } - - div.sphinxsidebar p.logo { - display: none; - } - - div.document { - width: 100%; - margin: 0; - } - - div.related { - display: block; - margin: 0; - padding: 10px 0 20px 0; - } - - div.related ul, - div.related ul li { - margin: 0; - padding: 0; - } - - div.footer { - display: none; - } - - div.bodywrapper { - margin: 0; - } - - div.body { - min-height: 0; - padding: 0; - } - - .rtd_doc_footer { - display: none; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .footer { - width: auto; - } - - .github { - display: none; - } -} - - -/* scrollbars */ - -::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -::-webkit-scrollbar-button:start:decrement, -::-webkit-scrollbar-button:end:increment { - display: block; - height: 10px; -} - -::-webkit-scrollbar-button:vertical:increment { - background-color: #fff; -} - -::-webkit-scrollbar-track-piece { - background-color: #eee; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:vertical { - height: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:horizontal { - width: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -/* misc. */ - -.revsys-inline { - display: none!important; -} \ No newline at end of file diff --git a/docs/_themes/flask/theme.conf b/docs/_themes/flask/theme.conf deleted file mode 100644 index 18c720f8..00000000 --- a/docs/_themes/flask/theme.conf +++ /dev/null @@ -1,9 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -touch_icon = diff --git a/docs/_themes/flask_small/layout.html b/docs/_themes/flask_small/layout.html deleted file mode 100644 index aa1716aa..00000000 --- a/docs/_themes/flask_small/layout.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "basic/layout.html" %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{% block footer %} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{# do not display relbars #} -{% block relbar1 %}{% endblock %} -{% block relbar2 %} - {% if theme_github_fork %} - Fork me on GitHub - {% endif %} -{% endblock %} -{% block sidebar1 %}{% endblock %} -{% block sidebar2 %}{% endblock %} diff --git a/docs/_themes/flask_small/static/flasky.css_t b/docs/_themes/flask_small/static/flasky.css_t deleted file mode 100644 index 802ebaff..00000000 --- a/docs/_themes/flask_small/static/flasky.css_t +++ /dev/null @@ -1,291 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- flasky theme based on nature theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - color: #000; - background: white; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 40px auto 0 auto; - width: 700px; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - text-align: right; - color: #888; - padding: 10px; - font-size: 14px; - width: 650px; - margin: 0 auto 40px auto; -} - -div.footer a { - color: #888; - text-decoration: underline; -} - -div.related { - line-height: 32px; - color: #888; -} - -div.related ul { - padding: 0 0 0 10px; -} - -div.related a { - color: #444; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body { - padding-bottom: 40px; /* saved for footer */ -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} - -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: white; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight{ - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.85em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td { - padding: 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -pre { - padding: 0; - margin: 15px -30px; - padding: 8px; - line-height: 1.3em; - padding: 7px 30px; - background: #eee; - border-radius: 2px; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; -} - -dl pre { - margin-left: -60px; - padding-left: 60px; -} - -dl.class { - margin-bottom: 50px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; -} - -a:hover tt { - background: #EEE; -} diff --git a/docs/_themes/flask_small/theme.conf b/docs/_themes/flask_small/theme.conf deleted file mode 100644 index 542b4625..00000000 --- a/docs/_themes/flask_small/theme.conf +++ /dev/null @@ -1,10 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -nosidebar = true -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -github_fork = '' diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py deleted file mode 100644 index 33f47449..00000000 --- a/docs/_themes/flask_theme_support.py +++ /dev/null @@ -1,86 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..5a89bf60 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,91 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 3 + +.. module:: socketio + +.. autoclass:: SimpleClient + :members: + :inherited-members: + +.. autoclass:: AsyncSimpleClient + :members: + :inherited-members: + +.. autoclass:: Client + :members: + :inherited-members: + +.. autoclass:: AsyncClient + :members: + :inherited-members: + +.. autoclass:: Server + :members: + :inherited-members: + +.. autoclass:: AsyncServer + :members: + :inherited-members: + +.. autoclass:: socketio.exceptions.ConnectionRefusedError + :members: + +.. autoclass:: WSGIApp + :members: + +.. autoclass:: ASGIApp + :members: + +.. autoclass:: Middleware + :members: + +.. autoclass:: ClientNamespace + :members: + :inherited-members: + +.. autoclass:: Namespace + :members: + :inherited-members: + +.. autoclass:: AsyncClientNamespace + :members: + :inherited-members: + +.. autoclass:: AsyncNamespace + :members: + :inherited-members: + +.. autoclass:: Manager + :members: + :inherited-members: + +.. autoclass:: PubSubManager + :members: + :inherited-members: + +.. autoclass:: KombuManager + :members: + :inherited-members: + +.. autoclass:: RedisManager + :members: + :inherited-members: + +.. autoclass:: KafkaManager + :members: + :inherited-members: + +.. autoclass:: AsyncManager + :members: + :inherited-members: + +.. autoclass:: AsyncRedisManager + :members: + :inherited-members: + +.. autoclass:: AsyncAioPikaManager + :members: + :inherited-members: diff --git a/docs/client.rst b/docs/client.rst new file mode 100644 index 00000000..1e23c749 --- /dev/null +++ b/docs/client.rst @@ -0,0 +1,662 @@ +The Socket.IO Clients +===================== + +This package contains two Socket.IO clients: + +- a "simple" client, which provides a straightforward API that is sufficient + for most applications +- an "event-driven" client, which provides access to all the features of the + Socket.IO protocol + +Each of these clients comes in two variants: one for the standard Python +library, and another for asynchronous applications built with the ``asyncio`` +package. + +Installation +------------ + +To install the standard Python client along with its dependencies, use the +following command:: + + pip install "python-socketio[client]" + +If instead you plan on using the ``asyncio`` client, then use this:: + + pip install "python-socketio[asyncio_client]" + +Using the Simple Client +----------------------- + +The advantage of the simple client is that it abstracts away the logic required +to maintain a Socket.IO connection. This client handles disconnections and +reconnections in a completely transparent way, without adding any complexity to +the application. + +Creating a Client Instance +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The easiest way to create a Socket.IO client is to use the context manager +interface:: + + import socketio + + # standard Python + with socketio.SimpleClient() as sio: + # ... connect to a server and use the client + # ... no need to manually disconnect! + + # asyncio + async with socketio.AsyncSimpleClient() as sio: + # ... connect to a server and use the client + # ... no need to manually disconnect! + + +With this usage the context manager will ensure that the client is properly +disconnected before exiting the ``with`` or ``async with`` block. + +If preferred, a client can be manually instantiated:: + + import socketio + + # standard Python + sio = socketio.SimpleClient() + + # asyncio + sio = socketio.AsyncSimpleClient() + +Connecting to a Server +~~~~~~~~~~~~~~~~~~~~~~ + +The connection to a server is established by calling the ``connect()`` +method:: + + sio.connect('http://localhost:5000') + +In the case of the ``asyncio`` client, the method is a coroutine:: + + await sio.connect('http://localhost:5000') + +By default the client first connects to the server using the long-polling +transport, and then attempts to upgrade the connection to use WebSocket. To +connect directly using WebSocket, use the ``transports`` argument:: + + sio.connect('http://localhost:5000', transports=['websocket']) + +Upon connection, the server assigns the client a unique session identifier. +The application can find this identifier in the ``sid`` attribute:: + + print('my sid is', sio.sid) + +The Socket.IO transport that is used in the connection can be obtained from the +``transport`` attribute:: + + print('my transport is', sio.transport) + +The transport is given as a string, and can be either ``'websocket'`` or +``'polling'``. + +TLS/SSL Support +^^^^^^^^^^^^^^^ + +The client supports TLS/SSL connections. To enable it, use a ``https://`` +connection URL:: + + sio.connect('https://example.com') + +Or when using ``asyncio``:: + + await sio.connect('https://example.com') + +The client verifies server certificates by default. Consult the documentation +for the event-driven client for information on how to customize this behavior. + +Emitting Events +~~~~~~~~~~~~~~~ + +The client can emit an event to the server using the ``emit()`` method:: + + sio.emit('my message', {'foo': 'bar'}) + +Or in the case of ``asyncio``, as a coroutine:: + + await sio.emit('my message', {'foo': 'bar'}) + +The arguments provided to the method are the name of the event to emit and the +optional data that is passed on to the server. The data can be of type ``str``, +``bytes``, ``dict``, ``list`` or ``tuple``. When sending a ``list`` or a +``tuple``, the elements in it need to be of any allowed types except ``tuple``. +When a tuple is used, the elements of the tuple will be passed as individual +arguments to the server-side event handler function. + +Receiving Events +~~~~~~~~~~~~~~~~ + +The client can wait for the server to emit an event with the ``receive()`` +method:: + + event = sio.receive() + print(f'received event: "{event[0]}" with arguments {event[1:]}') + +When using ``asyncio``, this method needs to be awaited:: + + event = await sio.receive() + print(f'received event: "{event[0]}" with arguments {event[1:]}') + +The return value of ``receive()`` is a list. The first element of this list is +the event name, while the remaining elements are the arguments passed by the +server. + +With the usage shown above, the ``receive()`` method will return only when an +event is received from the server. An optional timeout in seconds can be passed +to prevent the client from waiting forever:: + + from socketio.exceptions import TimeoutError + + try: + event = sio.receive(timeout=5) + except TimeoutError: + print('timed out waiting for event') + else: + print('received event:', event) + +Or with ``asyncio``:: + + from socketio.exceptions import TimeoutError + + try: + event = await sio.receive(timeout=5) + except TimeoutError: + print('timed out waiting for event') + else: + print('received event:', event) + +Disconnecting from the Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At any time the client can request to be disconnected from the server by +invoking the ``disconnect()`` method:: + + sio.disconnect() + +For the ``asyncio`` client this is a coroutine:: + + await sio.disconnect() + +Debugging and Troubleshooting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To help you debug issues, the client can be configured to output logs to the +terminal:: + + import socketio + + # standard Python + sio = socketio.Client(logger=True, engineio_logger=True) + + # asyncio + sio = socketio.AsyncClient(logger=True, engineio_logger=True) + +The ``logger`` argument controls logging related to the Socket.IO protocol, +while ``engineio_logger`` controls logs that originate in the low-level +Engine.IO transport. The value given to these arguments controls logging +behavior: + +* ``True``: Enables log output to ``stderr`` at the ``INFO`` level. +* ``False``: Enables log output to ``stderr`` at the ``ERROR`` level. This is + the default. +* A ``logging.Logger`` instance: Uses the provided logger without additional + configuration. + +Logging can help identify the cause of connection problems, unexpected +disconnections and other issues. + +Using the Event-Driven Client +----------------------------- + +Creating a Client Instance +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To instantiate an Socket.IO client, simply create an instance of the +appropriate client class:: + + import socketio + + # standard Python + sio = socketio.Client() + + # asyncio + sio = socketio.AsyncClient() + +Defining Event Handlers +~~~~~~~~~~~~~~~~~~~~~~~ + +The Socket.IO protocol is event based. When a server wants to communicate with +a client it *emits* an event. Each event has a name, and a list of +arguments. The client registers event handler functions with the +:func:`socketio.Client.event` or :func:`socketio.Client.on` decorators:: + + @sio.event + def message(data): + print('I received a message!') + + @sio.on('my message') + def on_message(data): + print('I received a message!') + +In the first example the event name is obtained from the name of the +handler function. The second example is slightly more verbose, but it +allows the event name to be different than the function name or to include +characters that are illegal in function names, such as spaces. + +For the ``asyncio`` client, event handlers can be regular functions as above, +or can also be coroutines:: + + @sio.event + async def message(data): + print('I received a message!') + +If the server includes arguments with an event, those are passed to the +handler function as arguments. + +Catch-All Event and Namespace Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A "catch-all" event handler is invoked for any events that do not have an +event handler. You can define a catch-all handler using ``'*'`` as event name:: + + @sio.on('*') + def any_event(event, sid, data): + pass + +Asyncio servers can also use a coroutine:: + + @sio.on('*') + async def any_event(event, sid, data): + pass + +A catch-all event handler receives the event name as a first argument. The +remaining arguments are the same as for a regular event handler. + +The ``connect`` and ``disconnect`` events have to be defined explicitly and are +not invoked on a catch-all event handler. + +Similarly, a "catch-all" namespace handler is invoked for any connected +namespaces that do not have an explicitly defined event handler. As with +catch-all events, ``'*'`` is used in place of a namespace:: + + @sio.on('my_event', namespace='*') + def my_event_any_namespace(namespace, sid, data): + pass + +For these events, the namespace is passed as first argument, followed by the +regular arguments of the event. + +Lastly, it is also possible to define a "catch-all" handler for all events on +all namespaces:: + + @sio.on('*', namespace='*') + def any_event_any_namespace(event, namespace, sid, data): + pass + +Event handlers with catch-all events and namespaces receive the event name and +the namespace as first and second arguments. + +Connect, Connect Error and Disconnect Event Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``connect``, ``connect_error`` and ``disconnect`` events are special; they +are invoked automatically when a client connects or disconnects from the +server:: + + @sio.event + def connect(): + print("I'm connected!") + + @sio.event + def connect_error(data): + print("The connection failed!") + + @sio.event + def disconnect(reason): + print("I'm disconnected! reason:", reason) + +The ``connect_error`` handler is invoked when a connection attempt fails. If +the server provides arguments, these are passed on to the handler. The server +can use an argument to provide information to the client regarding the +connection failure. + +The ``disconnect`` handler is invoked for application initiated disconnects, +server initiated disconnects, or accidental disconnects, for example due to +networking failures. In the case of an accidental disconnection, the client is +going to attempt to reconnect immediately after invoking the disconnect +handler. As soon as the connection is re-established the connect handler will +be invoked once again. The handler receives a ``reason`` argument which +provides the cause of the disconnection:: + + @sio.event + def disconnect(reason): + if reason == sio.reason.CLIENT_DISCONNECT: + print('the client disconnected') + elif reason == sio.reason.SERVER_DISCONNECT: + print('the server disconnected the client') + else: + print('disconnect reason:', reason) + +See the The :attr:`socketio.Client.reason` attribute for a list of possible +disconnection reasons. + +The ``connect``, ``connect_error`` and ``disconnect`` events have to be +defined explicitly and are not invoked on a catch-all event handler. + +Connecting to a Server +~~~~~~~~~~~~~~~~~~~~~~ + +The connection to a server is established by calling the ``connect()`` +method:: + + sio.connect('http://localhost:5000') + +In the case of the ``asyncio`` client, the method is a coroutine:: + + await sio.connect('http://localhost:5000') + +Upon connection, the server assigns the client a unique session identifier. +The application can find this identifier in the ``sid`` attribute:: + + print('my sid is', sio.sid) + +The Socket.IO transport that is used in the connection can be obtained from the +``transport`` attribute:: + + print('my transport is', sio.transport) + +The transport is given as a string, and can be either ``'websocket'`` or +``'polling'``. + +TLS/SSL Support +^^^^^^^^^^^^^^^ + +The client supports TLS/SSL connections. To enable it, use a ``https://`` +connection URL:: + + sio.connect('https://example.com') + +Or when using ``asyncio``:: + + await sio.connect('https://example.com') + +The client will verify the server certificate by default. To disable +certificate verification, or to use other less common options such as client +certificates, the client must be initialized with a custom HTTP session object +that is configured with the desired TLS/SSL options. + +The following example disables server certificate verification, which can be +useful when connecting to a server that uses a self-signed certificate:: + + http_session = requests.Session() + http_session.verify = False + sio = socketio.Client(http_session=http_session) + sio.connect('https://example.com') + +And when using ``asyncio``:: + + connector = aiohttp.TCPConnector(ssl=False) + http_session = aiohttp.ClientSession(connector=connector) + sio = socketio.AsyncClient(http_session=http_session) + await sio.connect('https://example.com') + +Instead of disabling certificate verification, you can provide a custom +certificate authority bundle to verify the certificate against:: + + http_session = requests.Session() + http_session.verify = '/path/to/ca.pem' + sio = socketio.Client(http_session=http_session) + sio.connect('https://example.com') + +And for ``asyncio``:: + + ssl_context = ssl.create_default_context() + ssl_context.load_verify_locations('/path/to/ca.pem') + connector = aiohttp.TCPConnector(ssl=ssl_context) + http_session = aiohttp.ClientSession(connector=connector) + sio = socketio.AsyncClient(http_session=http_session) + await sio.connect('https://example.com') + +Below you can see how to use a client certificate to authenticate against the +server:: + + http_session = requests.Session() + http_session.cert = ('/path/to/client/cert.pem', '/path/to/client/key.pem') + sio = socketio.Client(http_session=http_session) + sio.connect('https://example.com') + +And for ``asyncio``:: + + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain('/path/to/client/cert.pem', + '/path/to/client/key.pem') + connector = aiohttp.TCPConnector(ssl=ssl_context) + http_session = aiohttp.ClientSession(connector=connector) + sio = socketio.AsyncClient(http_session=http_session) + await sio.connect('https://example.com') + +Emitting Events +~~~~~~~~~~~~~~~ + +The client can emit an event to the server using the ``emit()`` method:: + + sio.emit('my message', {'foo': 'bar'}) + +Or in the case of ``asyncio``, as a coroutine:: + + await sio.emit('my message', {'foo': 'bar'}) + +The arguments provided to the method are the name of the event to emit and the +optional data that is passed on to the server. The data can be of type ``str``, +``bytes``, ``dict``, ``list`` or ``tuple``. When sending a ``list`` or a +``tuple``, the elements in it need to be of any allowed types except ``tuple``. +When a tuple is used, the elements of the tuple will be passed as individual +arguments to the server-side event handler function. + +The ``emit()`` method can be invoked inside an event handler as a response +to a server event, or in any other part of the application, including in +background tasks. + +Event Callbacks +~~~~~~~~~~~~~~~ + +When a server emits an event to a client, it can optionally provide a +callback function, to be invoked as a way of acknowledgment that the server +has processed the event. While this is entirely managed by the server, the +client can provide a list of return values that are to be passed on to the +callback function set up by the server. This is achieved simply by returning +the desired values from the handler function:: + + @sio.event + def my_event(sid, data): + # handle the message + return "OK", 123 + +Likewise, the client can request a callback function to be invoked after the +server has processed an event. The :func:`socketio.Server.emit` method has an +optional ``callback`` argument that can be set to a callable. If this +argument is given, the callable will be invoked after the server has processed +the event, and any values returned by the server handler will be passed as +arguments to this function. + +Namespaces +~~~~~~~~~~ + +The Socket.IO protocol supports multiple logical connections, all multiplexed +on the same physical connection. Clients can open multiple connections by +specifying a different *namespace* on each. Namespaces use a path syntax +starting with a forward slash. A list of namespaces can be given by the client +in the ``connect()`` call. For example, this example creates two logical +connections, the default one plus a second connection under the ``/chat`` +namespace:: + + sio.connect('http://localhost:5000', namespaces=['/chat']) + +To define event handlers on a namespace, the ``namespace`` argument must be +added to the corresponding decorator:: + + @sio.event(namespace='/chat') + def my_custom_event(sid, data): + pass + + @sio.on('connect', namespace='/chat') + def on_connect(): + print("I'm connected to the /chat namespace!") + +Likewise, the client can emit an event to the server on a namespace by +providing its in the ``emit()`` call:: + + sio.emit('my message', {'foo': 'bar'}, namespace='/chat') + +If the ``namespaces`` argument of the ``connect()`` call isn't given, any +namespaces used in event handlers are automatically connected. + +Class-Based Namespaces +~~~~~~~~~~~~~~~~~~~~~~ + +As an alternative to the decorator-based event handlers, the event handlers +that belong to a namespace can be created as methods of a subclass of +:class:`socketio.ClientNamespace`:: + + class MyCustomNamespace(socketio.ClientNamespace): + def on_connect(self): + pass + + def on_disconnect(self, reason): + pass + + def on_my_event(self, data): + self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/chat')) + +For asyncio based servers, namespaces must inherit from +:class:`socketio.AsyncClientNamespace`, and can define event handlers as +coroutines if desired:: + + class MyCustomNamespace(socketio.AsyncClientNamespace): + def on_connect(self): + pass + + def on_disconnect(self, reason): + pass + + async def on_my_event(self, data): + await self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/chat')) + +A catch-all class-based namespace handler can be defined by passing ``'*'`` as +the namespace during registration:: + + sio.register_namespace(MyCustomNamespace('*')) + +When class-based namespaces are used, any events received by the client are +dispatched to a method named as the event name with the ``on_`` prefix. For +example, event ``my_event`` will be handled by a method named ``on_my_event``. +If an event is received for which there is no corresponding method defined in +the namespace class, then the event is ignored. All event names used in +class-based namespaces must use characters that are legal in method names. + +As a convenience to methods defined in a class-based namespace, the namespace +instance includes versions of several of the methods in the +:class:`socketio.Client` and :class:`socketio.AsyncClient` classes that +default to the proper namespace when the ``namespace`` argument is not given. + +In the case that an event has a handler in a class-based namespace, and also a +decorator-based function handler, only the standalone function handler is +invoked. + +Disconnecting from the Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At any time the client can request to be disconnected from the server by +invoking the ``disconnect()`` method:: + + sio.disconnect() + +For the ``asyncio`` client this is a coroutine:: + + await sio.disconnect() + +Managing Background Tasks +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a client connection to the server is established, a few background +tasks will be spawned to keep the connection alive and handle incoming +events. The application running on the main thread is free to do any +work, as this is not going to prevent the functioning of the Socket.IO +client. + +If the application does not have anything to do in the main thread and +just wants to wait until the connection with the server ends, it can call +the ``wait()`` method:: + + sio.wait() + +Or in the ``asyncio`` version:: + + await sio.wait() + +For the convenience of the application, a helper function is provided to +start a custom background task:: + + def my_background_task(my_argument): + # do some background work here! + pass + + task = sio.start_background_task(my_background_task, 123) + +The arguments passed to this method are the background function and any +positional or keyword arguments to invoke the function with. + +Here is the ``asyncio`` version:: + + async def my_background_task(my_argument): + # do some background work here! + pass + + task = sio.start_background_task(my_background_task, 123) + +Note that this function is not a coroutine, since it does not wait for the +background function to end. The background function must be a coroutine. + +The ``sleep()`` method is a second convenience function that is provided for +the benefit of applications working with background tasks of their own:: + + sio.sleep(2) + +Or for ``asyncio``:: + + await sio.sleep(2) + +The single argument passed to the method is the number of seconds to sleep +for. + +Debugging and Troubleshooting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To help you debug issues, the client can be configured to output logs to the +terminal:: + + import socketio + + # standard Python + sio = socketio.Client(logger=True, engineio_logger=True) + + # asyncio + sio = socketio.AsyncClient(logger=True, engineio_logger=True) + +The ``logger`` argument controls logging related to the Socket.IO protocol, +while ``engineio_logger`` controls logs that originate in the low-level +Engine.IO transport. These arguments can be set to ``True`` to output logs to +``stderr``, or to an object compatible with Python's ``logging`` package +where the logs should be emitted to. A value of ``False`` disables logging. + +Logging can help identify the cause of connection problems, unexpected +disconnections and other issues. diff --git a/docs/conf.py b/docs/conf.py index a1f6a136..b5d242bc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,31 +1,39 @@ # -*- coding: utf-8 -*- # -# socketio documentation build configuration file, created by -# sphinx-quickstart on Sat Jun 13 23:41:23 2015. +# Configuration file for the Sphinx documentation builder. # -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config -import sys -import os -import shlex +# -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) -sys.path.append(os.path.abspath('_themes')) +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'python-socketio' +copyright = '2018, Miguel Grinberg' +author = 'Miguel Grinberg' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + -# -- General configuration ------------------------------------------------ +# -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -34,257 +42,146 @@ 'sphinx.ext.autodoc', ] +autodoc_member_order = 'alphabetical' + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: +# # source_suffix = ['.rst', '.md'] source_suffix = '.rst' -# The encoding of source files. -#source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = 'index' -# General information about the project. -project = u'socketio' -copyright = u'2015, Miguel Grinberg' -author = u'Miguel Grinberg' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.1' -# The full version, including alpha/beta/rc tags. -release = '0.1' - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False +pygments_style = None -# -- Options for HTML output ---------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'flask_small' #'alabaster' +# +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. +# html_theme_options = { - 'index_logo': 'logo.png', - 'github_fork': 'miguelgrinberg/python-socketio' -} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None + 'github_user': 'miguelgrinberg', + 'github_repo': 'python-socketio', + 'github_banner': True, + 'github_button': True, + 'github_type': 'star', + 'fixed_sidebar': True, -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'socketiodoc' +htmlhelp_basename = 'python-socketiodoc' + -# -- Options for LaTeX output --------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------ latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'socketio.tex', u'socketio Documentation', - u'Miguel Grinberg', 'manual'), + (master_doc, 'python-socketio.tex', 'python-socketio Documentation', + 'Miguel Grinberg', 'manual'), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - -# -- Options for manual page output --------------------------------------- +# -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'socketio', u'socketio Documentation', + (master_doc, 'python-socketio', 'python-socketio Documentation', [author], 1) ] -# If true, show URL addresses after external links. -#man_show_urls = False - -# -- Options for Texinfo output ------------------------------------------- +# -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'socketio', u'socketio Documentation', - author, 'socketio', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'python-socketio', 'python-socketio Documentation', + author, 'python-socketio', 'One line description of project.', + 'Miscellaneous'), ] -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] -# If false, no module index is generated. -#texinfo_domain_indices = True +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# -- Extension configuration ------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 4e4d6a7f..a475d571 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,722 +1,22 @@ -.. socketio documentation master file, created by - sphinx-quickstart on Sat Jun 13 23:41:23 2015. +.. python-socketio documentation master file, created by + sphinx-quickstart on Sun Nov 25 11:52:38 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -socketio documentation -====================== +python-socketio +=============== -This project implements a Python Socket.IO server that can run standalone or -integrated with a web application. The following are some of its -features: +This projects implements Socket.IO clients and servers that can run standalone +or integrated with a variety of Python web frameworks. -- Fully compatible with the - `Javascript `_, - `Swift `_, - `C++ `_ and - `Java `_ official - Socket.IO clients, plus any third party clients that comply with the - Socket.IO specification. -- Compatible with Python 2.7 and Python 3.3+. -- Supports large number of clients even on modest hardware when used with an - asynchronous server based on `asyncio `_ - (`sanic `_ and `aiohttp `_), - `eventlet `_ or `gevent `_. For - development and testing, any WSGI compliant multi-threaded server can also be - used. -- Includes a WSGI middleware that integrates Socket.IO traffic with standard - WSGI applications. -- Broadcasting of messages to all connected clients, or to subsets of them - assigned to "rooms". -- Optional support for multiple servers, connected through a messaging queue - such as Redis or RabbitMQ. -- Send messages to clients from external processes, such as Celery workers or - auxiliary scripts. -- Event-based architecture implemented with decorators that hides the details - of the protocol. -- Support for HTTP long-polling and WebSocket transports. -- Support for XHR2 and XHR browsers. -- Support for text and binary messages. -- Support for gzip and deflate HTTP compression. -- Configurable CORS responses, to avoid cross-origin problems with browsers. +.. toctree:: + :maxdepth: 3 -What is Socket.IO? ------------------- + intro + client + server + api -Socket.IO is a transport protocol that enables real-time bidirectional -event-based communication between clients (typically web browsers) and a -server. The original implementations of the client and server components are -written in JavaScript. - -Getting Started ---------------- - -The Socket.IO server can be installed with pip:: - - pip install python-socketio - -The following is a basic example of a Socket.IO server that uses the -`aiohttp `_ framework for asyncio (Python 3.5+ -only): - -.. code:: python - - from aiohttp import web - import socketio - - sio = socketio.AsyncServer() - app = web.Application() - sio.attach(app) - - async def index(request): - """Serve the client-side application.""" - with open('index.html') as f: - return web.Response(text=f.read(), content_type='text/html') - - @sio.on('connect', namespace='/chat') - def connect(sid, environ): - print("connect ", sid) - - @sio.on('chat message', namespace='/chat') - async def message(sid, data): - print("message ", data) - await sio.emit('reply', room=sid) - - @sio.on('disconnect', namespace='/chat') - def disconnect(sid): - print('disconnect ', sid) - - app.router.add_static('/static', 'static') - app.router.add_get('/', index) - - if __name__ == '__main__': - web.run_app(app) - -And below is a similar example, but using Flask and Eventlet. This example is -compatible with Python 2.7 and 3.3+:: - - import socketio - import eventlet - from flask import Flask, render_template - - sio = socketio.Server() - app = Flask(__name__) - - @app.route('/') - def index(): - """Serve the client-side application.""" - return render_template('index.html') - - @sio.on('connect') - def connect(sid, environ): - print('connect ', sid) - - @sio.on('my message') - def message(sid, data): - print('message ', data) - - @sio.on('disconnect') - def disconnect(sid): - print('disconnect ', sid) - - if __name__ == '__main__': - # wrap Flask application with socketio's middleware - app = socketio.Middleware(sio, app) - - # deploy as an eventlet WSGI server - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) - -The client-side application must include the -`socket.io-client `_ library -(versions 1.3.5 or newer recommended). - -Each time a client connects to the server the ``connect`` event handler is -invoked with the ``sid`` (session ID) assigned to the connection and the WSGI -environment dictionary. The server can inspect authentication or other headers -to decide if the client is allowed to connect. To reject a client the handler -must return ``False``. - -When the client sends an event to the server, the appropriate event handler is -invoked with the ``sid`` and the message, which can be a single or multiple -arguments. The application can define as many events as needed and associate -them with event handlers. An event is defined simply by a name. - -When a connection with a client is broken, the ``disconnect`` event is called, -allowing the application to perform cleanup. - -Server ------- - -Socket.IO servers are instances of class :class:`socketio.Server`, which can be -combined with a WSGI compliant application using :class:`socketio.Middleware`:: - - # create a Socket.IO server - sio = socketio.Server() - - # wrap WSGI application with socketio's middleware - app = socketio.Middleware(sio, app) - - -For asyncio based servers, the :class:`socketio.AsyncServer` class provides a -coroutine friendly server:: - - # create a Socket.IO server - sio = socketio.AsyncServer() - - # attach server to application - sio.attach(app) - -Event handlers for servers are register using the :func:`socketio.Server.on` -method:: - - @sio.on('my custom event') - def my_custom_event(): - pass - -For asyncio servers, event handlers can be regular functions or coroutines:: - - @sio.on('my custom event') - async def my_custom_event(): - await sio.emit('my reply') - -Rooms ------ - -Because Socket.IO is a bidirectional protocol, the server can send messages to -any connected client at any time. To make it easy to address groups of clients, -the application can put clients into rooms, and then address messages to the -entire room. - -When clients first connect, they are assigned to their own rooms, named with -the session ID (the ``sid`` argument passed to all event handlers). The -application is free to create additional rooms and manage which clients are in -them using the :func:`socketio.Server.enter_room` and -:func:`socketio.Server.leave_room` methods. Clients can be in as many rooms as -needed and can be moved between rooms as often as necessary. The individual -rooms assigned to clients when they connect are not special in any way, the -application is free to add or remove clients from them, though once it does -that it will lose the ability to address individual clients. - -:: - - @sio.on('enter room') - def enter_room(sid, data): - sio.enter_room(sid, data['room']) - - @sio.on('leave room') - def leave_room(sid, data): - sio.leave_room(sid, data['room']) - -The :func:`socketio.Server.emit` method takes an event name, a message payload -of type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``, and the recipient -room. When sending a ``tuple``, the elements in it need to be of any of the -other four allowed types. The elements of the tuple will be passed as multiple -arguments to the client-side callback function. To address an individual -client, the ``sid`` of that client should be given as room (assuming the -application did not alter these initial rooms). To address all connected -clients, the ``room`` argument should be omitted. - -:: - - @sio.on('my message') - def message(sid, data): - print('message ', data) - sio.emit('my reply', data, room='my room') - -Often when broadcasting a message to group of users in a room, it is desirable -that the sender does not receive its own message. The -:func:`socketio.Server.emit` method provides an optional ``skip_sid`` argument -to specify a client that should be skipped during the broadcast. - -:: - - @sio.on('my message') - def message(sid, data): - print('message ', data) - sio.emit('my reply', data, room='my room', skip_sid=sid) - -Responses ---------- - -When a client sends an event to the server, it can optionally provide a -callback function, to be invoked with a response provided by the server. The -server can provide a response simply by returning it from the corresponding -event handler. - -:: - - @sio.on('my event', namespace='/chat') - def my_event_handler(sid, data): - # handle the message - return "OK", 123 - -The event handler can return a single value, or a tuple with several values. -The callback function on the client side will be invoked with these returned -values as arguments. - -Callbacks ---------- - -The server can also request a response to an event sent to a client. The -:func:`socketio.Server.emit` method has an optional ``callback`` argument that -can be set to a callable. When this argument is given, the callable will be -invoked with the arguments returned by the client as a response. - -Using callback functions when broadcasting to multiple clients is not -recommended, as the callback function will be invoked once for each client -that received the message. - -Namespaces ----------- - -The Socket.IO protocol supports multiple logical connections, all multiplexed -on the same physical connection. Clients can open multiple connections by -specifying a different *namespace* on each. A namespace is given by the client -as a pathname following the hostname and port. For example, connecting to -*http://example.com:8000/chat* would open a connection to the namespace -*/chat*. - -Each namespace is handled independently from the others, with separate session -IDs (``sid``\ s), event handlers and rooms. It is important that applications -that use multiple namespaces specify the correct namespace when setting up -their event handlers and rooms, using the optional ``namespace`` argument -available in all the methods in the :class:`socketio.Server` class. - -When the ``namespace`` argument is omitted, set to ``None`` or to ``'/'``, a -default namespace is used. - -Class-Based Namespaces ----------------------- - -As an alternative to the decorator-based event handlers, the event handlers -that belong to a namespace can be created as methods of a subclass of -:class:`socketio.Namespace`:: - - class MyCustomNamespace(socketio.Namespace): - def on_connect(self, sid, environ): - pass - - def on_disconnect(self, sid): - pass - - def on_my_event(self, sid, data): - self.emit('my_response', data) - - sio.register_namespace(MyCustomNamespace('/test')) - -For asyncio based severs, namespaces must inherit from -:class:`socketio.AsyncNamespace`, and can define event handlers as regular -methods or coroutines:: - - class MyCustomNamespace(socketio.AsyncNamespace): - def on_connect(self, sid, environ): - pass - - def on_disconnect(self, sid): - pass - - async def on_my_event(self, sid, data): - await self.emit('my_response', data) - - sio.register_namespace(MyCustomNamespace('/test')) - -When class-based namespaces are used, any events received by the server are -dispatched to a method named as the event name with the ``on_`` prefix. For -example, event ``my_event`` will be handled by a method named ``on_my_event``. -If an event is received for which there is no corresponding method defined in -the namespace class, then the event is ignored. All event names used in -class-based namespaces must used characters that are legal in method names. - -As a convenience to methods defined in a class-based namespace, the namespace -instance includes versions of several of the methods in the -:class:`socketio.Server` and :class:`socketio.AsyncServer` classes that default -to the proper namespace when the ``namespace`` argument is not given. - -In the case that an event has a handler in a class-based namespace, and also a -decorator-based function handler, only the standalone function handler is -invoked. - -It is important to note that class-based namespaces are singletons. This means -that a single instance of a namespace class is used for all clients, and -consequently, a namespace instance cannot be used to store client specific -information. - -Using a Message Queue ---------------------- - -The Socket.IO server owns the socket connections to all the clients, so it is -the only process that can emit events to them. Unfortunately this becomes a -limitation for many applications that use more than one process. A common need -is to emit events to clients from a process other than the server, for example -a `Celery `_ worker. - -To enable these auxiliary processes to emit events, the server can be -configured to listen for externally issued events on a message queue such as -`Redis `_ or `RabbitMQ `_. -Processes that need to emit events to client then post these events to the -queue. - -Another situation in which the use of a message queue is necessary is with -high traffic applications that work with large number of clients. To support -these clients, it may be necessary to horizontally scale the Socket.IO -server by splitting the client list among multiple server processes. In this -type of installation, each server processes owns the connections to a subset -of the clients. To make broadcasting work in this environment, the servers -communicate with each other through the message queue. - -Kombu -~~~~~ - -One of the messaging options offered by this package to access the message -queue is `Kombu `_ , which means that -any message queue supported by this package can be used. Kombu can be installed -with pip:: - - pip install kombu - -To use RabbitMQ or other AMQP protocol compatible queues, that is the only -required dependency. But for other message queues, Kombu may require -additional packages. For example, to use a Redis queue, Kombu needs the Python -package for Redis installed as well:: - - pip install redis - -The appropriate message queue service, such as RabbitMQ or Redis, must also be -installed. To configure a Socket.IO server to connect to a Kombu queue, the -``client_manager`` argument must be passed in the server creation. The -following example instructs the server to connect to a Redis service running -on the same host and on the default port:: - - mgr = socketio.KombuManager('redis://') - sio = socketio.Server(client_manager=mgr) - -For a RabbitMQ queue also running on the local server with default -credentials, the configuration is as follows:: - - mgr = socketio.KombuManager('amqp://') - sio = socketio.Server(client_manager=mgr) - -The URL passed to the :class:`KombuManager` constructor is passed directly to -Kombu's `Connection object -`_, so -the Kombu documentation should be consulted for information on how to -connect to the message queue appropriately. - -Note that Kombu currently does not support asyncio, so it cannot be used with -the :class:`socketio.AsyncServer` class. - -Redis -~~~~~ - -To use a Redis message queue, the Python package for Redis must also be -installed:: - - # WSGI server - pip install redis - - # asyncio server - pip install aioredis - -Native Redis support is accessed through the :class:`socketio.RedisManager` and -:class:`socketio.AsyncRedisManager` classes. These classes connect directly to -the Redis store and use the queue's pub/sub functionality:: - - # WSGI server - mgr = socketio.RedisManager('redis://') - sio = socketio.Server(client_manager=mgr) - - # asyncio server - mgr = socketio.AsyncRedisManager('redis://') - sio = socketio.AsyncServer(client_manager=mgr) - -Horizontal scaling -~~~~~~~~~~~~~~~~~~ - -If multiple Socket.IO servers are connected to the same message queue, they -automatically communicate with each other and manage a combined client list, -without any need for additional configuration. When a load balancer such as -nginx is used, this provides virtually unlimited scaling capabilities for the -server. - -Emitting from external processes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To have a process other than a server connect to the queue to emit a message, -the same client manager classes can be used as standalone objects. In this -case, the ``write_only`` argument should be set to ``True`` to disable the -creation of a listening thread, which only makes sense in a server. For -example:: - - # connect to the redis queue through Kombu - external_sio = socketio.KombuManager('redis://', write_only=True) - - # emit an event - external_sio.emit('my event', data={'foo': 'bar'}, room='my room') - -Deployment ----------- - -The following sections describe a variety of deployment strategies for -Socket.IO servers. - -Sanic -~~~~~ - -`Sanic `_ is a very efficient asynchronous web -server for Python 3.5 and newer. - -Instances of class ``socketio.AsyncServer`` will automatically use Sanic for -asynchronous operations if the framework is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.AsyncServer(async_mode='sanic') - -A server configured for aiohttp must be attached to an existing application:: - - app = web.Application() - sio.attach(app) - -The Sanic application can define regular routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. - -The Sanic application is then executed in the usual manner:: - - if __name__ == '__main__': - app.run() - -aiohttp -~~~~~~~ - -`Aiohttp `_ is a framework with support for HTTP -and WebSocket, based on asyncio. Support for this framework is limited to Python -3.5 and newer. - -Instances of class ``socketio.AsyncServer`` will automatically use aiohttp -for asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.AsyncServer(async_mode='aiohttp') - -A server configured for aiohttp must be attached to an existing application:: - - app = web.Application() - sio.attach(app) - -The aiohttp application can define regular routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. - -The aiohttp application is then executed in the usual manner:: - - if __name__ == '__main__': - web.run_app(app) - -Eventlet -~~~~~~~~ - -`Eventlet `_ is a high performance concurrent networking -library for Python 2 and 3 that uses coroutines, enabling code to be written in -the same style used with the blocking standard library functions. An Socket.IO -server deployed with eventlet has access to the long-polling and WebSocket -transports. - -Instances of class ``socketio.Server`` will automatically use eventlet for -asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.Server(async_mode='eventlet') - -A server configured for eventlet is deployed as a regular WSGI application, -using the provided ``socketio.Middleware``:: - - app = socketio.Middleware(sio) - import eventlet - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) - -An alternative to running the eventlet WSGI server as above is to use -`gunicorn `_, a fully featured pure Python web server. The -command to launch the application under gunicorn is shown below:: - - $ gunicorn -k eventlet -w 1 module:app - -Due to limitations in its load balancing algorithm, gunicorn can only be used -with one worker process, so the ``-w`` option cannot be set to a value higher -than 1. A single eventlet worker can handle a large number of concurrent -clients, each handled by a greenlet. - -Eventlet provides a ``monkey_patch()`` function that replaces all the blocking -functions in the standard library with equivalent asynchronous versions. While -python-socketio does not require monkey patching, other libraries such as -database drivers are likely to require it. - -Gevent -~~~~~~ - -`Gevent `_ is another asynchronous framework based on -coroutines, very similar to eventlet. An Socket.IO server deployed with -gevent has access to the long-polling transport. If project -`gevent-websocket `_ is -installed, the WebSocket transport is also available. - -Instances of class ``socketio.Server`` will automatically use gevent for -asynchronous operations if the library is installed and eventlet is not -installed. To request gevent to be selected explicitly, the ``async_mode`` -option can be given in the constructor:: - - sio = socketio.Server(async_mode='gevent') - -A server configured for gevent is deployed as a regular WSGI application, -using the provided ``socketio.Middleware``:: - - app = socketio.Middleware(sio) - from gevent import pywsgi - pywsgi.WSGIServer(('', 8000), app).serve_forever() - -If the WebSocket transport is installed, then the server must be started as -follows:: - - from gevent import pywsgi - from geventwebsocket.handler import WebSocketHandler - app = socketio.Middleware(sio) - pywsgi.WSGIServer(('', 8000), app, - handler_class=WebSocketHandler).serve_forever() - -An alternative to running the gevent WSGI server as above is to use -`gunicorn `_, a fully featured pure Python web server. The -command to launch the application under gunicorn is shown below:: - - $ gunicorn -k gevent -w 1 module:app - -Or to include WebSocket:: - - $ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app - -Same as with eventlet, due to limitations in its load balancing algorithm, -gunicorn can only be used with one worker process, so the ``-w`` option cannot -be higher than 1. A single gevent worker can handle a large number of -concurrent clients through the use of greenlets. - -Gevent provides a ``monkey_patch()`` function that replaces all the blocking -functions in the standard library with equivalent asynchronous versions. While -python-socketio does not require monkey patching, other libraries such as -database drivers are likely to require it. - -Gevent with uWSGI -~~~~~~~~~~~~~~~~~ - -When using the uWSGI server in combination with gevent, the Socket.IO server -can take advantage of uWSGI's native WebSocket support. - -Instances of class ``socketio.Server`` will automatically use this option for -asynchronous operations if both gevent and uWSGI are installed and eventlet is -not installed. To request this asynchoronous mode explicitly, the -``async_mode`` option can be given in the constructor:: - - # gevent with uWSGI - sio = socketio.Server(async_mode='gevent_uwsgi') - -A complete explanation of the configuration and usage of the uWSGI server is -beyond the scope of this documentation. The uWSGI server is a fairly complex -package that provides a large and comprehensive set of options. It must be -compiled with WebSocket and SSL support for the WebSocket transport to be -available. As way of an introduction, the following command starts a uWSGI -server for the ``latency.py`` example on port 5000:: - - $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app - -Standard Threading Library -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -While not comparable to eventlet and gevent in terms of performance, -the Socket.IO server can also be configured to work with multi-threaded web -servers that use standard Python threads. This is an ideal setup to use with -development servers such as `Werkzeug `_. Only the -long-polling transport is currently available when using standard threads. - -Instances of class ``socketio.Server`` will automatically use the threading -mode if neither eventlet nor gevent are not installed. To request the -threading mode explicitly, the ``async_mode`` option can be given in the -constructor:: - - sio = socketio.Server(async_mode='threading') - -A server configured for threading is deployed as a regular web application, -using any WSGI complaint multi-threaded server. The example below deploys an -Socket.IO application combined with a Flask web application, using Flask's -development web server based on Werkzeug:: - - sio = socketio.Server(async_mode='threading') - app = Flask(__name__) - app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) - - # ... Socket.IO and Flask handler functions ... - - if __name__ == '__main__': - app.run(threaded=True) - -When using the threading mode, it is important to ensure that the WSGI server -can handle multiple concurrent requests using threads, since a client can have -up to two outstanding requests at any given time. The Werkzeug server is -single-threaded by default, so the ``threaded=True`` option is required. - -Note that servers that use worker processes instead of threads, such as -gunicorn, do not support a Socket.IO server configured in threading mode. - -Multi-process deployments -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Socket.IO is a stateful protocol, which makes horizontal scaling more -difficult. To deploy a cluster of Socket.IO processes (hosted on one or -multiple servers), the following conditions must be met: - -- Each Socket.IO process must be able to handle multiple requests, either by - using asyncio, eventlet, gevent, or standard threads. Worker processes that - only handle one request at a time are not supported. -- The load balancer must be configured to always forward requests from a - client to the same worker process. Load balancers call this *sticky - sessions*, or *session affinity*. -- The worker processes communicate with each other through a message queue, - which must be installed and configured. See the section on using message - queues above for instructions. - -API Reference -------------- - -.. module:: socketio - -.. autoclass:: Middleware - :members: - -.. autoclass:: Server - :members: - -.. autoclass:: AsyncServer - :members: - :inherited-members: - -.. autoclass:: Namespace - :members: - -.. autoclass:: AsyncNamespace - :members: - :inherited-members: - -.. autoclass:: BaseManager - :members: - -.. autoclass:: PubSubManager - :members: - -.. autoclass:: KombuManager - :members: - -.. autoclass:: RedisManager - :members: - -.. autoclass:: AsyncManager - :members: - :inherited-members: - -.. autoclass:: AsyncRedisManager - :members: +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/intro.rst b/docs/intro.rst new file mode 100644 index 00000000..9bb301df --- /dev/null +++ b/docs/intro.rst @@ -0,0 +1,210 @@ +.. socketio documentation master file, created by + sphinx-quickstart on Sat Jun 13 23:41:23 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Getting Started +=============== + +What is Socket.IO? +------------------ + +Socket.IO is a transport protocol that enables real-time bidirectional +event-based communication between clients (typically, though not always, +web browsers) and a server. The official implementations of the client +and server components are written in JavaScript. This package provides +Python implementations of both, each with standard and asyncio variants. + +Version compatibility +--------------------- + +The Socket.IO protocol has been through a number of revisions, and some of these +introduced backward incompatible changes, which means that the client and the +server must use compatible versions for everything to work. + +If you are using the Python client and server, the easiest way to ensure compatibility +is to use the same version of this package for the client and the server. If you are +using this package with a different client or server, then you must ensure the +versions are compatible. + +The version compatibility chart below maps versions of this package to versions +of the JavaScript reference implementation and the versions of the Socket.IO and +Engine.IO protocols. + ++------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ +| JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol revision | python-socketio version | python-engineio version | ++==============================+=============================+=============================+=========================+=========================+ +| 0.9.x | 1, 2 | 1, 2 | Not supported | Not supported | ++------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ +| 1.x and 2.x | 3, 4 | 3 | 4.x | 3.x | ++------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ +| 3.x and 4.x | 5 | 4 | 5.x | 4.x | ++------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ + +Client Examples +--------------- + +The example that follows shows a simple Python client: + +.. code:: python + + import socketio + + sio = socketio.Client() + + @sio.event + def connect(): + print('connection established') + + @sio.event + def my_message(data): + print('message received with ', data) + sio.emit('my response', {'response': 'my response'}) + + @sio.event + def disconnect(): + print('disconnected from server') + + sio.connect('http://localhost:5000') + sio.wait() + +Below is a similar client, coded for ``asyncio`` (Python 3.5+ only): + +.. code:: python + + import asyncio + import socketio + + sio = socketio.AsyncClient() + + @sio.event + async def connect(): + print('connection established') + + @sio.event + async def my_message(data): + print('message received with ', data) + await sio.emit('my response', {'response': 'my response'}) + + @sio.event + async def disconnect(): + print('disconnected from server') + + async def main(): + await sio.connect('http://localhost:5000') + await sio.wait() + + if __name__ == '__main__': + asyncio.run(main()) + +Client Features +--------------- + +- Can connect to other Socket.IO servers that are compatible with the + JavaScript Socket.IO reference server. +- Compatible with Python 3.8+. +- Two versions of the client, one for standard Python and another for + asyncio. +- Uses an event-based architecture implemented with decorators that + hides the details of the protocol. +- Implements HTTP long-polling and WebSocket transports. +- Automatically reconnects to the server if the connection is dropped. + +Server Examples +--------------- + +The following application is a basic server example that uses the Eventlet +asynchronous server: + +.. code:: python + + import eventlet + import socketio + + sio = socketio.Server() + app = socketio.WSGIApp(sio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'index.html'} + }) + + @sio.event + def connect(sid, environ): + print('connect ', sid) + + @sio.event + def my_message(sid, data): + print('message ', data) + + @sio.event + def disconnect(sid): + print('disconnect ', sid) + + if __name__ == '__main__': + eventlet.wsgi.server(eventlet.listen(('', 5000)), app) + +Below is a similar application, coded for ``asyncio`` (Python 3.5+ only) and the +Uvicorn web server: + +.. code:: python + + from aiohttp import web + import socketio + + sio = socketio.AsyncServer() + app = web.Application() + sio.attach(app) + + async def index(request): + """Serve the client-side application.""" + with open('index.html') as f: + return web.Response(text=f.read(), content_type='text/html') + + @sio.event + def connect(sid, environ): + print("connect ", sid) + + @sio.event + async def chat_message(sid, data): + print("message ", data) + + @sio.event + def disconnect(sid): + print('disconnect ', sid) + + app.router.add_static('/static', 'static') + app.router.add_get('/', index) + + if __name__ == '__main__': + web.run_app(app) + +Server Features +--------------- + +- Can connect to servers running other Socket.IO clients that are compatible + with the JavaScript reference client. +- Compatible with Python 3.8+. +- Two versions of the server, one for standard Python and another for + asyncio. +- Supports large number of clients even on modest hardware due to being + asynchronous. +- Can be hosted on any `WSGI `_ or + `ASGI `_ web server including + `Gunicorn `_, `Uvicorn `_, + `eventlet `_ and `gevent `_. +- Can be integrated with WSGI applications written in frameworks such as Flask, Django, + etc. +- Can be integrated with `aiohttp `_, + `FastAPI `_, `sanic `_ + and `tornado `_ ``asyncio`` applications. +- Broadcasting of messages to all connected clients, or to subsets of them + assigned to "rooms". +- Optional support for multiple servers, connected through a messaging queue + such as Redis or RabbitMQ. +- Send messages to clients from external processes, such as Celery workers or + auxiliary scripts. +- Event-based architecture implemented with decorators that hides the details + of the protocol. +- Support for HTTP long-polling and WebSocket transports. +- Support for XHR2 and XHR browsers. +- Support for text and binary messages. +- Support for gzip and deflate HTTP compression. +- Configurable CORS responses, to avoid cross-origin problems with browsers. diff --git a/docs/make.bat b/docs/make.bat index ada489d4..27f573b8 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,263 +1,35 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\socketio.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\socketio.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/server.rst b/docs/server.rst new file mode 100644 index 00000000..e73f14c5 --- /dev/null +++ b/docs/server.rst @@ -0,0 +1,1157 @@ +The Socket.IO Server +==================== + +This package contains two Socket.IO servers: + +- The :func:`socketio.Server` class creates a server compatible with the + Python standard library. +- The :func:`socketio.AsyncServer` class creates a server compatible with + the ``asyncio`` package. + +The methods in the two servers are the same, with the only difference that in +the ``asyncio`` server most methods are implemented as coroutines. + +Installation +------------ + +To install the Socket.IO server along with its dependencies, use the following +command:: + + pip install python-socketio + +Creating a Server Instance +-------------------------- + +A Socket.IO server is an instance of class :class:`socketio.Server`:: + + import socketio + + # create a Socket.IO server + sio = socketio.Server() + +For asyncio based servers, the :class:`socketio.AsyncServer` class provides +the same functionality, but in a coroutine friendly format:: + + import socketio + + # create a Socket.IO server + sio = socketio.AsyncServer() + +Running the Server +------------------ + +To run the Socket.IO application it is necessary to configure a web server to +receive incoming requests from clients and forward them to the Socket.IO +server instance. To simplify this task, several integrations are available, +including support for the `WSGI `_ +and `ASGI `_ standards. + +Running as a WSGI Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To configure the Socket.IO server as a WSGI application wrap the server +instance with the :class:`socketio.WSGIApp` class:: + + # wrap with a WSGI application + app = socketio.WSGIApp(sio) + +The resulting WSGI application can be executed with supported WSGI servers +such as `Werkzeug `_ for development and +`Gunicorn `_ for production. + +When combining Socket.IO with a web application written with a WSGI framework +such as Flask or Django, the ``WSGIApp`` class can wrap both applications +together and route traffic to them:: + + from mywebapp import app # a Flask, Django, etc. application + app = socketio.WSGIApp(sio, app) + +Running as an ASGI Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To configure the Socket.IO server as an ASGI application wrap the server +instance with the :class:`socketio.ASGIApp` class:: + + # wrap with ASGI application + app = socketio.ASGIApp(sio) + +The resulting ASGI application can be executed with an ASGI compliant web +server, for example `Uvicorn `_. + +Socket.IO can also be combined with a web application written with an ASGI +web framework such as FastAPI. In that case, the ``ASGIApp`` class can wrap +both applications together and route traffic to them:: + + from mywebapp import app # a FastAPI or other ASGI application + app = socketio.ASGIApp(sio, app) + +Serving Static Files +~~~~~~~~~~~~~~~~~~~~ + +The Socket.IO server can be configured to serve static files to clients. This +is particularly useful to deliver HTML, CSS and JavaScript files to clients +when this package is used without a companion web framework. + +Static files are configured with a Python dictionary in which each key/value +pair is a static file mapping rule. In its simplest form, this dictionary has +one or more static file URLs as keys, and the corresponding files in the server +as values:: + + static_files = { + '/': 'latency.html', + '/static/socket.io.js': 'static/socket.io.js', + '/static/style.css': 'static/style.css', + } + +With this example configuration, when the server receives a request for ``/`` +(the root URL) it will return the contents of the file ``latency.html`` in the +current directory, and will assign a content type based on the file extension, +in this case ``text/html``. + +Files with the ``.html``, ``.css``, ``.js``, ``.json``, ``.jpg``, ``.png``, +``.gif`` and ``.txt`` file extensions are automatically recognized and +assigned the correct content type. For files with other file extensions or +with no file extension, the ``application/octet-stream`` content type is used +as a default. + +If desired, an explicit content type for a static file can be given as follows:: + + static_files = { + '/': {'filename': 'latency.html', 'content_type': 'text/plain'}, + } + +It is also possible to configure an entire directory in a single rule, so that +all the files in it are served as static files:: + + static_files = { + '/static': './public', + } + +In this example any files with URLs starting with ``/static`` will be served +directly from the ``public`` folder in the current directory, so for example, +the URL ``/static/index.html`` will return local file ``./public/index.html`` +and the URL ``/static/css/styles.css`` will return local file +``./public/css/styles.css``. + +If a URL that ends in a ``/`` is requested, then a default filename of +``index.html`` is appended to it. In the previous example, a request for the +``/static/`` URL would return local file ``./public/index.html``. The default +filename to serve for slash-ending URLs can be set in the static files +dictionary with an empty key:: + + static_files = { + '/static': './public', + '': 'image.gif', + } + +With this configuration, a request for ``/static/`` would return +local file ``./public/image.gif``. A non-standard content type can also be +specified if needed:: + + static_files = { + '/static': './public', + '': {'filename': 'image.gif', 'content_type': 'text/plain'}, + } + +The static file configuration dictionary is given as the ``static_files`` +argument to the ``socketio.WSGIApp`` or ``socketio.ASGIApp`` classes:: + + # for standard WSGI applications + sio = socketio.Server() + app = socketio.WSGIApp(sio, static_files=static_files) + + # for asyncio-based ASGI applications + sio = socketio.AsyncServer() + app = socketio.ASGIApp(sio, static_files=static_files) + +The routing precedence in these two classes is as follows: + +- First, the path is checked against the Socket.IO endpoint. +- Next, the path is checked against the static file configuration, if present. +- If the path did not match the Socket.IO endpoint or any static file, control + is passed to the secondary application if configured, else a 404 error is + returned. + +Note: static file serving is intended for development use only, and as such +it lacks important features such as caching. Do not use in a production +environment. + +Events +------ + +The Socket.IO protocol is event based. When a client wants to communicate with +the server, or the server wants to communicate with one or more clients, they +*emit* an event to the other party. Each event has a name, and an optional list +of arguments. + +Listening to Events +~~~~~~~~~~~~~~~~~~~ + +To receive events from clients, the server application must register event +handler functions. These functions are invoked when the corresponding events +are emitted by clients. To register a handler for an event, the +:func:`socketio.Server.event` or :func:`socketio.Server.on` decorators are used:: + + @sio.event + def my_event(sid, data): + pass + + @sio.on('my custom event') + def another_event(sid, data): + pass + +In the first example the event name is obtained from the name of the handler +function. The second example is slightly more verbose, but it allows the event +name to be different than the function name or to include characters that are +illegal in function names, such as spaces. + +For asyncio servers, event handlers can optionally be given as coroutines:: + + @sio.event + async def my_event(sid, data): + pass + +The ``sid`` argument that is passed to all handlers is the Socket.IO session +id, a unique identifier that Socket.IO assigns to each client connection. All +the events sent by a given client will have the same ``sid`` value. + +Connect and Disconnect Events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``connect`` and ``disconnect`` events are special; they are invoked +automatically when a client connects or disconnects from the server:: + + @sio.event + def connect(sid, environ, auth): + print('connect ', sid) + + @sio.event + def disconnect(sid, reason): + print('disconnect ', sid, reason) + +The ``connect`` event is an ideal place to perform user authentication, and +any necessary mapping between user entities in the application and the ``sid`` +that was assigned to the client. + +In addition to the ``sid``, the connect handler receives ``environ`` as an +argument, with the request information in standard WSGI format, including HTTP +headers. The connect handler also receives the ``auth`` argument with any +authentication details passed by the client, or ``None`` if the client did not +pass any authentication. + +After inspecting the arguments, the connect event handler can return ``False`` +to reject the connection with the client. Sometimes it is useful to pass data +back to the client being rejected. In that case instead of returning ``False`` +a :class:`socketio.exceptions.ConnectionRefusedError` exception can be raised, +and all of its arguments will be sent to the client with the rejection +message:: + + from socketio.exceptions import ConnectionRefusedError + + @sio.event + def connect(sid, environ, auth): + raise ConnectionRefusedError('authentication failed') + +The disconnect handler receives the ``sid`` assigned to the client and a +``reason``, which provides the cause of the disconnection:: + + @sio.event + def disconnect(sid, reason): + if reason == sio.reason.CLIENT_DISCONNECT: + print('the client disconnected') + elif reason == sio.reason.SERVER_DISCONNECT: + print('the server disconnected the client') + else: + print('disconnect reason:', reason) + +See the The :attr:`socketio.Server.reason` attribute for a list of possible +disconnection reasons. + +Catch-All Event Handlers +~~~~~~~~~~~~~~~~~~~~~~~~ + +A "catch-all" event handler is invoked for any events that do not have an +event handler. You can define a catch-all handler using ``'*'`` as event name:: + + @sio.on('*') + def any_event(event, sid, data): + pass + +Asyncio servers can also use a coroutine:: + + @sio.on('*') + async def any_event(event, sid, data): + pass + +A catch-all event handler receives the event name as a first argument. The +remaining arguments are the same as for a regular event handler. + +Note that the ``connect`` and ``disconnect`` events have to be defined +explicitly and are not invoked on a catch-all event handler. + +Emitting Events to Clients +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Socket.IO is a bidirectional protocol, so at any time the server can send an +event to its connected clients. The :func:`socketio.Server.emit` method is +used for this task:: + + sio.emit('my event', {'data': 'foobar'}) + +The first argument is the event name, followed by an optional data payload of +type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``. When sending a +``list``, ``dict`` or ``tuple``, the elements are also constrained to the same +data types. When a ``tuple`` is sent, the elements of the tuple will be passed +as multiple arguments to the client-side event handler function. + +The above example will send the event to all the clients are connected. +Sometimes the server may want to send an event just to one particular client. +This can be achieved by adding a ``to`` argument to the emit call, with the +``sid`` of the client:: + + sio.emit('my event', {'data': 'foobar'}, to=user_sid) + +The ``to`` argument is used to identify the client that should receive the +event, and is set to the ``sid`` value assigned to that client's connection +with the server. When ``to`` is omitted, the event is broadcasted to all +connected clients. + +Acknowledging Events +~~~~~~~~~~~~~~~~~~~~ + +When a client sends an event to the server, it can optionally request to +receive acknowledgment from the server. The sending of acknowledgements is +automatically managed by the Socket.IO server, but the event handler function +can provide a list of values that are to be passed on to the client with the +acknowledgement simply by returning them:: + + @sio.event + def my_event(sid, data): + # handle the message + return "OK", 123 # <-- client will have these as acknowledgement + +Requesting Client Acknowledgements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Similar to how clients can request acknowledgements from the server, when the +server is emitting to a single client it can also ask the client to acknowledge +the event, and optionally return one or more values as a response. + +The Socket.IO server supports two ways of working with client acknowledgements. +The most convenient method is to replace :func:`socketio.Server.emit` with +:func:`socketio.Server.call`. The ``call()`` method will emit the event, and +then wait until the client sends an acknowledgement, returning any values +provided by the client:: + + response = sio.call('my event', {'data': 'foobar'}, to=user_sid) + +A much more primitive acknowledgement solution uses callback functions. The +:func:`socketio.Server.emit` method has an optional ``callback`` argument that +can be set to a callable. If this argument is given, the callable will be +invoked after the client has processed the event, and any values returned by +the client will be passed as arguments to this function:: + + def my_callback(): + print("callback invoked!") + + sio.emit('my event', {'data': 'foobar'}, to=user_sid, callback=my_callback) + +Rooms +----- + +To make it easy for the server to emit events to groups of related clients, +the application can put its clients into "rooms", and then address messages to +these rooms. + +In previous examples, the ``to`` argument of the :func:`socketio.SocketIO.emit` +method was used to designate a specific client as the recipient of the event. +The ``to`` argument can also be given the name of a room, and then all the +clients that are in that room will receive the event. + +The application can create as many rooms as needed and manage which clients are +in them using the :func:`socketio.Server.enter_room` and +:func:`socketio.Server.leave_room` methods. Clients can be in as many +rooms as needed and can be moved between rooms when necessary. + +:: + + @sio.event + def begin_chat(sid): + sio.enter_room(sid, 'chat_users') + + @sio.event + def exit_chat(sid): + sio.leave_room(sid, 'chat_users') + +In chat applications it is often desired that an event is broadcasted to all +the members of the room except one, which is the originator of the event such +as a chat message. The :func:`socketio.Server.emit` method provides an +optional ``skip_sid`` argument to indicate a client that should be skipped +during the broadcast. + +:: + + @sio.event + def my_message(sid, data): + sio.emit('my reply', data, room='chat_users', skip_sid=sid) + +Namespaces +---------- + +The Socket.IO protocol supports multiple logical connections, all multiplexed +on the same physical connection. Clients can open multiple connections by +specifying a different *namespace* on each. A namespace is given by the client +as a pathname following the hostname and port. For example, connecting to +*http://example.com:8000/chat* would open a connection to the namespace +*/chat*. + +Each namespace works independently from the others, with separate session +IDs (``sid``\ s), event handlers and rooms. Namespaces can be defined directly +in the event handler functions, or they can also be created as classes. + +Decorator-Based Namespaces +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Decorator-based namespaces are regular event handlers that include the +``namespace`` argument in their decorator:: + + @sio.event(namespace='/chat') + def my_custom_event(sid, data): + pass + + @sio.on('my custom event', namespace='/chat') + def my_custom_event(sid, data): + pass + +When emitting an event, the ``namespace`` optional argument is used to specify +which namespace to send it on. When the ``namespace`` argument is omitted, the +default Socket.IO namespace, which is named ``/``, is used. + +It is important that applications that use multiple namespaces specify the +correct namespace when setting up their event handlers and rooms using the +optional ``namespace`` argument. This argument must also be specified when +emitting events under a namespace. Most methods in the :class:`socketio.Server` +class have the optional ``namespace`` argument. + +Class-Based Namespaces +~~~~~~~~~~~~~~~~~~~~~~ + +As an alternative to the decorator-based namespaces, the event handlers that +belong to a namespace can be created as methods in a subclass of +:class:`socketio.Namespace`:: + + class MyCustomNamespace(socketio.Namespace): + def on_connect(self, sid, environ): + pass + + def on_disconnect(self, sid, reason): + pass + + def on_my_event(self, sid, data): + self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/test')) + +For asyncio based servers, namespaces must inherit from +:class:`socketio.AsyncNamespace`, and can define event handlers as coroutines +if desired:: + + class MyCustomNamespace(socketio.AsyncNamespace): + def on_connect(self, sid, environ): + pass + + def on_disconnect(self, sid, reason): + pass + + async def on_my_event(self, sid, data): + await self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/test')) + +When class-based namespaces are used, any events received by the server are +dispatched to a method named as the event name with the ``on_`` prefix. For +example, event ``my_event`` will be handled by a method named ``on_my_event``. +If an event is received for which there is no corresponding method defined in +the namespace class, then the event is ignored. All event names used in +class-based namespaces must use characters that are legal in method names. + +As a convenience to methods defined in a class-based namespace, the namespace +instance includes versions of several of the methods in the +:class:`socketio.Server` and :class:`socketio.AsyncServer` classes that default +to the proper namespace when the ``namespace`` argument is not given. + +In the case that an event has a handler in a class-based namespace, and also a +decorator-based function handler, only the standalone function handler is +invoked. + +It is important to note that class-based namespaces are singletons. This means +that a single instance of a namespace class is used for all clients, and +consequently, a namespace instance cannot be used to store client specific +information. + +Catch-All Namespaces +~~~~~~~~~~~~~~~~~~~~ + +Similarly to catch-all event handlers, a "catch-all" namespace can be used +when defining event handlers for any connected namespaces that do not have an +explicitly defined event handler. As with catch-all events, ``'*'`` is used in +place of a namespace:: + + @sio.on('my_event', namespace='*') + def my_event_any_namespace(namespace, sid, data): + pass + +For these events, the namespace is passed as first argument, followed by the +regular arguments of the event. + +A catch-all class-based namespace handler can be defined by passing ``'*'`` as +the namespace during registration:: + + sio.register_namespace(MyCustomNamespace('*')) + +A "catch-all" handler for all events on all namespaces can be defined as +follows:: + + @sio.on('*', namespace='*') + def any_event_any_namespace(event, namespace, sid, data): + pass + +Event handlers with catch-all events and namespaces receive the event name and +the namespace as first and second arguments. + +User Sessions +------------- + +The server can maintain application-specific information in a user session +dedicated to each connected client. Applications can use the user session to +write any details about the user that need to be preserved throughout the life +of the connection, such as usernames or user ids. + +The ``save_session()`` and ``get_session()`` methods are used to store and +retrieve information in the user session:: + + @sio.event + def connect(sid, environ): + username = authenticate_user(environ) + sio.save_session(sid, {'username': username}) + + @sio.event + def message(sid, data): + session = sio.get_session(sid) + print('message from ', session['username']) + +For the ``asyncio`` server, these methods are coroutines:: + + @sio.event + async def connect(sid, environ): + username = authenticate_user(environ) + await sio.save_session(sid, {'username': username}) + + @sio.event + async def message(sid, data): + session = await sio.get_session(sid) + print('message from ', session['username']) + +The session can also be manipulated with the `session()` context manager:: + + @sio.event + def connect(sid, environ): + username = authenticate_user(environ) + with sio.session(sid) as session: + session['username'] = username + + @sio.event + def message(sid, data): + with sio.session(sid) as session: + print('message from ', session['username']) + +For the ``asyncio`` server, an asynchronous context manager is used:: + + @sio.event + async def connect(sid, environ): + username = authenticate_user(environ) + async with sio.session(sid) as session: + session['username'] = username + + @sio.event + async def message(sid, data): + async with sio.session(sid) as session: + print('message from ', session['username']) + +The ``get_session()``, ``save_session()`` and ``session()`` methods take an +optional ``namespace`` argument. If this argument isn't provided, the session +is attached to the default namespace. + +Note: the contents of the user session are destroyed when the client +disconnects. In particular, user session contents are not preserved when a +client reconnects after an unexpected disconnection from the server. + +Cross-Origin Controls +--------------------- + +For security reasons, this server enforces a same-origin policy by default. In +practical terms, this means the following: + +- If an incoming HTTP or WebSocket request includes the ``Origin`` header, + this header must match the scheme and host of the connection URL. In case + of a mismatch, a 400 status code response is returned and the connection is + rejected. +- No restrictions are imposed on incoming requests that do not include the + ``Origin`` header. + +If necessary, the ``cors_allowed_origins`` option can be used to allow other +origins. This argument can be set to a string to set a single allowed origin, or +to a list to allow multiple origins. A special value of ``'*'`` can be used to +instruct the server to allow all origins, but this should be done with care, as +this could make the server vulnerable to Cross-Site Request Forgery (CSRF) +attacks. + +Monitoring and Administration +----------------------------- + +The Socket.IO server can be configured to accept connections from the official +`Socket.IO Admin UI `_. This tool provides +real-time information about currently connected clients, rooms in use and +events being emitted. It also allows an administrator to manually emit events, +change room assignments and disconnect clients. The hosted version of this tool +is available at `https://admin.socket.io `_. + +Given that enabling this feature can affect the performance of the server, it +is disabled by default. To enable it, call the +:func:`instrument() ` method. For example:: + + import os + import socketio + + sio = socketio.Server(cors_allowed_origins=[ + 'http://localhost:5000', + 'https://admin.socket.io', + ]) + sio.instrument(auth={ + 'username': 'admin', + 'password': os.environ['ADMIN_PASSWORD'], + }) + +This configures the server to accept connections from the hosted Admin UI +client. Administrators can then open https://admin.socket.io in their web +browsers and log in with username ``admin`` and the password given by the +``ADMIN_PASSWORD`` environment variable. To ensure the Admin UI front end is +allowed to connect, CORS is also configured. + +Consult the reference documentation to learn about additional configuration +options that are available. + +Debugging and Troubleshooting +----------------------------- + +To help you debug issues, the server can be configured to output logs to the +terminal:: + + import socketio + + # standard Python + sio = socketio.Server(logger=True, engineio_logger=True) + + # asyncio + sio = socketio.AsyncServer(logger=True, engineio_logger=True) + +The ``logger`` argument controls logging related to the Socket.IO protocol, +while ``engineio_logger`` controls logs that originate in the low-level +Engine.IO transport. The value given to these arguments controls logging +behavior: + +* ``True``: Enables log output to ``stderr`` at the ``INFO`` level. +* ``False``: Enables log output to ``stderr`` at the ``ERROR`` level. This is + the default. +* A ``logging.Logger`` instance: Uses the provided logger without additional + configuration. + +Logging can help identify the cause of connection problems, 400 responses, +bad performance and other issues. + +Concurrency and Web Server Integration +-------------------------------------- + +The Socket.IO server can be configured with different concurrency models +depending on the needs of the application and the web server that is used. The +concurrency model is given by the ``async_mode`` argument in the server. For +example:: + + sio = socketio.Server(async_mode='threading') + +The following sub-sections describe the available concurrency options for +synchronous and asynchronous servers. + +Standard Modes +~~~~~~~~~~~~~~ + +- ``threading``: the server will use Python threads for concurrency and will + run on any multi-threaded WSGI server. This is the default mode when no other + concurrency libraries are installed. +- ``gevent``: the server will use greenlets through the + `gevent `_ library for concurrency. A web server that + is compatible with ``gevent`` is required. +- ``gevent_uwsgi``: a variation of the ``gevent`` mode that is designed to work + with the `uWSGI `_ web server. +- ``eventlet``: the server will use greenlets through the + `eventlet `_ library for concurrency. A web server that + is compatible with ``eventlet`` is required. Use of ``eventlet`` is not + recommended due to this project being in maintenance mode. + +Asyncio Modes +~~~~~~~~~~~~~ + +The asynchronous options are all based on the +`asyncio `_ package of the +Python standard library, with minor variations depending on the web server +platform that is used. + +- ``asgi``: use of any + `ASGI `_ web server is required. +- ``aiohttp``: use of the `aiohttp `_ web + framework and server is required. +- ``tornado``: use of the `Tornado `_ web framework + and server is required. +- ``sanic``: use of the `Sanic `_ web framework + and server is required. When using Sanic, it is recommended to use the + ``asgi`` mode instead. + +.. _deployment-strategies: + +Deployment Strategies +--------------------- + +The following sections describe a variety of deployment strategies for +Socket.IO servers. + +Gunicorn +~~~~~~~~ + +The simplest deployment strategy for the Socket.IO server is to use the popular +`Gunicorn `_ web server in multi-threaded mode. The +Socket.IO server must be wrapped by the :class:`socketio.WSGIApp` class, so +that it is compatible with the WSGI protocol:: + + sio = socketio.Server(async_mode='threading') + app = socketio.WSGIApp(sio) + +If desired, the ``socketio.WSGIApp`` class can forward any traffic that is not +Socket.IO to another WSGI application, making it possible to deploy a standard +WSGI web application built with frameworks such as Flask or Django and the +Socket.IO server as a bundle:: + + sio = socketio.Server(async_mode='threading') + app = socketio.WSGIApp(sio, other_wsgi_app) + +The example that follows shows how to start a Socket.IO application using +Gunicorn's threaded worker class:: + + $ gunicorn --workers 1 --threads 100 --bind 127.0.0.1:5000 module:app + +With the above configuration the server will be able to handle close to 100 +concurrent clients. + +It is also possible to use more than one worker process, but this has two +additional requirements: + +- The clients must connect directly over WebSocket. The long-polling transport + is incompatible with the way Gunicorn load balances requests among workers. + To disable long-polling in the server, add ``transports=['websocket']`` in + the server constructor. Clients will have a similar option to initiate the + connection with WebSocket. +- The :func:`socketio.Server` instances in each worker must be configured with + a message queue to allow the workers to communicate with each other. See the + :ref:`using-a-message-queue` section for more information. + +When using multiple workers, the approximate number of connections the server +will be able to accept can be calculated as the number of workers multiplied by +the number of threads per worker. + +Note that Gunicorn can also be used alongside ``uvicorn``, ``gevent`` and +``eventlet``. These options are discussed under the appropriate sections below. + +Uvicorn (and other ASGI web servers) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When working with an asynchronous Socket.IO server, the easiest deployment +strategy is to use an ASGI web server such as +`Uvicorn `_. + +The ``socketio.ASGIApp`` class is an ASGI compatible application that can +forward Socket.IO traffic to a ``socketio.AsyncServer`` instance:: + + sio = socketio.AsyncServer(async_mode='asgi') + app = socketio.ASGIApp(sio) + +If desired, the ``socketio.ASGIApp`` class can forward any traffic that is not +Socket.IO to another ASGI application, making it possible to deploy a standard +ASGI web application built with a framework such as FastAPI and the Socket.IO +server as a bundle:: + + sio = socketio.AsyncServer(async_mode='asgi') + app = socketio.ASGIApp(sio, other_asgi_app) + +The following example starts the application with Uvicorn:: + + uvicorn --port 5000 module:app + +Uvicorn can also be used through its Gunicorn worker:: + + gunicorn --workers 1 --worker-class uvicorn.workers.UvicornWorker --bind 127.0.0.1:5000 + +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. + +Hypercorn, Daphne, and other ASGI servers +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +To use an ASGI web server other than Uvicorn, configure the application for +ASGI as shown above for Uvicorn, then follow the documentation of your chosen +web server to start the application. + +Aiohttp +~~~~~~~ + +Another option for deploying an asynchronous Socket.IO server is to use the +`Aiohttp `_ web framework and server. Instances +of class ``socketio.AsyncServer`` will automatically use Aiohttp +if the library is installed. To request its use explicitly, the ``async_mode`` +option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='aiohttp') + +A server configured for Aiohttp must be attached to an existing application:: + + app = web.Application() + sio.attach(app) + +The Aiohttp application can define regular routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The Aiohttp application is then executed in the usual manner:: + + if __name__ == '__main__': + web.run_app(app) + +Gevent +~~~~~~ + +When a multi-threaded web server is unable to satisfy the concurrency and +scalability requirements of the application, an option to try is +`Gevent `_. Gevent is a coroutine-based concurrency library +based on greenlets, which are significantly lighter than threads. + +Instances of class ``socketio.Server`` will automatically use Gevent if the +library is installed. To request gevent to be selected explicitly, the +``async_mode`` option can be given in the constructor:: + + sio = socketio.Server(async_mode='gevent') + +The Socket.IO server must be wrapped by the :class:`socketio.WSGIApp` class, so +that it is compatible with the WSGI protocol:: + + app = socketio.WSGIApp(sio) + +If desired, the ``socketio.WSGIApp`` class can forward any traffic that is not +Socket.IO to another WSGI application, making it possible to deploy a standard +WSGI web application built with frameworks such as Flask or Django and the +Socket.IO server as a bundle:: + + sio = socketio.Server(async_mode='gevent') + app = socketio.WSGIApp(sio, other_wsgi_app) + +A server configured for Gevent is deployed as a regular WSGI application +using the provided ``socketio.WSGIApp``:: + + from gevent import pywsgi + + pywsgi.WSGIServer(('', 8000), app).serve_forever() + +Gevent with Gunicorn +!!!!!!!!!!!!!!!!!!!! + +An alternative to running the gevent WSGI server as above is to use +`Gunicorn `_ with its Gevent worker. The command to launch the +application under Gunicorn and Gevent is shown below:: + + $ gunicorn -k gevent -w 1 -b 127.0.0.1:5000 module:app + +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. + +Gevent provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +the Socket.IO server does not require monkey patching, other libraries such as +database or message queue drivers are likely to require it. + +Gevent with uWSGI +!!!!!!!!!!!!!!!!! + +When using the uWSGI server in combination with gevent, the Socket.IO server +can take advantage of uWSGI's native WebSocket support. + +Instances of class ``socketio.Server`` will automatically use this option for +asynchronous operations if both gevent and uWSGI are installed and eventlet is +not installed. To request this asynchronous mode explicitly, the +``async_mode`` option can be given in the constructor:: + + # gevent with uWSGI + sio = socketio.Server(async_mode='gevent_uwsgi') + +A complete explanation of the configuration and usage of the uWSGI server is +beyond the scope of this documentation. The uWSGI server is a fairly complex +package that provides a large and comprehensive set of options. It must be +compiled with WebSocket and SSL support for the WebSocket transport to be +available. As way of an introduction, the following command starts a uWSGI +server for the ``latency.py`` example on port 5000:: + + $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app + +Tornado +~~~~~~~ + +Instances of class ``socketio.AsyncServer`` will automatically use +`Tornado `_ if the library is installed. To +request its use explicitly, the ``async_mode`` option can be given in the +constructor:: + + sio = socketio.AsyncServer(async_mode='tornado') + +A server configured for Tornado must include a request handler for +Socket.IO:: + + app = tornado.web.Application( + [ + (r"/socket.io/", socketio.get_tornado_handler(sio)), + ], + # ... other application options + ) + +The Tornado application can define other routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The Tornado application is then executed in the usual manner:: + + app.listen(port) + tornado.ioloop.IOLoop.current().start() + +Eventlet +~~~~~~~~ + +.. note:: + Eventlet is not in active development anymore, and for that reason the + current recommendation is to not use it for new projects. + +`Eventlet `_ is a high performance concurrent networking +library for Python that uses coroutines, enabling code to be written in the +same style used with the blocking standard library functions. An Socket.IO +server deployed with eventlet has access to the long-polling and WebSocket +transports. + +Instances of class ``socketio.Server`` will automatically use eventlet for +asynchronous operations if the library is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.Server(async_mode='eventlet') + +A server configured for eventlet is deployed as a regular WSGI application +using the provided ``socketio.WSGIApp``:: + + import eventlet + + app = socketio.WSGIApp(sio) + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + +Eventlet with Gunicorn +!!!!!!!!!!!!!!!!!!!!!! + +An alternative to running the eventlet WSGI server as above is to use +`gunicorn `_, a fully featured pure Python web server. The +command to launch the application under gunicorn is shown below:: + + $ gunicorn -k eventlet -w 1 module:app + +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. + +Eventlet provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +python-socketio does not require monkey patching, other libraries such as +database drivers are likely to require it. + +Sanic +~~~~~ + +.. note:: + The Sanic integration has not been updated in a long time. It is currently + recommended that a Sanic application is deployed with the ASGI integration. + +.. _using-a-message-queue: + +Using a Message Queue +--------------------- + +When working with distributed applications, it is often necessary to access +the functionality of the Socket.IO from multiple processes. There are two +specific use cases: + +- Highly available applications may want to use horizontal scaling of the + Socket.IO server to be able to handle very large number of concurrent + clients. +- Applications that use work queues such as + `Celery `_ may need to emit an event to a + client once a background job completes. The most convenient place to carry + out this task is the worker process that handled this job. + +As a solution to the above problems, the Socket.IO server can be configured +to connect to a message queue such as `Redis `_ or +`RabbitMQ `_, to communicate with other related +Socket.IO servers or auxiliary workers. + +Redis +~~~~~ + +To use a Redis message queue, a Python Redis client must be installed:: + + # socketio.Server class + pip install redis + +The Redis queue is configured through the :class:`socketio.RedisManager` and +:class:`socketio.AsyncRedisManager` classes. These classes connect directly to +the Redis store and use the queue's pub/sub functionality:: + + # socketio.Server class + mgr = socketio.RedisManager('redis://') + sio = socketio.Server(client_manager=mgr) + + # socketio.AsyncServer class + mgr = socketio.AsyncRedisManager('redis://') + sio = socketio.AsyncServer(client_manager=mgr) + +The ``client_manager`` argument instructs the server to connect to the given +message queue, and to coordinate with other processes connected to the queue. + +Kombu +~~~~~ + +`Kombu `_ is a Python package that +provides access to RabbitMQ and many other message queues. It can be installed +with pip:: + + pip install kombu + +To use RabbitMQ or other AMQP protocol compatible queues, that is the only +required dependency. But for other message queues, Kombu may require +additional packages. For example, to use a Redis queue via Kombu, the Python +package for Redis needs to be installed as well:: + + pip install redis + +The queue is configured through the :class:`socketio.KombuManager`:: + + mgr = socketio.KombuManager('amqp://') + sio = socketio.Server(client_manager=mgr) + +The connection URL passed to the :class:`KombuManager` constructor is passed +directly to Kombu's `Connection object +`_, so +the Kombu documentation should be consulted for information on how to build +the correct URL for a given message queue. + +Note that Kombu currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. + +Kafka +~~~~~ + +`Apache Kafka `_ is supported through the +`kafka-python `_ +package:: + + pip install kafka-python + +Access to Kafka is configured through the :class:`socketio.KafkaManager` +class:: + + mgr = socketio.KafkaManager('kafka://') + sio = socketio.Server(client_manager=mgr) + +Note that Kafka currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. + +AioPika +~~~~~~~ + +A RabbitMQ message queue is supported in asyncio applications through the +`AioPika `_ package:: +You need to install aio_pika with pip:: + + pip install aio_pika + +The RabbitMQ queue is configured through the +:class:`socketio.AsyncAioPikaManager` class:: + + mgr = socketio.AsyncAioPikaManager('amqp://') + sio = socketio.AsyncServer(client_manager=mgr) + +Deploying the Message Queue for Production +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For a production deployment there are a few recommendations to keep your +application secure. + +First of all, the message queue should never be listening on a public network +interface, to ensure that external clients never connect to it. For a single +node deployment, the queue should only listen on `localhost`. For a multi-node +system the use of a private network (VPC), where the communication between +servers can happen privately is highly recommended. + +In addition, all message queues support authentication and encryption, which +can strenthen the security of the deployment. Authentication ensures that only +the Socket.IO servers and related processes have access, while encryption +prevents data from being collected by a third-party that is listening on the +network. Access credentials can be included in the connection URLs that are +passed to the client managers. + +Horizontal Scaling +~~~~~~~~~~~~~~~~~~ + +Socket.IO is a stateful protocol, which makes horizontal scaling more +difficult. When deploying a cluster of Socket.IO processes, all processes must +connect to the message queue by passing the ``client_manager`` argument to the +server instance. This enables the workers to communicate and coordinate complex +operations such as broadcasts. + +If the long-polling transport is used, then there are two additional +requirements that must be met: + +- Each Socket.IO process must be able to handle multiple requests + concurrently. This is needed because long-polling clients send two + requests in parallel. Worker processes that can only handle one request at a + time are not supported. +- The load balancer must be configured to always forward requests from a + client to the same worker process, so that all requests coming from a client + are handled by the same node. Load balancers call this *sticky sessions*, or + *session affinity*. + +Emitting from external processes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To have a process other than a server connect to the queue to emit a message, +the same client manager classes can be used as standalone objects. In this +case, the ``write_only`` argument should be set to ``True`` to disable the +creation of a listening thread, which only makes sense in a server. For +example:: + + # connect to the redis queue as an external process + external_sio = socketio.RedisManager('redis://', write_only=True) + + # emit an event + external_sio.emit('my event', data={'foo': 'bar'}, room='my room') + +A limitation of the write-only client manager object is that it cannot receive +callbacks when emitting. When the external process needs to receive callbacks, +using a client to connect to the server with read and write support is a better +option than a write-only client manager. diff --git a/examples/README.rst b/examples/README.rst index a0ae1f65..82b918e5 100644 --- a/examples/README.rst +++ b/examples/README.rst @@ -1,15 +1,6 @@ Socket.IO Examples ================== -This directory contains several example Socket.IO applications, organized by -directory: - -wsgi ----- - -Examples that are compatible with the WSGI protocol and frameworks. - -aiohttp -------- - -Examples that are compatible with the aiohttp framework for asyncio. +This directory contains several example Socket.IO applications. Look in the +`server` directory for Socket.IO servers, and in the `client` and +`simple-client` directories for Socket.IO clients. diff --git a/examples/aiohttp/app.py b/examples/aiohttp/app.py deleted file mode 100755 index c9ac7d5b..00000000 --- a/examples/aiohttp/app.py +++ /dev/null @@ -1,88 +0,0 @@ -import asyncio - -from aiohttp import web - -import socketio - -sio = socketio.AsyncServer(async_mode='aiohttp') -app = web.Application() -sio.attach(app) - - -async def background_task(): - """Example of how to send server generated events to clients.""" - count = 0 - while True: - await sio.sleep(10) - count += 1 - await sio.emit('my response', {'data': 'Server generated event'}, - namespace='/test') - - -async def index(request): - with open('app.html') as f: - return web.Response(text=f.read(), content_type='text/html') - - -@sio.on('my event', namespace='/test') -async def test_message(sid, message): - await sio.emit('my response', {'data': message['data']}, room=sid, - namespace='/test') - - -@sio.on('my broadcast event', namespace='/test') -async def test_broadcast_message(sid, message): - await sio.emit('my response', {'data': message['data']}, namespace='/test') - - -@sio.on('join', namespace='/test') -async def join(sid, message): - sio.enter_room(sid, message['room'], namespace='/test') - await sio.emit('my response', {'data': 'Entered room: ' + message['room']}, - room=sid, namespace='/test') - - -@sio.on('leave', namespace='/test') -async def leave(sid, message): - sio.leave_room(sid, message['room'], namespace='/test') - await sio.emit('my response', {'data': 'Left room: ' + message['room']}, - room=sid, namespace='/test') - - -@sio.on('close room', namespace='/test') -async def close(sid, message): - await sio.emit('my response', - {'data': 'Room ' + message['room'] + ' is closing.'}, - room=message['room'], namespace='/test') - await sio.close_room(message['room'], namespace='/test') - - -@sio.on('my room event', namespace='/test') -async def send_room_message(sid, message): - await sio.emit('my response', {'data': message['data']}, - room=message['room'], namespace='/test') - - -@sio.on('disconnect request', namespace='/test') -async def disconnect_request(sid): - await sio.disconnect(sid, namespace='/test') - - -@sio.on('connect', namespace='/test') -async def test_connect(sid, environ): - await sio.emit('my response', {'data': 'Connected', 'count': 0}, room=sid, - namespace='/test') - - -@sio.on('disconnect', namespace='/test') -def test_disconnect(sid): - print('Client disconnected') - - -app.router.add_static('/static', 'static') -app.router.add_get('/', index) - - -if __name__ == '__main__': - sio.start_background_task(background_task) - web.run_app(app) diff --git a/examples/client/README.rst b/examples/client/README.rst new file mode 100644 index 00000000..ce5dab04 --- /dev/null +++ b/examples/client/README.rst @@ -0,0 +1,20 @@ +Socket.IO Client Examples +========================= + +This directory contains several example Socket.IO client applications, +organized by directory: + +sync +---- + +Examples that use standard Python thread concurrency. + +async +----- + +Examples that use Python's `asyncio` package for concurrency. + +javascript +---------- + +Examples that use the JavaScript version of Socket.IO for compatiblity testing. diff --git a/examples/client/async/README.rst b/examples/client/async/README.rst new file mode 100644 index 00000000..57102139 --- /dev/null +++ b/examples/client/async/README.rst @@ -0,0 +1,32 @@ +Socket.IO Async Client Examples +=============================== + +This directory contains example Socket.IO clients that work with the +``asyncio`` package of the Python standard library. + +latency_client.py +----------------- + +In this application the client sends *ping* messages to the server, which are +responded by the server with a *pong*. The client measures the time it takes +for each of these exchanges. + +This is an ideal application to measure the performance of the different +asynchronous modes supported by the Socket.IO server. + +fiddle_client.py +---------------- + +This is an extemely simple application based on the JavaScript example of the +same name. + +Running the Examples +-------------------- + +These examples work with the server examples of the same name. First run one +of the ``latency.py`` or ``fiddle.py`` versions from one of the +``examples/server`` subdirectories. On another terminal, then start the +corresponding client:: + + $ python latency_client.py + $ python fiddle_client.py diff --git a/examples/client/async/fiddle_client.py b/examples/client/async/fiddle_client.py new file mode 100644 index 00000000..e5aeb6cc --- /dev/null +++ b/examples/client/async/fiddle_client.py @@ -0,0 +1,28 @@ +import asyncio +import socketio + +sio = socketio.AsyncClient() + + +@sio.event +async def connect(): + print('connected to server') + + +@sio.event +async def disconnect(reason): + print('disconnected from server, reason:', reason) + + +@sio.event +def hello(a, b, c): + print(a, b, c) + + +async def start_server(): + await sio.connect('http://localhost:5000', auth={'token': 'my-token'}) + await sio.wait() + + +if __name__ == '__main__': + asyncio.run(start_server()) diff --git a/examples/client/async/latency_client.py b/examples/client/async/latency_client.py new file mode 100644 index 00000000..57be604d --- /dev/null +++ b/examples/client/async/latency_client.py @@ -0,0 +1,37 @@ +import asyncio +import time +import socketio + +loop = asyncio.get_event_loop() +sio = socketio.AsyncClient() +start_timer = None + + +async def send_ping(): + global start_timer + start_timer = time.time() + await sio.emit('ping_from_client') + + +@sio.event +async def connect(): + print('connected to server') + await send_ping() + + +@sio.event +async def pong_from_server(): + latency = time.time() - start_timer + print(f'latency is {latency * 1000:.2f} ms') + await sio.sleep(1) + if sio.connected: + await send_ping() + + +async def start_server(): + await sio.connect('http://localhost:5000') + await sio.wait() + + +if __name__ == '__main__': + loop.run_until_complete(start_server()) diff --git a/examples/client/javascript/README.md b/examples/client/javascript/README.md new file mode 100644 index 00000000..eaf55e28 --- /dev/null +++ b/examples/client/javascript/README.md @@ -0,0 +1,10 @@ + +# Socket.IO Fiddle + +``` +$ npm install +$ node fiddle-client.js # to run the fiddle example +$ node latency-client.js # to run the latency example +``` + +Optionally, specify a port by supplying the `PORT` env variable. diff --git a/examples/client/javascript/fiddle_client.js b/examples/client/javascript/fiddle_client.js new file mode 100644 index 00000000..9421b55c --- /dev/null +++ b/examples/client/javascript/fiddle_client.js @@ -0,0 +1,16 @@ +const io = require('socket.io-client') +const port = process.env.PORT || 5000; + +const socket = io('http://localhost:' + port, {auth: {token: 'my-token'}}); + +socket.on('connect', () => { + console.log(`connect ${socket.id}`); +}); + +socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); +}); + +socket.on('hello', (a, b, c) => { + console.log(a, b, c); +}); diff --git a/examples/client/javascript/latency_client.js b/examples/client/javascript/latency_client.js new file mode 100644 index 00000000..ef4d0863 --- /dev/null +++ b/examples/client/javascript/latency_client.js @@ -0,0 +1,24 @@ +const io = require('socket.io-client') +const port = process.env.PORT || 5000; + +const socket = io('http://localhost:' + port); +let last; +function send () { + last = new Date(); + socket.emit('ping_from_client'); +} + +socket.on('connect', () => { + console.log(`connect ${socket.id}`); + send(); +}); + +socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); +}); + +socket.on('pong_from_server', () => { + const latency = new Date() - last; + console.log('latency is ' + latency + ' ms'); + setTimeout(send, 1000); +}); diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json new file mode 100644 index 00000000..fef1fa8f --- /dev/null +++ b/examples/client/javascript/package-lock.json @@ -0,0 +1,1904 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "socketio-examples", + "version": "0.1.0", + "dependencies": { + "express": "^4.22.1", + "smoothie": "1.19.0", + "socket.io": "^4.8.0", + "socket.io-client": "^4.6.1" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" + }, + "node_modules/socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + } + }, + "dependencies": { + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "requires": { + "undici-types": "~6.19.2" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "requires": { + "side-channel": "^1.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" + }, + "socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "requires": { + "debug": "~4.3.4", + "ws": "~8.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + } + } +} diff --git a/examples/client/javascript/package.json b/examples/client/javascript/package.json new file mode 100644 index 00000000..8f1ec830 --- /dev/null +++ b/examples/client/javascript/package.json @@ -0,0 +1,10 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "dependencies": { + "express": "^4.22.1", + "smoothie": "1.19.0", + "socket.io": "^4.8.0", + "socket.io-client": "^4.6.1" + } +} diff --git a/examples/client/sync/README.rst b/examples/client/sync/README.rst new file mode 100644 index 00000000..efc4d6f9 --- /dev/null +++ b/examples/client/sync/README.rst @@ -0,0 +1,32 @@ +Socket.IO Client Examples +========================= + +This directory contains example Socket.IO clients that work with the +Python standard library. + +latency_client.py +----------------- + +In this application the client sends *ping* messages to the server, which are +responded by the server with a *pong*. The client measures the time it takes +for each of these exchanges. + +This is an ideal application to measure the performance of the different +asynchronous modes supported by the Socket.IO server. + +fiddle_client.py +---------------- + +This is an extemely simple application based on the JavaScript example of the +same name. + +Running the Examples +-------------------- + +These examples work with the server examples of the same name. First run one +of the ``latency.py`` or ``fiddle.py`` versions from one of the +``examples/server`` subdirectories. On another terminal, then start the +corresponding client:: + + $ python latency_client.py + $ python fiddle_client.py diff --git a/examples/client/sync/fiddle_client.py b/examples/client/sync/fiddle_client.py new file mode 100644 index 00000000..71a7a540 --- /dev/null +++ b/examples/client/sync/fiddle_client.py @@ -0,0 +1,23 @@ +import socketio + +sio = socketio.Client() + + +@sio.event +def connect(): + print('connected to server') + + +@sio.event +def disconnect(reason): + print('disconnected from server, reason:', reason) + + +@sio.event +def hello(a, b, c): + print(a, b, c) + + +if __name__ == '__main__': + sio.connect('http://localhost:5000', auth={'token': 'my-token'}) + sio.wait() diff --git a/examples/client/sync/latency_client.py b/examples/client/sync/latency_client.py new file mode 100644 index 00000000..94dcec9d --- /dev/null +++ b/examples/client/sync/latency_client.py @@ -0,0 +1,31 @@ +import time +import socketio + +sio = socketio.Client(logger=True, engineio_logger=True) +start_timer = None + + +def send_ping(): + global start_timer + start_timer = time.time() + sio.emit('ping_from_client') + + +@sio.event +def connect(): + print('connected to server') + send_ping() + + +@sio.event +def pong_from_server(): + latency = time.time() - start_timer + print(f'latency is {latency * 1000:.2f} ms') + sio.sleep(1) + if sio.connected: + send_ping() + + +if __name__ == '__main__': + sio.connect('http://localhost:5000') + sio.wait() diff --git a/examples/sanic/app.py b/examples/sanic/app.py deleted file mode 100755 index 8eeb6896..00000000 --- a/examples/sanic/app.py +++ /dev/null @@ -1,93 +0,0 @@ -import asyncio - -from sanic import Sanic -from sanic.response import html - -import socketio - -sio = socketio.AsyncServer(async_mode='sanic') -app = Sanic() -sio.attach(app) - - -async def background_task(): - """Example of how to send server generated events to clients.""" - count = 0 - while True: - await sio.sleep(10) - count += 1 - await sio.emit('my response', {'data': 'Server generated event'}, - namespace='/test') - - -@app.listener('before_server_start') -def before_server_start(sanic, loop): - sio.start_background_task(background_task) - - -@app.route('/') -async def index(request): - with open('app.html') as f: - return html(f.read()) - - -@sio.on('my event', namespace='/test') -async def test_message(sid, message): - await sio.emit('my response', {'data': message['data']}, room=sid, - namespace='/test') - - -@sio.on('my broadcast event', namespace='/test') -async def test_broadcast_message(sid, message): - await sio.emit('my response', {'data': message['data']}, namespace='/test') - - -@sio.on('join', namespace='/test') -async def join(sid, message): - sio.enter_room(sid, message['room'], namespace='/test') - await sio.emit('my response', {'data': 'Entered room: ' + message['room']}, - room=sid, namespace='/test') - - -@sio.on('leave', namespace='/test') -async def leave(sid, message): - sio.leave_room(sid, message['room'], namespace='/test') - await sio.emit('my response', {'data': 'Left room: ' + message['room']}, - room=sid, namespace='/test') - - -@sio.on('close room', namespace='/test') -async def close(sid, message): - await sio.emit('my response', - {'data': 'Room ' + message['room'] + ' is closing.'}, - room=message['room'], namespace='/test') - await sio.close_room(message['room'], namespace='/test') - - -@sio.on('my room event', namespace='/test') -async def send_room_message(sid, message): - await sio.emit('my response', {'data': message['data']}, - room=message['room'], namespace='/test') - - -@sio.on('disconnect request', namespace='/test') -async def disconnect_request(sid): - await sio.disconnect(sid, namespace='/test') - - -@sio.on('connect', namespace='/test') -async def test_connect(sid, environ): - await sio.emit('my response', {'data': 'Connected', 'count': 0}, room=sid, - namespace='/test') - - -@sio.on('disconnect', namespace='/test') -def test_disconnect(sid): - print('Client disconnected') - - -app.static('/static', './static') - - -if __name__ == '__main__': - app.run() diff --git a/examples/server/README.rst b/examples/server/README.rst new file mode 100644 index 00000000..2667f523 --- /dev/null +++ b/examples/server/README.rst @@ -0,0 +1,35 @@ +Socket.IO Server Examples +========================= + +This directory contains several example Socket.IO applications, organized by +directory: + +wsgi +---- + +Examples that are compatible with the WSGI protocol and frameworks. + +asgi +---- + +Examples that are compatible with the ASGI specification. + +aiohttp +------- + +Examples that are compatible with the aiohttp framework for asyncio. + +sanic +----- + +Examples that are compatible with the sanic framework for asyncio. + +tornado +------- + +Examples that are compatible with the tornado framework. + +javascript +---------- + +Examples that use the JavaScript version of Socket.IO for compatiblity testing. diff --git a/examples/aiohttp/README.rst b/examples/server/aiohttp/README.rst similarity index 86% rename from examples/aiohttp/README.rst rename to examples/server/aiohttp/README.rst index f1ce6aaf..1b0f4a42 100644 --- a/examples/aiohttp/README.rst +++ b/examples/server/aiohttp/README.rst @@ -23,17 +23,27 @@ time to the page. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. +fiddle.py +--------- + +This is a very simple application based on a JavaScript example of the same +name. + Running the Examples -------------------- To run these examples, create a virtual environment, install the requirements -and then run:: +and then run one of the following:: $ python app.py -or:: +:: $ python latency.py +:: + + $ python fiddle.py + You can then access the application from your web browser at ``http://localhost:8080``. diff --git a/examples/aiohttp/app.html b/examples/server/aiohttp/app.html old mode 100755 new mode 100644 similarity index 79% rename from examples/aiohttp/app.html rename to examples/server/aiohttp/app.html index 1012beab..627b9186 --- a/examples/aiohttp/app.html +++ b/examples/server/aiohttp/app.html @@ -1,21 +1,20 @@ - Flask-SocketIO Test + python-socketio test - + -

Flask-SocketIO Test

+

python-socketio test

Send:

diff --git a/examples/server/aiohttp/app.py b/examples/server/aiohttp/app.py new file mode 100644 index 00000000..1568ca1f --- /dev/null +++ b/examples/server/aiohttp/app.py @@ -0,0 +1,87 @@ +from aiohttp import web + +import socketio + +sio = socketio.AsyncServer(async_mode='aiohttp') +app = web.Application() +sio.attach(app) + + +async def background_task(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + await sio.sleep(10) + count += 1 + await sio.emit('my_response', {'data': 'Server generated event'}) + + +async def index(request): + with open('app.html') as f: + return web.Response(text=f.read(), content_type='text/html') + + +@sio.event +async def my_event(sid, message): + await sio.emit('my_response', {'data': message['data']}, room=sid) + + +@sio.event +async def my_broadcast_event(sid, message): + await sio.emit('my_response', {'data': message['data']}) + + +@sio.event +async def join(sid, message): + await sio.enter_room(sid, message['room']) + await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, + room=sid) + + +@sio.event +async def leave(sid, message): + await sio.leave_room(sid, message['room']) + await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, + room=sid) + + +@sio.event +async def close_room(sid, message): + await sio.emit('my_response', + {'data': 'Room ' + message['room'] + ' is closing.'}, + room=message['room']) + await sio.close_room(message['room']) + + +@sio.event +async def my_room_event(sid, message): + await sio.emit('my_response', {'data': message['data']}, + room=message['room']) + + +@sio.event +async def disconnect_request(sid): + await sio.disconnect(sid) + + +@sio.event +async def connect(sid, environ): + await sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) + + +@sio.event +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) + + +app.router.add_static('/static', 'static') +app.router.add_get('/', index) + + +async def init_app(): + sio.start_background_task(background_task) + return app + + +if __name__ == '__main__': + web.run_app(init_app(), port=5000) diff --git a/examples/server/aiohttp/fiddle.html b/examples/server/aiohttp/fiddle.html new file mode 100644 index 00000000..4b58e759 --- /dev/null +++ b/examples/server/aiohttp/fiddle.html @@ -0,0 +1,11 @@ + + + + + Socket.IO Fiddle + + + + + + diff --git a/examples/server/aiohttp/fiddle.py b/examples/server/aiohttp/fiddle.py new file mode 100644 index 00000000..64ce330d --- /dev/null +++ b/examples/server/aiohttp/fiddle.py @@ -0,0 +1,31 @@ +from aiohttp import web + +import socketio + +sio = socketio.AsyncServer(async_mode='aiohttp') +app = web.Application() +sio.attach(app) + + +async def index(request): + with open('fiddle.html') as f: + return web.Response(text=f.read(), content_type='text/html') + + +@sio.event +async def connect(sid, environ, auth): + print(f'connected auth={auth} sid={sid}') + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid, reason): + print('disconnected', sid, reason) + + +app.router.add_static('/static', 'static') +app.router.add_get('/', index) + + +if __name__ == '__main__': + web.run_app(app, port=5000) diff --git a/examples/sanic/latency.html b/examples/server/aiohttp/latency.html old mode 100755 new mode 100644 similarity index 91% rename from examples/sanic/latency.html rename to examples/server/aiohttp/latency.html index b238cd1c..b3450a61 --- a/examples/sanic/latency.html +++ b/examples/server/aiohttp/latency.html @@ -11,10 +11,10 @@

(connecting)

- + - + -

Flask-SocketIO Test

+

python-socketio test

Send:

diff --git a/examples/server/asgi/app.py b/examples/server/asgi/app.py new file mode 100644 index 00000000..996dc272 --- /dev/null +++ b/examples/server/asgi/app.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +# set instrument to `True` to accept connections from the official Socket.IO +# Admin UI hosted at https://admin.socket.io +instrument = True +admin_login = { + 'username': 'admin', + 'password': 'python', # change this to a strong secret for production use! +} + +import uvicorn +import socketio + +sio = socketio.AsyncServer( + async_mode='asgi', + cors_allowed_origins=None if not instrument else [ + 'http://localhost:5000', + 'https://admin.socket.io', # edit the allowed origins if necessary + ]) +if instrument: + sio.instrument(auth=admin_login) + +app = socketio.ASGIApp(sio, static_files={ + '/': 'app.html', +}) +background_task_started = False + + +async def background_task(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + await sio.sleep(10) + count += 1 + await sio.emit('my_response', {'data': 'Server generated event'}) + + +@sio.on('my_event') +async def test_message(sid, message): + await sio.emit('my_response', {'data': message['data']}, room=sid) + + +@sio.on('my_broadcast_event') +async def test_broadcast_message(sid, message): + await sio.emit('my_response', {'data': message['data']}) + + +@sio.on('join') +async def join(sid, message): + await sio.enter_room(sid, message['room']) + await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, + room=sid) + + +@sio.on('leave') +async def leave(sid, message): + await sio.leave_room(sid, message['room']) + await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, + room=sid) + + +@sio.on('close room') +async def close(sid, message): + await sio.emit('my_response', + {'data': 'Room ' + message['room'] + ' is closing.'}, + room=message['room']) + await sio.close_room(message['room']) + + +@sio.on('my_room_event') +async def send_room_message(sid, message): + await sio.emit('my_response', {'data': message['data']}, + room=message['room']) + + +@sio.on('disconnect request') +async def disconnect_request(sid): + await sio.disconnect(sid) + + +@sio.on('connect') +async def test_connect(sid, environ): + global background_task_started + if not background_task_started: + sio.start_background_task(background_task) + background_task_started = True + await sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) + + +@sio.on('disconnect') +def test_disconnect(sid, reason): + print('Client disconnected, reason:', reason) + + +if __name__ == '__main__': + if instrument: + print('The server is instrumented for remote administration.') + print( + 'Use the official Socket.IO Admin UI at https://admin.socket.io ' + 'with the following connection details:' + ) + print(' - Server URL: http://localhost:5000') + print(' - Username:', admin_login['username']) + print(' - Password:', admin_login['password']) + print('') + uvicorn.run(app, host='127.0.0.1', port=5000) diff --git a/examples/server/asgi/fastapi-fiddle.py b/examples/server/asgi/fastapi-fiddle.py new file mode 100644 index 00000000..b6902e0d --- /dev/null +++ b/examples/server/asgi/fastapi-fiddle.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +from fastapi import FastAPI +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles +import socketio +import uvicorn + +app = FastAPI() +app.mount('/static', StaticFiles(directory='static'), name='static') + +sio = socketio.AsyncServer(async_mode='asgi') +combined_asgi_app = socketio.ASGIApp(sio, app) + + +@app.get('/') +async def index(): + return FileResponse('fiddle.html') + + +@app.get('/hello') +async def hello(): + return {'message': 'Hello, World!'} + + +@sio.event +async def connect(sid, environ, auth): + print(f'connected auth={auth} sid={sid}') + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid): + print('disconnected', sid) + + +if __name__ == '__main__': + uvicorn.run(combined_asgi_app, host='127.0.0.1', port=5000) diff --git a/examples/server/asgi/fiddle.html b/examples/server/asgi/fiddle.html new file mode 100644 index 00000000..4b58e759 --- /dev/null +++ b/examples/server/asgi/fiddle.html @@ -0,0 +1,11 @@ + + + + + Socket.IO Fiddle + + + + + + diff --git a/examples/server/asgi/fiddle.py b/examples/server/asgi/fiddle.py new file mode 100644 index 00000000..402a3799 --- /dev/null +++ b/examples/server/asgi/fiddle.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +import uvicorn + +import socketio + +sio = socketio.AsyncServer(async_mode='asgi') +app = socketio.ASGIApp(sio, static_files={ + '/': 'fiddle.html', + '/static': 'static', +}) + + +@sio.event +async def connect(sid, environ, auth): + print(f'connected auth={auth} sid={sid}') + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid, reason): + print('disconnected', sid, reason) + + +if __name__ == '__main__': + uvicorn.run(app, host='127.0.0.1', port=5000) diff --git a/examples/aiohttp/latency.html b/examples/server/asgi/latency.html old mode 100755 new mode 100644 similarity index 91% rename from examples/aiohttp/latency.html rename to examples/server/asgi/latency.html index b238cd1c..b3450a61 --- a/examples/aiohttp/latency.html +++ b/examples/server/asgi/latency.html @@ -11,10 +11,10 @@

(connecting)

- + + + + diff --git a/examples/server/javascript/fiddle_public/main.js b/examples/server/javascript/fiddle_public/main.js new file mode 100644 index 00000000..3070a03e --- /dev/null +++ b/examples/server/javascript/fiddle_public/main.js @@ -0,0 +1,19 @@ +'use strict'; + +(function() { + + const socket = io(); + + socket.on('connect', () => { + console.log(`connect ${socket.id}`); + }); + + socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); + }); + + socket.on('hello', (a, b, c) => { + console.log(a, b, c); + }); + +})(); diff --git a/examples/server/javascript/latency.js b/examples/server/javascript/latency.js new file mode 100644 index 00000000..a2be9946 --- /dev/null +++ b/examples/server/javascript/latency.js @@ -0,0 +1,25 @@ +const express = require('express'); +const { createServer } = require("http"); +const { Server } = require("socket.io"); + +const app = express(); +const httpServer = createServer(app); +const io = new Server(httpServer); + +const port = process.env.PORT || 5000; + +app.use(express.static(__dirname + '/latency_public')); + +io.on('connection', socket => { + console.log(`connect ${socket.id}`); + + socket.on('ping_from_client', () => { + socket.emit('pong_from_server'); + }); + + socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); + }); +}); + +httpServer.listen(port, () => console.log(`server listening on port ${port}`)); diff --git a/examples/server/javascript/latency_public/index.html b/examples/server/javascript/latency_public/index.html new file mode 100644 index 00000000..857ed6a1 --- /dev/null +++ b/examples/server/javascript/latency_public/index.html @@ -0,0 +1,16 @@ + + + + Socket.IO Latency + + + +

Socket.IO Latency

+

(connecting)

+ + + + + + + diff --git a/examples/server/javascript/latency_public/index.js b/examples/server/javascript/latency_public/index.js new file mode 100644 index 00000000..a8975663 --- /dev/null +++ b/examples/server/javascript/latency_public/index.js @@ -0,0 +1,48 @@ +// helper +function $ (id) { return document.getElementById(id); } + +// chart +let smoothie; +let time; + +function render () { + if (smoothie) smoothie.stop(); + $('chart').width = document.body.clientWidth; + smoothie = new SmoothieChart(); + smoothie.streamTo($('chart'), 1000); + time = new TimeSeries(); + smoothie.addTimeSeries(time, { + strokeStyle: 'rgb(255, 0, 0)', + fillStyle: 'rgba(255, 0, 0, 0.4)', + lineWidth: 2 + }); +} + +// socket +const socket = io(); +let last; +function send () { + last = new Date(); + socket.emit('ping_from_client'); + $('transport').innerHTML = socket.io.engine.transport.name; +} + +socket.on('connect', () => { + if ($('chart').getContext) { + render(); + window.onresize = render; + } + send(); +}); + +socket.on('disconnect', () => { + if (smoothie) smoothie.stop(); + $('transport').innerHTML = '(disconnected)'; +}); + +socket.on('pong_from_server', () => { + const latency = new Date() - last; + $('latency').innerHTML = latency + 'ms'; + if (time) time.append(+new Date(), latency); + setTimeout(send, 100); +}); diff --git a/examples/wsgi/static/style.css b/examples/server/javascript/latency_public/style.css similarity index 100% rename from examples/wsgi/static/style.css rename to examples/server/javascript/latency_public/style.css diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json new file mode 100644 index 00000000..7a3ed280 --- /dev/null +++ b/examples/server/javascript/package-lock.json @@ -0,0 +1,1850 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "socketio-examples", + "version": "0.1.0", + "dependencies": { + "@socket.io/admin-ui": "^0.5.1", + "express": "^4.22.1", + "smoothie": "1.19.0", + "socket.io": "^4.8.0", + "socket.io-client": "^4.6.1" + } + }, + "node_modules/@socket.io/admin-ui": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@socket.io/admin-ui/-/admin-ui-0.5.1.tgz", + "integrity": "sha512-1dlGL2FGm6T+uL1e6iDvbo2eCINwvW7iVbjIblwh5kPPRM1SP8lmZrbFZf4QNJ/cqQ+JLcx49eXGM9WAB4TK7w==", + "dependencies": { + "@types/bcryptjs": "^2.4.2", + "bcryptjs": "^2.4.3", + "debug": "~4.3.1" + }, + "peerDependencies": { + "socket.io": ">=3.1.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" + }, + "node_modules/socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + } + }, + "dependencies": { + "@socket.io/admin-ui": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@socket.io/admin-ui/-/admin-ui-0.5.1.tgz", + "integrity": "sha512-1dlGL2FGm6T+uL1e6iDvbo2eCINwvW7iVbjIblwh5kPPRM1SP8lmZrbFZf4QNJ/cqQ+JLcx49eXGM9WAB4TK7w==", + "requires": { + "@types/bcryptjs": "^2.4.2", + "bcryptjs": "^2.4.3", + "debug": "~4.3.1" + } + }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "requires": { + "undici-types": "~6.19.2" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + } + } + }, + "engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "requires": { + "side-channel": "^1.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" + }, + "socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + } + }, + "socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "requires": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + } + }, + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + } + } +} diff --git a/examples/server/javascript/package.json b/examples/server/javascript/package.json new file mode 100644 index 00000000..84715872 --- /dev/null +++ b/examples/server/javascript/package.json @@ -0,0 +1,11 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "dependencies": { + "@socket.io/admin-ui": "^0.5.1", + "express": "^4.22.1", + "smoothie": "1.19.0", + "socket.io": "^4.8.0", + "socket.io-client": "^4.6.1" + } +} diff --git a/examples/sanic/README.rst b/examples/server/sanic/README.rst similarity index 87% rename from examples/sanic/README.rst rename to examples/server/sanic/README.rst index 53b41604..da2ef2db 100644 --- a/examples/sanic/README.rst +++ b/examples/server/sanic/README.rst @@ -26,17 +26,27 @@ time to the page. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. +fiddle.py +--------- + +This is a very simple application based on a JavaScript example of the same +name. + Running the Examples -------------------- To run these examples, create a virtual environment, install the requirements -and then run:: +and then run one of the following:: $ python app.py -or:: +:: $ python latency.py +:: + + $ python fiddle.py + You can then access the application from your web browser at ``http://localhost:8000``. diff --git a/examples/server/sanic/app.html b/examples/server/sanic/app.html new file mode 100644 index 00000000..b87b2df1 --- /dev/null +++ b/examples/server/sanic/app.html @@ -0,0 +1,90 @@ + + + + Python-SocketIO Test + + + + + +

Python-SocketIO Test

+

Send:

+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ +
+

Receive:

+

+ + diff --git a/examples/server/sanic/app.py b/examples/server/sanic/app.py new file mode 100644 index 00000000..447ddff6 --- /dev/null +++ b/examples/server/sanic/app.py @@ -0,0 +1,88 @@ +from sanic import Sanic +from sanic.response import html + +import socketio + +sio = socketio.AsyncServer(async_mode='sanic') +app = Sanic(__name__) +sio.attach(app) + + +async def background_task(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + await sio.sleep(10) + count += 1 + await sio.emit('my_response', {'data': 'Server generated event'}) + + +@app.listener('before_server_start') +def before_server_start(sanic, loop): + sio.start_background_task(background_task) + + +@app.route('/') +async def index(request): + with open('app.html') as f: + return html(f.read()) + + +@sio.event +async def my_event(sid, message): + await sio.emit('my_response', {'data': message['data']}, room=sid) + + +@sio.event +async def my_broadcast_event(sid, message): + await sio.emit('my_response', {'data': message['data']}) + + +@sio.event +async def join(sid, message): + await sio.enter_room(sid, message['room']) + await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, + room=sid) + + +@sio.event +async def leave(sid, message): + await sio.leave_room(sid, message['room']) + await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, + room=sid) + + +@sio.event +async def close_room(sid, message): + await sio.emit('my_response', + {'data': 'Room ' + message['room'] + ' is closing.'}, + room=message['room']) + await sio.close_room(message['room']) + + +@sio.event +async def my_room_event(sid, message): + await sio.emit('my_response', {'data': message['data']}, + room=message['room']) + + +@sio.event +async def disconnect_request(sid): + await sio.disconnect(sid) + + +@sio.event +async def connect(sid, environ): + await sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) + + +@sio.event +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) + + +app.static('/static', './static') + + +if __name__ == '__main__': + app.run() diff --git a/examples/server/sanic/fiddle.html b/examples/server/sanic/fiddle.html new file mode 100644 index 00000000..4b58e759 --- /dev/null +++ b/examples/server/sanic/fiddle.html @@ -0,0 +1,11 @@ + + + + + Socket.IO Fiddle + + + + + + diff --git a/examples/server/sanic/fiddle.py b/examples/server/sanic/fiddle.py new file mode 100644 index 00000000..405e6e56 --- /dev/null +++ b/examples/server/sanic/fiddle.py @@ -0,0 +1,32 @@ +from sanic import Sanic +from sanic.response import html + +import socketio + +sio = socketio.AsyncServer(async_mode='sanic') +app = Sanic(__name__) +sio.attach(app) + + +@app.route('/') +def index(request): + with open('fiddle.html') as f: + return html(f.read()) + + +@sio.event +async def connect(sid, environ, auth): + print(f'connected auth={auth} sid={sid}') + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid, reason): + print('disconnected', sid, reason) + + +app.static('/static', './static') + + +if __name__ == '__main__': + app.run() diff --git a/examples/server/sanic/latency.html b/examples/server/sanic/latency.html new file mode 100644 index 00000000..b3450a61 --- /dev/null +++ b/examples/server/sanic/latency.html @@ -0,0 +1,64 @@ + + + + Socket.IO Latency + + + +

Socket.IO Latency

+

(connecting)

+ + + + + + + + diff --git a/examples/sanic/latency.py b/examples/server/sanic/latency.py old mode 100755 new mode 100644 similarity index 84% rename from examples/sanic/latency.py rename to examples/server/sanic/latency.py index 2e1712f3..8f14992a --- a/examples/sanic/latency.py +++ b/examples/server/sanic/latency.py @@ -4,7 +4,7 @@ import socketio sio = socketio.AsyncServer(async_mode='sanic') -app = Sanic() +app = Sanic(__name__) sio.attach(app) @@ -14,8 +14,8 @@ def index(request): return html(f.read()) -@sio.on('ping_from_client') -async def ping(sid): +@sio.event +async def ping_from_client(sid): await sio.emit('pong_from_server', room=sid) app.static('/static', './static') diff --git a/examples/sanic/requirements.txt b/examples/server/sanic/requirements.txt similarity index 76% rename from examples/sanic/requirements.txt rename to examples/server/sanic/requirements.txt index 08e89727..4c829b3b 100644 --- a/examples/sanic/requirements.txt +++ b/examples/server/sanic/requirements.txt @@ -2,7 +2,7 @@ aiofiles==0.3.0 httptools==0.0.9 python_engineio python_socketio -sanic==0.3.1 +sanic==20.12.7 six==1.10.0 -ujson==1.35 +ujson==5.4.0 uvloop==0.8.0 diff --git a/examples/server/sanic/static/fiddle.js b/examples/server/sanic/static/fiddle.js new file mode 100644 index 00000000..3070a03e --- /dev/null +++ b/examples/server/sanic/static/fiddle.js @@ -0,0 +1,19 @@ +'use strict'; + +(function() { + + const socket = io(); + + socket.on('connect', () => { + console.log(`connect ${socket.id}`); + }); + + socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); + }); + + socket.on('hello', (a, b, c) => { + console.log(a, b, c); + }); + +})(); diff --git a/examples/server/sanic/static/style.css b/examples/server/sanic/static/style.css new file mode 100644 index 00000000..d20bcad9 --- /dev/null +++ b/examples/server/sanic/static/style.css @@ -0,0 +1,4 @@ +body { margin: 0; padding: 0; font-family: Helvetica Neue; } +h1 { margin: 100px 100px 10px; } +h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } +#latency { color: red; } diff --git a/examples/server/tornado/README.rst b/examples/server/tornado/README.rst new file mode 100644 index 00000000..977da107 --- /dev/null +++ b/examples/server/tornado/README.rst @@ -0,0 +1,49 @@ +Socket.IO Tornado Examples +========================== + +This directory contains example Socket.IO applications that are compatible +with the Tornado framework. These applications require Tornado 5 and Python +3.5 or later. + +app.py +------ + +A basic "kitchen sink" type application that allows the user to experiment +with most of the available features of the Socket.IO server. + +latency.py +---------- + +A port of the latency application included in the official Engine.IO +Javascript server. In this application the client sends *ping* messages to +the server, which are responded by the server with a *pong*. The client +measures the time it takes for each of these exchanges and plots these in real +time to the page. + +This is an ideal application to measure the performance of the different +asynchronous modes supported by the Socket.IO server. + +fiddle.py +--------- + +This is a very simple application based on a JavaScript example of the same +name. + +Running the Examples +-------------------- + +To run these examples, create a virtual environment, install the requirements +and then run one of the following:: + + $ python app.py + +:: + + $ python latency.py + +:: + + $ python fiddle.py + +You can then access the application from your web browser at +``http://localhost:5000``. diff --git a/examples/server/tornado/app.py b/examples/server/tornado/app.py new file mode 100644 index 00000000..58317d9b --- /dev/null +++ b/examples/server/tornado/app.py @@ -0,0 +1,98 @@ +import os + +import tornado.ioloop +from tornado.options import define, options, parse_command_line +import tornado.web + +import socketio + +define("port", default=5000, help="run on the given port", type=int) +define("debug", default=False, help="run in debug mode") + +sio = socketio.AsyncServer(async_mode='tornado') + + +async def background_task(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + await sio.sleep(10) + count += 1 + await sio.emit('my_response', {'data': 'Server generated event'}) + + +class MainHandler(tornado.web.RequestHandler): + def get(self): + self.render("app.html") + + +@sio.event +async def my_event(sid, message): + await sio.emit('my_response', {'data': message['data']}, room=sid) + + +@sio.event +async def my_broadcast_event(sid, message): + await sio.emit('my_response', {'data': message['data']}) + + +@sio.event +async def join(sid, message): + await sio.enter_room(sid, message['room']) + await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, + room=sid) + + +@sio.event +async def leave(sid, message): + await sio.leave_room(sid, message['room']) + await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, + room=sid) + + +@sio.event +async def close_room(sid, message): + await sio.emit('my_response', + {'data': 'Room ' + message['room'] + ' is closing.'}, + room=message['room']) + await sio.close_room(message['room']) + + +@sio.event +async def my_room_event(sid, message): + await sio.emit('my_response', {'data': message['data']}, + room=message['room']) + + +@sio.event +async def disconnect_request(sid): + await sio.disconnect(sid) + + +@sio.event +async def connect(sid, environ): + await sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) + + +@sio.event +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) + + +def main(): + parse_command_line() + app = tornado.web.Application( + [ + (r"/", MainHandler), + (r"/socket.io/", socketio.get_tornado_handler(sio)), + ], + template_path=os.path.join(os.path.dirname(__file__), "templates"), + static_path=os.path.join(os.path.dirname(__file__), "static"), + debug=options.debug, + ) + app.listen(options.port) + tornado.ioloop.IOLoop.current().start() + + +if __name__ == "__main__": + main() diff --git a/examples/server/tornado/fiddle.py b/examples/server/tornado/fiddle.py new file mode 100644 index 00000000..b3878a2a --- /dev/null +++ b/examples/server/tornado/fiddle.py @@ -0,0 +1,47 @@ +import os + +import tornado.ioloop +from tornado.options import define, options, parse_command_line +import tornado.web + +import socketio + +define("port", default=5000, help="run on the given port", type=int) +define("debug", default=False, help="run in debug mode") + +sio = socketio.AsyncServer(async_mode='tornado') + + +class MainHandler(tornado.web.RequestHandler): + def get(self): + self.render("fiddle.html") + + +@sio.event +async def connect(sid, environ, auth): + print(f'connected auth={auth} sid={sid}') + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid, reason): + print('disconnected', sid, reason) + + +def main(): + parse_command_line() + app = tornado.web.Application( + [ + (r"/", MainHandler), + (r"/socket.io/", socketio.get_tornado_handler(sio)), + ], + template_path=os.path.join(os.path.dirname(__file__), "templates"), + static_path=os.path.join(os.path.dirname(__file__), "static"), + debug=options.debug, + ) + app.listen(options.port) + tornado.ioloop.IOLoop.current().start() + + +if __name__ == "__main__": + main() diff --git a/examples/server/tornado/latency.py b/examples/server/tornado/latency.py new file mode 100644 index 00000000..571050b7 --- /dev/null +++ b/examples/server/tornado/latency.py @@ -0,0 +1,41 @@ +import os + +import tornado.ioloop +from tornado.options import define, options, parse_command_line +import tornado.web + +import socketio + +define("port", default=5000, help="run on the given port", type=int) +define("debug", default=False, help="run in debug mode") + +sio = socketio.AsyncServer(async_mode='tornado') + + +class MainHandler(tornado.web.RequestHandler): + def get(self): + self.render("latency.html") + + +@sio.event +async def ping_from_client(sid): + await sio.emit('pong_from_server', room=sid) + + +def main(): + parse_command_line() + app = tornado.web.Application( + [ + (r"/", MainHandler), + (r"/socket.io/", socketio.get_tornado_handler(sio)), + ], + template_path=os.path.join(os.path.dirname(__file__), "templates"), + static_path=os.path.join(os.path.dirname(__file__), "static"), + debug=options.debug, + ) + app.listen(options.port) + tornado.ioloop.IOLoop.current().start() + + +if __name__ == "__main__": + main() diff --git a/examples/server/tornado/requirements.txt b/examples/server/tornado/requirements.txt new file mode 100644 index 00000000..4e2915c4 --- /dev/null +++ b/examples/server/tornado/requirements.txt @@ -0,0 +1,4 @@ +tornado==6.5.1 +python-engineio +python_socketio +six==1.10.0 diff --git a/examples/server/tornado/static/fiddle.js b/examples/server/tornado/static/fiddle.js new file mode 100644 index 00000000..3070a03e --- /dev/null +++ b/examples/server/tornado/static/fiddle.js @@ -0,0 +1,19 @@ +'use strict'; + +(function() { + + const socket = io(); + + socket.on('connect', () => { + console.log(`connect ${socket.id}`); + }); + + socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); + }); + + socket.on('hello', (a, b, c) => { + console.log(a, b, c); + }); + +})(); diff --git a/examples/server/tornado/static/style.css b/examples/server/tornado/static/style.css new file mode 100644 index 00000000..d20bcad9 --- /dev/null +++ b/examples/server/tornado/static/style.css @@ -0,0 +1,4 @@ +body { margin: 0; padding: 0; font-family: Helvetica Neue; } +h1 { margin: 100px 100px 10px; } +h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } +#latency { color: red; } diff --git a/examples/server/tornado/templates/app.html b/examples/server/tornado/templates/app.html new file mode 100644 index 00000000..627b9186 --- /dev/null +++ b/examples/server/tornado/templates/app.html @@ -0,0 +1,90 @@ + + + + python-socketio test + + + + + +

python-socketio test

+

Send:

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ +
+

Receive:

+

+ + diff --git a/examples/server/tornado/templates/fiddle.html b/examples/server/tornado/templates/fiddle.html new file mode 100644 index 00000000..4b58e759 --- /dev/null +++ b/examples/server/tornado/templates/fiddle.html @@ -0,0 +1,11 @@ + + + + + Socket.IO Fiddle + + + + + + diff --git a/examples/server/tornado/templates/latency.html b/examples/server/tornado/templates/latency.html new file mode 100644 index 00000000..b3450a61 --- /dev/null +++ b/examples/server/tornado/templates/latency.html @@ -0,0 +1,64 @@ + + + + Socket.IO Latency + + + +

Socket.IO Latency

+

(connecting)

+ + + + + + + + diff --git a/examples/wsgi/README.rst b/examples/server/wsgi/README.rst similarity index 72% rename from examples/wsgi/README.rst rename to examples/server/wsgi/README.rst index 8886386d..72806b68 100644 --- a/examples/wsgi/README.rst +++ b/examples/server/wsgi/README.rst @@ -24,38 +24,48 @@ time to the page. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. -django_example --------------- +django_socketio +--------------- This is a version of the "app.py" application described above, that is based on the Django web framework. +fiddle.py +--------- + +This is a very simple application based on a JavaScript example of the same +name. + Running the Examples -------------------- To run these examples, create a virtual environment, install the requirements -and then run:: +and then run one of the following:: $ python app.py -or:: +:: $ python latency.py -or:: +:: $ cd django_example $ ./manage.py runserver +:: + + $ python fiddle + You can then access the application from your web browser at -``http://localhost:5000`` (``app.py`` and ``latency.py``) or +``http://localhost:5000`` (``app.py``, ``latency.py`` and ``fiddle.py``) or ``http://localhost:8000`` (``django_example``). -Near the top of the ``app.py`` and ``latency.py`` source files there is a -``async_mode`` variable that can be edited to swich to the other asynchornous -modes. Accepted values for ``async_mode`` are ``'threading'``, ``'eventlet'`` -and ``'gevent'``. For ``django_example``, the async mode can be set in the -``django_example/socketio_app/views.py`` module. +Near the top of the ``app.py``, ``latency.py`` and ``fiddle.py`` source files +there is a ``async_mode`` variable that can be edited to switch to the other +asynchornous modes. Accepted values for ``async_mode`` are ``'threading'``, +``'eventlet'`` and ``'gevent'``. For ``django_example``, the async mode can be +set in the ``django_example/socketio_app/views.py`` module. Note 1: when using the ``'eventlet'`` mode, the eventlet package must be installed in the virtual environment:: diff --git a/examples/server/wsgi/app.py b/examples/server/wsgi/app.py new file mode 100644 index 00000000..7fc871b4 --- /dev/null +++ b/examples/server/wsgi/app.py @@ -0,0 +1,138 @@ +# set async_mode to 'threading', 'eventlet', 'gevent' or 'gevent_uwsgi' to +# force a mode else, the best mode is selected automatically from what's +# installed +async_mode = None + +# set instrument to `True` to accept connections from the official Socket.IO +# Admin UI hosted at https://admin.socket.io +instrument = True +admin_login = { + 'username': 'admin', + 'password': 'python', # change this to a strong secret for production use! +} + +from flask import Flask, render_template +import socketio + +sio = socketio.Server( + async_mode=async_mode, + cors_allowed_origins=None if not instrument else [ + 'http://localhost:5000', + 'https://admin.socket.io', # edit the allowed origins if necessary + ]) +if instrument: + sio.instrument(auth=admin_login) + +app = Flask(__name__) +app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) +app.config['SECRET_KEY'] = 'secret!' +thread = None + + +def background_thread(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + sio.sleep(10) + count += 1 + sio.emit('my_response', {'data': 'Server generated event'}) + + +@app.route('/') +def index(): + global thread + if thread is None: + thread = sio.start_background_task(background_thread) + return render_template('index.html') + + +@sio.event +def my_event(sid, message): + sio.emit('my_response', {'data': message['data']}, room=sid) + + +@sio.event +def my_broadcast_event(sid, message): + sio.emit('my_response', {'data': message['data']}) + + +@sio.event +def join(sid, message): + sio.enter_room(sid, message['room']) + sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, + room=sid) + + +@sio.event +def leave(sid, message): + sio.leave_room(sid, message['room']) + sio.emit('my_response', {'data': 'Left room: ' + message['room']}, + room=sid) + + +@sio.event +def close_room(sid, message): + sio.emit('my_response', + {'data': 'Room ' + message['room'] + ' is closing.'}, + room=message['room']) + sio.close_room(message['room']) + + +@sio.event +def my_room_event(sid, message): + sio.emit('my_response', {'data': message['data']}, room=message['room']) + + +@sio.event +def disconnect_request(sid): + sio.disconnect(sid) + + +@sio.event +def connect(sid, environ): + sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) + + +@sio.event +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) + + +if __name__ == '__main__': + if instrument: + print('The server is instrumented for remote administration.') + print( + 'Use the official Socket.IO Admin UI at https://admin.socket.io ' + 'with the following connection details:' + ) + print(' - Server URL: http://localhost:5000') + print(' - Username:', admin_login['username']) + print(' - Password:', admin_login['password']) + print('') + if sio.async_mode == 'threading': + # deploy with Werkzeug + app.run(threaded=True) + elif sio.async_mode == 'eventlet': + # deploy with eventlet + import eventlet + import eventlet.wsgi + eventlet.wsgi.server(eventlet.listen(('', 5000)), app) + elif sio.async_mode == 'gevent': + # deploy with gevent + from gevent import pywsgi + try: + from geventwebsocket.handler import WebSocketHandler + websocket = True + except ImportError: + websocket = False + if websocket: + pywsgi.WSGIServer(('', 5000), app, + handler_class=WebSocketHandler).serve_forever() + else: + pywsgi.WSGIServer(('', 5000), app).serve_forever() + elif sio.async_mode == 'gevent_uwsgi': + print('Start the application through the uwsgi server. Example:') + print('uwsgi --http :5000 --gevent 1000 --http-websockets --master ' + '--wsgi-file app.py --callable app') + else: + print('Unknown async_mode: ' + sio.async_mode) diff --git a/examples/server/wsgi/django_socketio/README.md b/examples/server/wsgi/django_socketio/README.md new file mode 100644 index 00000000..b4659f47 --- /dev/null +++ b/examples/server/wsgi/django_socketio/README.md @@ -0,0 +1,26 @@ +django-socketio +=============== + +This is an example Django application integrated with Socket.IO. + +You can run it with the Django development web server: + +```bash +python manage.py runserver +``` + +When running in this mode, you will get an error message: + + RuntimeError: Cannot obtain socket from WSGI environment. + +This is expected, and it happens because the Django web server does not support +the WebSocket protocol. You can ignore the error, as the server will still work +using long-polling. + +To run the application with WebSocket enabled, you can use the Gunicorn web +server as follows: + + gunicorn -b :8000 --threads 100 --access-logfile - django_socketio.wsgi:application + +See the documentation for information on other supported deployment methods +that you can use to add support for WebSocket. diff --git a/examples/wsgi/django_example/django_example/__init__.py b/examples/server/wsgi/django_socketio/django_socketio/__init__.py similarity index 100% rename from examples/wsgi/django_example/django_example/__init__.py rename to examples/server/wsgi/django_socketio/django_socketio/__init__.py diff --git a/examples/server/wsgi/django_socketio/django_socketio/asgi.py b/examples/server/wsgi/django_socketio/django_socketio/asgi.py new file mode 100644 index 00000000..234d2d9e --- /dev/null +++ b/examples/server/wsgi/django_socketio/django_socketio/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for django_socketio project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_socketio.settings') + +application = get_asgi_application() diff --git a/examples/wsgi/django_example/django_example/settings.py b/examples/server/wsgi/django_socketio/django_socketio/settings.py similarity index 67% rename from examples/wsgi/django_example/django_example/settings.py rename to examples/server/wsgi/django_socketio/django_socketio/settings.py index 2affbddf..313462da 100644 --- a/examples/wsgi/django_example/django_example/settings.py +++ b/examples/server/wsgi/django_socketio/django_socketio/settings.py @@ -1,26 +1,26 @@ """ -Django settings for django_example project. +Django settings for django_socketio project. -Generated by 'django-admin startproject' using Django 1.11.1. +Generated by 'django-admin startproject' using Django 4.0.5. For more information on this file, see -https://docs.djangoproject.com/en/1.11/topics/settings/ +https://docs.djangoproject.com/en/4.0/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.11/ref/settings/ +https://docs.djangoproject.com/en/4.0/ref/settings/ """ -import os +from pathlib import Path -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '+vk#7#92ncb*y)8^$7sd&99%^+xc+t)nmamacbp8^vgjy(&g-9' +SECRET_KEY = 'django-insecure-&@-nkbrpe@%1_%ljh#oe@sw)6+k(&yn#r_)!5p)$22c^u#0@lj' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,13 +31,13 @@ # Application definition INSTALLED_APPS = [ - 'socketio_app', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'socketio_app', ] MIDDLEWARE = [ @@ -50,7 +50,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = 'django_example.urls' +ROOT_URLCONF = 'django_socketio.urls' TEMPLATES = [ { @@ -68,22 +68,22 @@ }, ] -WSGI_APPLICATION = 'django_example.wsgi.application' +WSGI_APPLICATION = 'django_socketio.wsgi.application' # Database -# https://docs.djangoproject.com/en/1.11/ref/settings/#databases +# https://docs.djangoproject.com/en/4.0/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'NAME': BASE_DIR / 'db.sqlite3', } } # Password validation -# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators +# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { @@ -102,7 +102,7 @@ # Internationalization -# https://docs.djangoproject.com/en/1.11/topics/i18n/ +# https://docs.djangoproject.com/en/4.0/topics/i18n/ LANGUAGE_CODE = 'en-us' @@ -110,12 +110,15 @@ USE_I18N = True -USE_L10N = True - USE_TZ = True # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.11/howto/static-files/ +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field -STATIC_URL = '/static/' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/examples/server/wsgi/django_socketio/django_socketio/urls.py b/examples/server/wsgi/django_socketio/django_socketio/urls.py new file mode 100644 index 00000000..c1565518 --- /dev/null +++ b/examples/server/wsgi/django_socketio/django_socketio/urls.py @@ -0,0 +1,23 @@ +"""django_socketio URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path +from django.conf.urls import include + +urlpatterns = [ + path('admin/', admin.site.urls), + path(r'', include('socketio_app.urls')), +] diff --git a/examples/wsgi/django_example/django_example/wsgi.py b/examples/server/wsgi/django_socketio/django_socketio/wsgi.py similarity index 51% rename from examples/wsgi/django_example/django_example/wsgi.py rename to examples/server/wsgi/django_socketio/django_socketio/wsgi.py index cc738a61..771cae2e 100644 --- a/examples/wsgi/django_example/django_example/wsgi.py +++ b/examples/server/wsgi/django_socketio/django_socketio/wsgi.py @@ -1,20 +1,20 @@ """ -WSGI config for django_example project. +WSGI config for django_socketio project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application -from socketio import Middleware +import socketio from socketio_app.views import sio -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_example.settings") +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_socketio.settings') django_app = get_wsgi_application() -application = Middleware(sio, django_app) +application = socketio.WSGIApp(sio, django_app) diff --git a/examples/server/wsgi/django_socketio/manage.py b/examples/server/wsgi/django_socketio/manage.py new file mode 100755 index 00000000..35ef79b5 --- /dev/null +++ b/examples/server/wsgi/django_socketio/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_socketio.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt new file mode 100644 index 00000000..e2e8f50c --- /dev/null +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -0,0 +1,10 @@ +asgiref==3.6.0 +bidict==0.22.1 +Django==4.2.27 +gunicorn==23.0.0 +h11==0.16.0 +python-engineio +python-socketio +simple-websocket +sqlparse==0.5.0 +wsproto==1.2.0 diff --git a/examples/wsgi/django_example/socketio_app/__init__.py b/examples/server/wsgi/django_socketio/socketio_app/__init__.py similarity index 100% rename from examples/wsgi/django_example/socketio_app/__init__.py rename to examples/server/wsgi/django_socketio/socketio_app/__init__.py diff --git a/examples/wsgi/django_example/socketio_app/admin.py b/examples/server/wsgi/django_socketio/socketio_app/admin.py similarity index 100% rename from examples/wsgi/django_example/socketio_app/admin.py rename to examples/server/wsgi/django_socketio/socketio_app/admin.py diff --git a/examples/wsgi/django_example/socketio_app/apps.py b/examples/server/wsgi/django_socketio/socketio_app/apps.py similarity index 63% rename from examples/wsgi/django_example/socketio_app/apps.py rename to examples/server/wsgi/django_socketio/socketio_app/apps.py index 555c1a88..e8e83ea4 100644 --- a/examples/wsgi/django_example/socketio_app/apps.py +++ b/examples/server/wsgi/django_socketio/socketio_app/apps.py @@ -2,4 +2,5 @@ class SocketioAppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' name = 'socketio_app' diff --git a/examples/wsgi/django_example/socketio_app/management/__init__.py b/examples/server/wsgi/django_socketio/socketio_app/migrations/__init__.py similarity index 100% rename from examples/wsgi/django_example/socketio_app/management/__init__.py rename to examples/server/wsgi/django_socketio/socketio_app/migrations/__init__.py diff --git a/examples/wsgi/django_example/socketio_app/models.py b/examples/server/wsgi/django_socketio/socketio_app/models.py similarity index 100% rename from examples/wsgi/django_example/socketio_app/models.py rename to examples/server/wsgi/django_socketio/socketio_app/models.py diff --git a/examples/wsgi/django_example/socketio_app/static/index.html b/examples/server/wsgi/django_socketio/socketio_app/static/index.html similarity index 81% rename from examples/wsgi/django_example/socketio_app/static/index.html rename to examples/server/wsgi/django_socketio/socketio_app/static/index.html index e83d5fe3..b10818f4 100644 --- a/examples/wsgi/django_example/socketio_app/static/index.html +++ b/examples/server/wsgi/django_socketio/socketio_app/static/index.html @@ -3,19 +3,18 @@ Django + SocketIO Test - + + + + diff --git a/examples/wsgi/templates/index.html b/examples/server/wsgi/templates/index.html old mode 100755 new mode 100644 similarity index 79% rename from examples/wsgi/templates/index.html rename to examples/server/wsgi/templates/index.html index 1012beab..e37a6cbd --- a/examples/wsgi/templates/index.html +++ b/examples/server/wsgi/templates/index.html @@ -1,21 +1,20 @@ - Flask-SocketIO Test + Python-SocketIO Test - + -

Flask-SocketIO Test

+

Python-SocketIO Test

Send:

diff --git a/examples/wsgi/templates/latency.html b/examples/server/wsgi/templates/latency.html old mode 100755 new mode 100644 similarity index 89% rename from examples/wsgi/templates/latency.html rename to examples/server/wsgi/templates/latency.html index 56b8c0a6..e241b5d0 --- a/examples/wsgi/templates/latency.html +++ b/examples/server/wsgi/templates/latency.html @@ -11,11 +11,11 @@

(connecting)

- +