Skip to content

Commit c9f4fdd

Browse files
feat: Add support for Demo DB (#237)
* feat: Add support for Demo DB Enable a Demo DB to trial the application without the need to introduce credentials. Disabled NVIDIA: There seems to be a bug generating Demos with stock that went through a split scenario * feat: Enhance demo database functionality and update handling --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent a971908 commit c9f4fdd

35 files changed

+972
-151
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ coverage.xml
6060
*.log
6161
local_settings.py
6262
*.sqlite3
63+
# Exception: Allow bundled demo database template for distribution
64+
!src/stonks_overwatch/fixtures/demo_db.sqlite3
6365
*.sqlite3-journal
6466

6567
# Flask stuff:

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ _This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) a
1010

1111
### Added
1212

13+
- **Application:**
14+
- Added "Demo Mode" for exploring the application without a broker connection
1315
- **Dividends**:
1416
- Added diversification filter by year
1517
- Added calendar navigation bars to quickly switch between years
@@ -68,6 +70,7 @@ _This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) a
6870
- Added tooltip showing the Gross/Tax dividend breakdown
6971
- **Application:**
7072
- Added "Release Notes" information
73+
- Added "Demo Mode" for exploring the application without a broker connection
7174

7275
### Changed
7376

docs/Developing-Stonks-Overwatch.md

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Developing Stonks Overwatch
22

3-
Stonks Overwatch is a portfolio tracker integrating with multiple brokers (DeGiro, Bitvavo, IBKR) using a **unified modern architecture** (2025). The system features factory patterns, dependency injection, interface-based design, and a centralized broker registry that dramatically simplifies development and maintenance.
3+
Stonks Overwatch is a portfolio tracker integrating with multiple brokers (DeGiro, Bitvavo, IBKR) using a
4+
**unified modern architecture** (2025). The system features factory patterns, dependency injection,
5+
interface-based design, and a centralized broker registry that dramatically simplifies development and maintenance.
46

57
## First Steps
68

