dtps-http 1.4.6__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.
- dtps_http-1.4.6/LICENSE.pdf +0 -0
- dtps_http-1.4.6/PKG-INFO +30 -0
- dtps_http-1.4.6/pyproject.toml +68 -0
- dtps_http-1.4.6/src/dtps/__init__.py +13 -0
- dtps_http-1.4.6/src/dtps/config.py +235 -0
- dtps_http-1.4.6/src/dtps/dtps_utils.py +121 -0
- dtps_http-1.4.6/src/dtps/ergo_create.py +454 -0
- dtps_http-1.4.6/src/dtps/ergo_ui.py +389 -0
- dtps_http-1.4.6/src/dtps/ergo_use.py +715 -0
- dtps_http-1.4.6/src/dtps/py.typed +0 -0
- dtps_http-1.4.6/src/dtps/structures.py +0 -0
- dtps_http-1.4.6/src/dtps_http/__init__.py +25 -0
- dtps_http-1.4.6/src/dtps_http/blob_manager.py +154 -0
- dtps_http-1.4.6/src/dtps_http/client.py +1726 -0
- dtps_http-1.4.6/src/dtps_http/constants.py +117 -0
- dtps_http-1.4.6/src/dtps_http/ergo_utils.py +41 -0
- dtps_http-1.4.6/src/dtps_http/exceptions.py +27 -0
- dtps_http-1.4.6/src/dtps_http/link_headers.py +62 -0
- dtps_http-1.4.6/src/dtps_http/object_queue.py +383 -0
- dtps_http-1.4.6/src/dtps_http/py.typed +0 -0
- dtps_http-1.4.6/src/dtps_http/server.py +2077 -0
- dtps_http-1.4.6/src/dtps_http/server_start.py +322 -0
- dtps_http-1.4.6/src/dtps_http/static/favicon.png +0 -0
- dtps_http-1.4.6/src/dtps_http/static/send.js +235 -0
- dtps_http-1.4.6/src/dtps_http/static/style.css +104 -0
- dtps_http-1.4.6/src/dtps_http/structures.py +823 -0
- dtps_http-1.4.6/src/dtps_http/types.py +131 -0
- dtps_http-1.4.6/src/dtps_http/types_of_source.py +615 -0
- dtps_http-1.4.6/src/dtps_http/urls.py +184 -0
- dtps_http-1.4.6/src/dtps_http/utils.py +252 -0
- dtps_http-1.4.6/src/dtps_http/utils_every_once_in_a_while.py +38 -0
- dtps_http-1.4.6/src/dtps_http_programs/__init__.py +13 -0
- dtps_http-1.4.6/src/dtps_http_programs/dtps_listen.py +112 -0
- dtps_http-1.4.6/src/dtps_http_programs/dtps_proxy.py +69 -0
- dtps_http-1.4.6/src/dtps_http_programs/dtps_send_continuous.py +38 -0
- dtps_http-1.4.6/src/dtps_http_programs/dtps_stats.py +104 -0
- dtps_http-1.4.6/src/dtps_http_programs/py.typed +0 -0
- dtps_http-1.4.6/src/dtps_http_programs/server_clock.py +59 -0
- dtps_http-1.4.6/src/dtps_http_programs/together.py +34 -0
|
Binary file
|
dtps_http-1.4.6/PKG-INFO
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dtps-http
|
|
3
|
+
Version: 1.4.6
|
|
4
|
+
Summary:
|
|
5
|
+
License-File: LICENSE.pdf
|
|
6
|
+
Author: Andrea Censi
|
|
7
|
+
Author-email: AndreaCensi@users.noreply.github.com
|
|
8
|
+
Requires-Python: >=3.8,<4.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: aiohttp (>=3,<4)
|
|
18
|
+
Requires-Dist: aiopubsub (>=3,<4)
|
|
19
|
+
Requires-Dist: cbor2 (>=5,<6)
|
|
20
|
+
Requires-Dist: jsonpatch (>=1.33,<2.0)
|
|
21
|
+
Requires-Dist: methodtools (>=0.4,<0.5)
|
|
22
|
+
Requires-Dist: prettyprinter (>=0.18.0,<0.19.0)
|
|
23
|
+
Requires-Dist: psutil (>=7,<8)
|
|
24
|
+
Requires-Dist: pydantic (==2.10.6)
|
|
25
|
+
Requires-Dist: pymdown-extensions (>=10,<11)
|
|
26
|
+
Requires-Dist: tcp_latency (==0.0.12)
|
|
27
|
+
Requires-Dist: timeout-decorator (>=0.5.0,<0.6.0)
|
|
28
|
+
Requires-Dist: typing_extensions (>=4,<5)
|
|
29
|
+
Requires-Dist: urllib3 (>=2,<3)
|
|
30
|
+
Requires-Dist: xxhash (>=3.4.1,<4.0.0)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
|
|
3
|
+
build-backend = "poetry_dynamic_versioning.backend"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[tool]
|
|
7
|
+
[tool.poetry]
|
|
8
|
+
name = "dtps-http"
|
|
9
|
+
version = "1.4.6"
|
|
10
|
+
description = ""
|
|
11
|
+
authors = ["Andrea Censi <AndreaCensi@users.noreply.github.com>"]
|
|
12
|
+
packages = [
|
|
13
|
+
{ from = "src", include = "dtps" },
|
|
14
|
+
{ from = "src", include = "dtps_http" },
|
|
15
|
+
{ from = "src", include = "dtps_http_programs" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
include = [
|
|
19
|
+
{ path = "src/dtps_http/static/**", format = ['sdist', 'wheel'] }
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.poetry.dependencies]
|
|
23
|
+
python = "^3.8"
|
|
24
|
+
aiohttp = "^3"
|
|
25
|
+
cbor2 = "^5"
|
|
26
|
+
tcp_latency = "0.0.12"
|
|
27
|
+
typing_extensions = "^4"
|
|
28
|
+
pymdown-extensions = "^10"
|
|
29
|
+
aiopubsub = "^3"
|
|
30
|
+
methodtools = "^0.4"
|
|
31
|
+
pydantic = "2.10.6"
|
|
32
|
+
psutil = "^7"
|
|
33
|
+
urllib3 = "^2"
|
|
34
|
+
jsonpatch = "^1.33"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# other dependencies: aiohttp-devtools pre-commit mkdocs mkdocs-material mkdocstrings[python]
|
|
38
|
+
# pip install "mkdocstrings[python]" mkdocs-material
|
|
39
|
+
timeout-decorator = "^0.5.0"
|
|
40
|
+
prettyprinter = "^0.18.0"
|
|
41
|
+
xxhash = "^3.4.1"
|
|
42
|
+
|
|
43
|
+
[tool.poetry.scripts]
|
|
44
|
+
|
|
45
|
+
dtps-http-py-server-example-clock = 'dtps_http_programs:clock_main'
|
|
46
|
+
dtps-http-py-server = 'dtps_http_programs:server_main'
|
|
47
|
+
dtps-http-py-client-stats = 'dtps_http_programs:dtps_stats_main'
|
|
48
|
+
dtps-http-py-proxy = 'dtps_http_programs:dtps_proxy_main'
|
|
49
|
+
dtps-http-py-send-continuous = 'dtps_http_programs:dtps_send_continuous_main'
|
|
50
|
+
dtps-http-py-listen= 'dtps_http_programs:dtps_listen_main'
|
|
51
|
+
dtps-http-py= 'dtps_http_programs:dtps_main'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
[tool.poetry-dynamic-versioning]
|
|
55
|
+
enable = false
|
|
56
|
+
dirty = false
|
|
57
|
+
metadata = false
|
|
58
|
+
tagged-metadata = false
|
|
59
|
+
vcs = "git"
|
|
60
|
+
style = "pep440"
|
|
61
|
+
strict = false
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
[tool.poetry-dynamic-versioning.substitution]
|
|
66
|
+
folders = [
|
|
67
|
+
{ path = "src" }
|
|
68
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
__version__ = "1.4.6"
|
|
2
|
+
|
|
3
|
+
from logging import DEBUG, getLogger
|
|
4
|
+
|
|
5
|
+
logger = getLogger(__name__)
|
|
6
|
+
logger.setLevel(DEBUG)
|
|
7
|
+
|
|
8
|
+
from .config import *
|
|
9
|
+
from .ergo_ui import *
|
|
10
|
+
from .dtps_utils import *
|
|
11
|
+
from dtps_http import RawData, TransformError
|
|
12
|
+
|
|
13
|
+
_ = RawData, TransformError
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import (
|
|
5
|
+
AsyncContextManager,
|
|
6
|
+
AsyncIterator,
|
|
7
|
+
cast,
|
|
8
|
+
ClassVar,
|
|
9
|
+
Dict,
|
|
10
|
+
List,
|
|
11
|
+
Mapping,
|
|
12
|
+
Optional,
|
|
13
|
+
Tuple,
|
|
14
|
+
TYPE_CHECKING,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from dtps_http import parse_url_unescape, ServerWrapped, URLString
|
|
18
|
+
from . import logger
|
|
19
|
+
from .ergo_ui import DTPSContext
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"context",
|
|
23
|
+
"context_cleanup",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def context(
|
|
28
|
+
base_name: str = "self", environment: Optional[Mapping[str, str]] = None, urls: Optional[List[str]] = None
|
|
29
|
+
) -> "DTPSContext":
|
|
30
|
+
"""
|
|
31
|
+
Initialize a DTPS interface from the environment from a given base name.
|
|
32
|
+
|
|
33
|
+
base_name is case-insensitive.
|
|
34
|
+
|
|
35
|
+
Environment variables of the form DTPS_BASE_<base_name> are used to get the info needed.
|
|
36
|
+
|
|
37
|
+
For example:
|
|
38
|
+
|
|
39
|
+
DTPS_BASE_SELF = "http://localhost:2120/" # use an existing server
|
|
40
|
+
DTPS_BASE_SELF = "http+unix://[socket]/" # use an existing unix socket
|
|
41
|
+
|
|
42
|
+
We can also use the special prefix "create:" to create a new server.
|
|
43
|
+
For example:
|
|
44
|
+
|
|
45
|
+
DTPS_BASE_SELF = "create:http://localhost:2120/" # create a new server
|
|
46
|
+
|
|
47
|
+
Moreover, we can use more than one base name, by adding a number at the end:
|
|
48
|
+
|
|
49
|
+
DTPS_BASE_SELF_0 = "create:http://localhost:2120/" #
|
|
50
|
+
DTPS_BASE_SELF_1 = "create:http+unix://[socket]/"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
You need to call context.aclose() at the end to clean up resources.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
c = context("mio", environment={'DTPS_BASE_MIO': url})
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
base_name = base_name.lower()
|
|
60
|
+
|
|
61
|
+
if environment is not None and urls is not None:
|
|
62
|
+
raise ValueError("You cannot create a context while passing both 'environment' and 'urls'")
|
|
63
|
+
|
|
64
|
+
if urls:
|
|
65
|
+
environment = environment_from_urls(base_name, urls)
|
|
66
|
+
|
|
67
|
+
if environment is None:
|
|
68
|
+
if base_name in ContextManager.instances:
|
|
69
|
+
return ContextManager.instances[base_name].get_context()
|
|
70
|
+
else:
|
|
71
|
+
context_manager = await create_context(base_name, environment)
|
|
72
|
+
ContextManager.instances[base_name] = context_manager
|
|
73
|
+
return context_manager.get_context()
|
|
74
|
+
else:
|
|
75
|
+
context_manager = await create_context(base_name, environment)
|
|
76
|
+
return context_manager.get_context()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if TYPE_CHECKING:
|
|
80
|
+
|
|
81
|
+
def context_cleanup(
|
|
82
|
+
base_name: str = "self", environment: Optional[Mapping[str, str]] = None
|
|
83
|
+
) -> AsyncContextManager[DTPSContext]: ...
|
|
84
|
+
|
|
85
|
+
else:
|
|
86
|
+
|
|
87
|
+
@asynccontextmanager
|
|
88
|
+
async def context_cleanup(
|
|
89
|
+
base_name: str = "self", environment: Optional[Mapping[str, str]] = None
|
|
90
|
+
) -> AsyncIterator[DTPSContext]:
|
|
91
|
+
"""Context manager to open a context and clean-up later."""
|
|
92
|
+
c = await context(base_name, environment)
|
|
93
|
+
try:
|
|
94
|
+
yield c
|
|
95
|
+
finally:
|
|
96
|
+
await c.aclose()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def create_context(base_name: str, environment: Optional[Mapping[str, str]]) -> "ContextManager":
|
|
100
|
+
contexts = get_context_info(environment)
|
|
101
|
+
if base_name not in contexts.contexts:
|
|
102
|
+
msg = f'Cannot find context "{base_name}" among {list(contexts.contexts)}'
|
|
103
|
+
raise KeyError(msg)
|
|
104
|
+
|
|
105
|
+
context_info = contexts.contexts[base_name]
|
|
106
|
+
logger.debug(f'Creating context "{base_name}" with {context_info} for environment {environment}')
|
|
107
|
+
return await ContextManager.create(base_name, context_info)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ContextManager:
|
|
111
|
+
instances: ClassVar[Dict[str, "ContextManager"]] = {}
|
|
112
|
+
|
|
113
|
+
context_info: "ContextInfo"
|
|
114
|
+
|
|
115
|
+
dtps_server_wrap: Optional[ServerWrapped]
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
async def create(cls, base_name: str, context_info: "ContextInfo") -> "ContextManager":
|
|
119
|
+
# if base_name in cls.instances:
|
|
120
|
+
# msg = f'Context "{base_name}" already exists'
|
|
121
|
+
# raise KeyError(msg)
|
|
122
|
+
|
|
123
|
+
# logger.info(f'Creating context "{base_name}" with {context_info}')
|
|
124
|
+
|
|
125
|
+
if context_info.is_create():
|
|
126
|
+
from .ergo_create import ContextManagerCreate
|
|
127
|
+
|
|
128
|
+
cm = ContextManagerCreate(base_name, context_info)
|
|
129
|
+
else:
|
|
130
|
+
from .ergo_use import ContextManagerUse
|
|
131
|
+
|
|
132
|
+
cm = ContextManagerUse(base_name, context_info)
|
|
133
|
+
|
|
134
|
+
# cls.instances[base_name] = cm
|
|
135
|
+
await cm.init()
|
|
136
|
+
return cm
|
|
137
|
+
|
|
138
|
+
def get_context(self) -> "DTPSContext":
|
|
139
|
+
raise NotImplementedError()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class ContextUrl:
|
|
144
|
+
url: URLString
|
|
145
|
+
create: bool
|
|
146
|
+
|
|
147
|
+
def __post_init__(self) -> None:
|
|
148
|
+
parse_url_unescape(self.url)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class ContextInfo:
|
|
153
|
+
urls: List[ContextUrl]
|
|
154
|
+
|
|
155
|
+
def is_create(self) -> bool:
|
|
156
|
+
return all(x.create for x in self.urls)
|
|
157
|
+
|
|
158
|
+
def get_tcp_and_unix(self) -> Tuple[List[Tuple[str, int]], List[str]]:
|
|
159
|
+
tcp: List[Tuple[str, int]] = []
|
|
160
|
+
unix: List[str] = []
|
|
161
|
+
for u in self.urls:
|
|
162
|
+
url_ = parse_url_unescape(u.url)
|
|
163
|
+
if url_.scheme == "http+unix":
|
|
164
|
+
host = url_.host
|
|
165
|
+
unix.append(host)
|
|
166
|
+
|
|
167
|
+
elif url_.scheme == "http" or url_.scheme == "https":
|
|
168
|
+
# rest = url.url[len("http://") :]
|
|
169
|
+
# host, _, rest = rest.partition("/")
|
|
170
|
+
# host, _, port = host.partition(":")
|
|
171
|
+
# port = int(port)
|
|
172
|
+
host = url_.host or "localhost"
|
|
173
|
+
if not url_.port:
|
|
174
|
+
port = 0
|
|
175
|
+
elif isinstance(url_.port, str):
|
|
176
|
+
port = int(url_.port)
|
|
177
|
+
else:
|
|
178
|
+
port = url_.port
|
|
179
|
+
|
|
180
|
+
tcp.append((host, port))
|
|
181
|
+
else:
|
|
182
|
+
msg = f'Invalid url "{url_}". Must start with "http://" or "http+unix://".'
|
|
183
|
+
raise ValueError(msg)
|
|
184
|
+
return tcp, unix
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class ContextsInfo:
|
|
189
|
+
contexts: Dict[str, ContextInfo]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
BASE = "DTPS_BASE_"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_context_info(environment: Optional[Mapping[str, str]]) -> ContextsInfo:
|
|
196
|
+
if environment is None:
|
|
197
|
+
environment = dict(os.environ)
|
|
198
|
+
|
|
199
|
+
contexts: Dict[str, ContextInfo] = {}
|
|
200
|
+
for k, v in environment.items():
|
|
201
|
+
if not k.startswith(BASE):
|
|
202
|
+
continue
|
|
203
|
+
rest = k[len(BASE) :]
|
|
204
|
+
|
|
205
|
+
name, _, rest = rest.partition("_")
|
|
206
|
+
|
|
207
|
+
name = name.lower()
|
|
208
|
+
if name not in contexts:
|
|
209
|
+
contexts[name] = ContextInfo(urls=[])
|
|
210
|
+
|
|
211
|
+
if v.startswith("create:"):
|
|
212
|
+
x = cast(URLString, v[len("create:") :])
|
|
213
|
+
create = True
|
|
214
|
+
|
|
215
|
+
else:
|
|
216
|
+
x = cast(URLString, v)
|
|
217
|
+
create = False
|
|
218
|
+
try:
|
|
219
|
+
parse_url_unescape(x)
|
|
220
|
+
except ValueError as e:
|
|
221
|
+
msg = f"Invalid url given by environment:\n{k} = {v}\nExtracted url: {x}"
|
|
222
|
+
raise ValueError(msg) from e
|
|
223
|
+
contexts[name].urls.append(ContextUrl(url=x, create=create))
|
|
224
|
+
|
|
225
|
+
for name, info in contexts.items():
|
|
226
|
+
all_create = all(x.create for x in info.urls)
|
|
227
|
+
all_not_create = all(not x.create for x in info.urls)
|
|
228
|
+
if not all_create and not all_not_create:
|
|
229
|
+
msg = f'Invalid context "{name}". All urls must be either "create:" or not.'
|
|
230
|
+
raise ValueError(msg)
|
|
231
|
+
return ContextsInfo(contexts=contexts)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def environment_from_urls(name: str, urls: List[str]):
|
|
235
|
+
return {f"{BASE}{name}_{i}": url for i, url in enumerate(urls)}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from asyncio import CancelledError, Queue
|
|
3
|
+
from typing import Any, Awaitable, Callable, List, Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
from dtps_http import RawData
|
|
6
|
+
from . import logger
|
|
7
|
+
from .ergo_ui import DTPSContext, SubscriptionInterface
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"process_lowdatasize_last_recent",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def process_lowdatasize_last_recent(
|
|
15
|
+
context: DTPSContext,
|
|
16
|
+
expensive_callback: Callable[[RawData], Awaitable[None]],
|
|
17
|
+
) -> "ExpensiveCallbackSubscription":
|
|
18
|
+
"""
|
|
19
|
+
Make sure we are not a slow reader even for an expensive callback.
|
|
20
|
+
|
|
21
|
+
Suitable for this case:
|
|
22
|
+
|
|
23
|
+
0) only the last message is important (e.g. joystick)
|
|
24
|
+
1) there are a lot of small messages
|
|
25
|
+
2) low-latency is important
|
|
26
|
+
2) the callback is expensive (e.g. write to hardware)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Note! If the callback uses *blocking IO* we need to do it
|
|
30
|
+
in a different process! (or thread)
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
s = ExpensiveCallbackSubscription(expensive_callback)
|
|
34
|
+
await s.init(context)
|
|
35
|
+
return s
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
X = TypeVar("X")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def queue_get_multiple(q: "Queue[X]") -> List[X]:
|
|
42
|
+
"""Gets a packet of ready messages from the queue (at least one)."""
|
|
43
|
+
first = await q.get()
|
|
44
|
+
msgs = [first]
|
|
45
|
+
while True:
|
|
46
|
+
try:
|
|
47
|
+
msgs.append(q.get_nowait())
|
|
48
|
+
except asyncio.QueueEmpty:
|
|
49
|
+
break
|
|
50
|
+
return msgs
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ExpensiveCallbackSubscription(SubscriptionInterface):
|
|
54
|
+
q: "Queue[RawData]"
|
|
55
|
+
sub: Optional[SubscriptionInterface]
|
|
56
|
+
task: "Optional[asyncio.Task[Any]]"
|
|
57
|
+
|
|
58
|
+
def __init__(self, expensive_callback: Callable[[RawData], Awaitable[None]]):
|
|
59
|
+
self.expensive_callback = expensive_callback
|
|
60
|
+
self.q = Queue()
|
|
61
|
+
self.sub = None
|
|
62
|
+
self.nreceived = 0
|
|
63
|
+
self.nprocessed = 0
|
|
64
|
+
self.nskipped = 0
|
|
65
|
+
|
|
66
|
+
async def init(self, context: DTPSContext) -> None:
|
|
67
|
+
self.sub = await context.subscribe(self.raw_callback)
|
|
68
|
+
self.task = asyncio.create_task(self.process_task())
|
|
69
|
+
|
|
70
|
+
async def process_task(self) -> None:
|
|
71
|
+
while True:
|
|
72
|
+
rd = await queue_get_multiple(self.q)
|
|
73
|
+
# only process the last one
|
|
74
|
+
last = rd[-1]
|
|
75
|
+
nskipped_now = len(rd) - 1
|
|
76
|
+
self.nprocessed += 1
|
|
77
|
+
self.nskipped += nskipped_now
|
|
78
|
+
|
|
79
|
+
if nskipped_now > 0:
|
|
80
|
+
percentage_skipped = self.nskipped / self.nreceived
|
|
81
|
+
logger.debug(
|
|
82
|
+
f"Skipped {nskipped_now} messages now in this iteration.\n"
|
|
83
|
+
f"Total received: {self.nreceived}. "
|
|
84
|
+
f"Total skipped: {self.nskipped} ({percentage_skipped:.2%})\n"
|
|
85
|
+
)
|
|
86
|
+
try:
|
|
87
|
+
await self.expensive_callback(last)
|
|
88
|
+
except CancelledError:
|
|
89
|
+
break
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"Error in expensive callback", exc_info=e)
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
async def raw_callback(self, r: RawData) -> None:
|
|
95
|
+
self.nreceived += 1
|
|
96
|
+
self.q.put_nowait(r)
|
|
97
|
+
|
|
98
|
+
async def aclose(self) -> None:
|
|
99
|
+
if self.nreceived == 0:
|
|
100
|
+
logger.debug("No messages received")
|
|
101
|
+
else:
|
|
102
|
+
|
|
103
|
+
percentage_skipped = self.nskipped / self.nreceived
|
|
104
|
+
percentage_processed = self.nprocessed / self.nreceived
|
|
105
|
+
|
|
106
|
+
logger.debug(
|
|
107
|
+
f"Final statistics:\n"
|
|
108
|
+
f" Total received: {self.nreceived:5} \n"
|
|
109
|
+
f" Total processed: {self.nprocessed:5} ({percentage_processed:.2%})\n"
|
|
110
|
+
f" Total skipped: {self.nskipped:5} ({percentage_skipped:.2%})\n"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if self.sub is not None:
|
|
114
|
+
await self.sub.unsubscribe()
|
|
115
|
+
self.sub = None
|
|
116
|
+
if self.task is not None:
|
|
117
|
+
self.task.cancel()
|
|
118
|
+
self.task = None
|
|
119
|
+
|
|
120
|
+
async def unsubscribe(self) -> None:
|
|
121
|
+
await self.aclose()
|