tcboard 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 (76) hide show
  1. tcboard-0.1.0/.gitignore +10 -0
  2. tcboard-0.1.0/.pre-commit-config.yaml +26 -0
  3. tcboard-0.1.0/LICENSE +21 -0
  4. tcboard-0.1.0/PKG-INFO +82 -0
  5. tcboard-0.1.0/README.md +43 -0
  6. tcboard-0.1.0/app.py +170 -0
  7. tcboard-0.1.0/conftest.py +3 -0
  8. tcboard-0.1.0/pyproject.toml +98 -0
  9. tcboard-0.1.0/tcboard/__init__.py +19 -0
  10. tcboard-0.1.0/tcboard/_version.py +24 -0
  11. tcboard-0.1.0/tcboard/alert.py +71 -0
  12. tcboard-0.1.0/tcboard/assets/favicon.ico +0 -0
  13. tcboard-0.1.0/tcboard/board.py +555 -0
  14. tcboard-0.1.0/tcboard/cli/__init__.py +0 -0
  15. tcboard-0.1.0/tcboard/cli/api.py +159 -0
  16. tcboard-0.1.0/tcboard/cli/db.py +182 -0
  17. tcboard-0.1.0/tcboard/cli/debug.py +108 -0
  18. tcboard-0.1.0/tcboard/cli/main.py +289 -0
  19. tcboard-0.1.0/tcboard/cli/remotectrl.py +167 -0
  20. tcboard-0.1.0/tcboard/cli/replay.py +139 -0
  21. tcboard-0.1.0/tcboard/cli/squoremqtt.py +239 -0
  22. tcboard-0.1.0/tcboard/cli/tptools.py +169 -0
  23. tcboard-0.1.0/tcboard/cli/util.py +75 -0
  24. tcboard-0.1.0/tcboard/cli/ws.py +476 -0
  25. tcboard-0.1.0/tcboard/courtstate.py +75 -0
  26. tcboard-0.1.0/tcboard/dbmanager.py +194 -0
  27. tcboard-0.1.0/tcboard/devinfo.py +42 -0
  28. tcboard-0.1.0/tcboard/exceptions.py +30 -0
  29. tcboard-0.1.0/tcboard/ext/__init__.py +0 -0
  30. tcboard-0.1.0/tcboard/ext/squore/__init__.py +9 -0
  31. tcboard-0.1.0/tcboard/ext/squore/devinfo.py +28 -0
  32. tcboard-0.1.0/tcboard/ext/squore/livedata.py +243 -0
  33. tcboard-0.1.0/tcboard/ext/squore/match.py +101 -0
  34. tcboard-0.1.0/tcboard/ext/squore/point.py +69 -0
  35. tcboard-0.1.0/tcboard/ext/tptools/__init__.py +0 -0
  36. tcboard-0.1.0/tcboard/ext/tptools/livedata.py +61 -0
  37. tcboard-0.1.0/tcboard/game.py +33 -0
  38. tcboard-0.1.0/tcboard/livedata.py +114 -0
  39. tcboard-0.1.0/tcboard/livestatus.py +61 -0
  40. tcboard-0.1.0/tcboard/match.py +91 -0
  41. tcboard-0.1.0/tcboard/matchslot.py +12 -0
  42. tcboard-0.1.0/tcboard/matchstate.py +241 -0
  43. tcboard-0.1.0/tcboard/modelabc.py +66 -0
  44. tcboard-0.1.0/tcboard/player.py +24 -0
  45. tcboard-0.1.0/tcboard/point.py +16 -0
  46. tcboard-0.1.0/tcboard/tournament.py +9 -0
  47. tcboard-0.1.0/tests/__init__.py +0 -0
  48. tcboard-0.1.0/tests/cli/__init__.py +0 -0
  49. tcboard-0.1.0/tests/cli/conftest.py +41 -0
  50. tcboard-0.1.0/tests/cli/test_clicontext.py +8 -0
  51. tcboard-0.1.0/tests/cli/test_main.py +8 -0
  52. tcboard-0.1.0/tests/cli/test_util.py +59 -0
  53. tcboard-0.1.0/tests/conftest.py +261 -0
  54. tcboard-0.1.0/tests/ext/__init__.py +0 -0
  55. tcboard-0.1.0/tests/ext/squore/__init__.py +0 -0
  56. tcboard-0.1.0/tests/ext/squore/conftest.py +255 -0
  57. tcboard-0.1.0/tests/ext/squore/test_devinfo.py +13 -0
  58. tcboard-0.1.0/tests/ext/squore/test_livedata.py +275 -0
  59. tcboard-0.1.0/tests/ext/squore/test_match.py +5 -0
  60. tcboard-0.1.0/tests/ext/squore/test_point.py +62 -0
  61. tcboard-0.1.0/tests/ext/tptools/__init__.py +0 -0
  62. tcboard-0.1.0/tests/ext/tptools/test_livedata.py +92 -0
  63. tcboard-0.1.0/tests/test_alert.py +107 -0
  64. tcboard-0.1.0/tests/test_board.py +74 -0
  65. tcboard-0.1.0/tests/test_courtstate.py +76 -0
  66. tcboard-0.1.0/tests/test_dbmanager.py +159 -0
  67. tcboard-0.1.0/tests/test_devinfo.py +28 -0
  68. tcboard-0.1.0/tests/test_exception.py +12 -0
  69. tcboard-0.1.0/tests/test_game.py +6 -0
  70. tcboard-0.1.0/tests/test_livedata.py +99 -0
  71. tcboard-0.1.0/tests/test_livestatus.py +33 -0
  72. tcboard-0.1.0/tests/test_match.py +47 -0
  73. tcboard-0.1.0/tests/test_matchslot.py +8 -0
  74. tcboard-0.1.0/tests/test_matchstate.py +218 -0
  75. tcboard-0.1.0/tests/test_modelabc.py +40 -0
  76. tcboard-0.1.0/watchtests.sh +23 -0
