launchdarkly-eventsource 1.5.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.
Files changed (26) hide show
  1. launchdarkly_eventsource-1.5.0/LICENSE +13 -0
  2. launchdarkly_eventsource-1.5.0/PKG-INFO +67 -0
  3. launchdarkly_eventsource-1.5.0/README.md +39 -0
  4. launchdarkly_eventsource-1.5.0/ld_eventsource/__init__.py +1 -0
  5. launchdarkly_eventsource-1.5.0/ld_eventsource/actions.py +176 -0
  6. launchdarkly_eventsource-1.5.0/ld_eventsource/config/__init__.py +4 -0
  7. launchdarkly_eventsource-1.5.0/ld_eventsource/config/connect_strategy.py +162 -0
  8. launchdarkly_eventsource-1.5.0/ld_eventsource/config/error_strategy.py +143 -0
  9. launchdarkly_eventsource-1.5.0/ld_eventsource/config/retry_delay_strategy.py +154 -0
  10. launchdarkly_eventsource-1.5.0/ld_eventsource/errors.py +62 -0
  11. launchdarkly_eventsource-1.5.0/ld_eventsource/http.py +128 -0
  12. launchdarkly_eventsource-1.5.0/ld_eventsource/reader.py +117 -0
  13. launchdarkly_eventsource-1.5.0/ld_eventsource/sse_client.py +328 -0
  14. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/__init__.py +0 -0
  15. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/helpers.py +84 -0
  16. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/http_util.py +216 -0
  17. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/test_error_strategy.py +49 -0
  18. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/test_headers.py +215 -0
  19. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/test_http_connect_strategy.py +241 -0
  20. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/test_http_connect_strategy_with_sse_client.py +122 -0
  21. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/test_reader.py +128 -0
  22. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/test_retry_delay_strategy.py +102 -0
  23. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/test_sse_client_basic.py +94 -0
  24. launchdarkly_eventsource-1.5.0/ld_eventsource/testing/test_sse_client_retry.py +190 -0
  25. launchdarkly_eventsource-1.5.0/ld_eventsource/version.py +1 -0
  26. launchdarkly_eventsource-1.5.0/pyproject.toml +77 -0
