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.

Files changed (26) hide show
  1. carconnectivity_database/__init__.py +0 -0
  2. carconnectivity_database/carconnectivity_database_base.py +26 -0
  3. carconnectivity_plugin_database-0.1a1.dist-info/METADATA +94 -0
  4. carconnectivity_plugin_database-0.1a1.dist-info/RECORD +26 -0
  5. carconnectivity_plugin_database-0.1a1.dist-info/WHEEL +5 -0
  6. carconnectivity_plugin_database-0.1a1.dist-info/entry_points.txt +2 -0
  7. carconnectivity_plugin_database-0.1a1.dist-info/licenses/LICENSE +21 -0
  8. carconnectivity_plugin_database-0.1a1.dist-info/top_level.txt +2 -0
  9. carconnectivity_plugins/database/__init__.py +0 -0
  10. carconnectivity_plugins/database/_version.py +21 -0
  11. carconnectivity_plugins/database/model/__init__.py +5 -0
  12. carconnectivity_plugins/database/model/alembic.ini +100 -0
  13. carconnectivity_plugins/database/model/attribute.py +30 -0
  14. carconnectivity_plugins/database/model/attribute_value.py +121 -0
  15. carconnectivity_plugins/database/model/base.py +4 -0
  16. carconnectivity_plugins/database/model/carconnectivity_schema/README +1 -0
  17. carconnectivity_plugins/database/model/carconnectivity_schema/__init__.py +0 -0
  18. carconnectivity_plugins/database/model/carconnectivity_schema/env.py +78 -0
  19. carconnectivity_plugins/database/model/carconnectivity_schema/script.py.mako +24 -0
  20. carconnectivity_plugins/database/model/carconnectivity_schema/versions/__init__.py +0 -0
  21. carconnectivity_plugins/database/model/datetime_decorator.py +44 -0
  22. carconnectivity_plugins/database/model/migrations.py +24 -0
  23. carconnectivity_plugins/database/model/timedelta_decorator.py +33 -0
  24. carconnectivity_plugins/database/plugin.py +263 -0
  25. carconnectivity_plugins/database/ui/plugin_ui.py +48 -0
  26. 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
+ [![GitHub sourcecode](https://img.shields.io/badge/Source-GitHub-green)](https://github.com/tillsteinbach/CarConnectivity-plugin-database/)
53
+ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/tillsteinbach/CarConnectivity-plugin-database)](https://github.com/tillsteinbach/CarConnectivity-plugin-database/releases/latest)
54
+ [![GitHub](https://img.shields.io/github/license/tillsteinbach/CarConnectivity-plugin-database)](https://github.com/tillsteinbach/CarConnectivity-plugin-database/blob/master/LICENSE)
55
+ [![GitHub issues](https://img.shields.io/github/issues/tillsteinbach/CarConnectivity-plugin-database)](https://github.com/tillsteinbach/CarConnectivity-plugin-database/issues)
56
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/carconnectivity-plugin-database?label=PyPI%20Downloads)](https://pypi.org/project/carconnectivity-plugin-database/)
57
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/carconnectivity-plugin-database)](https://pypi.org/project/carconnectivity-plugin-database/)
58
+ [![Donate at PayPal](https://img.shields.io/badge/Donate-PayPal-2997d8)](https://www.paypal.com/donate?hosted_button_id=2BVFF5GJ9SXAJ)
59
+ [![Sponsor at Github](https://img.shields.io/badge/Sponsor-GitHub-28a745)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ carconnectivity-database = carconnectivity_database.carconnectivity_database_base:main
@@ -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.
@@ -0,0 +1,2 @@
1
+ carconnectivity_database
2
+ carconnectivity_plugins
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,4 @@
1
+ """Module just storing the Base class for SQLAlchemy models."""
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+
4
+ Base = declarative_base()
@@ -0,0 +1 @@
1
+ Generic single-database configuration.
@@ -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"}
@@ -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"
@@ -0,0 +1,8 @@
1
+ {% extends 'base.html' %}
2
+
3
+ {% block header %}
4
+ <h1>{% block title %}Database Plugin Status {% endblock %}</h1>
5
+ {% endblock %}
6
+
7
+ {% block content %}
8
+ {% endblock %}