@@ -0,0 +1,10 @@
1
+ /.pytest_cache
2
+ __pycache__
3
+ /.stfolder
4
+ .*.sw?
5
+ *.py[oc]
6
+ /.stignore
7
+ /.envrc
8
+ .coverage
9
+ *.sqlite
10
+ /.direnv
@@ -0,0 +1,26 @@
1
+ # See https://pre-commit.com for more information
2
+ # See https://pre-commit.com/hooks.html for more hooks
3
+ repos:
4
+ - repo: https://github.com/pre-commit/mirrors-mypy
5
+ rev: v2.4.0
6
+ hooks:
7
+ - id: mypy
8
+ additional_dependencies:
9
+ - pydantic
10
+ - click
11
+ - click_extra (>= 9.4.1)
12
+ - fastapi
13
+ - starlette
14
+ - pytest
15
+ - pytest-asyncio
16
+ - tptools
17
+ - click-async-plugins (>= 0.7.4)
18
+ - repo: https://github.com/astral-sh/ruff-pre-commit
19
+ rev: v0.16.10
20
+ hooks:
21
+ - id: ruff-check
22
+ args: [--show-fixes]
23
+ - id: ruff-format
24
+ args: [--diff]
25
+
26
+ fail_fast: true
tcboard-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 martin f. krafft
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.
tcboard-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.5
2
+ Name: tcboard
3
+ Version: 0.1.0
4
+ Summary: Server to collect and provide tournament data
5
+ Author-email: "martin f. krafft" <tcboard@pobox.madduck.net>
6
+ License-File: LICENSE
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.13
11
+ Requires-Dist: aiomqtt
12
+ Requires-Dist: aiosqlite
13
+ Requires-Dist: aiostream
14
+ Requires-Dist: click
15
+ Requires-Dist: click-async-plugins>=0.7.4
16
+ Requires-Dist: click-extra>=9.4.1
17
+ Requires-Dist: fastapi
18
+ Requires-Dist: pydantic
19
+ Requires-Dist: tabulate
20
+ Requires-Dist: tptools
21
+ Requires-Dist: uvicorn
22
+ Requires-Dist: uvloop; sys_platform == 'linux'
23
+ Provides-Extra: dev
24
+ Requires-Dist: click-extra[pytest]; extra == 'dev'
25
+ Requires-Dist: coverage; extra == 'dev'
26
+ Requires-Dist: fastapi[standard]; extra == 'dev'
27
+ Requires-Dist: ipdb; extra == 'dev'
28
+ Requires-Dist: mypy; extra == 'dev'
29
+ Requires-Dist: pre-commit; extra == 'dev'
30
+ Requires-Dist: pytest; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio; extra == 'dev'
32
+ Requires-Dist: pytest-cov; extra == 'dev'
33
+ Requires-Dist: pytest-mock; extra == 'dev'
34
+ Requires-Dist: pytest-ruff; extra == 'dev'
35
+ Requires-Dist: ruff; extra == 'dev'
36
+ Requires-Dist: types-python-dateutil; extra == 'dev'
37
+ Requires-Dist: types-tabulate; extra == 'dev'
38
+ Description-Content-Type: text/markdown
39
+
40
+ [![Style and unit tests badge](https://github.com/madduck/tcboard/actions/workflows/0-testing.yml/badge.svg)](https://github.com/madduck/tcboard/actions/workflows/0-testing.yml)
41
+
42
+ # TCBoard — Server to collect and provide tournament data
43
+
44
+ TODO
45
+
46
+ ## Contributing
47
+
48
+ To contribute, please ensure you have the appropriate dependencies installed:
49
+
50
+ ```
51
+ pip install -e .[dev]
52
+ ```
53
+
54
+ and then install the Git pre-commit hooks that ensure that any commits conform
55
+ with the coding-style used by this project.
56
+
57
+ ```
58
+ pre-commit install
59
+ ```
60
+
61
+ All code (except for the CLI) is 100% test-covered, and all
62
+ contributions are expected to keep this up. Use `pytest` to run the test suite.
63
+
64
+ Note that all code is typed, and typing is part of test-coverage.
65
+
66
+ ## Known Problems
67
+
68
+ None at this point (but there will be some).
69
+
70
+ ## TODO
71
+
72
+ In addition to various comments including the word "TODO" in the code, there are
73
+ a few things left to do:
74
+
75
+ * Test the CLI
76
+ * Test the HTTP APIs
77
+
78
+ ## Legalese
79
+
80
+ `tcboard` is © 2024–6 martin f. krafft <tcboard@pobox.madduck.net>.
81
+
82
+ It is released under the terms of the MIT Licence.
@@ -0,0 +1,43 @@
1
+ [![Style and unit tests badge](https://github.com/madduck/tcboard/actions/workflows/0-testing.yml/badge.svg)](https://github.com/madduck/tcboard/actions/workflows/0-testing.yml)
2
+
3
+ # TCBoard — Server to collect and provide tournament data
4
+
5
+ TODO
6
+
7
+ ## Contributing
8
+
9
+ To contribute, please ensure you have the appropriate dependencies installed:
10
+
11
+ ```
12
+ pip install -e .[dev]
13
+ ```
14
+
15
+ and then install the Git pre-commit hooks that ensure that any commits conform
16
+ with the coding-style used by this project.
17
+
18
+ ```
19
+ pre-commit install
20
+ ```
21
+
22
+ All code (except for the CLI) is 100% test-covered, and all
23
+ contributions are expected to keep this up. Use `pytest` to run the test suite.
24
+
25
+ Note that all code is typed, and typing is part of test-coverage.
26
+
27
+ ## Known Problems
28
+
29
+ None at this point (but there will be some).
30
+
31
+ ## TODO
32
+
33
+ In addition to various comments including the word "TODO" in the code, there are
34
+ a few things left to do:
35
+
36
+ * Test the CLI
37
+ * Test the HTTP APIs
38
+
39
+ ## Legalese
40
+
41
+ `tcboard` is © 2024–6 martin f. krafft <tcboard@pobox.madduck.net>.
42
+
43
+ It is released under the terms of the MIT Licence.
tcboard-0.1.0/app.py ADDED
@@ -0,0 +1,170 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import pathlib
5
+ import sys
6
+ import warnings
7
+ from collections.abc import AsyncGenerator
8
+ from contextlib import AsyncExitStack, asynccontextmanager
9
+ from functools import partial
10
+ from typing import Literal
11
+
12
+ from click_async_plugins import (
13
+ ITC,
14
+ PluginFactory,
15
+ create_plugin_task,
16
+ setup_plugins,
17
+ )
18
+ from fastapi import FastAPI
19
+ from httpx2 import URL
20
+ from tptools.util import is_truish, silence_logger
21
+
22
+ from tcboard.board import TCBoard
23
+ from tcboard.cli.api import configure_api
24
+ from tcboard.cli.db import database_backend
25
+ from tcboard.cli.debug import debug_key_press_handler
26
+ from tcboard.cli.main import (
27
+ make_app,
28
+ on_board_update,
29
+ process_squore_livedata,
30
+ receive_tournament,
31
+ )
32
+ from tcboard.cli.remotectrl import remote_control_publisher
33
+ from tcboard.cli.squoremqtt import listen_for_mqtt_messages
34
+ from tcboard.cli.tptools import setup_for_tptools
35
+ from tcboard.cli.util import CliContext
36
+ from tcboard.cli.ws import configure_for_websockets
37
+
38
+ logging.getLogger().setLevel(logging.DEBUG)
39
+
40
+ if not sys.warnoptions:
41
+ warnings.simplefilter("default")
42
+ logging.captureWarnings(True)
43
+
44
+
45
+ THIRD_PARTY_LOG_LEVELS = [
46
+ ("asyncio", logging.INFO),
47
+ ("aiosqlite", logging.INFO),
48
+ ("watchfiles", logging.WARNING),
49
+ ("uvicorn", logging.INFO),
50
+ ("uvicorn.error", logging.WARNING),
51
+ ("click_async_plugins", logging.DEBUG),
52
+ ("httpcore2", logging.INFO),
53
+ ("httpx2", logging.INFO),
54
+ ]
55
+
56
+ if is_truish(os.getenv("LESS_DEBUG", os.getenv("LESSDEBUG"))):
57
+ LOG_LEVELS = [
58
+ (__name__, logging.INFO),
59
+ ("tptools", logging.INFO),
60
+ ("tcboard.cli", logging.INFO),
61
+ ("tcboard.dbmanager", logging.INFO),
62
+ ("tcboard.board", logging.INFO),
63
+ ]
64
+ else:
65
+ LOG_LEVELS = []
66
+
67
+ for name, level in LOG_LEVELS + THIRD_PARTY_LOG_LEVELS:
68
+ silence_logger(name, level=level)
69
+
70
+ logger = logging.getLogger(__name__)
71
+
72
+
73
+ MQTT_BROKER = os.getenv("MQTT_BROKER", "127.0.0.1:1883")
74
+
75
+ DB_FILE: pathlib.Path | Literal[":memory:"] | None
76
+ if (envfile := os.getenv("DB", os.getenv("DB_FILE"))) is None:
77
+ DB_FILE = pathlib.Path(__file__).parent / "db.sqlite"
78
+
79
+ elif envfile == ":memory:":
80
+ DB_FILE = None
81
+
82
+ else:
83
+ DB_FILE = pathlib.Path(envfile)
84
+
85
+ database_backend = partial(database_backend, file=DB_FILE)
86
+
87
+ debug_match_ids: list[str] = []
88
+ if dmis := os.getenv("DEBUG_MATCH_IDS", ""):
89
+ debug_match_ids = dmis.split(",")
90
+
91
+
92
+ @asynccontextmanager
93
+ async def app_lifespan(api: FastAPI) -> AsyncGenerator[None]:
94
+ board = TCBoard(debug_match_ids=debug_match_ids)
95
+ itc = ITC()
96
+ itc.set("board", board)
97
+
98
+ clictx = CliContext(api=api, itc=itc)
99
+
100
+ board.register_update_function(partial(on_board_update, itc=itc))
101
+
102
+ broker = MQTT_BROKER.split(":")
103
+ port: int | str
104
+ if len(broker) == 3:
105
+ proto, host, port = broker
106
+ assert proto == "mqtt"
107
+ host = host[2:]
108
+
109
+ elif len(broker) == 2:
110
+ host, port = broker
111
+
112
+ else:
113
+ port = 1883
114
+ host = broker[0]
115
+
116
+ squoremqtt_client = partial(
117
+ listen_for_mqtt_messages, server=host, port=int(port), ignore_retained=True
118
+ )
119
+ remote_control = partial(remote_control_publisher)
120
+
121
+ BOOTSTRAP_URL = os.getenv("BOOTSTRAP_URL")
122
+ tptools = partial(
123
+ setup_for_tptools,
124
+ load_from_url=None if BOOTSTRAP_URL is None else URL(BOOTSTRAP_URL),
125
+ )
126
+
127
+ factories: list[PluginFactory] = [
128
+ debug_key_press_handler,
129
+ database_backend,
130
+ tptools,
131
+ receive_tournament,
132
+ process_squore_livedata,
133
+ squoremqtt_client,
134
+ remote_control,
135
+ configure_for_websockets,
136
+ configure_api,
137
+ ]
138
+
139
+ try:
140
+ async with AsyncExitStack() as stack:
141
+ tasks = await setup_plugins(factories, stack=stack, clictx=clictx)
142
+
143
+ try:
144
+ async with asyncio.TaskGroup() as tg:
145
+ plugin_task = partial(
146
+ create_plugin_task, create_task_fn=tg.create_task
147
+ )
148
+ for task in tasks:
149
+ plugin_task(task)
150
+ logger.debug("Tasks:")
151
+ for t in tg._tasks:
152
+ logger.debug(f" {t}")
153
+ yield
154
+ raise asyncio.CancelledError
155
+
156
+ except asyncio.CancelledError:
157
+ logger.info("Exiting…")
158
+
159
+ except* RuntimeError:
160
+ logger.exception("A runtime error occurred")
161
+
162
+ except* Exception as exc:
163
+ import ipdb
164
+
165
+ logger.exception("")
166
+ ipdb.set_trace() # noqa: E402 E702 I001 # fmt: skip
167
+ _ = exc
168
+
169
+
170
+ app = make_app(lifespan=app_lifespan)
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logging.getLogger("asyncio").setLevel(logging.WARN)
@@ -0,0 +1,98 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.build.hooks.vcs]
6
+ version-file = "tcboard/_version.py"
7
+
8
+ [tool.hatch.version]
9
+ source = "vcs"
10
+
11
+ [tool.hatch.build.targets.sdist]
12
+ exclude = [".direnv", ".gitignore", ".github"]
13
+
14
+ [project]
15
+ name = "tcboard"
16
+ dynamic = ["version"]
17
+ authors = [{ name = "martin f. krafft", email = "tcboard@pobox.madduck.net" }]
18
+ description = "Server to collect and provide tournament data"
19
+ readme = "README.md"
20
+ requires-python = ">=3.13"
21
+ classifiers = [
22
+ "Programming Language :: Python :: 3",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Operating System :: OS Independent",
25
+ ]
26
+
27
+ dependencies = [
28
+ "tptools",
29
+ "pydantic",
30
+ "uvloop; sys_platform == 'linux'",
31
+ "click",
32
+ "click-extra (>= 9.4.1)",
33
+ "click-async-plugins (>= 0.7.4)",
34
+ "fastapi",
35
+ "uvicorn",
36
+ "aiomqtt",
37
+ "aiosqlite",
38
+ "aiostream",
39
+ "tabulate",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "fastapi[standard]",
45
+ "pytest",
46
+ "pytest-mock",
47
+ "pytest-asyncio",
48
+ "pytest-ruff",
49
+ "click-extra[pytest]",
50
+ "ipdb",
51
+ "mypy",
52
+ "ruff",
53
+ "pre-commit",
54
+ "coverage",
55
+ "pytest-cov",
56
+ "types-tabulate",
57
+ "types-python-dateutil",
58
+ ]
59
+
60
+ [project.scripts]
61
+ tcboard = "tcboard.cli.main:tcboard"
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests", "integration"]
65
+ addopts = """
66
+ -ra
67
+ --pdbcls=IPython.terminal.debugger:TerminalPdb
68
+ --cov=tcboard --cov-report term-missing:skip-covered --no-cov-on-fail
69
+ --ruff --ruff-format --ruff-config 'lint.ignore = ["I001", "F401", "F841"]'
70
+ """
71
+ asyncio_default_fixture_loop_scope = "function"
72
+ log_level = "WARNING"
73
+ log_format = "%(name)s %(levelname)s %(message)s"
74
+
75
+ [tool.ruff]
76
+ line-length = 88
77
+
78
+ [tool.ruff.lint]
79
+ select = ["B", "C", "E", "F", "I", "W", "B9"]
80
+
81
+ [tool.mypy]
82
+ strict = true
83
+ warn_unreachable = true
84
+
85
+ [[tool.mypy.overrides]]
86
+ module = "ipdb"
87
+ ignore_missing_imports = true
88
+
89
+ [tool.coverage.report]
90
+ exclude_also = [
91
+ 'def __repr__',
92
+ 'if TYPE_CHECKING:',
93
+ 'class .*\bProtocol\):',
94
+ '@overload',
95
+ '@(abc\.)?abstractmethod',
96
+ 'raise NotImplementedError',
97
+ 'raise AssertionError',
98
+ ]
@@ -0,0 +1,19 @@
1
+ from .board import TCBoard
2
+ from .exceptions import TCBoardException
3
+ from .match import TCMatch
4
+ from .tournament import TCTournament
5
+
6
+ VERSION: str | None
7
+ try:
8
+ from ._version import version as VERSION
9
+
10
+ except ImportError: # pragma: nocover
11
+ Version = None
12
+
13
+ __all__ = [
14
+ "TCBoard",
15
+ "TCBoardException",
16
+ "TCMatch",
17
+ "TCTournament",
18
+ "VERSION",
19
+ ]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,71 @@
1
+ # needed < 3.14 so that annotations aren't evaluated
2
+ from __future__ import annotations
3
+
4
+ import traceback
5
+ import uuid
6
+ from datetime import datetime
7
+ from types import TracebackType
8
+ from typing import Any, Self, cast
9
+
10
+ from pydantic import (
11
+ UUID4,
12
+ BaseModel,
13
+ Field,
14
+ )
15
+
16
+
17
+ class Alert(BaseModel):
18
+ text: str
19
+ id: UUID4 = Field(default_factory=uuid.uuid4)
20
+ detail: str | None = None
21
+ matchid: str | None = None
22
+ deviceid: str | None = None
23
+ timestamp: datetime | None = None
24
+ traceback: str | None = Field(default=None, repr=False)
25
+ cleared: datetime | None = None
26
+
27
+ def model_post_init(self, _: Any) -> None:
28
+ if self.timestamp is None:
29
+ self.timestamp = datetime.now()
30
+
31
+ @classmethod
32
+ def from_exception(cls, exc: Exception, **kwargs: Any) -> Self:
33
+ text = kwargs.pop("text", None) or str(exc)
34
+ return cls(text=text, traceback=cls.format_tb(exc.__traceback__), **kwargs)
35
+
36
+ @staticmethod
37
+ def format_tb(tback: TracebackType | None) -> str | None:
38
+ if tback is None:
39
+ return None
40
+ return "\n".join(traceback.format_tb(tback))
41
+
42
+ def clear(self, timestamp: datetime | None = None) -> None:
43
+ self.cleared = timestamp if timestamp is not None else datetime.now()
44
+
45
+ def __hash__(self) -> int:
46
+ return hash((self.text, self.matchid, self.cleared))
47
+
48
+ def __eq__(self, other: object) -> bool:
49
+ try:
50
+ other = cast(Alert, other)
51
+ return (self.text, self.matchid, self.cleared) == (
52
+ other.text,
53
+ other.matchid,
54
+ other.cleared,
55
+ )
56
+ except AttributeError:
57
+ return False
58
+
59
+ def __str__(self) -> str:
60
+ text = self.text
61
+ if self.detail:
62
+ text = f"{text}: {self.detail}"
63
+
64
+ ret = [text]
65
+ if self.matchid is not None:
66
+ ret.append(f"matchid={self.matchid}")
67
+
68
+ if self.deviceid is not None:
69
+ ret.append(f"deviceid={self.deviceid}")
70
+
71
+ return ", ".join(ret)
Binary file