@@ -0,0 +1,13 @@
1
+ Copyright 2021 Catamorphic, Co.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: launchdarkly-eventsource
3
+ Version: 1.5.0
4
+ Summary: LaunchDarkly SSE Client
5
+ License: Apache-2.0
6
+ License-File: LICENSE
7
+ Author: LaunchDarkly
8
+ Author-email: dev@launchdarkly.com
9
+ Requires-Python: >=3.9
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Dist: urllib3 (>=1.26.0,<3)
23
+ Project-URL: Documentation, https://launchdarkly-python-sdk.readthedocs.io/en/latest/
24
+ Project-URL: Homepage, https://docs.launchdarkly.com/sdk/server-side/python
25
+ Project-URL: Repository, https://github.com/launchdarkly/python-eventsource
26
+ Description-Content-Type: text/markdown
27
+
28
+ # LaunchDarkly SSE Client for Python
29
+
30
+ [![Run CI](https://github.com/launchdarkly/python-eventsource/actions/workflows/ci.yml/badge.svg)](https://github.com/launchdarkly/python-eventsource/actions/workflows/ci.yml)
31
+ [![PyPI](https://img.shields.io/pypi/v/launchdarkly-eventsource.svg?maxAge=2592000)](https://pypi.python.org/pypi/launchdarkly-eventsource)
32
+ [![readthedocs](https://readthedocs.org/projects/launchdarkly-sse-client-library/badge/)](https://launchdarkly-sse-client-library.readthedocs.io/en/latest/)
33
+
34
+ ## Overview
35
+
36
+ The `launchdarkly/python-eventsource` package allows Python developers to consume Server-Sent-Events (SSE) from a remote API. The SSE specification is defined here: [https://html.spec.whatwg.org/multipage/server-sent-events.html](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events)
37
+
38
+ This package's primary purpose is to support the [LaunchDarkly SDK for Python](https://github.com/launchdarkly/python-server-sdk), but it can be used independently. In its simplest configuration, it emulates the behavior of the EventSource API as defined in the SSE specification, with the addition of exponential backoff behavior for retries. However, it also includes optional features used by LaunchDarkly SDKs that are not part of the core specification, such as:
39
+
40
+ * Customizing the backoff/jitter behavior.
41
+ * Setting read timeouts, custom headers, and other HTTP request properties.
42
+ * Specifying that connections should be retried under circumstances where the standard EventSource behavior would not retry them, such as if the server returns an HTTP error status.
43
+
44
+ This is a synchronous implementation which blocks the caller's thread when reading events or reconnecting. By default, it uses `urllib3` to make HTTP requests, but it can be configured to read any input stream.
45
+
46
+ ## Supported Python versions
47
+
48
+ This version of the package is compatible with Python 3.9 and higher.
49
+
50
+ ## Contributing
51
+
52
+ We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK.
53
+
54
+ ## About LaunchDarkly
55
+
56
+ * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:
57
+ * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
58
+ * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
59
+ * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
60
+ * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
61
+ * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
62
+ * Explore LaunchDarkly
63
+ * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information
64
+ * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
65
+ * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation
66
+ * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates
67
+
@@ -0,0 +1,39 @@
1
+ # LaunchDarkly SSE Client for Python
2
+
3
+ [![Run CI](https://github.com/launchdarkly/python-eventsource/actions/workflows/ci.yml/badge.svg)](https://github.com/launchdarkly/python-eventsource/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/launchdarkly-eventsource.svg?maxAge=2592000)](https://pypi.python.org/pypi/launchdarkly-eventsource)
5
+ [![readthedocs](https://readthedocs.org/projects/launchdarkly-sse-client-library/badge/)](https://launchdarkly-sse-client-library.readthedocs.io/en/latest/)
6
+
7
+ ## Overview
8
+
9
+ The `launchdarkly/python-eventsource` package allows Python developers to consume Server-Sent-Events (SSE) from a remote API. The SSE specification is defined here: [https://html.spec.whatwg.org/multipage/server-sent-events.html](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events)
10
+
11
+ This package's primary purpose is to support the [LaunchDarkly SDK for Python](https://github.com/launchdarkly/python-server-sdk), but it can be used independently. In its simplest configuration, it emulates the behavior of the EventSource API as defined in the SSE specification, with the addition of exponential backoff behavior for retries. However, it also includes optional features used by LaunchDarkly SDKs that are not part of the core specification, such as:
12
+
13
+ * Customizing the backoff/jitter behavior.
14
+ * Setting read timeouts, custom headers, and other HTTP request properties.
15
+ * Specifying that connections should be retried under circumstances where the standard EventSource behavior would not retry them, such as if the server returns an HTTP error status.
16
+
17
+ This is a synchronous implementation which blocks the caller's thread when reading events or reconnecting. By default, it uses `urllib3` to make HTTP requests, but it can be configured to read any input stream.
18
+
19
+ ## Supported Python versions
20
+
21
+ This version of the package is compatible with Python 3.9 and higher.
22
+
23
+ ## Contributing
24
+
25
+ We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK.
26
+
27
+ ## About LaunchDarkly
28
+
29
+ * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:
30
+ * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
31
+ * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
32
+ * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
33
+ * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
34
+ * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
35
+ * Explore LaunchDarkly
36
+ * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information
37
+ * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
38
+ * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation
39
+ * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates
@@ -0,0 +1 @@
1
+ from ld_eventsource.sse_client import *
@@ -0,0 +1,176 @@
1
+ import json
2
+ from typing import Any, Dict, Optional
3
+
4
+ from ld_eventsource.errors import ExceptionWithHeaders
5
+
6
+
7
+ class Action:
8
+ """
9
+ Base class for objects that can be returned by :attr:`.SSEClient.all`.
10
+ """
11
+
12
+ pass
13
+
14
+
15
+ class Event(Action):
16
+ """
17
+ An event received by :class:`.SSEClient`.
18
+
19
+ Instances of this class are returned by both :attr:`.SSEClient.events` and
20
+ :attr:`.SSEClient.all`.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ event: str = 'message',
26
+ data: str = '',
27
+ id: Optional[str] = None,
28
+ last_event_id: Optional[str] = None,
29
+ ):
30
+ self._event = event
31
+ self._data = data
32
+ self._id = id
33
+ self._last_event_id = last_event_id
34
+
35
+ @property
36
+ def event(self) -> str:
37
+ """
38
+ The event type, or "message" if not specified.
39
+ """
40
+ return self._event
41
+
42
+ @property
43
+ def data(self) -> str:
44
+ """
45
+ The event data.
46
+ """
47
+ return self._data
48
+
49
+ @property
50
+ def id(self) -> Optional[str]:
51
+ """
52
+ The value of the ``id:`` field for this event, or `None` if omitted.
53
+ """
54
+ return self._id
55
+
56
+ @property
57
+ def last_event_id(self) -> Optional[str]:
58
+ """
59
+ The value of the most recent ``id:`` field of an event seen in this stream so far.
60
+ """
61
+ return self._last_event_id
62
+
63
+ def __eq__(self, other):
64
+ if not isinstance(other, Event):
65
+ return False
66
+ return (
67
+ self._event == other._event
68
+ and self._data == other._data
69
+ and self._id == other._id
70
+ and self.last_event_id == other.last_event_id
71
+ )
72
+
73
+ def __repr__(self):
74
+ return "Event(event=\"%s\", data=%s, id=%s, last_event_id=%s)" % (
75
+ self._event,
76
+ json.dumps(self._data),
77
+ "None" if self._id is None else json.dumps(self._id),
78
+ "None" if self._last_event_id is None else json.dumps(self._last_event_id),
79
+ )
80
+
81
+
82
+ class Comment(Action):
83
+ """
84
+ A comment received by :class:`.SSEClient`.
85
+
86
+ Comment lines (any line beginning with a colon) have no significance in the SSE specification
87
+ and can be ignored, but if you want to see them, use :attr:`.SSEClient.all`. They will never
88
+ be returned by :attr:`.SSEClient.events`.
89
+ """
90
+
91
+ def __init__(self, comment: str):
92
+ self._comment = comment
93
+
94
+ @property
95
+ def comment(self) -> str:
96
+ """
97
+ The comment text, not including the leading colon.
98
+ """
99
+ return self._comment
100
+
101
+ def __eq__(self, other):
102
+ return isinstance(other, Comment) and self._comment == other._comment
103
+
104
+ def __repr__(self):
105
+ return ":" + self._comment
106
+
107
+
108
+ class Start(Action):
109
+ """
110
+ Indicates that :class:`.SSEClient` has successfully connected to a stream.
111
+
112
+ Instances of this class are only available from :attr:`.SSEClient.all`.
113
+ A ``Start`` is returned for the first successful connection. If the client reconnects
114
+ after a failure, there will be a :class:`.Fault` followed by a ``Start``.
115
+
116
+ Each ``Start`` action may include HTTP response headers from the connection. These headers
117
+ are available via the :attr:`headers` property. On reconnection, a new ``Start`` will be
118
+ emitted with the headers from the new connection, which may differ from the previous one.
119
+ """
120
+
121
+ def __init__(self, headers: Optional[Dict[str, Any]] = None):
122
+ self._headers = headers
123
+
124
+ @property
125
+ def headers(self) -> Optional[Dict[str, Any]]:
126
+ """
127
+ The HTTP response headers from the stream connection, if available.
128
+
129
+ The headers dict uses case-insensitive keys (via urllib3's HTTPHeaderDict).
130
+
131
+ :return: the response headers, or ``None`` if not available
132
+ """
133
+ return self._headers
134
+
135
+
136
+ class Fault(Action):
137
+ """
138
+ Indicates that :class:`.SSEClient` encountered an error or end of stream.
139
+
140
+ Instances of this class are only available from :attr:`.SSEClient.all`.
141
+
142
+ If you receive a Fault, the SSEClient is now in an inactive state since either a
143
+ connection attempt has failed or an existing connection has been closed. The SSEClient
144
+ will attempt to reconnect if you either call :meth:`.SSEClient.start()`
145
+ or simply continue reading events after this point.
146
+
147
+ When the error includes HTTP response headers (such as for :class:`.HTTPStatusError`
148
+ or :class:`.HTTPContentTypeError`), they are accessible via the :attr:`headers` property.
149
+ """
150
+
151
+ def __init__(self, error: Optional[Exception]):
152
+ self.__error = error
153
+
154
+ @property
155
+ def error(self) -> Optional[Exception]:
156
+ """
157
+ The exception that caused the stream to fail, if any. If this is ``None``, it means
158
+ that the stream simply ran out of data, i.e. the server shut down the connection
159
+ in an orderly way after sending an EOF chunk as defined by chunked transfer encoding.
160
+ """
161
+ return self.__error
162
+
163
+ @property
164
+ def headers(self) -> Optional[Dict[str, Any]]:
165
+ """
166
+ The HTTP response headers from the failed connection, if available.
167
+
168
+ This property returns headers when the error is an exception that includes them,
169
+ such as :class:`.HTTPStatusError` or :class:`.HTTPContentTypeError`. For other
170
+ error types or when the stream ended normally, this returns ``None``.
171
+
172
+ :return: the response headers, or ``None`` if not available
173
+ """
174
+ if isinstance(self.__error, ExceptionWithHeaders):
175
+ return self.__error.headers
176
+ return None
@@ -0,0 +1,4 @@
1
+ from .connect_strategy import (ConnectionClient, ConnectionResult,
2
+ ConnectStrategy)
3
+ from .error_strategy import ErrorStrategy
4
+ from .retry_delay_strategy import RetryDelayStrategy
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from logging import Logger
5
+ from typing import Any, Callable, Dict, Iterator, Optional, Union
6
+
7
+ from urllib3 import PoolManager
8
+
9
+ from ld_eventsource.http import (DynamicQueryParams, _HttpClientImpl,
10
+ _HttpConnectParams)
11
+
12
+
13
+ class ConnectStrategy:
14
+ """
15
+ An abstraction for how :class:`.SSEClient` should obtain an input stream.
16
+
17
+ The default implementation is :meth:`http()`, which makes HTTP requests with ``urllib3``.
18
+ Or, if you want to consume an input stream from some other source, you can create your own
19
+ subclass of :class:`ConnectStrategy`.
20
+
21
+ Instances of this class should be immutable and should not contain any state that is specific
22
+ to one active stream. The :class:`ConnectionClient` that they produce is stateful and belongs
23
+ to a single :class:`.SSEClient`.
24
+ """
25
+
26
+ def create_client(self, logger: Logger) -> ConnectionClient:
27
+ """
28
+ Creates a client instance.
29
+
30
+ This is called once when an :class:`.SSEClient` is created. The SSEClient returns the
31
+ returned :class:`ConnectionClient` and uses it to perform all subsequent connection attempts.
32
+
33
+ :param logger: the logger being used by the SSEClient
34
+ """
35
+ raise NotImplementedError("ConnectStrategy base class cannot be used by itself")
36
+
37
+ @staticmethod
38
+ def http(
39
+ url: str,
40
+ headers: Optional[dict] = None,
41
+ pool: Optional[PoolManager] = None,
42
+ urllib3_request_options: Optional[dict] = None,
43
+ query_params: Optional[DynamicQueryParams] = None
44
+ ) -> ConnectStrategy:
45
+ """
46
+ Creates the default HTTP implementation, specifying request parameters.
47
+
48
+ :param url: the stream URL
49
+ :param headers: optional HTTP headers to add to the request
50
+ :param pool: optional urllib3 ``PoolManager`` to provide an HTTP client
51
+ :param urllib3_request_options: optional ``kwargs`` to add to the ``request`` call; these
52
+ can include any parameters supported by ``urllib3``, such as ``timeout``
53
+ :param query_params: optional callable that can be used to affect query parameters
54
+ dynamically for each connection attempt
55
+ """
56
+ return _HttpConnectStrategy(
57
+ _HttpConnectParams(url, headers, pool, urllib3_request_options, query_params)
58
+ )
59
+
60
+
61
+ class ConnectionClient:
62
+ """
63
+ An object provided by :class:`.ConnectStrategy` that is retained by a single
64
+ :class:`.SSEClient` to perform all connection attempts by that instance.
65
+
66
+ For the default HTTP implementation, this represents an HTTP connection pool.
67
+ """
68
+
69
+ def connect(self, last_event_id: Optional[str]) -> ConnectionResult:
70
+ """
71
+ Attempts to connect to a stream. Raises an exception if unsuccessful.
72
+
73
+ :param last_event_id: the current value of :attr:`SSEClient.last_event_id`
74
+ (should be sent to the server to support resuming an interrupted stream)
75
+ :return: a :class:`ConnectionResult` representing the stream
76
+ """
77
+ raise NotImplementedError(
78
+ "ConnectionClient base class cannot be used by itself"
79
+ )
80
+
81
+ def close(self):
82
+ """
83
+ Does whatever is necessary to release resources when the SSEClient is closed.
84
+ """
85
+ pass
86
+
87
+ def __enter__(self):
88
+ return self
89
+
90
+ def __exit__(self, type, value, traceback):
91
+ self.close()
92
+
93
+
94
+ class ConnectionResult:
95
+ """
96
+ The return type of :meth:`ConnectionClient.connect()`.
97
+ """
98
+
99
+ def __init__(self, stream: Iterator[bytes], closer: Optional[Callable], headers: Optional[Dict[str, Any]] = None):
100
+ self.__stream = stream
101
+ self.__closer = closer
102
+ self.__headers = headers
103
+
104
+ @property
105
+ def stream(self) -> Iterator[bytes]:
106
+ """
107
+ An iterator that returns chunks of data.
108
+ """
109
+ return self.__stream
110
+
111
+ @property
112
+ def headers(self) -> Optional[Dict[str, Any]]:
113
+ """
114
+ The HTTP response headers, if available.
115
+
116
+ For HTTP connections, this contains the headers from the SSE stream response.
117
+ For non-HTTP connections, this will be ``None``.
118
+
119
+ The headers dict uses case-insensitive keys (via urllib3's HTTPHeaderDict).
120
+ """
121
+ return self.__headers
122
+
123
+ def close(self):
124
+ """
125
+ Does whatever is necessary to release the connection.
126
+ """
127
+ if self.__closer:
128
+ self.__closer()
129
+ self.__closer = None
130
+
131
+ def __enter__(self):
132
+ return self
133
+
134
+ def __exit__(self, type, value, traceback):
135
+ self.close()
136
+
137
+
138
+ # _HttpConnectStrategy and _HttpConnectionClient are defined here rather than in http.py to avoid
139
+ # a circular module reference.
140
+
141
+
142
+ class _HttpConnectStrategy(ConnectStrategy):
143
+ def __init__(self, params: _HttpConnectParams):
144
+ self.__params = params
145
+
146
+ def create_client(self, logger: Logger) -> ConnectionClient:
147
+ return _HttpConnectionClient(self.__params, logger)
148
+
149
+
150
+ class _HttpConnectionClient(ConnectionClient):
151
+ def __init__(self, params: _HttpConnectParams, logger: Logger):
152
+ self.__impl = _HttpClientImpl(params, logger)
153
+
154
+ def connect(self, last_event_id: Optional[str]) -> ConnectionResult:
155
+ stream, closer, headers = self.__impl.connect(last_event_id)
156
+ return ConnectionResult(stream, closer, headers)
157
+
158
+ def close(self):
159
+ self.__impl.close()
160
+
161
+
162
+ __all__ = ['ConnectStrategy', 'ConnectionClient', 'ConnectionResult']
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Callable, Optional, Tuple
5
+
6
+
7
+ class ErrorStrategy:
8
+ """
9
+ Base class of strategies for determining how SSEClient should handle a stream error or the
10
+ end of a stream.
11
+
12
+ The parameter that SSEClient passes to :meth:`apply()` is either ``None`` if the server ended
13
+ the stream normally, or an exception. If it is an exception, it could be an I/O exception
14
+ (failure to connect, broken connection, etc.), or one of the error types defined in this
15
+ package such as :class:`.HTTPStatusError`.
16
+
17
+ The two options for the result are:
18
+
19
+ * :const:`FAIL`: This means that SSEClient should throw an exception to the caller-- or, in
20
+ the case of a stream ending without an error, it should simply stop iterating through events.
21
+ * :const:`CONTINUE`: This means that you intend to keep reading events, so SSEClient should
22
+ transparently retry the connection. If you are reading from :attr:`.SSEClient.all`,
23
+ you will also receive a :class:`.Fault` describing the error.
24
+
25
+ With either option, it is still always possible to explicitly reconnect the stream by calling
26
+ :meth:`.SSEClient.start()` again, or simply by trying to read from :attr:`.SSEClient.events`
27
+ or :attr:`.SSEClient.all` again.
28
+
29
+ Subclasses should be immutable. To implement strategies that behave differently on consecutive
30
+ retries, the strategy should return a new instance of its own class as the second return value
31
+ from ``apply``, rather than modifying the state of the existing instance. This makes it easy
32
+ for SSEClient to reset to the original error-handling state when appropriate by simply reusing
33
+ the original instance.
34
+ """
35
+
36
+ FAIL = True
37
+ CONTINUE = False
38
+
39
+ def apply(self, exception: Optional[Exception]) -> Tuple[bool, ErrorStrategy]:
40
+ """Applies the strategy to determine what to do after a failure.
41
+
42
+ :param exception: an exception, or ``None`` if the stream simply ended
43
+ :return: a tuple where the first element is either :const:`FAIL` to raise an exception
44
+ or :const:`CONTINUE` to continue, and the second element is the strategy object to
45
+ use next time (which could be ``self``)
46
+ """
47
+ raise NotImplementedError("ErrorStrategy base class cannot be used by itself")
48
+
49
+ @staticmethod
50
+ def always_fail() -> ErrorStrategy:
51
+ """
52
+ Specifies that SSEClient should always treat an error as a stream failure. This is the
53
+ default behavior if you do not configure another.
54
+ """
55
+ return _LambdaErrorStrategy(lambda e: (ErrorStrategy.FAIL, None))
56
+
57
+ @staticmethod
58
+ def always_continue() -> ErrorStrategy:
59
+ """
60
+ Specifies that SSEClient should never raise an exception, but should transparently retry
61
+ or, if :attr:`.SSEClient.all` is being used, return the error as a :class:`.Fault`.
62
+
63
+ Be aware that using this mode could cause connection attempts to block indefinitely if
64
+ the server is unavailable.
65
+ """
66
+ return _LambdaErrorStrategy(lambda e: (ErrorStrategy.CONTINUE, None))
67
+
68
+ @staticmethod
69
+ def continue_with_max_attempts(max_attempts: int) -> ErrorStrategy:
70
+ """
71
+ Specifies that SSEClient should automatically retry after an error for up to this
72
+ number of consecutive attempts, but should fail after that point.
73
+
74
+ :param max_attempts: the maximum number of consecutive retries
75
+ """
76
+ return _MaxAttemptsErrorStrategy(max_attempts, 0)
77
+
78
+ @staticmethod
79
+ def continue_with_time_limit(max_time: float) -> ErrorStrategy:
80
+ """
81
+ Specifies that SSEClient should automatically retry after a failure and can retry
82
+ repeatedly until this amount of time has elapsed, but should fail after that point.
83
+
84
+ :param max_time: the time limit, in seconds
85
+ """
86
+ return _TimeLimitErrorStrategy(max_time, 0)
87
+
88
+ @staticmethod
89
+ def from_lambda(
90
+ fn: Callable[[Optional[Exception]], Tuple[bool, Optional[ErrorStrategy]]]
91
+ ) -> ErrorStrategy:
92
+ """
93
+ Convenience method for creating an ErrorStrategy whose ``apply`` method is equivalent to
94
+ the given lambda.
95
+
96
+ The one difference is that the second return value is an ``Optional[ErrorStrategy]`` which
97
+ can be None to mean "no change", since the lambda cannot reference the strategy's ``self``.
98
+ """
99
+ return _LambdaErrorStrategy(fn)
100
+
101
+
102
+ class _LambdaErrorStrategy(ErrorStrategy):
103
+ def __init__(
104
+ self, fn: Callable[[Optional[Exception]], Tuple[bool, Optional[ErrorStrategy]]]
105
+ ):
106
+ self.__fn = fn
107
+
108
+ def apply(self, exception: Optional[Exception]) -> Tuple[bool, ErrorStrategy]:
109
+ should_raise, maybe_next = self.__fn(exception)
110
+ return (should_raise, maybe_next or self)
111
+
112
+
113
+ class _MaxAttemptsErrorStrategy(ErrorStrategy):
114
+ def __init__(self, max_attempts: int, counter: int):
115
+ self.__max_attempts = max_attempts
116
+ self.__counter = counter
117
+
118
+ def apply(self, exception: Optional[Exception]) -> Tuple[bool, ErrorStrategy]:
119
+ if self.__counter >= self.__max_attempts:
120
+ return (ErrorStrategy.FAIL, self)
121
+ return (
122
+ ErrorStrategy.CONTINUE,
123
+ _MaxAttemptsErrorStrategy(self.__max_attempts, self.__counter + 1),
124
+ )
125
+
126
+
127
+ class _TimeLimitErrorStrategy(ErrorStrategy):
128
+ def __init__(self, max_time: float, start_time: float):
129
+ self.__max_time = max_time
130
+ self.__start_time = start_time
131
+
132
+ def apply(self, exception: Optional[Exception]) -> Tuple[bool, ErrorStrategy]:
133
+ if self.__start_time == 0:
134
+ return (
135
+ ErrorStrategy.CONTINUE,
136
+ _TimeLimitErrorStrategy(self.__max_time, time.time()),
137
+ )
138
+ if (time.time() - self.__start_time) < self.__max_time:
139
+ return (ErrorStrategy.CONTINUE, self)
140
+ return (ErrorStrategy.FAIL, self)
141
+
142
+
143
+ __all__ = ['ErrorStrategy']