varco-ws 0.1.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,209 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ todo*.txt
205
+
206
+ # Marimo
207
+ marimo/_static/
208
+ marimo/_lsp/
209
+ __marimo__/
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: varco-ws
3
+ Version: 0.1.0
4
+ Summary: WebSocket and SSE event bus backends for varco — real-time browser push
5
+ Project-URL: Homepage, https://github.com/edoardoscarpaci/varco
6
+ Project-URL: Repository, https://github.com/edoardoscarpaci/varco/tree/main/varco_ws
7
+ Project-URL: Issues, https://github.com/edoardoscarpaci/varco/issues
8
+ Author-email: "edoardo.scarpaci" <edoardo.scarpaci@gmail.com>
9
+ License: Apache-2.0
10
+ Keywords: async,domain-events,event-bus,fastapi,sse,varco,websocket
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: varco-core
File without changes
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "varco-ws"
3
+ version = "0.1.0"
4
+ description = "WebSocket and SSE event bus backends for varco — real-time browser push"
5
+ authors = [{ name = "edoardo.scarpaci", email = "edoardo.scarpaci@gmail.com" }]
6
+ readme = "README.md"
7
+ license = { text = "Apache-2.0" }
8
+ requires-python = ">=3.12"
9
+ keywords = ["fastapi", "websocket", "sse", "async", "event-bus", "domain-events", "varco"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: Apache Software License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = [
21
+ # Workspace sibling — resolved from the monorepo via [tool.uv.sources] below.
22
+ "varco-core",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/edoardoscarpaci/varco"
27
+ Repository = "https://github.com/edoardoscarpaci/varco/tree/main/varco_ws"
28
+ Issues = "https://github.com/edoardoscarpaci/varco/issues"
29
+
30
+ [tool.uv.sources]
31
+ # Resolve varco-core from the workspace instead of PyPI during development.
32
+ varco-core = { workspace = true }
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "pytest>=8.0",
37
+ "pytest-asyncio>=0.24",
38
+ ]
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["varco_ws"]
46
+
47
+ [tool.pytest.ini_options]
48
+ asyncio_mode = "auto"
49
+ testpaths = ["tests"]
File without changes
@@ -0,0 +1,261 @@
1
+ """
2
+ tests.test_di
3
+ =============
4
+ Unit tests for varco_ws.di — WebSocketConfiguration and SSEConfiguration.
5
+
6
+ Covers:
7
+ WebSocketConfiguration — provides a WebSocketEventBus singleton that
8
+ wraps the container's AbstractEventBus.
9
+ SSEConfiguration — provides an SSEEventBus singleton that wraps
10
+ the container's AbstractEventBus.
11
+ varco_ws public __init__ — DI classes are exported at the package level.
12
+
13
+ All tests use InMemoryEventBus — no real broker required.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import pytest
19
+
20
+ from varco_core.event.base import AbstractEventBus
21
+ from varco_core.event.memory import InMemoryEventBus
22
+
23
+ from varco_ws import SSEConfiguration, WebSocketConfiguration
24
+ from varco_ws.di import SSEConfiguration as SSEConfigurationDirect
25
+ from varco_ws.di import WebSocketConfiguration as WebSocketConfigurationDirect
26
+ from varco_ws.sse import SSEEventBus
27
+ from varco_ws.websocket import WebSocketEventBus
28
+
29
+
30
+ # ── Helpers ────────────────────────────────────────────────────────────────────
31
+
32
+
33
+ def _make_container_with_bus() -> tuple[object, InMemoryEventBus]:
34
+ """
35
+ Build a minimal providify DIContainer with an InMemoryEventBus registered
36
+ as AbstractEventBus. Returns (container, bus).
37
+ """
38
+ try:
39
+ from providify import DIContainer
40
+ except ImportError:
41
+ pytest.skip("providify not installed — skipping DI tests")
42
+
43
+ container = DIContainer()
44
+ bus = InMemoryEventBus()
45
+ # Register InMemoryEventBus as the AbstractEventBus implementation.
46
+ container.provide(lambda: bus, AbstractEventBus)
47
+ return container, bus
48
+
49
+
50
+ # ── __init__ re-export tests ───────────────────────────────────────────────────
51
+
52
+
53
+ def test_websocket_configuration_exported_from_init() -> None:
54
+ """
55
+ WebSocketConfiguration must be importable from the top-level varco_ws package.
56
+ This guards against accidental removal from __init__.py.
57
+ """
58
+ assert WebSocketConfiguration is WebSocketConfigurationDirect
59
+
60
+
61
+ def test_sse_configuration_exported_from_init() -> None:
62
+ """
63
+ SSEConfiguration must be importable from the top-level varco_ws package.
64
+ """
65
+ assert SSEConfiguration is SSEConfigurationDirect
66
+
67
+
68
+ # ── WebSocketConfiguration tests ──────────────────────────────────────────────
69
+
70
+
71
+ def test_websocket_configuration_provides_websocket_event_bus() -> None:
72
+ """
73
+ After installing WebSocketConfiguration, the container must resolve
74
+ WebSocketEventBus.
75
+ """
76
+ container, _ = _make_container_with_bus()
77
+ container.install(WebSocketConfiguration)
78
+
79
+ ws_bus = container.get(WebSocketEventBus)
80
+ assert isinstance(ws_bus, WebSocketEventBus)
81
+
82
+
83
+ def test_websocket_configuration_wraps_registered_bus() -> None:
84
+ """
85
+ The WebSocketEventBus provided by WebSocketConfiguration must wrap
86
+ the AbstractEventBus that was registered in the container.
87
+ """
88
+ container, bus = _make_container_with_bus()
89
+ container.install(WebSocketConfiguration)
90
+
91
+ ws_bus = container.get(WebSocketEventBus)
92
+ # Internal attribute _bus must be the registered InMemoryEventBus.
93
+ assert ws_bus._bus is bus
94
+
95
+
96
+ def test_websocket_configuration_singleton() -> None:
97
+ """
98
+ WebSocketEventBus must be a singleton — resolving it twice returns the
99
+ same instance.
100
+ """
101
+ container, _ = _make_container_with_bus()
102
+ container.install(WebSocketConfiguration)
103
+
104
+ first = container.get(WebSocketEventBus)
105
+ second = container.get(WebSocketEventBus)
106
+ assert first is second
107
+
108
+
109
+ def test_websocket_configuration_bus_not_started_after_install() -> None:
110
+ """
111
+ The WebSocketEventBus must NOT be started automatically by the DI module.
112
+ Callers must call start() explicitly in their lifespan handler.
113
+
114
+ DESIGN: not starting automatically avoids an asyncio.Loop dependency at
115
+ installation time — the container may be built synchronously before an
116
+ event loop is running.
117
+ """
118
+ container, _ = _make_container_with_bus()
119
+ container.install(WebSocketConfiguration)
120
+
121
+ ws_bus = container.get(WebSocketEventBus)
122
+ # Internal subscription handle is None when not started.
123
+ assert ws_bus._subscription is None
124
+
125
+
126
+ # ── SSEConfiguration tests ────────────────────────────────────────────────────
127
+
128
+
129
+ def test_sse_configuration_provides_sse_event_bus() -> None:
130
+ """
131
+ After installing SSEConfiguration, the container must resolve SSEEventBus.
132
+ """
133
+ container, _ = _make_container_with_bus()
134
+ container.install(SSEConfiguration)
135
+
136
+ sse_bus = container.get(SSEEventBus)
137
+ assert isinstance(sse_bus, SSEEventBus)
138
+
139
+
140
+ def test_sse_configuration_wraps_registered_bus() -> None:
141
+ """
142
+ The SSEEventBus provided by SSEConfiguration must wrap the
143
+ AbstractEventBus that was registered in the container.
144
+ """
145
+ container, bus = _make_container_with_bus()
146
+ container.install(SSEConfiguration)
147
+
148
+ sse_bus = container.get(SSEEventBus)
149
+ assert sse_bus._bus is bus
150
+
151
+
152
+ def test_sse_configuration_singleton() -> None:
153
+ """SSEEventBus must be a singleton."""
154
+ container, _ = _make_container_with_bus()
155
+ container.install(SSEConfiguration)
156
+
157
+ first = container.get(SSEEventBus)
158
+ second = container.get(SSEEventBus)
159
+ assert first is second
160
+
161
+
162
+ def test_sse_configuration_bus_not_started_after_install() -> None:
163
+ """
164
+ The SSEEventBus must NOT be started automatically. Callers start it
165
+ in the FastAPI lifespan handler.
166
+ """
167
+ container, _ = _make_container_with_bus()
168
+ container.install(SSEConfiguration)
169
+
170
+ sse_bus = container.get(SSEEventBus)
171
+ assert sse_bus._subscription is None
172
+
173
+
174
+ # ── Combined WS + SSE tests ───────────────────────────────────────────────────
175
+
176
+
177
+ def test_both_configurations_can_be_installed_together() -> None:
178
+ """
179
+ Installing both WebSocketConfiguration and SSEConfiguration in the same
180
+ container must not conflict — they provide different types.
181
+ """
182
+ container, _ = _make_container_with_bus()
183
+ container.install(WebSocketConfiguration)
184
+ container.install(SSEConfiguration)
185
+
186
+ ws_bus = container.get(WebSocketEventBus)
187
+ sse_bus = container.get(SSEEventBus)
188
+
189
+ # Different types — different instances.
190
+ assert isinstance(ws_bus, WebSocketEventBus)
191
+ assert isinstance(sse_bus, SSEEventBus)
192
+ assert ws_bus is not sse_bus
193
+
194
+
195
+ async def test_ws_bus_is_functional_after_start() -> None:
196
+ """
197
+ The WebSocketEventBus obtained from the container must be fully functional
198
+ after manually calling start().
199
+
200
+ DESIGN: functional test to confirm the DI-provided adapter delivers events,
201
+ not just that it was constructed correctly.
202
+ """
203
+ import asyncio
204
+
205
+ from varco_core.event.base import Event
206
+
207
+ class PingEvent(Event):
208
+ __event_type__ = "test.ping"
209
+ count: int = 0
210
+
211
+ container, bus = _make_container_with_bus()
212
+ container.install(WebSocketConfiguration)
213
+
214
+ ws_bus = container.get(WebSocketEventBus)
215
+ await ws_bus.start()
216
+
217
+ class FakeWebSocket:
218
+ sent: list[str] = []
219
+
220
+ async def send_text(self, msg: str) -> None:
221
+ self.sent.append(msg)
222
+
223
+ fake_ws = FakeWebSocket()
224
+ async with ws_bus.connect(fake_ws):
225
+ await bus.publish(PingEvent(count=1))
226
+ await asyncio.sleep(0)
227
+ await asyncio.sleep(0)
228
+
229
+ assert len(fake_ws.sent) == 1
230
+
231
+ await ws_bus.stop()
232
+
233
+
234
+ async def test_sse_bus_is_functional_after_start() -> None:
235
+ """
236
+ The SSEEventBus obtained from the container must be fully functional
237
+ after manually calling start().
238
+ """
239
+ import asyncio
240
+
241
+ from varco_core.event.base import Event
242
+
243
+ class PongEvent(Event):
244
+ __event_type__ = "test.pong"
245
+ value: str = ""
246
+
247
+ container, bus = _make_container_with_bus()
248
+ container.install(SSEConfiguration)
249
+
250
+ sse_bus = container.get(SSEEventBus)
251
+ await sse_bus.start()
252
+
253
+ async with sse_bus.subscribe() as conn:
254
+ await bus.publish(PongEvent(value="hello"))
255
+ await asyncio.sleep(0)
256
+
257
+ message = await asyncio.wait_for(conn._queue.get(), timeout=1.0)
258
+ assert "test.pong" in message
259
+ assert "hello" in message
260
+
261
+ await sse_bus.stop()