Skip to content

Commit 14148d3

Browse files
authored
V0.3 add infra repo details (#133)
* update to v0.3.5 and add daq-infrastructure to README * bug fix JSON file load and save method signature * adds models for database config files * update config models * bug fix db configs and add tests * update DB logic significantly * add stub code in pytest * fix pending bugs * do codestyle * add psycopg2-binary * change version to 0.3.7
1 parent accfa52 commit 14148d3

File tree

15 files changed

+664
-277
lines changed

15 files changed

+664
-277
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88

99
✓ means ready to try
1010

11-
## [v0.3.5] - 2025-10-18
11+
## [v0.3.7] - 2025-10-30
12+
13+
- supports MQTT
14+
15+
## [v0.3.6] - 2025-10-18
1216

1317
- supports MongoDB as a database for property persistence (see infrastructure project in README to quick-setup)
1418
- adds HTTP handlers for reading/writing multiple properties at once (which used to be supported <0.2.11 but removed later until now by mistake)

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ This implementation is based on RPC, built ground-up in python keeping both the
1515
## To Install
1616

1717
From pip - `pip install hololinked` <br>
18-
From conda - `conda install -c conda-forge hololinked`
18+
From conda - `conda install -c conda-forge hololinked` (not recommended, only upto v0.2.11 which do not have docs anymore, to be fixed in future) <br>
1919

20-
Or, clone the repository (main branch for latest codebase) and install `pip install .` / `pip install -e .`. The conda env `hololinked.yml` or [uv environment `uv.lock`](#setup-development-environment) can also help to setup all dependencies. Currently the dependencies are hard pinned to promote stability, therefore consider using a virtual environment.
20+
Or, clone the repository (main branch for latest codebase) and install `pip install .` / `pip install -e .`. The [uv environment `uv.lock`](#setup-development-environment) can also help to setup all dependencies. Currently the dependencies are hard pinned to promote stability, therefore consider using a virtual environment.
2121

2222
## Usage/Quickstart
2323

@@ -531,8 +531,8 @@ In React, the Thing Description may be fetched inside `useEffect` hook, the clie
531531
## Resources
532532
533533
- [examples repository](https://github.com/hololinked-dev/examples) - detailed examples for both clients and servers
534+
- [infrastructure components](https://github.com/hololinked-dev/daq-system-infrastructure) - docker compose files to setup postgres or mongo databases with admin interfaces, Identity and Access Management system among other components.
534535
- [helper GUI](https://github.com/hololinked-dev/thing-control-panel) - view & interact with your object's actions, properties and events.
535-
- [infrastructure components](https://github.com/hololinked-dev/daq-system-infrastructure) - docker compose files to setup postgres or mongo databases with admin interfaces, Identity and Access Management system, MQTT broker among other components.
536536
- [live demo](https://control-panel.hololinked.dev/#https://examples.hololinked.dev/simulations/oscilloscope/resources/wot-td) - an example of an oscilloscope available for live test
537537
538538
> You may use a script deployment/automation tool to remote stop and start servers, in an attempt to remotely control your hardware scripts.

hololinked.yml

Lines changed: 0 additions & 108 deletions
This file was deleted.

hololinked/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.6"
1+
__version__ = "0.3.7"

hololinked/serializers/serializers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,12 @@ def dumps(self, data) -> bytes:
181181
data = pythonjson.dumps(data, ensure_ascii=False, allow_nan=True, default=self.default)
182182
return data.encode("utf-8")
183183

184-
def dump(self, data: typing.Dict[str, typing.Any], file_desc) -> None:
184+
@classmethod
185+
def dump(cls, data: typing.Dict[str, typing.Any], file_desc) -> None:
185186
"write JSON to file"
186-
pythonjson.dump(data, file_desc, ensure_ascii=False, allow_nan=True, default=self.default)
187+
pythonjson.dump(data, file_desc, ensure_ascii=False, allow_nan=True, default=cls.default)
187188

189+
@classmethod
188190
def load(cls, file_desc) -> JSONSerializable:
189191
"load JSON from file"
190192
return pythonjson.load(file_desc)

hololinked/storage/__init__.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@
44

55

66
def prepare_object_storage(instance, **kwargs):
7-
if kwargs.get(
7+
use_json_file = kwargs.get(
88
"use_json_file", instance.__class__.use_json_file if hasattr(instance.__class__, "use_json_file") else False
9-
):
10-
filename = kwargs.get("json_filename", f"{get_a_filename_from_instance(instance, extension='json')}")
11-
instance.db_engine = ThingJSONStorage(filename=filename, instance=instance)
12-
elif kwargs.get(
9+
)
10+
use_mongo = kwargs.get(
1311
"use_mongo_db", instance.__class__.use_mongo_db if hasattr(instance.__class__, "use_mongo_db") else False
14-
):
15-
config_file = kwargs.get("db_config_file", None)
16-
instance.db_engine = MongoThingDB(instance=instance, config_file=config_file)
17-
elif kwargs.get(
12+
)
13+
json_filename = kwargs.get("json_filename", f"{get_a_filename_from_instance(instance, extension='json')}")
14+
use_default_db = kwargs.get(
1815
"use_default_db", instance.__class__.use_default_db if hasattr(instance.__class__, "use_default_db") else False
19-
):
16+
)
17+
db_config_file = kwargs.get("db_config_file", None)
18+
19+
if use_json_file:
20+
instance.db_engine = ThingJSONStorage(filename=json_filename, instance=instance)
21+
elif use_mongo:
2022
config_file = kwargs.get("db_config_file", None)
21-
instance.db_engine = ThingDB(instance=instance, config_file=config_file)
23+
instance.db_engine = MongoThingDB(instance=instance, config_file=config_file)
24+
elif use_default_db or db_config_file:
25+
instance.db_engine = ThingDB(instance=instance, config_file=db_config_file)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from typing import Literal
2+
from pydantic import BaseModel, ConfigDict, Field, model_validator
3+
from pydantic.types import StrictStr, SecretStr, StrictInt
4+
5+
6+
class SQLDBConfig(BaseModel):
7+
"""Configuration validator for SQL databases for PostgreSQL and MySQL"""
8+
9+
provider: Literal["postgresql", "mysql"] = "postgresql"
10+
"""Database provider, postgresql or mysql"""
11+
host: StrictStr = "localhost"
12+
"""PostgreSQL server host"""
13+
port: StrictInt = 5432
14+
"""server port, default 5432"""
15+
database: StrictStr = "hololinked"
16+
"""database name, default hololinked"""
17+
user: StrictStr = "hololinked"
18+
"""user name, default hololinked, recommended not to use admin user"""
19+
password: SecretStr = Field(default=SecretStr(""), repr=False)
20+
"""user password, default empty"""
21+
dialect: Literal["asyncpg", "psycopg", "asyncmy", "mysqldb", ""] = ""
22+
"""dialect to use, default psycopg for postgresql"""
23+
uri: SecretStr = ""
24+
"""Full database URI, overrides other settings, default empty"""
25+
26+
model_config = ConfigDict(extra="forbid")
27+
28+
@property
29+
def URL(self) -> str:
30+
if self.uri:
31+
return self.uri.get_secret_value()
32+
if self.provider == "postgresql":
33+
return f"postgresql{'+' + self.dialect if self.dialect else ''}://{self.user}:{self.password.get_secret_value()}@{self.host}:{self.port}/{self.database}"
34+
return f"mysql{'+' + self.dialect if self.dialect else ''}://{self.user}:{self.password.get_secret_value()}@{self.host}:{self.port}/{self.database}"
35+
36+
@model_validator(mode="after")
37+
def _at_least_one(self):
38+
if not self.uri and (not self.host or not self.port or not self.database or not self.user or not self.password):
39+
raise ValueError("Provide either database URI or all of 'host', 'port', 'database', 'user', and 'password'")
40+
return self
41+
42+
43+
class SQLiteConfig(BaseModel):
44+
"""Configuration validator for SQLite database"""
45+
46+
provider: Literal["sqlite"] = "sqlite"
47+
"""Database provider, only sqlite is supported"""
48+
dialect: SecretStr = "pysqlite"
49+
"""dialect to use, aiosqlite for async, pysqlite for sync"""
50+
file: str = ""
51+
"""SQLite database file, default is empty string, which leads to an DB with name of thing ID"""
52+
in_memory: bool = False
53+
"""Use in-memory SQLite database, default False as it is not persistent"""
54+
uri: SecretStr = ""
55+
"""Full database URI, overrides other settings, default empty"""
56+
57+
model_config = ConfigDict(extra="forbid")
58+
59+
@property
60+
def URL(self) -> str:
61+
if self.uri:
62+
return self.uri.get_secret_value()
63+
if self.in_memory:
64+
return f"sqlite+{self.dialect}:///:memory:"
65+
elif self.file:
66+
return f"sqlite+{self.dialect}:///{self.file}"
67+
raise NotImplementedError("Either 'uri' or 'file' or 'in_memory' must be provided for SQLiteConfig")
68+
69+
70+
class MongoDBConfig(BaseModel):
71+
"""Configuration validator for MongoDB database"""
72+
73+
provider: Literal["mongo"] = "mongo"
74+
"""Database provider, only mongo is supported"""
75+
host: StrictStr = "localhost"
76+
"""MongoDB server host"""
77+
port: StrictInt = 27017
78+
"""server port, default 27017"""
79+
database: StrictStr = "hololinked"
80+
"""database name, default hololinked"""
81+
user: StrictStr = ""
82+
"""user name, default empty, recommended not to use admin user"""
83+
password: SecretStr = ""
84+
"""user password, default empty"""
85+
authSource: StrictStr = ""
86+
"""authentication source database, default empty"""
87+
uri: SecretStr = ""
88+
"""Full database URI, overrides other settings, default empty"""
89+
90+
model_config = ConfigDict(extra="forbid")
91+
92+
@property
93+
def URL(self) -> str:
94+
if self.uri:
95+
return self.uri.get_secret_value()
96+
if self.user and self.password:
97+
if self.authSource:
98+
return f"mongodb://{self.user}:{self.password.get_secret_value()}@{self.host}:{self.port}/?authSource={self.authSource}"
99+
return f"mongodb://{self.user}:{self.password.get_secret_value()}@{self.host}:{self.port}/"
100+
return f"mongodb://{self.host}:{self.port}/"
101+
102+
@model_validator(mode="after")
103+
def _at_least_one(self):
104+
if not self.uri and (not self.host or not self.port or not self.database or not self.user or not self.password):
105+
raise ValueError("Provide either database URI or all of 'host', 'port', 'database', 'user', and 'password'")
106+
return self

0 commit comments

Comments
 (0)