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.
@@ -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__ = 'Maxime Petazzoni <maxime.petazzoni@bulix.org>'
12
- __email__ = 'maxime.petazzoni@bulix.org'
13
- __copyright__ = 'Copyright (C) 2016-2017 SignalFx, Inc. All rights reserved.'
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(object):
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='utf-8'):
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('Initialized SSE client from event source %s',
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((b'\r\r', b'\n\n', b'\r\n\r\n')):
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('Saw invalid field %s while parsing '
76
- 'Server Side Event', field)
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 == 'data':
95
- event.__dict__[field] += value + '\n'
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('\n'):
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 'message'
120
+ event.event = event.event or "message"
109
121
 
110
122
  # Dispatch the event
111
- self._logger.debug('Dispatching %s...', event)
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(object):
131
+ class Event:
120
132
  """Representation of an event from the event stream."""
121
133
 
122
- def __init__(self, id=None, event='message', data='', retry=None):
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 = '{0} event'.format(self.event)
141
+ s = f"{self.event} event"
130
142
  if self.id:
131
- s += ' #{0}'.format(self.id)
143
+ s += f" #{self.id}"
132
144
  if self.data:
133
- s += ', {0} byte{1}'.format(len(self.data),
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 += ', no data'
147
+ s += ", no data"
137
148
  if self.retry:
138
- s += ', retry in {0}ms'.format(self.retry)
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
@@ -0,0 +1,2 @@
1
+ name = "sseclient-py"
2
+ version = "2.0.0"