bussdcc-framework 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.
Files changed (38) hide show
  1. bussdcc_framework-0.1.0/.github/workflows/workflow.yml +62 -0
  2. bussdcc_framework-0.1.0/.gitignore +209 -0
  3. bussdcc_framework-0.1.0/CHANGELOG.md +8 -0
  4. bussdcc_framework-0.1.0/LICENSE +21 -0
  5. bussdcc_framework-0.1.0/Makefile +44 -0
  6. bussdcc_framework-0.1.0/PKG-INFO +33 -0
  7. bussdcc_framework-0.1.0/README.md +1 -0
  8. bussdcc_framework-0.1.0/pyproject.toml +56 -0
  9. bussdcc_framework-0.1.0/setup.cfg +4 -0
  10. bussdcc_framework-0.1.0/src/bussdcc_framework/__init__.py +5 -0
  11. bussdcc_framework-0.1.0/src/bussdcc_framework/events/__init__.py +4 -0
  12. bussdcc_framework-0.1.0/src/bussdcc_framework/events/framework.py +19 -0
  13. bussdcc_framework-0.1.0/src/bussdcc_framework/events/sink.py +9 -0
  14. bussdcc_framework-0.1.0/src/bussdcc_framework/events/system.py +13 -0
  15. bussdcc_framework-0.1.0/src/bussdcc_framework/events/web.py +11 -0
  16. bussdcc_framework-0.1.0/src/bussdcc_framework/interface/__init__.py +3 -0
  17. bussdcc_framework-0.1.0/src/bussdcc_framework/interface/web/__init__.py +5 -0
  18. bussdcc_framework-0.1.0/src/bussdcc_framework/interface/web/base.py +9 -0
  19. bussdcc_framework-0.1.0/src/bussdcc_framework/interface/web/factory.py +38 -0
  20. bussdcc_framework-0.1.0/src/bussdcc_framework/interface/web/interface.py +53 -0
  21. bussdcc_framework-0.1.0/src/bussdcc_framework/interface/web/templates/base.html +80 -0
  22. bussdcc_framework-0.1.0/src/bussdcc_framework/interface/web/templates/home.html +21 -0
  23. bussdcc_framework-0.1.0/src/bussdcc_framework/process/__init__.py +5 -0
  24. bussdcc_framework-0.1.0/src/bussdcc_framework/process/system_identity.py +22 -0
  25. bussdcc_framework-0.1.0/src/bussdcc_framework/runtime/__init__.py +5 -0
  26. bussdcc_framework-0.1.0/src/bussdcc_framework/runtime/runtime.py +39 -0
  27. bussdcc_framework-0.1.0/src/bussdcc_framework/runtime/sink/__init__.py +9 -0
  28. bussdcc_framework-0.1.0/src/bussdcc_framework/runtime/sink/console.py +44 -0
  29. bussdcc_framework-0.1.0/src/bussdcc_framework/runtime/sink/jsonl.py +93 -0
  30. bussdcc_framework-0.1.0/src/bussdcc_framework/runtime/sink/protocol.py +10 -0
  31. bussdcc_framework-0.1.0/src/bussdcc_framework/service/__init__.py +5 -0
  32. bussdcc_framework-0.1.0/src/bussdcc_framework/service/system_identity.py +35 -0
  33. bussdcc_framework-0.1.0/src/bussdcc_framework/version.py +14 -0
  34. bussdcc_framework-0.1.0/src/bussdcc_framework.egg-info/PKG-INFO +33 -0
  35. bussdcc_framework-0.1.0/src/bussdcc_framework.egg-info/SOURCES.txt +36 -0
  36. bussdcc_framework-0.1.0/src/bussdcc_framework.egg-info/dependency_links.txt +1 -0
  37. bussdcc_framework-0.1.0/src/bussdcc_framework.egg-info/requires.txt +7 -0
  38. bussdcc_framework-0.1.0/src/bussdcc_framework.egg-info/top_level.txt +1 -0
