gw_data 0.3.0__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.
- gw_data/__init__.py +4 -0
- gw_data/config.py +19 -0
- gw_data/db/__init__.py +0 -0
- gw_data/db/models/__init__.py +37 -0
- gw_data/db/models/_base.py +5 -0
- gw_data/db/models/connectivity_edge.py +80 -0
- gw_data/db/models/customer.py +18 -0
- gw_data/db/models/g_node.py +83 -0
- gw_data/db/models/installation.py +52 -0
- gw_data/db/models/installer.py +17 -0
- gw_data/db/models/message.py +51 -0
- gw_data/db/models/position_point.py +46 -0
- gw_data/db/models/reading.py +55 -0
- gw_data/db/models/reading_channel.py +62 -0
- gw_data/db/models/user.py +35 -0
- gw_data/db/models/user_installation_role.py +38 -0
- gw_data/db/scripts/0_db_create.psql +4 -0
- gw_data/db/scripts/1_db_user_setup.psql +16 -0
- gw_data/db/scripts/2_db_schema_setup.sql +11 -0
- gw_data/db/scripts/3_db_alembic_upgrade.sh +2 -0
- gw_data/db/scripts/4_db_seed.py +129 -0
- gw_data/db/scripts/_XX_drop_all.sql +13 -0
- gw_data/db/scripts/seed_data/homes.csv +6 -0
- gw_data/db/session.py +0 -0
- gw_data/sema/__init__.py +15 -0
- gw_data/sema/base.py +184 -0
- gw_data/sema/codec.py +168 -0
- gw_data/sema/definitions/enums/base.g.node.class/000.yaml +55 -0
- gw_data/sema/definitions/enums/g.node.status/000.yaml +35 -0
- gw_data/sema/definitions/formats/left.right.dot.yaml +27 -0
- gw_data/sema/definitions/formats/uuid4.str.yaml +29 -0
- gw_data/sema/definitions/registry.yaml +77 -0
- gw_data/sema/definitions/types/g.node.gt/004.yaml +169 -0
- gw_data/sema/definitions/types/position.point.gt/000.yaml +86 -0
- gw_data/sema/enums/__init__.py +7 -0
- gw_data/sema/enums/base_g_node_class.py +29 -0
- gw_data/sema/enums/g_node_status.py +28 -0
- gw_data/sema/enums/gw_str_enum.py +97 -0
- gw_data/sema/enums/old_versions/__init__.py +0 -0
- gw_data/sema/indexes/dependency_closure.yaml +19 -0
- gw_data/sema/indexes/local_names.yaml +6 -0
- gw_data/sema/indexes/lookup.yaml +35 -0
- gw_data/sema/indexes/reverse_dependencies.yaml +21 -0
- gw_data/sema/indexes/seed_expanded.yaml +29 -0
- gw_data/sema/indexes/versions.yaml +31 -0
- gw_data/sema/property_format.py +56 -0
- gw_data/sema/tests/test_property_format.py +47 -0
- gw_data/sema/types/__init__.py +7 -0
- gw_data/sema/types/g_node_gt.py +106 -0
- gw_data/sema/types/old_versions/__init__.py +0 -0
- gw_data/sema/types/position_point_gt.py +31 -0
- gw_data-0.3.0.dist-info/METADATA +189 -0
- gw_data-0.3.0.dist-info/RECORD +55 -0
- gw_data-0.3.0.dist-info/WHEEL +4 -0
- gw_data-0.3.0.dist-info/entry_points.txt +2 -0
gw_data/__init__.py
ADDED
gw_data/config.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from pydantic import ConfigDict, SecretStr
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
|
|
5
|
+
DEFAULT_ENV_FILE = ".env"
|
|
6
|
+
|
|
7
|
+
class Settings(BaseSettings):
|
|
8
|
+
db_url: SecretStr = SecretStr(
|
|
9
|
+
"postgresql+psycopg://gw_admin@localhost:5432/gridworks"
|
|
10
|
+
)
|
|
11
|
+
log_level: str = "INFO"
|
|
12
|
+
log_dir: str = "~/.local/state/gridworks/gw_data/log"
|
|
13
|
+
db_echo: bool = False
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(
|
|
16
|
+
env_prefix="gw_data_",
|
|
17
|
+
env_nested_delimiter="__",
|
|
18
|
+
extra="ignore",
|
|
19
|
+
)
|
gw_data/db/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""List of all the models"""
|
|
2
|
+
|
|
3
|
+
from gw_data.db.models.position_point import PositionPointSql
|
|
4
|
+
from gw_data.db.models.g_node import GNodeSql
|
|
5
|
+
from gw_data.db.models.connectivity_edge import ConnectivityEdgeSql
|
|
6
|
+
|
|
7
|
+
from gw_data.db.models.customer import CustomerSql
|
|
8
|
+
from gw_data.db.models.installer import InstallerSql
|
|
9
|
+
from gw_data.db.models.user import UserSql
|
|
10
|
+
from gw_data.db.models.installation import InstallationSql
|
|
11
|
+
|
|
12
|
+
from gw_data.db.models.reading_channel import ReadingChannelSql
|
|
13
|
+
from gw_data.db.models.message import MessageSql
|
|
14
|
+
from gw_data.db.models.reading import ReadingSql
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# import position_point
|
|
18
|
+
# import g_node
|
|
19
|
+
# import connectivity_edge
|
|
20
|
+
# import customer
|
|
21
|
+
# import installer
|
|
22
|
+
# import user
|
|
23
|
+
# import data_channel
|
|
24
|
+
# import message
|
|
25
|
+
# import reading
|
|
26
|
+
__all__ = [
|
|
27
|
+
"PositionPointSql",
|
|
28
|
+
"GNodeSql",
|
|
29
|
+
"ConnectivityEdgeSql",
|
|
30
|
+
"CustomerSql",
|
|
31
|
+
"InstallerSql",
|
|
32
|
+
"UserSql",
|
|
33
|
+
"InstallationSql",
|
|
34
|
+
"ReadingChannelSql",
|
|
35
|
+
"MessageSql",
|
|
36
|
+
"ReadingSql"
|
|
37
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLAlchemy models for the GridNodeRegistry.
|
|
3
|
+
|
|
4
|
+
Each SQL row corresponds to a serialized ASL GT snapshot.
|
|
5
|
+
ASL types are used for validation (via the codec) before any insert/update.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import (
|
|
14
|
+
Uuid,
|
|
15
|
+
Enum,
|
|
16
|
+
DateTime,
|
|
17
|
+
ForeignKey,
|
|
18
|
+
UniqueConstraint,
|
|
19
|
+
func,
|
|
20
|
+
)
|
|
21
|
+
from sqlalchemy.orm import (
|
|
22
|
+
Mapped,
|
|
23
|
+
mapped_column
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from gw_data.db.models._base import Base
|
|
27
|
+
from gw_data.sema.enums import GNodeStatus
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConnectivityEdgeSql(Base):
|
|
31
|
+
__tablename__ = "connectivity_edges"
|
|
32
|
+
|
|
33
|
+
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
|
|
34
|
+
|
|
35
|
+
from_g_node_id: Mapped[uuid.UUID] = mapped_column(
|
|
36
|
+
ForeignKey("g_nodes.id"), index=True
|
|
37
|
+
)
|
|
38
|
+
to_g_node_id: Mapped[uuid.UUID] = mapped_column(
|
|
39
|
+
ForeignKey("g_nodes.id"), index=True
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
status: Mapped[GNodeStatus] = mapped_column(
|
|
43
|
+
Enum(GNodeStatus, name="connectivity_edge_status", inherit_schema=True)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
47
|
+
DateTime(timezone=True), server_default=func.now()
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__table_args__ = (
|
|
51
|
+
UniqueConstraint(
|
|
52
|
+
"from_g_node_id", "to_g_node_id",
|
|
53
|
+
name="uq_connectivity_edges_from_to"
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# # -------------------
|
|
58
|
+
# # ASL ↔ SQL Helpers
|
|
59
|
+
# # -------------------
|
|
60
|
+
|
|
61
|
+
# def to_gt(self) -> ConnectivityEdgeGt:
|
|
62
|
+
# return ConnectivityEdgeGt(
|
|
63
|
+
# id=self.id,
|
|
64
|
+
# from_g_node_id=self.from_g_node_id,
|
|
65
|
+
# to_g_node_id=self.to_g_node_id,
|
|
66
|
+
# from_g_node_alias=self.from_g_node_alias,
|
|
67
|
+
# to_g_node_alias=self.to_g_node_alias,
|
|
68
|
+
# status=self.status,
|
|
69
|
+
# )
|
|
70
|
+
|
|
71
|
+
# @staticmethod
|
|
72
|
+
# def from_gt(gt: ConnectivityEdgeGt) -> "ConnectivityEdgeSql":
|
|
73
|
+
# return ConnectivityEdgeSql(
|
|
74
|
+
# id=gt.id,
|
|
75
|
+
# from_g_node_id=gt.from_g_node_id,
|
|
76
|
+
# to_g_node_id=gt.to_g_node_id,
|
|
77
|
+
# from_g_node_alias=gt.from_g_node_alias,
|
|
78
|
+
# to_g_node_alias=gt.to_g_node_alias,
|
|
79
|
+
# status=gt.status,
|
|
80
|
+
# )
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import Uuid
|
|
5
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.orm import (
|
|
8
|
+
Mapped,
|
|
9
|
+
mapped_column
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from gw_data.db.models._base import Base
|
|
13
|
+
|
|
14
|
+
class CustomerSql(Base):
|
|
15
|
+
__tablename__ = "customers"
|
|
16
|
+
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
|
|
17
|
+
primary_contact: Mapped[JSONB] = mapped_column(JSONB, nullable=False)
|
|
18
|
+
secondary_contact: Mapped[JSONB] = mapped_column(JSONB, nullable=False)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import (
|
|
8
|
+
func,
|
|
9
|
+
Uuid,
|
|
10
|
+
String,
|
|
11
|
+
Enum,
|
|
12
|
+
DateTime,
|
|
13
|
+
ForeignKey,
|
|
14
|
+
)
|
|
15
|
+
from sqlalchemy.orm import (
|
|
16
|
+
Mapped,
|
|
17
|
+
mapped_column,
|
|
18
|
+
relationship,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from gw_data.db.models._base import Base
|
|
22
|
+
from gw_data.db.models.position_point import PositionPointSql
|
|
23
|
+
from gw_data.sema.enums import BaseGNodeClass, GNodeStatus
|
|
24
|
+
|
|
25
|
+
class GNodeSql(Base):
|
|
26
|
+
__tablename__ = "g_nodes"
|
|
27
|
+
|
|
28
|
+
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
|
|
29
|
+
alias: Mapped[str] = mapped_column(String, index=True, unique=True)
|
|
30
|
+
prev_alias: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
31
|
+
|
|
32
|
+
base_class: Mapped[BaseGNodeClass] = mapped_column(
|
|
33
|
+
Enum(BaseGNodeClass, name="base_g_node_class", inherit_schema=True),
|
|
34
|
+
nullable=True
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
g_node_class: Mapped[str] = mapped_column(String)
|
|
38
|
+
|
|
39
|
+
status: Mapped[GNodeStatus] = mapped_column(
|
|
40
|
+
Enum(GNodeStatus, name="g_node_status", inherit_schema=True)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
position_point_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
44
|
+
ForeignKey("position_points.id"), nullable=True
|
|
45
|
+
)
|
|
46
|
+
position_point: Mapped[Optional[PositionPointSql]] = relationship()
|
|
47
|
+
|
|
48
|
+
display_name: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
49
|
+
|
|
50
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
51
|
+
DateTime(timezone=True), server_default=func.now()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# # -------------------
|
|
55
|
+
# # ASL ↔ SQL Helpers
|
|
56
|
+
# # -------------------
|
|
57
|
+
|
|
58
|
+
# def to_gt(self) -> GNodeGt:
|
|
59
|
+
# """Serialize SQL row → ASL GT."""
|
|
60
|
+
# return GNodeGt(
|
|
61
|
+
# g_node_id=self.id,
|
|
62
|
+
# alias=self.alias,
|
|
63
|
+
# base_class=self.base_class,
|
|
64
|
+
# g_node_class=self.g_node_class,
|
|
65
|
+
# status=self.status,
|
|
66
|
+
# prev_alias=self.prev_alias,
|
|
67
|
+
# position_point_id=self.position_point_id,
|
|
68
|
+
# display_name=self.display_name,
|
|
69
|
+
# )
|
|
70
|
+
|
|
71
|
+
# @staticmethod
|
|
72
|
+
# def from_gt(gt: GNodeGt) -> "GNodeSql":
|
|
73
|
+
# """Create SQL model from an ASL GT instance (already validated)."""
|
|
74
|
+
# return GNodeSql(
|
|
75
|
+
# id=gt.g_node_id,
|
|
76
|
+
# alias=gt.alias,
|
|
77
|
+
# prev_alias=gt.prev_alias,
|
|
78
|
+
# base_class=gt.base_class,
|
|
79
|
+
# g_node_class=gt.g_node_class,
|
|
80
|
+
# status=gt.status,
|
|
81
|
+
# position_point_id=gt.position_point_id,
|
|
82
|
+
# display_name=gt.display_name,
|
|
83
|
+
# )
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import (
|
|
5
|
+
String,
|
|
6
|
+
Uuid,
|
|
7
|
+
ForeignKey
|
|
8
|
+
)
|
|
9
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
10
|
+
|
|
11
|
+
from sqlalchemy.orm import (
|
|
12
|
+
Mapped,
|
|
13
|
+
mapped_column,
|
|
14
|
+
relationship
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from gw_data.db.models._base import Base
|
|
18
|
+
from gw_data.db.models.customer import CustomerSql
|
|
19
|
+
from gw_data.db.models.g_node import GNodeSql
|
|
20
|
+
from gw_data.db.models.installer import InstallerSql
|
|
21
|
+
|
|
22
|
+
class InstallationSql(Base):
|
|
23
|
+
__tablename__ = "installations"
|
|
24
|
+
|
|
25
|
+
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
|
|
26
|
+
g_node_id: Mapped[str] = mapped_column(
|
|
27
|
+
ForeignKey("g_nodes.id"),
|
|
28
|
+
nullable=False
|
|
29
|
+
)
|
|
30
|
+
g_node: Mapped[GNodeSql] = relationship(lazy="joined")
|
|
31
|
+
|
|
32
|
+
display_name: Mapped[str] = mapped_column(String, nullable=False)
|
|
33
|
+
customer_id: Mapped[uuid.UUID] = mapped_column(
|
|
34
|
+
ForeignKey("customers.id"),
|
|
35
|
+
nullable=False
|
|
36
|
+
)
|
|
37
|
+
customer: Mapped[CustomerSql] = relationship()
|
|
38
|
+
|
|
39
|
+
installer_id: Mapped[uuid.UUID] = mapped_column(
|
|
40
|
+
ForeignKey("installers.id"),
|
|
41
|
+
nullable=False
|
|
42
|
+
)
|
|
43
|
+
installer: Mapped[InstallerSql] = relationship()
|
|
44
|
+
|
|
45
|
+
address: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
|
46
|
+
alert_status: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
|
47
|
+
hardware_layout: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
|
48
|
+
representation_status: Mapped[dict] = mapped_column(JSONB, nullable=True)
|
|
49
|
+
house_parameters: Mapped[dict] = mapped_column(JSONB, nullable=True)
|
|
50
|
+
scada_ip_address: Mapped[str] = mapped_column(String, nullable=True)
|
|
51
|
+
scada_git_commit: Mapped[str] = mapped_column(String, nullable=True)
|
|
52
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import Uuid
|
|
5
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.orm import (
|
|
8
|
+
Mapped,
|
|
9
|
+
mapped_column,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from gw_data.db.models._base import Base
|
|
13
|
+
|
|
14
|
+
class InstallerSql(Base):
|
|
15
|
+
__tablename__ = "installers"
|
|
16
|
+
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
|
|
17
|
+
info: Mapped[JSONB] = mapped_column(JSONB, nullable=False)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import (
|
|
6
|
+
Uuid,
|
|
7
|
+
String,
|
|
8
|
+
DateTime,
|
|
9
|
+
Index
|
|
10
|
+
)
|
|
11
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
12
|
+
|
|
13
|
+
from sqlalchemy.orm import (
|
|
14
|
+
Mapped,
|
|
15
|
+
mapped_column,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from gw_data.db.models._base import Base
|
|
19
|
+
|
|
20
|
+
class MessageSql(Base):
|
|
21
|
+
__tablename__ = "messages"
|
|
22
|
+
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
|
|
23
|
+
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, primary_key=True, index=True)
|
|
24
|
+
|
|
25
|
+
# This is not a foreign key because we may receive messages from nodes that are not yet in the database
|
|
26
|
+
from_alias: Mapped[str] = mapped_column(String, nullable=False)
|
|
27
|
+
|
|
28
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
29
|
+
persisted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
30
|
+
|
|
31
|
+
message_type_name: Mapped[str] = mapped_column(String, nullable=False)
|
|
32
|
+
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
|
33
|
+
|
|
34
|
+
__table_args__ = (
|
|
35
|
+
# TimescaleDB automatically creates this one; we need to include it so Alembic doesn't get confused
|
|
36
|
+
Index("messages_timestamp_idx", timestamp.desc()),
|
|
37
|
+
Index(
|
|
38
|
+
"ix_from_type_message",
|
|
39
|
+
"from_alias",
|
|
40
|
+
"message_type_name",
|
|
41
|
+
"persisted_at"
|
|
42
|
+
),
|
|
43
|
+
{
|
|
44
|
+
'timescaledb_hypertable': {
|
|
45
|
+
'time_column_name': 'timestamp',
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def test_fn(self):
|
|
51
|
+
self.abc.defe = 3
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import (
|
|
7
|
+
func,
|
|
8
|
+
Uuid,
|
|
9
|
+
DateTime,
|
|
10
|
+
)
|
|
11
|
+
from sqlalchemy.orm import (
|
|
12
|
+
Mapped,
|
|
13
|
+
mapped_column
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from gw_data.db.models._base import Base
|
|
17
|
+
from gw_data.sema.types import PositionPointGt
|
|
18
|
+
|
|
19
|
+
class PositionPointSql(Base):
|
|
20
|
+
__tablename__ = "position_points"
|
|
21
|
+
|
|
22
|
+
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
|
|
23
|
+
latitude_micro_deg: Mapped[int] = mapped_column()
|
|
24
|
+
longitude_micro_deg: Mapped[int] = mapped_column()
|
|
25
|
+
|
|
26
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
27
|
+
DateTime(timezone=True), server_default=func.now()
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def to_gt(self) -> PositionPointGt:
|
|
31
|
+
"""Serialize database row → ASL GT."""
|
|
32
|
+
return PositionPointGt(
|
|
33
|
+
id=str(self.id),
|
|
34
|
+
latitude_micro_deg=self.latitude_micro_deg,
|
|
35
|
+
longitude_micro_deg=self.longitude_micro_deg,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def from_gt(gt: PositionPointGt) -> "PositionPointSql":
|
|
40
|
+
"""Create SQL row from ASL GT after full ASL validation."""
|
|
41
|
+
return PositionPointSql(
|
|
42
|
+
id=uuid.UUID(gt.id),
|
|
43
|
+
latitude_micro_deg=gt.latitude_micro_deg,
|
|
44
|
+
longitude_micro_deg=gt.longitude_micro_deg,
|
|
45
|
+
)
|
|
46
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import (
|
|
6
|
+
BigInteger,
|
|
7
|
+
DateTime,
|
|
8
|
+
ForeignKey,
|
|
9
|
+
Index,
|
|
10
|
+
UniqueConstraint
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from sqlalchemy.orm import (
|
|
14
|
+
Mapped,
|
|
15
|
+
mapped_column,
|
|
16
|
+
relationship,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from gw_data.db.models._base import Base
|
|
20
|
+
from gw_data.db.models.reading_channel import ReadingChannelSql
|
|
21
|
+
|
|
22
|
+
class ReadingSql(Base):
|
|
23
|
+
__tablename__ = "readings"
|
|
24
|
+
channel_id: Mapped[uuid.UUID] = mapped_column(
|
|
25
|
+
ForeignKey("reading_channels.id"),
|
|
26
|
+
nullable=False
|
|
27
|
+
)
|
|
28
|
+
channel: Mapped[ReadingChannelSql] = relationship()
|
|
29
|
+
|
|
30
|
+
# This is not a foreign key for two reasons:
|
|
31
|
+
# 1. TimescaleDB does not allow foreign keys between hypertables
|
|
32
|
+
# 2. We may conceivably have readings that do not have an associated message
|
|
33
|
+
# (e.g., from a server cron job or an admin action)
|
|
34
|
+
#
|
|
35
|
+
# Also note that since they are both hypertables, any queries that map between readings and messages
|
|
36
|
+
# should correlate based on time to improve performance.
|
|
37
|
+
message_id: Mapped[uuid.UUID] = mapped_column(
|
|
38
|
+
nullable=False,
|
|
39
|
+
index=True
|
|
40
|
+
)
|
|
41
|
+
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
|
42
|
+
value: Mapped[BigInteger] = mapped_column(BigInteger, nullable=False)
|
|
43
|
+
|
|
44
|
+
__table_args__ = (
|
|
45
|
+
# TimescaleDB automatically creates this one; we need to include it so Alembic doesn't get confused
|
|
46
|
+
Index("readings_timestamp_idx", timestamp.desc()),
|
|
47
|
+
UniqueConstraint("channel_id", "timestamp", name="readings_channel_id_timestamp_key")
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# This table does not need a true primary key -- but SQLAlchemy requires one.
|
|
51
|
+
# So we define a synthetic PK out of two columns that should always be unique.
|
|
52
|
+
__mapper_args__ = {
|
|
53
|
+
"primary_key": [channel_id, timestamp]
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import (
|
|
5
|
+
Index,
|
|
6
|
+
Uuid,
|
|
7
|
+
Boolean,
|
|
8
|
+
String,
|
|
9
|
+
DateTime,
|
|
10
|
+
UniqueConstraint
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from sqlalchemy.orm import (
|
|
14
|
+
Mapped,
|
|
15
|
+
mapped_column,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from gw_data.db.models._base import Base
|
|
19
|
+
|
|
20
|
+
class ReadingChannelSql(Base):
|
|
21
|
+
"""
|
|
22
|
+
Reading Channel.
|
|
23
|
+
|
|
24
|
+
A reading channel is a source of data for readings, with metadata common to the readings.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__tablename__ = "reading_channels"
|
|
28
|
+
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
|
|
29
|
+
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
30
|
+
terminal_asset_alias: Mapped[str] = mapped_column(String, nullable=False)
|
|
31
|
+
display_name: Mapped[str] = mapped_column(String, nullable=False)
|
|
32
|
+
unit: Mapped[str] = mapped_column(String, nullable=False)
|
|
33
|
+
unit_type: Mapped[str] = mapped_column(String, nullable=False)
|
|
34
|
+
channel_type: Mapped[str] = mapped_column(String, nullable=False)
|
|
35
|
+
deactivated_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
36
|
+
|
|
37
|
+
__table_args__ = (
|
|
38
|
+
Index("terminal_asset_alias", "name"),
|
|
39
|
+
UniqueConstraint(
|
|
40
|
+
"terminal_asset_alias",
|
|
41
|
+
"name",
|
|
42
|
+
"deactivated_date",
|
|
43
|
+
name="unique_name_terminal_asset_deactivated_date",
|
|
44
|
+
postgresql_nulls_not_distinct=True
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __repr__(self):
|
|
49
|
+
active_status = f"deactivated {self.deactivated_date.isoformat()}" if self.self.deactivated_date is not None else "active"
|
|
50
|
+
return (
|
|
51
|
+
f"<ReadingChannelSql(name='{self.name}', "
|
|
52
|
+
f"unit='{self.unit}', terminal asset={self.terminal_asset_alias.split(".")[-2]}, [{active_status}])>"
|
|
53
|
+
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def __str__(self):
|
|
57
|
+
active_status = f"deactivated {self.deactivated_date.isoformat()}" if self.self.deactivated_date is not None else "active"
|
|
58
|
+
return (
|
|
59
|
+
f"ReadingChannelSql(name:{self.name},"
|
|
60
|
+
f"unit: {self.unit}, terminal asset: {self.terminal_asset_alias.split(".")[-2]}, [{active_status}])"
|
|
61
|
+
)
|
|
62
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import (
|
|
6
|
+
String,
|
|
7
|
+
DateTime,
|
|
8
|
+
Boolean,
|
|
9
|
+
Uuid,
|
|
10
|
+
)
|
|
11
|
+
from sqlalchemy.sql import func
|
|
12
|
+
|
|
13
|
+
from sqlalchemy.orm import (
|
|
14
|
+
Mapped,
|
|
15
|
+
mapped_column,
|
|
16
|
+
relationship,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from gw_data.db.models._base import Base
|
|
20
|
+
from gw_data.db.models.user_installation_role import UserInstallationRoleSql
|
|
21
|
+
|
|
22
|
+
class UserSql(Base):
|
|
23
|
+
__tablename__ = "users"
|
|
24
|
+
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
|
|
25
|
+
username: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
|
26
|
+
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
27
|
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
28
|
+
last_login: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
29
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
30
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
31
|
+
|
|
32
|
+
installation_roles: Mapped[list[UserInstallationRoleSql]] = relationship(uselist=True, lazy="joined")
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return f"User(id={self.id!r}, username={self.username!r}, is_active={self.is_active!r})"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import ForeignKey, String, UniqueConstraint
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
5
|
+
|
|
6
|
+
from gw_data.db.models._base import Base
|
|
7
|
+
from gw_data.db.models.installation import InstallationSql
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UserInstallationRoleSql(Base):
|
|
11
|
+
__tablename__ = "user_installation_roles"
|
|
12
|
+
user_id: Mapped[uuid.UUID] = mapped_column(
|
|
13
|
+
ForeignKey("users.id"),
|
|
14
|
+
nullable=False
|
|
15
|
+
)
|
|
16
|
+
role: Mapped[str] = mapped_column(String, nullable=False)
|
|
17
|
+
installation_id: Mapped[uuid.UUID] = mapped_column(
|
|
18
|
+
ForeignKey("installations.id"),
|
|
19
|
+
nullable=True
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# A null installation_id associates the role to ALL installations (e.g. for admin users)
|
|
23
|
+
installations: Mapped[list[InstallationSql]] = relationship(
|
|
24
|
+
"InstallationSql",
|
|
25
|
+
uselist=True,
|
|
26
|
+
lazy="joined",
|
|
27
|
+
primaryjoin="or_(InstallationSql.id==UserInstallationRoleSql.installation_id, UserInstallationRoleSql.installation_id == None)"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__table_args__ = (
|
|
31
|
+
UniqueConstraint("user_id", "installation_id", name="user_installation_roles_user_installation_key"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# This table does not need a true primary key -- but SQLAlchemy requires one.
|
|
35
|
+
# So we define a synthetic PK out of two columns that should always be unique.
|
|
36
|
+
__mapper_args__ = {
|
|
37
|
+
"primary_key": [user_id, installation_id]
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- The following needs to be done by a powerful role (like postgresql or tsdbadmin), and doesn't need a specific database connection
|
|
2
|
+
|
|
3
|
+
-- Basic security config
|
|
4
|
+
REVOKE CONNECT ON DATABASE tsdb FROM PUBLIC;
|
|
5
|
+
|
|
6
|
+
CREATE USER gw_admin;
|
|
7
|
+
GRANT ALL ON DATABASE tsdb TO gw_admin;
|
|
8
|
+
\password gw_admin
|
|
9
|
+
|
|
10
|
+
CREATE USER gw_journalkeeper;
|
|
11
|
+
GRANT CONNECT ON DATABASE tsdb TO gw_journalkeeper;
|
|
12
|
+
\password gw_journalkeeper
|
|
13
|
+
|
|
14
|
+
CREATE USER gw_visualizer;
|
|
15
|
+
GRANT CONNECT ON DATABASE tsdb TO gw_visualizer;
|
|
16
|
+
\password gw_visualizer
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- The following should be done by gw_admin while connected to tsdb
|
|
2
|
+
|
|
3
|
+
CREATE SCHEMA gridworks;
|
|
4
|
+
|
|
5
|
+
GRANT USAGE ON SCHEMA gridworks TO gw_journalkeeper;
|
|
6
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA gridworks GRANT SELECT,INSERT,UPDATE,DELETE ON TABLES TO gw_journalkeeper;
|
|
7
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA gridworks GRANT ALL PRIVILEGES ON SEQUENCES TO gw_journalkeeper;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA gridworks TO gw_visualizer;
|
|
10
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA gridworks GRANT SELECT ON TABLES TO gw_visualizer;
|
|
11
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA gridworks GRANT ALL PRIVILEGES ON SEQUENCES TO gw_visualizer;
|