Skip to content

Commit 6c25e78

Browse files
committed
feat: add runtime configuration override capability
- Add SurranticConfig class for runtime configuration - Add tests for configuration override - Update documentation - Bump version to 0.1.1
1 parent 5fb9b56 commit 6c25e78

File tree

5 files changed

+160
-10
lines changed

5 files changed

+160
-10
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,37 @@ print(f"Updated at: {user.updated}") # Automatically set
8686

8787
## Configuration
8888

89+
### Database Connection
90+
91+
By default, Surrantic uses environment variables for database configuration:
92+
93+
```bash
94+
SURREAL_ADDRESS=ws://localhost:8000
95+
SURREAL_USER=root
96+
SURREAL_PASS=root
97+
SURREAL_NAMESPACE=test
98+
SURREAL_DATABASE=test
99+
```
100+
101+
You can also override these settings directly in your code using `SurranticConfig`:
102+
103+
```python
104+
from surrantic import SurranticConfig
105+
106+
# Override all or some of the connection settings
107+
SurranticConfig.configure(
108+
address="ws://mydb:8000",
109+
user="myuser",
110+
password="mypass",
111+
namespace="myns",
112+
database="mydb"
113+
)
114+
115+
# Your models will now use the new configuration
116+
user = User(name="John", email="john@example.com")
117+
await user.asave() # Uses the custom configuration
118+
```
119+
89120
### Logging
90121