@@ -0,0 +1,62 @@
1
+ name: Github Actions Workflow
2
+ on:
3
+ push:
4
+ workflow_dispatch:
5
+ jobs:
6
+ check:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - name: Checkout code
10
+ uses: actions/checkout@v4
11
+ - name: Set up Python
12
+ uses: actions/setup-python@v5
13
+ with:
14
+ python-version: "3.13"
15
+ - name: make setup
16
+ run: make setup
17
+ - name: make check
18
+ run: make check
19
+ release-please:
20
+ runs-on: ubuntu-latest
21
+ needs: check
22
+ if: github.ref == 'refs/heads/master'
23
+ permissions:
24
+ contents: write
25
+ issues: write
26
+ pull-requests: write
27
+ steps:
28
+ - name: release-please
29
+ id: release-please
30
+ uses: googleapis/release-please-action@v4
31
+ with:
32
+ release-type: python
33
+ outputs:
34
+ release_created: ${{ steps.release-please.outputs.release_created }}
35
+ tag_name: ${{ steps.release-please.outputs.tag_name }}
36
+ publish:
37
+ runs-on: ubuntu-latest
38
+ needs: release-please
39
+ if: needs.release-please.outputs.release_created
40
+ environment: pypi
41
+ permissions:
42
+ contents: write
43
+ id-token: write
44
+ steps:
45
+ - name: Checkout code
46
+ uses: actions/checkout@v4
47
+ with:
48
+ ref: ${{ needs.release-please.outputs.tag_name }}
49
+ - name: Set up Python
50
+ uses: actions/setup-python@v5
51
+ with:
52
+ python-version: "3.12"
53
+ - name: make build
54
+ run: make build
55
+ - name: make build-check
56
+ run: make build-check
57
+ - name: Publish to GitHub Releases
58
+ env:
59
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60
+ run: gh release upload ${{ needs.release-please.outputs.tag_name }} dist/*
61
+ - name: Publish to PyPI
62
+ run: make publish
@@ -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
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
208
+
209
+ /data/
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-03-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * initial commit ([0c837b7](https://github.com/jbussdieker/bussdcc-framework/commit/0c837b7021923a01c4782e17b28693d0133e6c96))
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joshua B. Bussdieker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,44 @@
1
+ .PHONY: setup check typecheck lint format clean
2
+
3
+ setup: .venv/bin/python
4
+ .venv/bin/python -m pip install -e .[dev]
5
+ check: typecheck lint
6
+ typecheck: .venv/bin/mypy
7
+ .venv/bin/mypy --strict src
8
+ lint: .venv/bin/black
9
+ .venv/bin/black --check .
10
+ format: .venv/bin/black
11
+ .venv/bin/black .
12
+ build: .venv/bin/build
13
+ .venv/bin/python -m build .
14
+ build-check: .venv/bin/twine
15
+ .venv/bin/twine check dist/*
16
+ publish-testpypi: .venv/bin/twine
17
+ .venv/bin/twine upload --repository testpypi dist/*
18
+ publish: .venv/bin/twine
19
+ .venv/bin/twine upload dist/*
20
+ clean:
21
+ rm -rf dist build .venv
22
+ .venv/bin/mypy: .venv/bin/python
23
+ .venv/bin/pip install mypy
24
+ .venv/bin/build: .venv/bin/python
25
+ .venv/bin/pip install build
26
+ .venv/bin/black: .venv/bin/python
27
+ .venv/bin/pip install black
28
+ .venv/bin/twine: .venv/bin/python
29
+ .venv/bin/pip install twine
30
+ .venv/bin/python:
31
+ python3 -m venv --system-site-packages .venv
32
+ context:
33
+ @echo "# README.md"
34
+ @cat README.md
35
+ @echo ""
36
+ @echo ""
37
+ @echo "# pyproject.toml"
38
+ @cat pyproject.toml
39
+ @echo ""
40
+ @echo ""
41
+ @for f in $$(find src -type f -name "*.html"); do echo "# $$f"; cat $$f; echo; echo; done
42
+ @echo ""
43
+ @echo ""
44
+ @for f in $$(find src -type f -name "*.py"); do echo "# $$f"; cat $$f; echo; echo; done
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: bussdcc-framework
3
+ Version: 0.1.0
4
+ Summary: bussdcc-framework
5
+ Author-email: "Joshua B. Bussdieker" <jbussdieker@gmail.com>
6
+ Maintainer-email: "Joshua B. Bussdieker" <jbussdieker@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/jbussdieker/bussdcc-framework
9
+ Project-URL: Documentation, https://github.com/jbussdieker/bussdcc-framework/blob/main/README.md
10
+ Project-URL: Repository, https://github.com/jbussdieker/bussdcc-framework
11
+ Project-URL: Issues, https://github.com/jbussdieker/bussdcc-framework/issues
12
+ Project-URL: Changelog, https://github.com/jbussdieker/bussdcc-framework/blob/main/CHANGELOG.md
13
+ Keywords: framework
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Development Status :: 2 - Pre-Alpha
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Environment :: Console
19
+ Classifier: Intended Audience :: Developers
20
+ Classifier: Natural Language :: English
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: bussdcc==0.23.0
26
+ Requires-Dist: flask>=3.1
27
+ Requires-Dist: flask-socketio>=5.6
28
+ Requires-Dist: bootstrap-flask==2.5.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: types-Flask-SocketIO; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # BussDCC Framework
@@ -0,0 +1 @@
1
+ # BussDCC Framework
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "bussdcc-framework"
3
+ dynamic = [
4
+ "version",
5
+ ]
6
+ dependencies = [
7
+ "bussdcc==0.23.0",
8
+ "flask>=3.1",
9
+ "flask-socketio>=5.6",
10
+ "bootstrap-flask==2.5.0",
11
+ ]
12
+ description = "bussdcc-framework"
13
+ readme = "README.md"
14
+ requires-python = ">=3.11"
15
+ authors = [
16
+ { name = "Joshua B. Bussdieker", email = "jbussdieker@gmail.com" },
17
+ ]
18
+ maintainers = [
19
+ { name = "Joshua B. Bussdieker", email = "jbussdieker@gmail.com" },
20
+ ]
21
+ classifiers = [
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Development Status :: 2 - Pre-Alpha",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Operating System :: OS Independent",
26
+ "Environment :: Console",
27
+ "Intended Audience :: Developers",
28
+ "Natural Language :: English",
29
+ "Typing :: Typed",
30
+ ]
31
+ keywords = [
32
+ "framework",
33
+ ]
34
+ license = "MIT"
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "types-Flask-SocketIO",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/jbussdieker/bussdcc-framework"
43
+ Documentation = "https://github.com/jbussdieker/bussdcc-framework/blob/main/README.md"
44
+ Repository = "https://github.com/jbussdieker/bussdcc-framework"
45
+ Issues = "https://github.com/jbussdieker/bussdcc-framework/issues"
46
+ Changelog = "https://github.com/jbussdieker/bussdcc-framework/blob/main/CHANGELOG.md"
47
+
48
+ [build-system]
49
+ requires = [
50
+ "setuptools",
51
+ "setuptools-scm",
52
+ ]
53
+ build-backend = "setuptools.build_meta"
54
+
55
+ [tool.setuptools_scm]
56
+ fallback_version = "0.0.0"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ from .version import __version__
2
+
3
+ __all__ = [
4
+ "__version__",
5
+ ]
@@ -0,0 +1,4 @@
1
+ from .web import *
2
+ from .system import *
3
+ from .framework import *
4
+ from .sink import *
@@ -0,0 +1,19 @@
1
+ from typing import Optional
2
+ from dataclasses import dataclass
3
+
4
+ from bussdcc.events import EventSchema
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class FrameworkBooted(EventSchema):
9
+ name = "framework.booted"
10
+
11
+ version: str
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class FrameworkShuttingDown(EventSchema):
16
+ name = "framework.shutting_down"
17
+
18
+ version: str
19
+ reason: Optional[str]
@@ -0,0 +1,9 @@
1
+ from typing import Optional
2
+ from dataclasses import dataclass
3
+
4
+ from bussdcc.events import EventSchema
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class SinkHandlerError(EventSchema):
9
+ name = "sink.handler.error"
@@ -0,0 +1,13 @@
1
+ from typing import Optional
2
+ from dataclasses import dataclass
3
+
4
+ from bussdcc.events import EventSchema
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class SystemIdentityEvent(EventSchema):
9
+ name = "system.identity"
10
+
11
+ hostname: str
12
+ model: Optional[str]
13
+ serial: Optional[str]
@@ -0,0 +1,11 @@
1
+ from dataclasses import dataclass
2
+
3
+ from bussdcc.events import EventSchema
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class WebInterfaceStarted(EventSchema):
8
+ name = "interface.web.started"
9
+
10
+ host: str
11
+ port: int
@@ -0,0 +1,3 @@
1
+ from .web import WebInterface
2
+
3
+ __all__ = ["WebInterface"]
@@ -0,0 +1,5 @@
1
+ from .interface import WebInterface
2
+
3
+ __all__ = [
4
+ "WebInterface",
5
+ ]
@@ -0,0 +1,9 @@
1
+ from flask import Flask
2
+ from flask_socketio import SocketIO
3
+
4
+ from bussdcc.context import ContextProtocol
5
+
6
+
7
+ class FlaskApp(Flask):
8
+ ctx: ContextProtocol
9
+ socketio: SocketIO
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+
3
+ from flask import render_template
4
+ from flask_socketio import SocketIO
5
+ from flask_bootstrap import Bootstrap5 # type: ignore[import-untyped]
6
+
7
+ from bussdcc.context import ContextProtocol
8
+
9
+ from .base import FlaskApp
10
+
11
+
12
+ def create_app(ctx: ContextProtocol) -> FlaskApp:
13
+ app = FlaskApp(__name__)
14
+ Bootstrap5(app)
15
+ socketio = SocketIO(app, cors_allowed_origins="*", async_mode="threading")
16
+
17
+ app.ctx = ctx
18
+ app.socketio = socketio
19
+
20
+ @app.context_processor
21
+ def get_context() -> dict[str, Any]:
22
+ app_name = ctx.state.get("app.name")
23
+ app_version = ctx.state.get("app.version")
24
+ system_identity = ctx.state.get("system.identity")
25
+ runtime_version = ctx.state.get("runtime.version")
26
+
27
+ return dict(
28
+ app_name=app_name,
29
+ app_version=app_version,
30
+ system_identity=system_identity,
31
+ runtime_version=runtime_version,
32
+ )
33
+
34
+ @app.route("/")
35
+ def home() -> str:
36
+ return render_template("home.html")
37
+
38
+ return app
@@ -0,0 +1,53 @@
1
+ import threading
2
+ from abc import abstractmethod
3
+
4
+ from bussdcc.process import Process
5
+ from bussdcc.context import ContextProtocol
6
+ from bussdcc.event import Event
7
+
8
+ from bussdcc_framework import events
9
+
10
+ from werkzeug.serving import make_server
11
+
12
+ from .factory import create_app
13
+
14
+
15
+ class WebInterface(Process):
16
+ name = "web"
17
+
18
+ def __init__(self, host: str, port: int) -> None:
19
+ self._thread: threading.Thread | None = None
20
+ self.host = host
21
+ self.port = port
22
+
23
+ @abstractmethod
24
+ def register_routes(self, ctx: ContextProtocol) -> None: ...
25
+
26
+ def start(self, ctx: ContextProtocol) -> None:
27
+ self.app = create_app(ctx)
28
+ self.socketio = self.app.socketio
29
+ self.register_routes(ctx)
30
+ self._thread = threading.Thread(
31
+ target=self._run,
32
+ name=self.name,
33
+ daemon=True,
34
+ )
35
+ self._thread.start()
36
+ ctx.emit(events.WebInterfaceStarted(host=self.host, port=self.port))
37
+
38
+ def _run(self) -> None:
39
+ self._server = make_server(
40
+ host=self.host,
41
+ port=self.port,
42
+ app=self.app,
43
+ threaded=True,
44
+ )
45
+
46
+ self._server.serve_forever()
47
+
48
+ def stop(self, ctx: ContextProtocol) -> None:
49
+ if hasattr(self, "_server"):
50
+ self._server.shutdown()
51
+
52
+ if self._thread:
53
+ self._thread.join(timeout=5)
@@ -0,0 +1,80 @@
1
+ {% from 'bootstrap5/utils.html' import render_messages %}
2
+ <!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ {% block head %}
6
+ <title>System</title>
7
+ <meta charset="utf-8">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
9
+ {% block styles %}
10
+ {{ bootstrap.load_css() }}
11
+ {% endblock %}
12
+ {% block scripts %}
13
+ {{ bootstrap.load_js() }}
14
+ <script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.8.1/socket.io.min.js"></script>
15
+ <script src="//cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
16
+ <script src="//cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
17
+ <script type="text/javascript" charset="utf-8">
18
+ // Create a single global socket connection
19
+ var socket = io({
20
+ // optional: configure auto-reconnect
21
+ reconnection: true,
22
+ reconnectionAttempts: Infinity, // keep trying forever
23
+ reconnectionDelay: 1000, // start with 1s delay
24
+ reconnectionDelayMax: 5000 // max delay between attempts
25
+ });
26
+ socket.on('connect', function() {
27
+ socket.emit('connected', { data: 'connected!' });
28
+ console.log("✅ Connected to socket, id:", socket.id);
29
+ });
30
+ socket.on('disconnect', function(reason) {
31
+ console.warn("⚠️ Socket disconnected:", reason);
32
+ });
33
+ socket.on('reconnect_attempt', function(attempt) {
34
+ console.log(`🔄 Attempting to reconnect (#${attempt})...`);
35
+ });
36
+ socket.on('reconnect', function(attempt) {
37
+ console.log(`✅ Reconnected to socket after ${attempt} attempts, id: ${socket.id}`);
38
+ socket.emit('connected', { data: 'reconnected!' });
39
+ });
40
+ socket.on('reconnect_error', function(error) {
41
+ console.error("❌ Reconnection error:", error);
42
+ });
43
+ socket.on('reconnect_failed', function() {
44
+ console.error("❌ Could not reconnect to socket after multiple attempts");
45
+ });
46
+ </script>
47
+ {% endblock %}
48
+ {% endblock %}
49
+ </head>
50
+ <body class="bg-light">
51
+ <nav class="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
52
+ <div class="container">
53
+ <a class="navbar-brand" href="{{ url_for("home") }}">System</a>
54
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
55
+ <span class="navbar-toggler-icon"></span>
56
+ </button>
57
+ <div class="collapse navbar-collapse" id="navbarSupportedContent">
58
+ <ul class="navbar-nav me-auto mb-2 mb-lg-0">
59
+ <li class="nav-item">
60
+ <a class="nav-link {{ "active" if request.blueprint == None }}" aria-current="page" href="{{ url_for("home") }}">Home</a>
61
+ </li>
62
+ </ul>
63
+ </div>
64
+ </div>
65
+ </nav>
66
+
67
+ {% block container %}
68
+ <div class="container mt-2">
69
+ {{ render_messages() }}
70
+ {% block content %}{% endblock %}
71
+ </div>
72
+ {% endblock %}
73
+
74
+ <footer class="mt-4">
75
+ <p class="text-center text-body-secondary">
76
+ system-health {{ app_version }} • bussdcc {{ runtime_version }}
77
+ </p>
78
+ </footer>
79
+ </body>
80
+ </html>
@@ -0,0 +1,21 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="row">
5
+ <div class="col">
6
+ <div class="card mb-3">
7
+ <div class="card-header">Host Information</div>
8
+ <div class="card-body p-2">
9
+ <dl class="mb-0">
10
+ <dt>Host Name</dt>
11
+ <dd>{{ system_identity.hostname }}</dd>
12
+ <dt>Model</dt>
13
+ <dd>{{ system_identity.model or "—"}}</dd>
14
+ <dt>Serial</dt>
15
+ <dd>{{ system_identity.serial or "—" }}</dd>
16
+ </dl>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ {% endblock %}
@@ -0,0 +1,5 @@
1
+ from .system_identity import SystemIdentityProcess
2
+
3
+ __all__ = [
4
+ "SystemIdentityProcess",
5
+ ]
@@ -0,0 +1,22 @@
1
+ from bussdcc.process import Process
2
+ from bussdcc.context import ContextProtocol
3
+ from bussdcc.event import Event
4
+ from bussdcc import events as runtime_events
5
+
6
+ from bussdcc_framework import events
7
+
8
+
9
+ class SystemIdentityProcess(Process):
10
+ name = "system_identity"
11
+
12
+ def handle_event(self, ctx: ContextProtocol, evt: Event[object]) -> None:
13
+ payload = evt.payload
14
+
15
+ if isinstance(payload, runtime_events.RuntimeBooted):
16
+ ctx.state.set("runtime.version", payload.version)
17
+
18
+ elif isinstance(payload, events.FrameworkBooted):
19
+ ctx.state.set("framework.version", payload.version)
20
+
21
+ elif isinstance(payload, events.SystemIdentityEvent):
22
+ ctx.state.set("system.identity", payload)
@@ -0,0 +1,5 @@
1
+ from .runtime import Runtime
2
+
3
+ __all__ = [
4
+ "Runtime",
5
+ ]
@@ -0,0 +1,39 @@
1
+ import traceback
2
+ from typing import Optional
3
+
4
+ from bussdcc.runtime import SignalRuntime
5
+ from bussdcc.event import Event
6
+
7
+ from bussdcc_framework import events, __version__ as version
8
+
9
+ from .sink import EventSinkProtocol
10
+
11
+
12
+ class Runtime(SignalRuntime):
13
+ def __init__(self) -> None:
14
+ super().__init__()
15
+ self._sinks: list[EventSinkProtocol] = []
16
+
17
+ def boot(self) -> None:
18
+ for sink in self._sinks:
19
+ sink.start(self.ctx)
20
+ super().boot()
21
+ self.ctx.emit(events.FrameworkBooted(version=version))
22
+
23
+ def _dispatch(self, evt: Event[object]) -> None:
24
+ for sink in self._sinks:
25
+ try:
26
+ sink.handle(evt)
27
+ except Exception as e:
28
+ print(repr(e))
29
+ print(traceback.format_exc())
30
+ # TODO: self.ctx.emit(events.SinkHandlerError())
31
+
32
+ def shutdown(self, reason: Optional[str] = None) -> None:
33
+ self.ctx.emit(events.FrameworkShuttingDown(version=version, reason=reason))
34
+ super().shutdown(reason)
35
+
36
+ def add_sink(self, sink: EventSinkProtocol) -> None:
37
+ if self._booted:
38
+ raise RuntimeError("Cannot add sinks after boot")
39
+ self._sinks.append(sink)
@@ -0,0 +1,9 @@
1
+ from .protocol import EventSinkProtocol
2
+ from .console import ConsoleSink
3
+ from .jsonl import JsonlSink
4
+
5
+ __all__ = [
6
+ "EventSinkProtocol",
7
+ "ConsoleSink",
8
+ "JsonlSink",
9
+ ]
@@ -0,0 +1,44 @@
1
+ from typing import Any
2
+ import json
3
+
4
+ from bussdcc.event import Event
5
+ from bussdcc.events import EventSchema
6
+ from bussdcc.context import ContextProtocol
7
+
8
+ from .protocol import EventSinkProtocol
9
+
10
+
11
+ class ConsoleSink(EventSinkProtocol):
12
+ def start(self, ctx: ContextProtocol) -> None:
13
+ pass
14
+
15
+ def stop(self) -> None:
16
+ pass
17
+
18
+ def handle(self, evt: Event[object]) -> None:
19
+ if not evt.time:
20
+ return
21
+
22
+ payload = evt.payload
23
+ if not isinstance(payload, EventSchema):
24
+ print("Invalid event", payload)
25
+ return
26
+
27
+ record = {
28
+ "time": evt.time.isoformat(),
29
+ "name": payload.name,
30
+ "data": self.transform(evt),
31
+ }
32
+
33
+ line = json.dumps(record, separators=(",", ":"))
34
+ print(line)
35
+
36
+ def transform(self, evt: Event[object]) -> Any:
37
+ """
38
+ Override to customize JSON output.
39
+
40
+ Must return a JSON-serializable dict.
41
+ Should not mutate evt.
42
+ """
43
+ if hasattr(evt.payload, "to_dict"):
44
+ return evt.payload.to_dict()
@@ -0,0 +1,93 @@
1
+ import json
2
+ import threading
3
+ from pathlib import Path
4
+ from datetime import datetime, timezone, timedelta
5
+ from typing import Any, TextIO
6
+
7
+ from bussdcc.event import Event
8
+ from bussdcc.events import EventSchema
9
+ from bussdcc.context import ContextProtocol
10
+
11
+ from .protocol import EventSinkProtocol
12
+
13
+
14
+ class JsonlSink(EventSinkProtocol):
15
+ def __init__(
16
+ self,
17
+ root: str | Path,
18
+ interval: float = 600.0,
19
+ ):
20
+ self.root = Path(root)
21
+ self.interval = timedelta(seconds=interval)
22
+
23
+ self._file: TextIO | None = None
24
+ self._current_segment_start: datetime | None = None
25
+ self._lock = threading.Lock()
26
+
27
+ def start(self, ctx: ContextProtocol) -> None:
28
+ self.root.mkdir(parents=True, exist_ok=True)
29
+
30
+ def stop(self) -> None:
31
+ if self._file:
32
+ self._file.close()
33
+ self._file = None
34
+
35
+ def handle(self, evt: Event[object]) -> None:
36
+ if not evt.time:
37
+ return
38
+
39
+ payload = evt.payload
40
+ if not isinstance(payload, EventSchema):
41
+ print("Invalid event", payload)
42
+ return
43
+
44
+ segment_start = self._segment_start(evt.time)
45
+
46
+ with self._lock:
47
+ if segment_start != self._current_segment_start:
48
+ self._rotate(segment_start)
49
+
50
+ record = {
51
+ "time": evt.time.isoformat(),
52
+ "name": payload.name,
53
+ "data": self.transform(evt),
54
+ }
55
+
56
+ line = json.dumps(record, separators=(",", ":"))
57
+ assert self._file is not None
58
+ self._file.write(line + "\n")
59
+
60
+ def transform(self, evt: Event[object]) -> Any:
61
+ """
62
+ Override to customize JSON output.
63
+
64
+ Must return a JSON-serializable dict.
65
+ Should not mutate evt.
66
+ """
67
+ if hasattr(evt.payload, "to_dict"):
68
+ return evt.payload.to_dict()
69
+
70
+ def _segment_start(self, dt: datetime) -> datetime:
71
+ if dt.tzinfo is None:
72
+ dt = dt.replace(tzinfo=timezone.utc)
73
+
74
+ interval_seconds = self.interval.total_seconds()
75
+
76
+ timestamp = dt.timestamp()
77
+ bucket = int(timestamp // interval_seconds) * interval_seconds
78
+
79
+ return datetime.fromtimestamp(bucket, tz=dt.tzinfo)
80
+
81
+ def _rotate(self, segment_start: datetime) -> None:
82
+ if self._file:
83
+ self._file.close()
84
+
85
+ self._current_segment_start = segment_start
86
+
87
+ day_dir = self.root / segment_start.strftime("%Y-%m-%d")
88
+ day_dir.mkdir(parents=True, exist_ok=True)
89
+
90
+ filename = segment_start.strftime("%H-%M-%S.jsonl")
91
+ path = day_dir / filename
92
+
93
+ self._file = path.open("a", buffering=1)
@@ -0,0 +1,10 @@
1
+ from typing import Protocol
2
+
3
+ from bussdcc.context.protocol import ContextProtocol
4
+ from bussdcc.event import Event
5
+
6
+
7
+ class EventSinkProtocol(Protocol):
8
+ def start(self, ctx: ContextProtocol) -> None: ...
9
+ def stop(self) -> None: ...
10
+ def handle(self, evt: Event[object]) -> None: ...
@@ -0,0 +1,5 @@
1
+ from .system_identity import SystemIdentityService
2
+
3
+ __all__ = [
4
+ "SystemIdentityService",
5
+ ]
@@ -0,0 +1,35 @@
1
+ import socket
2
+ from pathlib import Path
3
+
4
+ from bussdcc.service import Service
5
+ from bussdcc.context import ContextProtocol
6
+
7
+ from bussdcc_framework import events
8
+
9
+
10
+ class SystemIdentityService(Service):
11
+ name = "system_identity"
12
+
13
+ def start(self, ctx: ContextProtocol) -> None:
14
+ hostname = socket.gethostname()
15
+ model = self._read("/proc/device-tree/model")
16
+ serial = self._cpuinfo_field("Serial")
17
+
18
+ ctx.emit(
19
+ events.SystemIdentityEvent(hostname=hostname, model=model, serial=serial)
20
+ )
21
+
22
+ def _read(self, path: str) -> str | None:
23
+ try:
24
+ return Path(path).read_text().strip("\x00\n")
25
+ except Exception:
26
+ return None
27
+
28
+ def _cpuinfo_field(self, key: str) -> str | None:
29
+ try:
30
+ for line in Path("/proc/cpuinfo").read_text().splitlines():
31
+ if line.startswith(key):
32
+ return line.split(":", 1)[1].strip()
33
+ except Exception:
34
+ pass
35
+ return None
@@ -0,0 +1,14 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ PACKAGE_NAME = "bussdcc-framework"
4
+
5
+
6
+ def get_version() -> str:
7
+ try:
8
+ return version(PACKAGE_NAME)
9
+ except PackageNotFoundError:
10
+ return "0.0.0"
11
+
12
+
13
+ __name__ = PACKAGE_NAME
14
+ __version__ = get_version()
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: bussdcc-framework
3
+ Version: 0.1.0
4
+ Summary: bussdcc-framework
5
+ Author-email: "Joshua B. Bussdieker" <jbussdieker@gmail.com>
6
+ Maintainer-email: "Joshua B. Bussdieker" <jbussdieker@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/jbussdieker/bussdcc-framework
9
+ Project-URL: Documentation, https://github.com/jbussdieker/bussdcc-framework/blob/main/README.md
10
+ Project-URL: Repository, https://github.com/jbussdieker/bussdcc-framework
11
+ Project-URL: Issues, https://github.com/jbussdieker/bussdcc-framework/issues
12
+ Project-URL: Changelog, https://github.com/jbussdieker/bussdcc-framework/blob/main/CHANGELOG.md
13
+ Keywords: framework
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Development Status :: 2 - Pre-Alpha
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Environment :: Console
19
+ Classifier: Intended Audience :: Developers
20
+ Classifier: Natural Language :: English
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: bussdcc==0.23.0
26
+ Requires-Dist: flask>=3.1
27
+ Requires-Dist: flask-socketio>=5.6
28
+ Requires-Dist: bootstrap-flask==2.5.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: types-Flask-SocketIO; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # BussDCC Framework
@@ -0,0 +1,36 @@
1
+ .gitignore
2
+ CHANGELOG.md
3
+ LICENSE
4
+ Makefile
5
+ README.md
6
+ pyproject.toml
7
+ .github/workflows/workflow.yml
8
+ src/bussdcc_framework/__init__.py
9
+ src/bussdcc_framework/version.py
10
+ src/bussdcc_framework.egg-info/PKG-INFO
11
+ src/bussdcc_framework.egg-info/SOURCES.txt
12
+ src/bussdcc_framework.egg-info/dependency_links.txt
13
+ src/bussdcc_framework.egg-info/requires.txt
14
+ src/bussdcc_framework.egg-info/top_level.txt
15
+ src/bussdcc_framework/events/__init__.py
16
+ src/bussdcc_framework/events/framework.py
17
+ src/bussdcc_framework/events/sink.py
18
+ src/bussdcc_framework/events/system.py
19
+ src/bussdcc_framework/events/web.py
20
+ src/bussdcc_framework/interface/__init__.py
21
+ src/bussdcc_framework/interface/web/__init__.py
22
+ src/bussdcc_framework/interface/web/base.py
23
+ src/bussdcc_framework/interface/web/factory.py
24
+ src/bussdcc_framework/interface/web/interface.py
25
+ src/bussdcc_framework/interface/web/templates/base.html
26
+ src/bussdcc_framework/interface/web/templates/home.html
27
+ src/bussdcc_framework/process/__init__.py
28
+ src/bussdcc_framework/process/system_identity.py
29
+ src/bussdcc_framework/runtime/__init__.py
30
+ src/bussdcc_framework/runtime/runtime.py
31
+ src/bussdcc_framework/runtime/sink/__init__.py
32
+ src/bussdcc_framework/runtime/sink/console.py
33
+ src/bussdcc_framework/runtime/sink/jsonl.py
34
+ src/bussdcc_framework/runtime/sink/protocol.py
35
+ src/bussdcc_framework/service/__init__.py
36
+ src/bussdcc_framework/service/system_identity.py
@@ -0,0 +1,7 @@
1
+ bussdcc==0.23.0
2
+ flask>=3.1
3
+ flask-socketio>=5.6
4
+ bootstrap-flask==2.5.0
5
+
6
+ [dev]
7
+ types-Flask-SocketIO
@@ -0,0 +1 @@
1
+ bussdcc_framework