carconnectivity-plugin-database 0.1a1__py3-none-any.whl
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.
Potentially problematic release.
This version of carconnectivity-plugin-database might be problematic. Click here for more details.
- carconnectivity_database/__init__.py +0 -0
- carconnectivity_database/carconnectivity_database_base.py +26 -0
- carconnectivity_plugin_database-0.1a1.dist-info/METADATA +94 -0
- carconnectivity_plugin_database-0.1a1.dist-info/RECORD +26 -0
- carconnectivity_plugin_database-0.1a1.dist-info/WHEEL +5 -0
- carconnectivity_plugin_database-0.1a1.dist-info/entry_points.txt +2 -0
- carconnectivity_plugin_database-0.1a1.dist-info/licenses/LICENSE +21 -0
- carconnectivity_plugin_database-0.1a1.dist-info/top_level.txt +2 -0
- carconnectivity_plugins/database/__init__.py +0 -0
- carconnectivity_plugins/database/_version.py +21 -0
- carconnectivity_plugins/database/model/__init__.py +5 -0
- carconnectivity_plugins/database/model/alembic.ini +100 -0
- carconnectivity_plugins/database/model/attribute.py +30 -0
- carconnectivity_plugins/database/model/attribute_value.py +121 -0
- carconnectivity_plugins/database/model/base.py +4 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/README +1 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/__init__.py +0 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/env.py +78 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/script.py.mako +24 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/versions/__init__.py +0 -0
- carconnectivity_plugins/database/model/datetime_decorator.py +44 -0
- carconnectivity_plugins/database/model/migrations.py +24 -0
- carconnectivity_plugins/database/model/timedelta_decorator.py +33 -0
- carconnectivity_plugins/database/plugin.py +263 -0
- carconnectivity_plugins/database/ui/plugin_ui.py +48 -0
- carconnectivity_plugins/database/ui/templates/database/status.html +8 -0
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Module containing the commandline interface for the carconnectivity package."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from carconnectivity.carconnectivity_base import CLI
|
|
8
|
+
|
|
9
|
+
from carconnectivity_plugins.database._version import __version__
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger("carconnectivity-database")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
"""
|
|
19
|
+
Entry point for the car connectivity database application.
|
|
20
|
+
|
|
21
|
+
This function initializes and starts the command-line interface (CLI) for the
|
|
22
|
+
car connectivity application using the specified logger and application name.
|
|
23
|
+
"""
|
|
24
|
+
cli: CLI = CLI(logger=LOG, name='carconnectivity-database', description='Commandline Interface to interact with Car Services of various brands',
|
|
25
|
+
subversion=__version__)
|
|
26
|
+
cli.main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: carconnectivity-plugin-database
|
|
3
|
+
Version: 0.1a1
|
|
4
|
+
Summary: CarConnectivity plugin for storing data to Databases
|
|
5
|
+
Author: Till Steinbach
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2021 Till Steinbach
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Classifier: Development Status :: 3 - Alpha
|
|
29
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
31
|
+
Classifier: Intended Audience :: System Administrators
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
37
|
+
Classifier: Topic :: Utilities
|
|
38
|
+
Classifier: Topic :: System :: Monitoring
|
|
39
|
+
Classifier: Topic :: Home Automation
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Requires-Dist: carconnectivity>=0.8
|
|
44
|
+
Requires-Dist: sqlalchemy~=2.0.34
|
|
45
|
+
Requires-Dist: psycopg2-binary~=2.9.9
|
|
46
|
+
Requires-Dist: alembic~=1.13.2
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# CarConnectivity Plugin for Database storage
|
|
52
|
+
[](https://github.com/tillsteinbach/CarConnectivity-plugin-database/)
|
|
53
|
+
[](https://github.com/tillsteinbach/CarConnectivity-plugin-database/releases/latest)
|
|
54
|
+
[](https://github.com/tillsteinbach/CarConnectivity-plugin-database/blob/master/LICENSE)
|
|
55
|
+
[](https://github.com/tillsteinbach/CarConnectivity-plugin-database/issues)
|
|
56
|
+
[](https://pypi.org/project/carconnectivity-plugin-database/)
|
|
57
|
+
[](https://pypi.org/project/carconnectivity-plugin-database/)
|
|
58
|
+
[](https://www.paypal.com/donate?hosted_button_id=2BVFF5GJ9SXAJ)
|
|
59
|
+
[](https://github.com/sponsors/tillsteinbach)
|
|
60
|
+
|
|
61
|
+
[CarConnectivity](https://github.com/tillsteinbach/CarConnectivity) is a python API to connect to various car services. If you want to store the data collected from your vehicle to a relational database (e.g. MySQL, PostgreSQL, or SQLite) this plugin will help you.
|
|
62
|
+
|
|
63
|
+
### Install using PIP
|
|
64
|
+
If you want to use the CarConnectivity Plugin for Databases, the easiest way is to obtain it from [PyPI](https://pypi.org/project/carconnectivity-plugin-database/). Just install it using:
|
|
65
|
+
```bash
|
|
66
|
+
pip3 install carconnectivity-plugin-database
|
|
67
|
+
```
|
|
68
|
+
after you installed CarConnectivity
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
In your carconnectivity.json configuration add a section for the database plugin like this. A documentation of all possible config options can be found [here](https://github.com/tillsteinbach/CarConnectivity-plugin-database/tree/main/doc/Config.md).
|
|
72
|
+
```
|
|
73
|
+
{
|
|
74
|
+
"carConnectivity": {
|
|
75
|
+
"connectors": [
|
|
76
|
+
...
|
|
77
|
+
]
|
|
78
|
+
"plugins": [
|
|
79
|
+
{
|
|
80
|
+
"type": "database",
|
|
81
|
+
"config": {
|
|
82
|
+
"db_url": "sqlite:///carconnectivity.db"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Updates
|
|
91
|
+
If you want to update, the easiest way is:
|
|
92
|
+
```bash
|
|
93
|
+
pip3 install carconnectivity-plugin-database --upgrade
|
|
94
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
carconnectivity_database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
carconnectivity_database/carconnectivity_database_base.py,sha256=tsCWJJlBody2IDSw55WPkl-q4sAGNWHgid1iK7nQ5Jk,842
|
|
3
|
+
carconnectivity_plugin_database-0.1a1.dist-info/licenses/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
|
|
4
|
+
carconnectivity_plugins/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
carconnectivity_plugins/database/_version.py,sha256=7RVZDNVLiBlSQw2ZRHUKaSt9y7gnMixptmDyf1GD1mk,514
|
|
6
|
+
carconnectivity_plugins/database/plugin.py,sha256=g_OWJNR-v7QA8G_atvCX22S3Ff4DB14M_lPNAmpix-Y,15281
|
|
7
|
+
carconnectivity_plugins/database/model/__init__.py,sha256=6TKjVHWZWXZWA_uqK38zioF8CN8Bh_LqPvEzX3Kh_FE,306
|
|
8
|
+
carconnectivity_plugins/database/model/alembic.ini,sha256=_cT30ZkhLipi-fFq1NOPzBfpd_FTqj55co53TMuLGSM,2702
|
|
9
|
+
carconnectivity_plugins/database/model/attribute.py,sha256=sIeAz1bJ12REvv5cck2Hii938u6PhLygNPBKFiSNetY,1076
|
|
10
|
+
carconnectivity_plugins/database/model/attribute_value.py,sha256=yh7KrLVnh0Jr1xcU19-vlKA4i6HF4_wzZCnRzVyCGfI,5067
|
|
11
|
+
carconnectivity_plugins/database/model/base.py,sha256=bYej-MB6Xp4On2hQA3k5BRvwxz5frKjFLKX7Arq61HQ,147
|
|
12
|
+
carconnectivity_plugins/database/model/datetime_decorator.py,sha256=284bx5oQ5ACErm26qnif171QtKtF8efI2IBDXlyffzw,1444
|
|
13
|
+
carconnectivity_plugins/database/model/migrations.py,sha256=x6XMnzTFfhIDRvEZ4B1jR73dFdrNUcpMCWeyAi324QI,897
|
|
14
|
+
carconnectivity_plugins/database/model/timedelta_decorator.py,sha256=mdB9ZibJV-4v5H4EY25yxg407O2r6b5D960wMmCqLuE,976
|
|
15
|
+
carconnectivity_plugins/database/model/carconnectivity_schema/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
|
|
16
|
+
carconnectivity_plugins/database/model/carconnectivity_schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
carconnectivity_plugins/database/model/carconnectivity_schema/env.py,sha256=trMAoTgw_EWjjTPnw1fWbKyDGEpAtbvd1exqznTozaE,2093
|
|
18
|
+
carconnectivity_plugins/database/model/carconnectivity_schema/script.py.mako,sha256=8_xgA-gm_OhehnO7CiIijWgnm00ZlszEHtIHrAYFJl0,494
|
|
19
|
+
carconnectivity_plugins/database/model/carconnectivity_schema/versions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
carconnectivity_plugins/database/ui/plugin_ui.py,sha256=LQOuXIeBOLH4Gg0Qqjmzu9BOHI0Rir9KLq3gZF5Rlr4,1815
|
|
21
|
+
carconnectivity_plugins/database/ui/templates/database/status.html,sha256=OypmDrRib6tBM1fC7ijyTmF9HI53XlpF4tq_diXiLKc,162
|
|
22
|
+
carconnectivity_plugin_database-0.1a1.dist-info/METADATA,sha256=OHWDxC8rQYtVRl7O0moCAfKlKkioVh11FneAcEpdLSU,4809
|
|
23
|
+
carconnectivity_plugin_database-0.1a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
carconnectivity_plugin_database-0.1a1.dist-info/entry_points.txt,sha256=YB5RERxEA_pxYQesyAyzV8hmPM2o3_F9FZbnSQaSuLQ,105
|
|
25
|
+
carconnectivity_plugin_database-0.1a1.dist-info/top_level.txt,sha256=836FhYp_OWsQq5Vyjb95dilEMQPHUYCTZa2GBgNFEVE,49
|
|
26
|
+
carconnectivity_plugin_database-0.1a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Till Steinbach
|
|
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.
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.1a1'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 1, 'a1')
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
""" This module contains the database model for car connectivity plugins."""
|
|
2
|
+
|
|
3
|
+
from .attribute import Attribute # noqa: F401
|
|
4
|
+
from .attribute_value import AttributeIntegerValue, AttributeBooleanValue, AttributeFloatValue, AttributeStringValue, AttributeDatetimeValue, \
|
|
5
|
+
AttributeEnumValue # noqa: F401
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# A generic, single database configuration.
|
|
2
|
+
|
|
3
|
+
[alembic]
|
|
4
|
+
# path to migration scripts
|
|
5
|
+
script_location = carconnectivity_schema
|
|
6
|
+
|
|
7
|
+
# template used to generate migration files
|
|
8
|
+
# file_template = %%(rev)s_%%(slug)s
|
|
9
|
+
|
|
10
|
+
# sys.path path, will be prepended to sys.path if present.
|
|
11
|
+
# defaults to the current working directory.
|
|
12
|
+
prepend_sys_path = .
|
|
13
|
+
|
|
14
|
+
# timezone to use when rendering the date within the migration file
|
|
15
|
+
# as well as the filename.
|
|
16
|
+
# If specified, requires the python-dateutil library that can be
|
|
17
|
+
# installed by adding `alembic[tz]` to the pip requirements
|
|
18
|
+
# string value is passed to dateutil.tz.gettz()
|
|
19
|
+
# leave blank for localtime
|
|
20
|
+
# timezone =
|
|
21
|
+
|
|
22
|
+
# max length of characters to apply to the
|
|
23
|
+
# "slug" field
|
|
24
|
+
# truncate_slug_length = 40
|
|
25
|
+
|
|
26
|
+
# set to 'true' to run the environment during
|
|
27
|
+
# the 'revision' command, regardless of autogenerate
|
|
28
|
+
# revision_environment = false
|
|
29
|
+
|
|
30
|
+
# set to 'true' to allow .pyc and .pyo files without
|
|
31
|
+
# a source .py file to be detected as revisions in the
|
|
32
|
+
# versions/ directory
|
|
33
|
+
# sourceless = false
|
|
34
|
+
|
|
35
|
+
# version location specification; This defaults
|
|
36
|
+
# to vwsfriend-schema/versions. When using multiple version
|
|
37
|
+
# directories, initial revisions must be specified with --version-path.
|
|
38
|
+
# The path separator used here should be the separator specified by "version_path_separator"
|
|
39
|
+
# version_locations = %(here)s/bar:%(here)s/bat:vwsfriend-schema/versions
|
|
40
|
+
|
|
41
|
+
# version path separator; As mentioned above, this is the character used to split
|
|
42
|
+
# version_locations. Valid values are:
|
|
43
|
+
#
|
|
44
|
+
# version_path_separator = :
|
|
45
|
+
# version_path_separator = ;
|
|
46
|
+
# version_path_separator = space
|
|
47
|
+
version_path_separator = os # default: use os.pathsep
|
|
48
|
+
|
|
49
|
+
# the output encoding used when revision files
|
|
50
|
+
# are written from script.py.mako
|
|
51
|
+
# output_encoding = utf-8
|
|
52
|
+
|
|
53
|
+
sqlalchemy.url = sqlite:///carconnectivity-schema/reference.db
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
[post_write_hooks]
|
|
57
|
+
# post_write_hooks defines scripts or Python functions that are run
|
|
58
|
+
# on newly generated revision scripts. See the documentation for further
|
|
59
|
+
# detail and examples
|
|
60
|
+
|
|
61
|
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
|
62
|
+
# hooks = black
|
|
63
|
+
# black.type = console_scripts
|
|
64
|
+
# black.entrypoint = black
|
|
65
|
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
|
66
|
+
|
|
67
|
+
# Logging configuration
|
|
68
|
+
[loggers]
|
|
69
|
+
keys = root,sqlalchemy,alembic
|
|
70
|
+
|
|
71
|
+
[handlers]
|
|
72
|
+
keys = console
|
|
73
|
+
|
|
74
|
+
[formatters]
|
|
75
|
+
keys = generic
|
|
76
|
+
|
|
77
|
+
[logger_root]
|
|
78
|
+
level = WARN
|
|
79
|
+
handlers = console
|
|
80
|
+
qualname =
|
|
81
|
+
|
|
82
|
+
[logger_sqlalchemy]
|
|
83
|
+
level = WARN
|
|
84
|
+
handlers =
|
|
85
|
+
qualname = sqlalchemy.engine
|
|
86
|
+
|
|
87
|
+
[logger_alembic]
|
|
88
|
+
level = INFO
|
|
89
|
+
handlers =
|
|
90
|
+
qualname = alembic
|
|
91
|
+
|
|
92
|
+
[handler_console]
|
|
93
|
+
class = StreamHandler
|
|
94
|
+
args = (sys.stderr,)
|
|
95
|
+
level = NOTSET
|
|
96
|
+
formatter = generic
|
|
97
|
+
|
|
98
|
+
[formatter_generic]
|
|
99
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
100
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Module implements the database model for attributes"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
6
|
+
|
|
7
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from carconnectivity.attributes import GenericAttribute
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# pylint: disable-next=too-few-public-methods
|
|
14
|
+
class Attribute(Base):
|
|
15
|
+
"""Model for storing attributes in the database."""
|
|
16
|
+
__tablename__: str = 'attribute'
|
|
17
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
18
|
+
path: Mapped[Optional[str]] = mapped_column(unique=True, nullable=True)
|
|
19
|
+
|
|
20
|
+
def __init__(self, path: str) -> None:
|
|
21
|
+
self.path = path
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_generic_attribute(cls, attribute: GenericAttribute) -> Attribute:
|
|
25
|
+
"""Create an Attribute instance from a GenericAttribute.
|
|
26
|
+
Args:
|
|
27
|
+
attribute (GenericAttribute): The generic attribute to convert.
|
|
28
|
+
Returns:
|
|
29
|
+
Attribute: An instance of the Attribute class."""
|
|
30
|
+
return cls(path=attribute.get_absolute_path())
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Module implements the database model for attribute values."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Integer, Boolean, Float, String, ForeignKey
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship, declared_attr
|
|
10
|
+
|
|
11
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
12
|
+
from carconnectivity_plugins.database.model.datetime_decorator import DatetimeDecorator
|
|
13
|
+
from carconnectivity_plugins.database.model.timedelta_decorator import TimedeltaDecorator
|
|
14
|
+
|
|
15
|
+
from carconnectivity_plugins.database.model.attribute import Attribute
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# pylint: disable-next=too-few-public-methods
|
|
19
|
+
class AttributeValue(Base):
|
|
20
|
+
"""Base class for storing attribute values in the database."""
|
|
21
|
+
__abstract__ = True
|
|
22
|
+
|
|
23
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
24
|
+
attribute_id: Mapped[int] = mapped_column(Integer, ForeignKey("attribute.id"), nullable=False)
|
|
25
|
+
start_date: Mapped[datetime] = mapped_column(DatetimeDecorator(timezone=True), nullable=False)
|
|
26
|
+
end_date: Mapped[Optional[datetime]] = mapped_column(DatetimeDecorator(timezone=True), nullable=True)
|
|
27
|
+
|
|
28
|
+
@declared_attr
|
|
29
|
+
# pylint: disable-next=no-self-argument
|
|
30
|
+
def attribute(cls):
|
|
31
|
+
"""Relationship to the Attribute model."""
|
|
32
|
+
return relationship("Attribute")
|
|
33
|
+
|
|
34
|
+
def __init__(self, attribute: Attribute, start_date: datetime) -> None:
|
|
35
|
+
self.attribute = attribute
|
|
36
|
+
self.start_date = start_date
|
|
37
|
+
self.end = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# pylint: disable-next=too-few-public-methods
|
|
41
|
+
class AttributeIntegerValue(AttributeValue):
|
|
42
|
+
"""Model for storing integer values of attributes in the database."""
|
|
43
|
+
__tablename__: str = 'attribute_integer_value'
|
|
44
|
+
|
|
45
|
+
value: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, sort_order=1)
|
|
46
|
+
|
|
47
|
+
def __init__(self, attribute: Attribute, start_date: datetime, value: Optional[int]) -> None:
|
|
48
|
+
super().__init__(attribute=attribute, start_date=start_date)
|
|
49
|
+
self.value = value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# pylint: disable-next=too-few-public-methods
|
|
53
|
+
class AttributeBooleanValue(AttributeValue):
|
|
54
|
+
"""Model for storing booleans values of attributes in the database."""
|
|
55
|
+
__tablename__: str = 'attribute_boolean_value'
|
|
56
|
+
|
|
57
|
+
value: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True, sort_order=1)
|
|
58
|
+
|
|
59
|
+
def __init__(self, attribute: Attribute, start_date: datetime, value: Optional[bool]) -> None:
|
|
60
|
+
super().__init__(attribute=attribute, start_date=start_date)
|
|
61
|
+
self.value = value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# pylint: disable-next=too-few-public-methods
|
|
65
|
+
class AttributeFloatValue(AttributeValue):
|
|
66
|
+
"""Model for storing float values of attributes in the database."""
|
|
67
|
+
__tablename__: str = 'attribute_float_value'
|
|
68
|
+
|
|
69
|
+
value: Mapped[Optional[float]] = mapped_column(Float, nullable=True, sort_order=1)
|
|
70
|
+
|
|
71
|
+
def __init__(self, attribute: Attribute, start_date: datetime, value: Optional[float]) -> None:
|
|
72
|
+
super().__init__(attribute=attribute, start_date=start_date)
|
|
73
|
+
self.value = value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# pylint: disable-next=too-few-public-methods
|
|
77
|
+
class AttributeStringValue(AttributeValue):
|
|
78
|
+
"""Model for storing string values of attributes in the database."""
|
|
79
|
+
__tablename__: str = 'attribute_string_value'
|
|
80
|
+
|
|
81
|
+
value: Mapped[Optional[str]] = mapped_column(String, nullable=True, sort_order=1)
|
|
82
|
+
|
|
83
|
+
def __init__(self, attribute: Attribute, start_date: datetime, value: Optional[str]) -> None:
|
|
84
|
+
super().__init__(attribute=attribute, start_date=start_date)
|
|
85
|
+
self.value = value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# pylint: disable-next=too-few-public-methods
|
|
89
|
+
class AttributeDatetimeValue(AttributeValue):
|
|
90
|
+
"""Model for storing datetime values of attributes in the database."""
|
|
91
|
+
__tablename__: str = 'attribute_datetime_value'
|
|
92
|
+
|
|
93
|
+
value: Mapped[Optional[datetime]] = mapped_column(DatetimeDecorator(timezone=True), nullable=True, sort_order=1)
|
|
94
|
+
|
|
95
|
+
def __init__(self, attribute: Attribute, start_date: datetime, value: Optional[datetime]) -> None:
|
|
96
|
+
super().__init__(attribute=attribute, start_date=start_date)
|
|
97
|
+
self.value = value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# pylint: disable-next=too-few-public-methods
|
|
101
|
+
class AttributeDurationValue(AttributeValue):
|
|
102
|
+
"""Model for storing timedelta values of attributes in the database."""
|
|
103
|
+
__tablename__: str = 'attribute_duration_value'
|
|
104
|
+
|
|
105
|
+
value: Mapped[Optional[timedelta]] = mapped_column(TimedeltaDecorator(), nullable=True, sort_order=1)
|
|
106
|
+
|
|
107
|
+
def __init__(self, attribute: Attribute, start_date: datetime, value: Optional[timedelta]) -> None:
|
|
108
|
+
super().__init__(attribute=attribute, start_date=start_date)
|
|
109
|
+
self.value = value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# pylint: disable-next=too-few-public-methods
|
|
113
|
+
class AttributeEnumValue(AttributeValue):
|
|
114
|
+
"""Model for storing enum values of attributes in the database."""
|
|
115
|
+
__tablename__: str = 'attribute_enum_value'
|
|
116
|
+
|
|
117
|
+
value: Mapped[Optional[str]] = mapped_column(String, nullable=True, sort_order=1)
|
|
118
|
+
|
|
119
|
+
def __init__(self, attribute: Attribute, start_date: datetime, value: Optional[str]) -> None:
|
|
120
|
+
super().__init__(attribute=attribute, start_date=start_date)
|
|
121
|
+
self.value = value
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Generic single-database configuration.
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# pylint: skip-file
|
|
2
|
+
from logging.config import fileConfig
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import engine_from_config
|
|
5
|
+
from sqlalchemy import pool
|
|
6
|
+
|
|
7
|
+
from alembic import context
|
|
8
|
+
|
|
9
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
10
|
+
|
|
11
|
+
# this is the Alembic Config object, which provides
|
|
12
|
+
# access to the values within the .ini file in use.
|
|
13
|
+
config = context.config
|
|
14
|
+
|
|
15
|
+
# Interpret the config file for Python logging.
|
|
16
|
+
# This line sets up loggers basically.
|
|
17
|
+
if config.attributes.get('configure_logger', True):
|
|
18
|
+
fileConfig(config.config_file_name)
|
|
19
|
+
|
|
20
|
+
target_metadata = Base.metadata
|
|
21
|
+
|
|
22
|
+
# other values from the config, defined by the needs of env.py,
|
|
23
|
+
# can be acquired:
|
|
24
|
+
# my_important_option = config.get_main_option("my_important_option")
|
|
25
|
+
# ... etc.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_migrations_offline():
|
|
29
|
+
"""Run migrations in 'offline' mode.
|
|
30
|
+
|
|
31
|
+
This configures the context with just a URL
|
|
32
|
+
and not an Engine, though an Engine is acceptable
|
|
33
|
+
here as well. By skipping the Engine creation
|
|
34
|
+
we don't even need a DBAPI to be available.
|
|
35
|
+
|
|
36
|
+
Calls to context.execute() here emit the given string to the
|
|
37
|
+
script output.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
41
|
+
context.configure(
|
|
42
|
+
url=url,
|
|
43
|
+
target_metadata=target_metadata,
|
|
44
|
+
literal_binds=True,
|
|
45
|
+
dialect_opts={"paramstyle": "named"},
|
|
46
|
+
compare_type=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
with context.begin_transaction():
|
|
50
|
+
context.run_migrations()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_migrations_online():
|
|
54
|
+
"""Run migrations in 'online' mode.
|
|
55
|
+
|
|
56
|
+
In this scenario we need to create an Engine
|
|
57
|
+
and associate a connection with the context.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
connectable = engine_from_config(
|
|
61
|
+
config.get_section(config.config_ini_section),
|
|
62
|
+
prefix="sqlalchemy.",
|
|
63
|
+
poolclass=pool.NullPool,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
with connectable.connect() as connection:
|
|
67
|
+
context.configure(
|
|
68
|
+
connection=connection, target_metadata=target_metadata, compare_type=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
with context.begin_transaction():
|
|
72
|
+
context.run_migrations()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if context.is_offline_mode():
|
|
76
|
+
run_migrations_offline()
|
|
77
|
+
else:
|
|
78
|
+
run_migrations_online()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from alembic import op
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
${imports if imports else ""}
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = ${repr(up_revision)}
|
|
14
|
+
down_revision = ${repr(down_revision)}
|
|
15
|
+
branch_labels = ${repr(branch_labels)}
|
|
16
|
+
depends_on = ${repr(depends_on)}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade():
|
|
20
|
+
${upgrades if upgrades else "pass"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def downgrade():
|
|
24
|
+
${downgrades if downgrades else "pass"}
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Decorator for handling datetime objects in SQLAlchemy models.
|
|
2
|
+
This decorator ensures that datetime values are stored in UTC and retrieved in UTC,
|
|
3
|
+
while also converting naive datetime objects to the local timezone before storing.
|
|
4
|
+
It is designed to work with SQLAlchemy's DateTime type."""
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import sqlalchemy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# pylint: disable=too-many-ancestors
|
|
10
|
+
class DatetimeDecorator(sqlalchemy.types.TypeDecorator):
|
|
11
|
+
"""Decorator for handling datetime objects in SQLAlchemy models."""
|
|
12
|
+
impl = sqlalchemy.types.DateTime
|
|
13
|
+
cache_ok = True
|
|
14
|
+
|
|
15
|
+
def process_literal_param(self, value, dialect):
|
|
16
|
+
"""Process literal parameter for SQLAlchemy."""
|
|
17
|
+
if value is None:
|
|
18
|
+
return None
|
|
19
|
+
return f"'{value.isoformat()}'"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def python_type(self):
|
|
23
|
+
"""Return the Python type handled by this decorator."""
|
|
24
|
+
return datetime
|
|
25
|
+
|
|
26
|
+
LOCAL_TIMEZONE = datetime.utcnow().astimezone().tzinfo
|
|
27
|
+
|
|
28
|
+
def process_bind_param(self, value, dialect):
|
|
29
|
+
if value is None:
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
if value.tzinfo is None:
|
|
33
|
+
value = value.astimezone(self.LOCAL_TIMEZONE)
|
|
34
|
+
|
|
35
|
+
return value.astimezone(timezone.utc)
|
|
36
|
+
|
|
37
|
+
def process_result_value(self, value, dialect):
|
|
38
|
+
if value is None:
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
if value.tzinfo is None:
|
|
42
|
+
return value.replace(tzinfo=timezone.utc)
|
|
43
|
+
|
|
44
|
+
return value.astimezone(timezone.utc)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
""" Module for running database migrations using Alembic."""
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from alembic.command import upgrade, stamp
|
|
5
|
+
from alembic.config import Config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_database_migrations(dsn: str, stamp_only: bool = False):
|
|
9
|
+
"""Run database migrations using Alembic."""
|
|
10
|
+
# retrieves the directory that *this* file is in
|
|
11
|
+
migrations_dir = os.path.dirname(os.path.realpath(__file__))
|
|
12
|
+
# this assumes the alembic.ini is also contained in this same directory
|
|
13
|
+
config_file = os.path.join(migrations_dir, "alembic.ini")
|
|
14
|
+
|
|
15
|
+
config = Config(file_=config_file)
|
|
16
|
+
config.attributes['configure_logger'] = False
|
|
17
|
+
config.set_main_option("script_location", migrations_dir + '/carconnectivity_schema')
|
|
18
|
+
config.set_main_option('sqlalchemy.url', dsn)
|
|
19
|
+
|
|
20
|
+
if stamp_only:
|
|
21
|
+
stamp(config, "head")
|
|
22
|
+
else:
|
|
23
|
+
# upgrade the database to the latest revision
|
|
24
|
+
upgrade(config, "head")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Decorator for handling timedelta objects in SQLAlchemy models.
|
|
2
|
+
This decorator ensures that duration values are stored in microsecond format"""
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
import sqlalchemy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# pylint: disable=too-many-ancestors
|
|
8
|
+
class TimedeltaDecorator(sqlalchemy.types.TypeDecorator):
|
|
9
|
+
"""Decorator for handling datetime objects in SQLAlchemy models."""
|
|
10
|
+
impl = sqlalchemy.types.Float
|
|
11
|
+
cache_ok = True
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def python_type(self):
|
|
15
|
+
"""Return the Python type handled by this decorator."""
|
|
16
|
+
return timedelta
|
|
17
|
+
|
|
18
|
+
def process_bind_param(self, value, dialect):
|
|
19
|
+
if value is None:
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
return value.total_seconds()
|
|
23
|
+
|
|
24
|
+
def process_result_value(self, value, dialect):
|
|
25
|
+
if value is None:
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
return timedelta(seconds=value)
|
|
29
|
+
|
|
30
|
+
def process_literal_param(self, value, dialect):
|
|
31
|
+
if value is None:
|
|
32
|
+
return value
|
|
33
|
+
return value.total_seconds()
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Module implements the plugin to connect with Database"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Engine, create_engine, text, inspect
|
|
9
|
+
from sqlalchemy.orm import sessionmaker, scoped_session
|
|
10
|
+
from sqlalchemy.exc import OperationalError
|
|
11
|
+
from sqlalchemy.orm.session import Session
|
|
12
|
+
|
|
13
|
+
from carconnectivity.errors import ConfigurationError
|
|
14
|
+
from carconnectivity.util import config_remove_credentials
|
|
15
|
+
|
|
16
|
+
from carconnectivity.observable import Observable
|
|
17
|
+
|
|
18
|
+
from carconnectivity.attributes import GenericAttribute, IntegerAttribute, BooleanAttribute, FloatAttribute, StringAttribute, DateAttribute, EnumAttribute, \
|
|
19
|
+
DurationAttribute
|
|
20
|
+
|
|
21
|
+
from carconnectivity_plugins.base.plugin import BasePlugin
|
|
22
|
+
|
|
23
|
+
from carconnectivity_plugins.database._version import __version__
|
|
24
|
+
from carconnectivity_plugins.database.model.migrations import run_database_migrations
|
|
25
|
+
|
|
26
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
27
|
+
from carconnectivity_plugins.database.model.attribute import Attribute
|
|
28
|
+
from carconnectivity_plugins.database.model.attribute_value import AttributeIntegerValue, AttributeBooleanValue, AttributeFloatValue, AttributeStringValue, \
|
|
29
|
+
AttributeDatetimeValue, AttributeDurationValue, AttributeEnumValue
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from typing import Dict, Optional, Any
|
|
33
|
+
from carconnectivity.carconnectivity import CarConnectivity
|
|
34
|
+
|
|
35
|
+
LOG: logging.Logger = logging.getLogger("carconnectivity.plugins.database")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Plugin(BasePlugin): # pylint: disable=too-many-instance-attributes
|
|
39
|
+
"""
|
|
40
|
+
Plugin class for Database connectivity.
|
|
41
|
+
Args:
|
|
42
|
+
car_connectivity (CarConnectivity): An instance of CarConnectivity.
|
|
43
|
+
config (Dict): Configuration dictionary containing connection details.
|
|
44
|
+
"""
|
|
45
|
+
def __init__(self, plugin_id: str, car_connectivity: CarConnectivity, config: Dict) -> None:
|
|
46
|
+
BasePlugin.__init__(self, plugin_id=plugin_id, car_connectivity=car_connectivity, config=config, log=LOG)
|
|
47
|
+
|
|
48
|
+
self._background_thread: Optional[threading.Thread] = None
|
|
49
|
+
self._stop_event = threading.Event()
|
|
50
|
+
|
|
51
|
+
LOG.info("Loading database plugin with config %s", config_remove_credentials(config))
|
|
52
|
+
|
|
53
|
+
if 'db_url' in config:
|
|
54
|
+
self.active_config['db_url'] = config['db_url']
|
|
55
|
+
else:
|
|
56
|
+
raise ConfigurationError('db_url must be configured in the plugin config')
|
|
57
|
+
|
|
58
|
+
connect_args = {}
|
|
59
|
+
if 'postgresql' in self.active_config['db_url']:
|
|
60
|
+
connect_args['options'] = '-c timezone=utc'
|
|
61
|
+
self.engine: Engine = create_engine(self.active_config['db_url'], pool_pre_ping=True, connect_args=connect_args)
|
|
62
|
+
session_factory: sessionmaker[Session] = sessionmaker(bind=self.engine)
|
|
63
|
+
scoped_session_factory: scoped_session[Session] = scoped_session(session_factory)
|
|
64
|
+
self.session: Session = scoped_session_factory()
|
|
65
|
+
|
|
66
|
+
self.attribute_map: Dict[GenericAttribute, Attribute] = {}
|
|
67
|
+
|
|
68
|
+
def startup(self) -> None:
|
|
69
|
+
LOG.info("Starting database plugin")
|
|
70
|
+
self._background_thread = threading.Thread(target=self._background_loop, daemon=False)
|
|
71
|
+
self._background_thread.name = 'carconnectivity.plugins.database-background'
|
|
72
|
+
self._background_thread.start()
|
|
73
|
+
self.healthy._set_value(value=True) # pylint: disable=protected-access
|
|
74
|
+
LOG.debug("Starting Database plugin done")
|
|
75
|
+
return super().startup()
|
|
76
|
+
|
|
77
|
+
def _background_loop(self) -> None:
|
|
78
|
+
self._stop_event.clear()
|
|
79
|
+
first_run: bool = True
|
|
80
|
+
while not self._stop_event.is_set():
|
|
81
|
+
try:
|
|
82
|
+
self.session.execute(text('SELECT 1')).all()
|
|
83
|
+
self.healthy._set_value(value=True) # pylint: disable=protected-access
|
|
84
|
+
if first_run:
|
|
85
|
+
LOG.info('Database connection established successfully')
|
|
86
|
+
first_run = False
|
|
87
|
+
if not inspect(self.engine).has_table("alembic_version"):
|
|
88
|
+
LOG.info('It looks like you have an empty database will create all tables')
|
|
89
|
+
Base.metadata.create_all(self.engine)
|
|
90
|
+
run_database_migrations(dsn=self.active_config['db_url'], stamp_only=True)
|
|
91
|
+
else:
|
|
92
|
+
LOG.info('It looks like you have an existing database will check if an upgrade is necessary')
|
|
93
|
+
run_database_migrations(dsn=self.active_config['db_url'])
|
|
94
|
+
LOG.info('Database upgrade done')
|
|
95
|
+
|
|
96
|
+
self.car_connectivity.add_observer(observer=self.__on_attribute_update, flag=Observable.ObserverEvent.ALL, on_transaction_end=True)
|
|
97
|
+
|
|
98
|
+
for attribute in self.car_connectivity.get_attributes(recursive=True):
|
|
99
|
+
self.register_attribute(attribute, commit=False)
|
|
100
|
+
self.session.commit()
|
|
101
|
+
|
|
102
|
+
except OperationalError as err:
|
|
103
|
+
LOG.error('Could not establish a connection to database, will try again after 10 seconds: %s', err)
|
|
104
|
+
self.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
105
|
+
self._stop_event.wait(10)
|
|
106
|
+
|
|
107
|
+
def shutdown(self) -> None:
|
|
108
|
+
self._stop_event.set()
|
|
109
|
+
if self._background_thread is not None:
|
|
110
|
+
self._background_thread.join()
|
|
111
|
+
return super().shutdown()
|
|
112
|
+
|
|
113
|
+
def get_version(self) -> str:
|
|
114
|
+
return __version__
|
|
115
|
+
|
|
116
|
+
def get_type(self) -> str:
|
|
117
|
+
return "carconnectivity-plugin-database"
|
|
118
|
+
|
|
119
|
+
def get_name(self) -> str:
|
|
120
|
+
return "Database Plugin"
|
|
121
|
+
|
|
122
|
+
def __on_attribute_update(self, element: Any, flags: Observable.ObserverEvent):
|
|
123
|
+
"""
|
|
124
|
+
Callback for attribute updates.
|
|
125
|
+
Args:
|
|
126
|
+
element (Any): The updated element.
|
|
127
|
+
flags (Observable.ObserverEvent): Flags indicating the type of update.
|
|
128
|
+
"""
|
|
129
|
+
if isinstance(element, GenericAttribute):
|
|
130
|
+
if flags & Observable.ObserverEvent.ENABLED:
|
|
131
|
+
LOG.debug('Attribute %s enabled', element.name)
|
|
132
|
+
self.register_attribute(element, commit=True)
|
|
133
|
+
elif flags & Observable.ObserverEvent.DISABLED:
|
|
134
|
+
LOG.debug('Attribute %s disabled', element.name)
|
|
135
|
+
if element in self.attribute_map:
|
|
136
|
+
del self.attribute_map[element]
|
|
137
|
+
elif flags & Observable.ObserverEvent.VALUE_CHANGED:
|
|
138
|
+
LOG.debug('Attribute %s value changed', element.name)
|
|
139
|
+
self.update_value(element, commit=True)
|
|
140
|
+
|
|
141
|
+
def register_attribute(self, attribute: GenericAttribute, commit: bool = True) -> Attribute:
|
|
142
|
+
""" Register an attribute in the database.
|
|
143
|
+
Args:
|
|
144
|
+
attribute (GenericAttribute): The attribute to register.
|
|
145
|
+
commit (bool): Whether to commit the session after registering.
|
|
146
|
+
Returns:
|
|
147
|
+
Attribute: The database attribute instance."""
|
|
148
|
+
database_attribute: Optional[Attribute] = self.session.query(Attribute).filter(Attribute.path == attribute.get_absolute_path()).first()
|
|
149
|
+
if database_attribute is None:
|
|
150
|
+
database_attribute = Attribute.from_generic_attribute(attribute)
|
|
151
|
+
self.session.add(database_attribute)
|
|
152
|
+
self.attribute_map[attribute] = database_attribute
|
|
153
|
+
LOG.debug('Registering attribute %s', attribute.name)
|
|
154
|
+
self.update_value(attribute=attribute, commit=commit)
|
|
155
|
+
LOG.debug('Updated value for attribute %s to %s', attribute.name, attribute.value)
|
|
156
|
+
if commit:
|
|
157
|
+
self.session.commit()
|
|
158
|
+
return database_attribute
|
|
159
|
+
|
|
160
|
+
# pylint: disable-next=too-many-branches, too-many-statements
|
|
161
|
+
def update_value(self, attribute: GenericAttribute, commit: bool = True) -> None:
|
|
162
|
+
""" Update the value of an attribute in the database.
|
|
163
|
+
Args:
|
|
164
|
+
attribute (GenericAttribute): The attribute to update.
|
|
165
|
+
commit (bool): Whether to commit the session after updating."""
|
|
166
|
+
if attribute not in self.attribute_map:
|
|
167
|
+
database_attribute: Attribute = self.register_attribute(attribute, commit=commit)
|
|
168
|
+
else:
|
|
169
|
+
database_attribute = self.attribute_map[attribute]
|
|
170
|
+
if attribute.last_updated is not None:
|
|
171
|
+
if isinstance(attribute, IntegerAttribute):
|
|
172
|
+
last_integer_value: Optional[AttributeIntegerValue] = self.session.query(AttributeIntegerValue) \
|
|
173
|
+
.filter(AttributeIntegerValue.attribute == database_attribute) \
|
|
174
|
+
.order_by(AttributeIntegerValue.start_date.desc()).first()
|
|
175
|
+
if last_integer_value is None or last_integer_value.value != attribute.value:
|
|
176
|
+
last_integer_value = AttributeIntegerValue(attribute=database_attribute, start_date=attribute.last_updated, value=attribute.value)
|
|
177
|
+
last_integer_value.end_date = attribute.last_updated
|
|
178
|
+
self.session.add(last_integer_value)
|
|
179
|
+
LOG.debug('Updating value for attribute %s to %s', attribute.name, attribute.value)
|
|
180
|
+
else:
|
|
181
|
+
last_integer_value.value = attribute.value
|
|
182
|
+
last_integer_value.end_date = attribute.last_updated
|
|
183
|
+
elif isinstance(attribute, BooleanAttribute):
|
|
184
|
+
last_boolean_value: Optional[AttributeBooleanValue] = self.session.query(AttributeBooleanValue) \
|
|
185
|
+
.filter(AttributeBooleanValue.attribute == database_attribute) \
|
|
186
|
+
.order_by(AttributeBooleanValue.start_date.desc()).first()
|
|
187
|
+
if last_boolean_value is None or last_boolean_value.value != attribute.value:
|
|
188
|
+
last_boolean_value = AttributeBooleanValue(attribute=database_attribute, start_date=attribute.last_updated, value=attribute.value)
|
|
189
|
+
last_boolean_value.end_date = attribute.last_updated
|
|
190
|
+
self.session.add(last_boolean_value)
|
|
191
|
+
LOG.debug('Updating value for attribute %s to %s', attribute.name, attribute.value)
|
|
192
|
+
else:
|
|
193
|
+
last_boolean_value.value = attribute.value
|
|
194
|
+
last_boolean_value.end_date = attribute.last_updated
|
|
195
|
+
elif isinstance(attribute, FloatAttribute):
|
|
196
|
+
last_float_value: Optional[AttributeFloatValue] = self.session.query(AttributeFloatValue) \
|
|
197
|
+
.filter(AttributeFloatValue.attribute == database_attribute) \
|
|
198
|
+
.order_by(AttributeFloatValue.start_date.desc()).first()
|
|
199
|
+
if last_float_value is None or last_float_value.value != attribute.value:
|
|
200
|
+
last_float_value = AttributeFloatValue(attribute=database_attribute, start_date=attribute.last_updated, value=attribute.value)
|
|
201
|
+
last_float_value.end_date = attribute.last_updated
|
|
202
|
+
self.session.add(last_float_value)
|
|
203
|
+
LOG.debug('Updating value for attribute %s to %s', attribute.name, attribute.value)
|
|
204
|
+
else:
|
|
205
|
+
last_float_value.value = attribute.value
|
|
206
|
+
last_float_value.end_date = attribute.last_updated
|
|
207
|
+
elif isinstance(attribute, StringAttribute):
|
|
208
|
+
last_string_value: Optional[AttributeStringValue] = self.session.query(AttributeStringValue) \
|
|
209
|
+
.filter(AttributeStringValue.attribute == database_attribute) \
|
|
210
|
+
.order_by(AttributeStringValue.start_date.desc()).first()
|
|
211
|
+
if last_string_value is None or last_string_value.value != attribute.value:
|
|
212
|
+
last_string_value = AttributeStringValue(attribute=database_attribute, start_date=attribute.last_updated, value=attribute.value)
|
|
213
|
+
last_string_value.end_date = attribute.last_updated
|
|
214
|
+
self.session.add(last_string_value)
|
|
215
|
+
LOG.debug('Updating value for attribute %s to %s', attribute.name, attribute.value)
|
|
216
|
+
else:
|
|
217
|
+
last_string_value.value = attribute.value
|
|
218
|
+
last_string_value.end_date = attribute.last_updated
|
|
219
|
+
elif isinstance(attribute, DateAttribute):
|
|
220
|
+
last_datetime_value: Optional[AttributeDatetimeValue] = self.session.query(AttributeDatetimeValue) \
|
|
221
|
+
.filter(AttributeDatetimeValue.attribute == database_attribute) \
|
|
222
|
+
.order_by(AttributeDatetimeValue.start_date.desc()).first()
|
|
223
|
+
if last_datetime_value is None or last_datetime_value.value != attribute.value:
|
|
224
|
+
last_datetime_value = AttributeDatetimeValue(attribute=database_attribute, start_date=attribute.last_updated, value=attribute.value)
|
|
225
|
+
last_datetime_value.end_date = attribute.last_updated
|
|
226
|
+
self.session.add(last_datetime_value)
|
|
227
|
+
LOG.debug('Updating value for attribute %s to %s', attribute.name, attribute.value)
|
|
228
|
+
else:
|
|
229
|
+
last_datetime_value.value = attribute.value
|
|
230
|
+
last_datetime_value.end_date = attribute.last_updated
|
|
231
|
+
elif isinstance(attribute, DurationAttribute):
|
|
232
|
+
last_duration_value: Optional[AttributeDurationValue] = self.session.query(AttributeDurationValue) \
|
|
233
|
+
.filter(AttributeDurationValue.attribute == database_attribute) \
|
|
234
|
+
.order_by(AttributeDurationValue.start_date.desc()).first()
|
|
235
|
+
if last_duration_value is None or last_duration_value.value != attribute.value:
|
|
236
|
+
last_duration_value = AttributeDurationValue(attribute=database_attribute, start_date=attribute.last_updated, value=attribute.value)
|
|
237
|
+
last_duration_value.end_date = attribute.last_updated
|
|
238
|
+
self.session.add(last_duration_value)
|
|
239
|
+
LOG.debug('Updating value for attribute %s to %s', attribute.name, attribute.value)
|
|
240
|
+
else:
|
|
241
|
+
last_duration_value.value = attribute.value
|
|
242
|
+
last_duration_value.end_date = attribute.last_updated
|
|
243
|
+
elif isinstance(attribute, EnumAttribute):
|
|
244
|
+
last_enum_value: Optional[AttributeEnumValue] = self.session.query(AttributeEnumValue) \
|
|
245
|
+
.filter(AttributeEnumValue.attribute == database_attribute) \
|
|
246
|
+
.order_by(AttributeEnumValue.start_date.desc()).first()
|
|
247
|
+
if attribute.value is None:
|
|
248
|
+
value: Optional[str] = None
|
|
249
|
+
else:
|
|
250
|
+
value = attribute.value.value
|
|
251
|
+
if last_enum_value is None or last_enum_value.value != value:
|
|
252
|
+
last_enum_value = AttributeEnumValue(attribute=database_attribute, start_date=attribute.last_updated, value=value)
|
|
253
|
+
last_enum_value.end_date = attribute.last_updated
|
|
254
|
+
self.session.add(last_enum_value)
|
|
255
|
+
LOG.debug('Updating value for attribute %s to %s', attribute.name, attribute.value)
|
|
256
|
+
else:
|
|
257
|
+
last_enum_value.value = value
|
|
258
|
+
last_enum_value.end_date = attribute.last_updated
|
|
259
|
+
else:
|
|
260
|
+
LOG.debug('Attribute type %s is not supported for database storage', type(attribute).__name__)
|
|
261
|
+
return
|
|
262
|
+
if commit:
|
|
263
|
+
self.session.commit()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
""" User interface for the Database plugin in the Car Connectivity application. """
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import flask
|
|
8
|
+
import flask_login
|
|
9
|
+
|
|
10
|
+
from carconnectivity_plugins.base.plugin import BasePlugin
|
|
11
|
+
from carconnectivity_plugins.base.ui.plugin_ui import BasePluginUI
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from typing import Optional, List, Dict, Union, Literal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PluginUI(BasePluginUI):
|
|
18
|
+
"""
|
|
19
|
+
A user interface class for the Database plugin in the Car Connectivity application.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, plugin: BasePlugin):
|
|
22
|
+
blueprint: Optional[flask.Blueprint] = flask.Blueprint(name=plugin.id, import_name='carconnectivity-plugin-database', url_prefix=f'/{plugin.id}',
|
|
23
|
+
template_folder=os.path.dirname(__file__) + '/templates')
|
|
24
|
+
super().__init__(plugin, blueprint=blueprint)
|
|
25
|
+
|
|
26
|
+
@self.blueprint.route('/', methods=['GET'])
|
|
27
|
+
def root():
|
|
28
|
+
return flask.redirect(flask.url_for('plugins.database.status'))
|
|
29
|
+
|
|
30
|
+
@self.blueprint.route('/status', methods=['GET'])
|
|
31
|
+
@flask_login.login_required
|
|
32
|
+
def status():
|
|
33
|
+
return flask.render_template('database/status.html', current_app=flask.current_app, plugin=self.plugin)
|
|
34
|
+
|
|
35
|
+
def get_nav_items(self) -> List[Dict[Literal['text', 'url', 'sublinks', 'divider'], Union[str, List]]]:
|
|
36
|
+
"""
|
|
37
|
+
Generates a list of navigation items for the Database plugin UI.
|
|
38
|
+
"""
|
|
39
|
+
return super().get_nav_items() + [{"text": "Status", "url": flask.url_for('plugins.database.status')}]
|
|
40
|
+
|
|
41
|
+
def get_title(self) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Returns the title of the plugin.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: The title of the plugin, which is "Database".
|
|
47
|
+
"""
|
|
48
|
+
return "Database"
|