@@ -151,7 +153,7 @@ for broker in ["degiro", "bitvavo", "ibkr"]:
151153
print(f"❌ {broker} {service_type.value}: {e}")
152154
```
153155
154-
The demo database can use used with `make run demo=true`
156+
The demo database can be used with `make run demo=true`. The application will automatically route all database operations to the demo database using the built-in database routing system.
155157
156158
```shell
157159
make briefcase-package
@@ -232,15 +234,112 @@ The modern configuration supports multiple brokers:
232234
233235
### Create a Demo Database
234236
237+
The demo database allows users to explore the application features without connecting to real broker accounts. It contains synthetic transaction data and market information for demonstration purposes.
238+
239+
#### For Developers: Generating the Demo Database
240+
241+
To regenerate the demo database from scratch:
242+
243+
```shell
244+
poetry run python -m scripts.generate_demo_db \
245+
--start-date "2024-01-01" \
246+
--num-transactions 150 \
247+
--initial-deposit 10000 \
248+
--monthly-deposit 500
249+
```
250+
251+
This command will:
252+
1. Create a fresh demo database with synthetic transaction data
253+
2. Generate realistic market data for popular stocks and ETFs
254+
3. Copy the database to `src/stonks_overwatch/fixtures/demo_db.sqlite3` for bundling with Briefcase distributions
255+
4. The bundled template should be committed to version control
256+
257+
For more details on available parameters:
258+
235259
```shell
236-
poetry run python ./scripts/generate_demo_db.py --help
260+
poetry run python -m scripts.generate_demo_db --help
237261
```
238262
239-
This command will create a demo database with sample data for multiple brokers. It is useful for testing purposes or to showcase the application without needing real broker accounts.
263+
> **Important**: After generating a new demo database, commit the updated `src/stonks_overwatch/fixtures/demo_db.sqlite3` file to git. This ensures the latest demo data is bundled with all distributions.
264+
265+
#### For Users: Demo Mode in the Native App
266+
267+
When users activate demo mode via the application menu:
268+
1. The application checks if a demo database exists in the user's data directory
269+
2. If the bundled demo database is different (detected by comparing SHA256 hashes):
270+
- The existing demo database is backed up to `demo_db.sqlite3.backup`
271+
- The new demo database is copied from the application bundle
272+
- This ensures users always get the latest demo data after app updates
273+
3. The application switches to demo mode and applies any pending migrations
274+
4. All broker API connections are disabled in demo mode
275+
276+
The demo database is distributed as a pre-populated SQLite file (approximately 384KB), providing instant access to demo features.
277+
278+
> **Note**: When updating the application to a new version with updated demo data, the old demo database is automatically backed up. Users' actual portfolio data in the production database is never affected by demo mode operations.
279+
280+
#### Demo Database Location
281+
282+
- **Bundled template**: `src/stonks_overwatch/fixtures/demo_db.sqlite3` (read-only, in git)
283+
- **User's working copy**: `$STONKS_OVERWATCH_DATA_DIR/demo_db.sqlite3` (created on first demo activation)
284+
285+
The demo database can be used with `make run demo=true`. The application will automatically route all database operations to the demo database using the built-in database routing system.
286+
287+
### Demo Mode Database Routing
288+
289+
The application features an advanced database routing system that allows seamless switching between production and demo databases without requiring server restarts.
290+
291+
#### How It Works
292+
293+
The application supports two database configurations:
294+
- **Production Database** (`db.sqlite3`): Contains real user data
295+
- **Demo Database** (`demo_db.sqlite3`): Contains demo/sample data for testing
240296
241-
> This script expects the `config/config.json` file to be present and properly configured.
297+
The `DatabaseRouter` class automatically routes all database operations to the appropriate database based on the `DEMO_MODE` environment variable:
298+
299+
- When `DEMO_MODE=False` (or unset): Routes to the production database
300+
- When `DEMO_MODE=True`: Routes to the demo database
301+
302+
#### Database Configuration
303+
304+
Both databases are defined in `settings.py`:
305+
306+
```python
307+
DATABASES = {
308+
"default": {
309+
"ENGINE": "django.db.backends.sqlite3",
310+
"NAME": Path(STONKS_OVERWATCH_DATA_DIR).resolve().joinpath("db.sqlite3"),
311+
# ... production database settings
312+
},
313+
"demo": {
314+
"ENGINE": "django.db.backends.sqlite3",
315+
"NAME": Path(STONKS_OVERWATCH_DATA_DIR).resolve().joinpath("demo_db.sqlite3"),
316+
# ... demo database settings
317+
},
318+
}
319+
320+
DATABASE_ROUTERS = ["stonks_overwatch.utils.database.db_router.DatabaseRouter"]
321+
```
322+
323+
#### Benefits of Database Routing
324+
325+
1. **No Server Restart Required**: Database switching happens instantly
326+
2. **Data Isolation**: Production and demo data are completely separate
327+
3. **Transparent Operation**: All existing code works without modification
328+
4. **Consistent Schema**: Both databases maintain the same structure through migrations
329+
330+
#### Migration Handling
331+
332+
Both databases support migrations independently:
333+
334+
```shell
335+
# Apply migrations to production database
336+
python manage.py migrate --database=default
337+
338+
# Apply migrations to demo database
339+
python manage.py migrate --database=demo
340+
```
242341
243-
The demo database can be used with `make run demo=true`
342+
The router ensures migrations can be applied to both databases as needed, maintaining schema consistency.
244343
245344
## Dump and Load a Database
246345
@@ -275,7 +374,7 @@ Example configuration to enable offline mode for multiple brokers:
275374
}
276375
```
277376
278-
The offline mode can be used together with `demo=true` to load the demo database and run the application without any external API calls.
377+
The offline mode can be used together with `demo=true` to load the demo database and run the application without any external API calls. The database routing system ensures that demo data is completely isolated from production data, making it safe to experiment with different configurations.
279378
280379
### Broker-Specific Development
281380

scripts/dump_db.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import argparse
1616
import os
17-
from pathlib import Path
1817

1918
# Django setup
2019
from django.core import serializers
@@ -26,17 +25,20 @@
2625
# Set up Django environment and logging
2726
setup_script_environment()
2827

29-
from stonks_overwatch.settings import STONKS_OVERWATCH_DATA_DIR # noqa: E402
28+
from stonks_overwatch.settings import DATABASES # noqa: E402
3029
from stonks_overwatch.utils.database.db_utils import dump_database # noqa: E402
3130

3231

33-
def load_database(input_file):
32+
def load_database(input_file, database="default"):
3433
"""Load database content from the JSON file"""
3534
if not os.path.exists(input_file):
3635
print(f"Error: Input file {input_file} not found")
3736
return
3837

39-
existing_db_file = Path(STONKS_OVERWATCH_DATA_DIR).resolve().joinpath("db.sqlite3")
38+
if database == "demo":
39+
os.environ["DEMO_MODE"] = "True"
40+
41+
existing_db_file = DATABASES[database]["NAME"]
4042

4143
if os.path.exists(existing_db_file):
4244
print(f"Warning: Existing database file '{existing_db_file}' found.")
@@ -61,7 +63,7 @@ def load_database(input_file):
6163
return
6264

6365
print("Creating new database...")
64-
call_command("migrate")
66+
call_command("migrate", database=database)
6567

6668
print(f"Loading database from {input_file}...")
6769

@@ -70,11 +72,11 @@ def load_database(input_file):
7072
with open(input_file, "r", encoding="utf-8") as f:
7173
data = f.read()
7274

73-
# Deserialize and save
75+
# Deserialize and save to the correct database
7476
objects_loaded = 0
75-
with transaction.atomic():
77+
with transaction.atomic(using=database):
7678
for obj in serializers.deserialize("json", data):
77-
obj.save()
79+
obj.save(using=database)
7880
objects_loaded += 1
7981

8082
print(f"Successfully loaded {objects_loaded} objects from {input_file}")
@@ -91,10 +93,24 @@ def main():
9193
# Dump command
9294
dump_parser = subparsers.add_parser("dump", help="Dump database to file")
9395
dump_parser.add_argument("--output", "-o", default="db_dump.zip", help="Output file name (default: db_dump.zip)")
96+
dump_parser.add_argument(
97+
"--database",
98+
"-d",
99+
default="default",
100+
choices=["default", "demo"],
101+
help="Database to use (default: default, options: default, demo)",
102+
)
94103

95104
# Load command
96105
load_parser = subparsers.add_parser("load", help="Load database from file")
97106
load_parser.add_argument("--input", "-i", required=True, help="Input file name")
107+
load_parser.add_argument(
108+
"--database",
109+
"-d",
110+
default="default",
111+
choices=["default", "demo"],
112+
help="Database to use (default: default, options: default, demo)",
113+
)
98114

99115
args = parser.parse_args()
100116

@@ -103,9 +119,9 @@ def main():
103119
return
104120

105121
if args.command == "dump":
106-
dump_database(output_file=args.output)
122+
dump_database(output_file=args.output, database=args.database)
107123
elif args.command == "load":
108-
load_database(input_file=args.input)
124+
load_database(input_file=args.input, database=args.database)
109125

110126

111127
if __name__ == "__main__":

0 commit comments

Comments
 (0)