sseclient-py 1.7.2__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sseclient_py-2.0.0/.github/workflows/ci.yml +43 -0
- sseclient_py-2.0.0/.github/workflows/publish.yml +46 -0
- sseclient_py-2.0.0/.gitignore +66 -0
- sseclient_py-2.0.0/.pre-commit-config.yaml +15 -0
- sseclient_py-2.0.0/PKG-INFO +103 -0
- sseclient_py-2.0.0/README.rst +78 -0
- sseclient_py-2.0.0/RELEASING.md +21 -0
- sseclient_py-2.0.0/pyproject.toml +56 -0
- {sseclient-py-1.7.2 → sseclient_py-2.0.0}/sseclient/__init__.py +42 -31
- sseclient_py-2.0.0/sseclient/__init__.pyi +26 -0
- sseclient_py-2.0.0/sseclient/py.typed +0 -0
- sseclient_py-2.0.0/sseclient/version.py +2 -0
- sseclient_py-2.0.0/tests/test_sseclient.py +331 -0
- sseclient_py-2.0.0/tests/typing_smoke.py +21 -0
- sseclient_py-2.0.0/uv.lock +1113 -0
- sseclient-py-1.7.2/PKG-INFO +0 -62
- sseclient-py-1.7.2/README.rst +0 -45
- sseclient-py-1.7.2/setup.cfg +0 -7
- sseclient-py-1.7.2/setup.py +0 -32
- sseclient-py-1.7.2/sseclient/version.py +0 -2
- sseclient-py-1.7.2/sseclient_py.egg-info/PKG-INFO +0 -62
- sseclient-py-1.7.2/sseclient_py.egg-info/SOURCES.txt +0 -12
- sseclient-py-1.7.2/sseclient_py.egg-info/dependency_links.txt +0 -1
- sseclient-py-1.7.2/sseclient_py.egg-info/pbr.json +0 -1
- sseclient-py-1.7.2/sseclient_py.egg-info/top_level.txt +0 -1
- sseclient-py-1.7.2/sseclient_py.egg-info/zip-safe +0 -1
- {sseclient-py-1.7.2 → sseclient_py-2.0.0}/LICENSE +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Python ${{ matrix.python-version }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
22
|
+
with:
|
|
23
|
+
persist-credentials: false
|
|
24
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
enable-cache: true
|
|
28
|
+
- run: uv run --frozen pytest -q
|
|
29
|
+
|
|
30
|
+
checks:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
34
|
+
with:
|
|
35
|
+
persist-credentials: false
|
|
36
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.15"
|
|
39
|
+
enable-cache: true
|
|
40
|
+
- run: uv run --frozen pre-commit run --all-files
|
|
41
|
+
- run: uv run --frozen mypy --strict --python-version 3.11 tests/typing_smoke.py
|
|
42
|
+
- run: uv build
|
|
43
|
+
- run: uv run --frozen twine check dist/*
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
15
|
+
with:
|
|
16
|
+
persist-credentials: false
|
|
17
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.15"
|
|
20
|
+
- name: Check release version
|
|
21
|
+
env:
|
|
22
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
23
|
+
run: uv run --frozen python -c 'import os; from sseclient.version import version; assert os.environ["RELEASE_TAG"] == f"sseclient-py-{version}", "Release tag does not match package version"'
|
|
24
|
+
- run: uv run --frozen pytest -q
|
|
25
|
+
- run: uv build
|
|
26
|
+
- run: uv run --frozen twine check dist/*
|
|
27
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
28
|
+
with:
|
|
29
|
+
name: dist
|
|
30
|
+
path: dist/*
|
|
31
|
+
if-no-files-found: error
|
|
32
|
+
|
|
33
|
+
publish:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment: pypi
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist
|
|
44
|
+
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
45
|
+
with:
|
|
46
|
+
attestations: true
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# macOS
|
|
2
|
+
.DS_Store
|
|
3
|
+
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
# C extensions
|
|
10
|
+
*.so
|
|
11
|
+
|
|
12
|
+
# Distribution / packaging
|
|
13
|
+
.Python
|
|
14
|
+
.venv/
|
|
15
|
+
env/
|
|
16
|
+
build/
|
|
17
|
+
develop-eggs/
|
|
18
|
+
dist/
|
|
19
|
+
downloads/
|
|
20
|
+
eggs/
|
|
21
|
+
.eggs/
|
|
22
|
+
lib/
|
|
23
|
+
lib64/
|
|
24
|
+
parts/
|
|
25
|
+
sdist/
|
|
26
|
+
var/
|
|
27
|
+
*.egg-info/
|
|
28
|
+
.installed.cfg
|
|
29
|
+
*.egg
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
# Usually these files are written by a python script from a template
|
|
33
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Django stuff:
|
|
57
|
+
*.log
|
|
58
|
+
|
|
59
|
+
# Sphinx documentation
|
|
60
|
+
docs/_build/
|
|
61
|
+
|
|
62
|
+
# PyBuilder
|
|
63
|
+
target/
|
|
64
|
+
|
|
65
|
+
#Ipython Notebook
|
|
66
|
+
.ipynb_checkpoints
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v6.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-added-large-files
|
|
9
|
+
|
|
10
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
11
|
+
rev: v0.17.5
|
|
12
|
+
hooks:
|
|
13
|
+
- id: ruff-check
|
|
14
|
+
args: [ --fix ]
|
|
15
|
+
- id: ruff-format
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sseclient-py
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Server-Sent Events client for Python
|
|
5
|
+
Project-URL: Repository, https://github.com/mpetazzoni/sseclient
|
|
6
|
+
Project-URL: Issues, https://github.com/mpetazzoni/sseclient/issues
|
|
7
|
+
Project-URL: PyPI, https://pypi.org/project/sseclient-py/
|
|
8
|
+
Author-email: Maxime Petazzoni <maxime.petazzoni@bulix.org>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: eventsource,server-sent-events,sse
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/x-rst
|
|
25
|
+
|
|
26
|
+
Server-Sent Events client for Python
|
|
27
|
+
====================================
|
|
28
|
+
|
|
29
|
+
``sseclient-py`` turns a streaming HTTP response into Server-Sent Events (SSE).
|
|
30
|
+
It works with ``requests``, ``urllib3`` and ``httpx``, and has no dependencies.
|
|
31
|
+
Python 3.11 or newer is required.
|
|
32
|
+
|
|
33
|
+
Installation
|
|
34
|
+
------------
|
|
35
|
+
|
|
36
|
+
.. code:: console
|
|
37
|
+
|
|
38
|
+
$ pip install sseclient-py
|
|
39
|
+
|
|
40
|
+
Usage
|
|
41
|
+
-----
|
|
42
|
+
|
|
43
|
+
.. code:: python
|
|
44
|
+
|
|
45
|
+
import requests
|
|
46
|
+
import sseclient
|
|
47
|
+
|
|
48
|
+
url = "https://example.com/events"
|
|
49
|
+
with requests.get(
|
|
50
|
+
url,
|
|
51
|
+
headers={"Accept": "text/event-stream"},
|
|
52
|
+
stream=True,
|
|
53
|
+
timeout=(3.05, 60),
|
|
54
|
+
) as response:
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
for event in sseclient.SSEClient(response).events():
|
|
57
|
+
print(event.id, event.event, event.data)
|
|
58
|
+
|
|
59
|
+
The timeout is a connect/read timeout, not a limit on the whole stream. Adjust
|
|
60
|
+
the read timeout to allow for your server's keep-alive interval.
|
|
61
|
+
|
|
62
|
+
Other HTTP clients
|
|
63
|
+
------------------
|
|
64
|
+
|
|
65
|
+
Pass the client's byte stream to ``SSEClient``:
|
|
66
|
+
|
|
67
|
+
.. code:: python
|
|
68
|
+
|
|
69
|
+
import httpx
|
|
70
|
+
import urllib3
|
|
71
|
+
|
|
72
|
+
# urllib3
|
|
73
|
+
http = urllib3.PoolManager()
|
|
74
|
+
response = http.request("GET", url, preload_content=False)
|
|
75
|
+
client = sseclient.SSEClient(response.stream())
|
|
76
|
+
|
|
77
|
+
# httpx
|
|
78
|
+
with httpx.stream("GET", url) as response:
|
|
79
|
+
client = sseclient.SSEClient(response.iter_bytes())
|
|
80
|
+
for event in client.events():
|
|
81
|
+
print(event.data)
|
|
82
|
+
|
|
83
|
+
Reconnecting
|
|
84
|
+
------------
|
|
85
|
+
|
|
86
|
+
Manage connections and retries in your application. Reconnect when ``events()``
|
|
87
|
+
ends or the HTTP client raises a connection or read-timeout error. Use a delay
|
|
88
|
+
or backoff between attempts.
|
|
89
|
+
|
|
90
|
+
Resuming depends on the server. When ``event.id is not None``, remember that ID,
|
|
91
|
+
including an empty string (which clears it). Send a non-empty remembered ID in
|
|
92
|
+
the next request's ``Last-Event-ID`` header. ID-only updates are not exposed by
|
|
93
|
+
this parser; duplicate-free delivery is not guaranteed.
|
|
94
|
+
|
|
95
|
+
The yielded events have ``id``, ``event``, ``data`` and ``retry`` attributes.
|
|
96
|
+
Comments and events without data are ignored.
|
|
97
|
+
|
|
98
|
+
Resources
|
|
99
|
+
---------
|
|
100
|
+
|
|
101
|
+
- `SSE specification <https://html.spec.whatwg.org/multipage/server-sent-events.html>`_
|
|
102
|
+
- `PyPI <https://pypi.org/project/sseclient-py/>`_
|
|
103
|
+
- `Source and issues <https://github.com/mpetazzoni/sseclient>`_
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Server-Sent Events client for Python
|
|
2
|
+
====================================
|
|
3
|
+
|
|
4
|
+
``sseclient-py`` turns a streaming HTTP response into Server-Sent Events (SSE).
|
|
5
|
+
It works with ``requests``, ``urllib3`` and ``httpx``, and has no dependencies.
|
|
6
|
+
Python 3.11 or newer is required.
|
|
7
|
+
|
|
8
|
+
Installation
|
|
9
|
+
------------
|
|
10
|
+
|
|
11
|
+
.. code:: console
|
|
12
|
+
|
|
13
|
+
$ pip install sseclient-py
|
|
14
|
+
|
|
15
|
+
Usage
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
.. code:: python
|
|
19
|
+
|
|
20
|
+
import requests
|
|
21
|
+
import sseclient
|
|
22
|
+
|
|
23
|
+
url = "https://example.com/events"
|
|
24
|
+
with requests.get(
|
|
25
|
+
url,
|
|
26
|
+
headers={"Accept": "text/event-stream"},
|
|
27
|
+
stream=True,
|
|
28
|
+
timeout=(3.05, 60),
|
|
29
|
+
) as response:
|
|
30
|
+
response.raise_for_status()
|
|
31
|
+
for event in sseclient.SSEClient(response).events():
|
|
32
|
+
print(event.id, event.event, event.data)
|
|
33
|
+
|
|
34
|
+
The timeout is a connect/read timeout, not a limit on the whole stream. Adjust
|
|
35
|
+
the read timeout to allow for your server's keep-alive interval.
|
|
36
|
+
|
|
37
|
+
Other HTTP clients
|
|
38
|
+
------------------
|
|
39
|
+
|
|
40
|
+
Pass the client's byte stream to ``SSEClient``:
|
|
41
|
+
|
|
42
|
+
.. code:: python
|
|
43
|
+
|
|
44
|
+
import httpx
|
|
45
|
+
import urllib3
|
|
46
|
+
|
|
47
|
+
# urllib3
|
|
48
|
+
http = urllib3.PoolManager()
|
|
49
|
+
response = http.request("GET", url, preload_content=False)
|
|
50
|
+
client = sseclient.SSEClient(response.stream())
|
|
51
|
+
|
|
52
|
+
# httpx
|
|
53
|
+
with httpx.stream("GET", url) as response:
|
|
54
|
+
client = sseclient.SSEClient(response.iter_bytes())
|
|
55
|
+
for event in client.events():
|
|
56
|
+
print(event.data)
|
|
57
|
+
|
|
58
|
+
Reconnecting
|
|
59
|
+
------------
|
|
60
|
+
|
|
61
|
+
Manage connections and retries in your application. Reconnect when ``events()``
|
|
62
|
+
ends or the HTTP client raises a connection or read-timeout error. Use a delay
|
|
63
|
+
or backoff between attempts.
|
|
64
|
+
|
|
65
|
+
Resuming depends on the server. When ``event.id is not None``, remember that ID,
|
|
66
|
+
including an empty string (which clears it). Send a non-empty remembered ID in
|
|
67
|
+
the next request's ``Last-Event-ID`` header. ID-only updates are not exposed by
|
|
68
|
+
this parser; duplicate-free delivery is not guaranteed.
|
|
69
|
+
|
|
70
|
+
The yielded events have ``id``, ``event``, ``data`` and ``retry`` attributes.
|
|
71
|
+
Comments and events without data are ignored.
|
|
72
|
+
|
|
73
|
+
Resources
|
|
74
|
+
---------
|
|
75
|
+
|
|
76
|
+
- `SSE specification <https://html.spec.whatwg.org/multipage/server-sent-events.html>`_
|
|
77
|
+
- `PyPI <https://pypi.org/project/sseclient-py/>`_
|
|
78
|
+
- `Source and issues <https://github.com/mpetazzoni/sseclient>`_
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
Releases are built and published by `.github/workflows/publish.yml` when a GitHub release is published. The workflow checks the tag against the package version, runs the tests and package checks, then uploads the wheel and source distribution with PyPI provenance attestations.
|
|
4
|
+
|
|
5
|
+
## One-time PyPI setup
|
|
6
|
+
|
|
7
|
+
No API token is stored in GitHub. In [PyPI project management](https://pypi.org/manage/projects/), open `sseclient-py`, then **Publishing**, and add a GitHub publisher with these exact values:
|
|
8
|
+
|
|
9
|
+
- Owner: `mpetazzoni`
|
|
10
|
+
- Repository: `sseclient`
|
|
11
|
+
- Workflow name: `publish.yml`
|
|
12
|
+
- Environment name: `pypi`
|
|
13
|
+
|
|
14
|
+
Optionally create the `pypi` environment in the GitHub repository settings and require maintainer approval before publishing. If configured on PyPI, the environment name must match.
|
|
15
|
+
|
|
16
|
+
## Making a release
|
|
17
|
+
|
|
18
|
+
1. Update `version` in `sseclient/version.py` and merge the release changes to `main`.
|
|
19
|
+
2. Create a GitHub release from `main` with tag `sseclient-py-<version>`, for example `sseclient-py-2.0.0`.
|
|
20
|
+
3. Publish the GitHub release. The workflow publishes the same version to PyPI and attaches attestations automatically.
|
|
21
|
+
4. Check the workflow result and the new PyPI release's **Provenance** section.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sseclient-py"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Server-Sent Events client for Python"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
readme = "README.rst"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Maxime Petazzoni", email = "maxime.petazzoni@bulix.org" },
|
|
13
|
+
]
|
|
14
|
+
license = "Apache-2.0"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
keywords = ["sse", "server-sent-events", "eventsource"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 5 - Production/Stable",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: Python :: 3.15",
|
|
28
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
29
|
+
]
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://github.com/mpetazzoni/sseclient"
|
|
34
|
+
Issues = "https://github.com/mpetazzoni/sseclient/issues"
|
|
35
|
+
PyPI = "https://pypi.org/project/sseclient-py/"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.version]
|
|
38
|
+
path = "sseclient/version.py"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["sseclient"]
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"mypy",
|
|
46
|
+
"pre-commit",
|
|
47
|
+
"pytest",
|
|
48
|
+
"ruff",
|
|
49
|
+
"twine",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
target-version = "py311"
|
|
54
|
+
|
|
55
|
+
[tool.uv]
|
|
56
|
+
required-version = ">=0.13.5,<0.14"
|
|
@@ -4,26 +4,23 @@ Server Side Events (SSE) client for Python.
|
|
|
4
4
|
Provides a generator of SSE received through an existing HTTP response.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
# Copyright (C) 2016-2017 SignalFx, Inc. All rights reserved.
|
|
8
|
-
|
|
9
7
|
import logging
|
|
10
8
|
|
|
11
|
-
__author__ =
|
|
12
|
-
__email__ =
|
|
13
|
-
|
|
14
|
-
__all__ = ['SSEClient']
|
|
9
|
+
__author__ = "Maxime Petazzoni <maxime.petazzoni@bulix.org>"
|
|
10
|
+
__email__ = "maxime.petazzoni@bulix.org"
|
|
11
|
+
__all__ = ["SSEClient"]
|
|
15
12
|
|
|
16
|
-
_FIELD_SEPARATOR =
|
|
13
|
+
_FIELD_SEPARATOR = ":"
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
class SSEClient
|
|
16
|
+
class SSEClient:
|
|
20
17
|
"""Implementation of a SSE client.
|
|
21
18
|
|
|
22
19
|
See http://www.w3.org/TR/2009/WD-eventsource-20091029/ for the
|
|
23
20
|
specification.
|
|
24
21
|
"""
|
|
25
22
|
|
|
26
|
-
def __init__(self, event_source, char_enc=
|
|
23
|
+
def __init__(self, event_source, char_enc="utf-8"):
|
|
27
24
|
"""Initialize the SSE client over an existing, ready to consume
|
|
28
25
|
event source.
|
|
29
26
|
|
|
@@ -32,8 +29,7 @@ class SSEClient(object):
|
|
|
32
29
|
io.BinaryIOBase, like an httplib or urllib3 HTTPResponse object.
|
|
33
30
|
"""
|
|
34
31
|
self._logger = logging.getLogger(self.__class__.__module__)
|
|
35
|
-
self._logger.debug(
|
|
36
|
-
event_source)
|
|
32
|
+
self._logger.debug("Initialized SSE client from event source %s", event_source)
|
|
37
33
|
self._event_source = event_source
|
|
38
34
|
self._char_enc = char_enc
|
|
39
35
|
|
|
@@ -44,23 +40,38 @@ class SSEClient(object):
|
|
|
44
40
|
event into multiple HTTP chunks in the response. It is thus necessary
|
|
45
41
|
to correctly stitch together consecutive response chunks and find the
|
|
46
42
|
SSE delimiter (empty new line) to yield full, correct event chunks."""
|
|
47
|
-
data = b
|
|
43
|
+
data = b""
|
|
44
|
+
pending_cr = False
|
|
48
45
|
for chunk in self._event_source:
|
|
49
46
|
for line in chunk.splitlines(True):
|
|
47
|
+
# A CRLF split between chunks is still a single line ending.
|
|
48
|
+
if pending_cr:
|
|
49
|
+
pending_cr = False
|
|
50
|
+
if line == b"\n":
|
|
51
|
+
continue
|
|
52
|
+
if line.endswith(b"\r\n"):
|
|
53
|
+
line = line[:-2] + b"\n"
|
|
54
|
+
elif line.endswith(b"\r"):
|
|
55
|
+
line = line[:-1] + b"\n"
|
|
56
|
+
pending_cr = True
|
|
50
57
|
data += line
|
|
51
|
-
if data.endswith(
|
|
58
|
+
if data.endswith(b"\n\n"):
|
|
52
59
|
yield data
|
|
53
|
-
data = b
|
|
60
|
+
data = b""
|
|
54
61
|
if data:
|
|
55
62
|
yield data
|
|
56
63
|
|
|
57
64
|
def events(self):
|
|
65
|
+
first_line = True
|
|
58
66
|
for chunk in self._read():
|
|
59
67
|
event = Event()
|
|
60
68
|
# Split before decoding so splitlines() only uses \r and \n
|
|
61
69
|
for line in chunk.splitlines():
|
|
62
70
|
# Decode the line.
|
|
63
71
|
line = line.decode(self._char_enc)
|
|
72
|
+
if first_line:
|
|
73
|
+
line = line.removeprefix("\ufeff")
|
|
74
|
+
first_line = False
|
|
64
75
|
|
|
65
76
|
# Lines starting with a separator are comments and are to be
|
|
66
77
|
# ignored.
|
|
@@ -72,27 +83,28 @@ class SSEClient(object):
|
|
|
72
83
|
|
|
73
84
|
# Ignore unknown fields.
|
|
74
85
|
if field not in event.__dict__:
|
|
75
|
-
self._logger.debug(
|
|
76
|
-
|
|
86
|
+
self._logger.debug(
|
|
87
|
+
"Saw invalid field %s while parsing Server Side Event", field
|
|
88
|
+
)
|
|
77
89
|
continue
|
|
78
90
|
|
|
79
91
|
if len(data) > 1:
|
|
80
92
|
# From the spec:
|
|
81
93
|
# "If value starts with a single U+0020 SPACE character,
|
|
82
94
|
# remove it from value."
|
|
83
|
-
if data[1].startswith(
|
|
95
|
+
if data[1].startswith(" "):
|
|
84
96
|
value = data[1][1:]
|
|
85
97
|
else:
|
|
86
98
|
value = data[1]
|
|
87
99
|
else:
|
|
88
100
|
# If no value is present after the separator,
|
|
89
101
|
# assume an empty value.
|
|
90
|
-
value =
|
|
102
|
+
value = ""
|
|
91
103
|
|
|
92
104
|
# The data field may come over multiple lines and their values
|
|
93
105
|
# are concatenated with each other.
|
|
94
|
-
if field ==
|
|
95
|
-
event.__dict__[field] += value +
|
|
106
|
+
if field == "data":
|
|
107
|
+
event.__dict__[field] += value + "\n"
|
|
96
108
|
else:
|
|
97
109
|
event.__dict__[field] = value
|
|
98
110
|
|
|
@@ -101,14 +113,14 @@ class SSEClient(object):
|
|
|
101
113
|
continue
|
|
102
114
|
|
|
103
115
|
# If the data field ends with a newline, remove it.
|
|
104
|
-
if event.data.endswith(
|
|
116
|
+
if event.data.endswith("\n"):
|
|
105
117
|
event.data = event.data[0:-1]
|
|
106
118
|
|
|
107
119
|
# Empty event names default to 'message'
|
|
108
|
-
event.event = event.event or
|
|
120
|
+
event.event = event.event or "message"
|
|
109
121
|
|
|
110
122
|
# Dispatch the event
|
|
111
|
-
self._logger.debug(
|
|
123
|
+
self._logger.debug("Dispatching %s...", event)
|
|
112
124
|
yield event
|
|
113
125
|
|
|
114
126
|
def close(self):
|
|
@@ -116,24 +128,23 @@ class SSEClient(object):
|
|
|
116
128
|
self._event_source.close()
|
|
117
129
|
|
|
118
130
|
|
|
119
|
-
class Event
|
|
131
|
+
class Event:
|
|
120
132
|
"""Representation of an event from the event stream."""
|
|
121
133
|
|
|
122
|
-
def __init__(self, id=None, event=
|
|
134
|
+
def __init__(self, id=None, event="message", data="", retry=None):
|
|
123
135
|
self.id = id
|
|
124
136
|
self.event = event
|
|
125
137
|
self.data = data
|
|
126
138
|
self.retry = retry
|
|
127
139
|
|
|
128
140
|
def __str__(self):
|
|
129
|
-
s =
|
|
141
|
+
s = f"{self.event} event"
|
|
130
142
|
if self.id:
|
|
131
|
-
s +=
|
|
143
|
+
s += f" #{self.id}"
|
|
132
144
|
if self.data:
|
|
133
|
-
s +=
|
|
134
|
-
's' if len(self.data) else '')
|
|
145
|
+
s += ", {} byte{}".format(len(self.data), "s" if len(self.data) else "")
|
|
135
146
|
else:
|
|
136
|
-
s +=
|
|
147
|
+
s += ", no data"
|
|
137
148
|
if self.retry:
|
|
138
|
-
s +=
|
|
149
|
+
s += f", retry in {self.retry}ms"
|
|
139
150
|
return s
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Server Side Events (SSE) client for Python.
|
|
3
|
+
|
|
4
|
+
Provides a generator of SSE received through an existing HTTP response.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Generator, Iterable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
__all__ = ["SSEClient"]
|
|
11
|
+
|
|
12
|
+
class SSEClient:
|
|
13
|
+
"""SSE client interface."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, event_source: Iterable[bytes], char_enc: str = "utf-8"): ...
|
|
16
|
+
# Keep all parameters explicit for Python 3.11 type checkers.
|
|
17
|
+
def events(self) -> Generator[Event, None, None]: ... # noqa: UP043
|
|
18
|
+
def close(self) -> None: ...
|
|
19
|
+
|
|
20
|
+
class Event:
|
|
21
|
+
"""Representation of an event from the event stream."""
|
|
22
|
+
|
|
23
|
+
id: str | None
|
|
24
|
+
event: str
|
|
25
|
+
data: str
|
|
26
|
+
retry: Any | None
|
|
File without changes
|