91122
Surrantic includes configurable logging:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "surrantic"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "A simple Pydantic ORM implementation for SurrealDB"
55
readme = "README.md"
66
authors = [

src/surrantic/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from .base import ObjectModel
1+
from .base import ObjectModel, SurranticConfig
22

3-
__all__ = ["ObjectModel"]
3+
__all__ = ["ObjectModel", "SurranticConfig"]

src/surrantic/base.py

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,56 @@ def _prepare_data(obj: BaseModel) -> str:
4545
items.append(f"{field_name}: {_prepare_value(value)}")
4646
return "{ " + ", ".join(items) + " }"
4747

48+
class SurranticConfig:
49+
"""Configuration class for Surrantic database connection.
50+
51+
This class allows overriding the default database configuration that would
52+
otherwise be loaded from environment variables.
53+
"""
54+
_instance = None
55+
56+
def __init__(self):
57+
self.address = SURREAL_ADDRESS
58+
self.user = SURREAL_USER
59+
self.password = SURREAL_PASS
60+
self.namespace = SURREAL_NAMESPACE
61+
self.database = SURREAL_DATABASE
62+
63+
@classmethod
64+
def get_instance(cls) -> 'SurranticConfig':
65+
"""Get the singleton instance of SurranticConfig"""
66+
if cls._instance is None:
67+
cls._instance = cls()
68+
return cls._instance
69+
70+
@classmethod
71+
def configure(cls,
72+
address: Optional[str] = None,
73+
user: Optional[str] = None,
74+
password: Optional[str] = None,
75+
namespace: Optional[str] = None,
76+
database: Optional[str] = None) -> None:
77+
"""Configure the database connection parameters.
78+
79+
Args:
80+
address: The SurrealDB server address
81+
user: The username for authentication
82+
password: The password for authentication
83+
namespace: The namespace to use
84+
database: The database to use
85+
"""
86+
config = cls.get_instance()
87+
if address is not None:
88+
config.address = address
89+
if user is not None:
90+
config.user = user
91+
if password is not None:
92+
config.password = password
93+
if namespace is not None:
94+
config.namespace = namespace
95+
if database is not None:
96+
config.database = database
97+
4898
class ObjectModel(BaseModel):
4999
"""Base model class for SurrealDB objects with CRUD operations.
50100
@@ -82,11 +132,12 @@ async def _get_db(cls) -> AsyncGenerator[AsyncSurrealDB, None]:
82132
Yields:
83133
AsyncSurrealDB: The configured database connection
84134
"""
85-
db = AsyncSurrealDB(url=SURREAL_ADDRESS)
135+
config = SurranticConfig.get_instance()
136+
db = AsyncSurrealDB(url=config.address)
86137
try:
87138
await db.connect()
88-
await db.sign_in(SURREAL_USER, SURREAL_PASS)
89-
await db.use(SURREAL_NAMESPACE, SURREAL_DATABASE)
139+
await db.sign_in(config.user, config.password)
140+
await db.use(config.namespace, config.database)
90141
logger.debug("Database connection established")
91142
yield db
92143
finally:
@@ -99,13 +150,14 @@ def _get_sync_db(cls) -> Generator[SurrealDB, None, None]:
99150
"""Get a configured synchronous database connection as a context manager.
100151
101152
Yields:
102-
SurrealDB: The configured synchronous database connection
153+
SurrealDB: The configured database connection
103154
"""
104-
db = SurrealDB(SURREAL_ADDRESS)
155+
config = SurranticConfig.get_instance()
156+
db = SurrealDB(url=config.address)
105157
try:
106158
db.connect()
107-
db.sign_in(SURREAL_USER, SURREAL_PASS)
108-
db.use(SURREAL_NAMESPACE, SURREAL_DATABASE)
159+
db.sign_in(config.user, config.password)
160+
db.use(config.namespace, config.database)
109161
logger.debug("Database connection established")
110162
yield db
111163
finally:

tests/test_base.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,70 @@ async def test_aget_all_with_ordering(mock_async_db: AsyncMock) -> None:
201201
await TestModel.aget_all(order_by="name", order_direction="DESC")
202202

203203
mock_async_db.query.assert_called_once_with("SELECT * FROM test_table ORDER BY name DESC")
204+
205+
def test_surrantic_config_singleton():
206+
from surrantic.base import SurranticConfig
207+
208+
config1 = SurranticConfig.get_instance()
209+
config2 = SurranticConfig.get_instance()
210+
211+
assert config1 is config2
212+
213+
def test_surrantic_config_default_values():
214+
from surrantic.base import SurranticConfig, SURREAL_ADDRESS, SURREAL_USER, SURREAL_PASS, SURREAL_NAMESPACE, SURREAL_DATABASE
215+
216+
config = SurranticConfig.get_instance()
217+
218+
assert config.address == SURREAL_ADDRESS
219+
assert config.user == SURREAL_USER
220+
assert config.password == SURREAL_PASS
221+
assert config.namespace == SURREAL_NAMESPACE
222+
assert config.database == SURREAL_DATABASE
223+
224+
def test_surrantic_config_override():
225+
from surrantic.base import SurranticConfig
226+
227+
# Store original values
228+
config = SurranticConfig.get_instance()
229+
original_address = config.address
230+
original_user = config.user
231+
232+
# Override some values
233+
SurranticConfig.configure(
234+
address="ws://testdb:8000",
235+
user="testuser"
236+
)
237+
238+
# Check overridden values
239+
assert config.address == "ws://testdb:8000"
240+
assert config.user == "testuser"
241+
242+
# Check non-overridden values remain the same
243+
assert config.password == original_user
244+
245+
# Reset for other tests
246+
SurranticConfig.configure(
247+
address=original_address,
248+
user=original_user
249+
)
250+
251+
@pytest.mark.asyncio
252+
async def test_db_connection_uses_config(mock_async_db: AsyncMock):
253+
from surrantic.base import SurranticConfig
254+
255+
# Configure custom connection details
256+
SurranticConfig.configure(
257+
address="ws://testdb:8000",
258+
user="testuser",
259+
password="testpass",
260+
namespace="testns",
261+
database="testdb"
262+
)
263+
264+
model = TestModel(name="Test", age=25)
265+
await model.asave()
266+
267+
# Verify the connection was made with our custom config
268+
mock_async_db.connect.assert_called_once()
269+
mock_async_db.sign_in.assert_called_once_with("testuser", "testpass")
270+
mock_async_db.use.assert_called_once_with("testns", "testdb")

0 commit comments

Comments
 (0)