Skip to content

Commit fafa5d4

Browse files
authored
Merge pull request #25 from OpenMined/chore/persist-settings
persist public url in settings
2 parents 11f6ca6 + cd97cf0 commit fafa5d4

6 files changed

Lines changed: 122 additions & 15 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""add settings table
2+
3+
Revision ID: e18675e6eecb
4+
Revises: 9b73c6b77fa4
5+
Create Date: 2026-01-13 12:39:41.971893
6+
7+
"""
8+
9+
from collections.abc import Sequence
10+
11+
import sqlalchemy as sa
12+
import sqlmodel # noqa: F401 - Added for SQLModel support
13+
from alembic import op
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "e18675e6eecb"
17+
down_revision: str | None = "9b73c6b77fa4"
18+
branch_labels: str | Sequence[str] | None = None
19+
depends_on: str | Sequence[str] | None = None
20+
21+
22+
def upgrade() -> None:
23+
op.create_table(
24+
"settings",
25+
sa.Column("id", sa.Integer(), nullable=False),
26+
sa.Column("public_url", sa.String(), nullable=True),
27+
sa.Column("updated_at", sa.DateTime(), nullable=False),
28+
sa.PrimaryKeyConstraint("id"),
29+
)
30+
31+
32+
def downgrade() -> None:
33+
op.drop_table("settings")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
"""Settings component for public URL management."""
22

3+
from syftai_space.components.settings.entities import Settings
34
from syftai_space.components.settings.handlers import SettingsHandler
5+
from syftai_space.components.settings.repository import SettingsRepository
46
from syftai_space.components.settings.schemas import (
57
PublicUrlResponse,
68
UpdatePublicUrlRequest,
79
)
810

911
__all__ = [
12+
"Settings",
1013
"SettingsHandler",
14+
"SettingsRepository",
1115
"PublicUrlResponse",
1216
"UpdatePublicUrlRequest",
1317
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Settings database entities."""
2+
3+
from datetime import datetime, timezone
4+
5+
from sqlmodel import Field, SQLModel
6+
7+
8+
class Settings(SQLModel, table=True):
9+
"""Application settings entity - singleton row (id=1)."""
10+
11+
__tablename__ = "settings"
12+
13+
id: int = Field(default=1, primary_key=True)
14+
public_url: str | None = Field(
15+
default=None, description="Public URL for the SyftAI Space"
16+
)
17+
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

backend/syftai_space/components/settings/handlers.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pydantic import HttpUrl
55

66
from syftai_space.components.marketplaces.handlers import MarketplaceHandler
7+
from syftai_space.components.settings.repository import SettingsRepository
78
from syftai_space.components.settings.schemas import PublicUrlResponse
89
from syftai_space.components.shared.syfthub_client import SyftHubClient, SyftHubError
910
from syftai_space.components.tenants.entities import Tenant
@@ -14,33 +15,37 @@ class SettingsHandler:
1415
"""Handler for settings business logic."""
1516

1617
def __init__(
17-
self, marketplace_handler: MarketplaceHandler, config: AppSettings
18+
self,
19+
settings_repository: SettingsRepository,
20+
marketplace_handler: MarketplaceHandler,
21+
config: AppSettings,
1822
) -> None:
1923
"""Initialize the settings handler.
2024
2125
Args:
26+
settings_repository: Repository for settings persistence
2227
marketplace_handler: Marketplace handler for syncing to SyftHub
2328
config: Application settings
2429
"""
30+
self.settings_repository = settings_repository
2531
self.marketplace_handler = marketplace_handler
2632
self.config = config
2733

2834
def get_public_url(self) -> PublicUrlResponse:
29-
"""Get the current public URL.
35+
"""Get the current public URL from database (source of truth).
3036
3137
Returns:
3238
Public URL response
3339
"""
34-
return PublicUrlResponse(
35-
public_url=str(self.config.public_url) if self.config.public_url else None
36-
)
40+
settings = self.settings_repository.get_settings()
41+
return PublicUrlResponse(public_url=settings.public_url)
3742

3843
def update_public_url(
3944
self, tenant: Tenant, new_url: HttpUrl | str
4045
) -> PublicUrlResponse:
4146
"""Update the public URL.
4247
43-
Updates the local config and syncs to SyftHub marketplace.
48+
Updates the database (source of truth) and syncs to SyftHub marketplace.
4449
4550
Args:
4651
tenant: Tenant context
@@ -52,19 +57,18 @@ def update_public_url(
5257
Raises:
5358
HTTPException: If sync to marketplace fails
5459
"""
55-
# Convert to HttpUrl if str, then store
56-
if isinstance(new_url, str):
57-
new_url = HttpUrl(new_url)
60+
# Convert to string for storage
61+
url_str = str(new_url) if new_url else None
5862

59-
# Update local config
60-
self.config.public_url = new_url
63+
# Update database (source of truth)
64+
self.settings_repository.update_public_url(url_str)
6165

6266
# Sync to SyftHub if marketplace is configured
6367
try:
6468
marketplace = self.marketplace_handler.get_default_marketplace(tenant)
6569
with SyftHubClient(str(marketplace.url)) as syfthub:
6670
syfthub.login(marketplace.email, marketplace.password)
67-
syfthub.update_profile(domain=str(new_url))
71+
syfthub.update_profile(domain=url_str)
6872
except HTTPException as e:
6973
if e.status_code == 404:
7074
# No marketplace configured, just update local config
@@ -77,4 +81,12 @@ def update_public_url(
7781
detail=f"Failed to sync public URL to marketplace: {e.message}",
7882
) from e
7983

80-
return PublicUrlResponse(public_url=str(new_url))
84+
return PublicUrlResponse(public_url=url_str)
85+
86+
def initialize_from_config(self) -> None:
87+
"""Initialize settings from config on startup.
88+
89+
If SYFT_PUBLIC_URL env var is set, it overwrites the database value.
90+
"""
91+
if self.config.public_url:
92+
self.settings_repository.update_public_url(str(self.config.public_url))
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Settings repository for database operations."""
2+
3+
from datetime import datetime, timezone
4+
5+
from syftai_space.components.settings.entities import Settings
6+
from syftai_space.components.shared.database import BaseRepository, Database
7+
8+
9+
class SettingsRepository(BaseRepository[Settings]):
10+
"""Repository for Settings CRUD operations."""
11+
12+
def __init__(self, db: Database):
13+
super().__init__(db, Settings)
14+
15+
def get_settings(self) -> Settings:
16+
"""Get the singleton settings row, creating if not exists."""
17+
settings = self.get_by_id(1)
18+
if not settings:
19+
settings = self.create(Settings(id=1))
20+
return settings
21+
22+
def update_public_url(self, url: str | None) -> Settings:
23+
"""Update the public_url setting."""
24+
settings = self.get_settings()
25+
settings.public_url = url
26+
settings.updated_at = datetime.now(timezone.utc)
27+
return self.update(settings)

backend/syftai_space/main.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
from syftai_space.components.settings.handlers import SettingsHandler
7373

7474
# Import settings components
75+
from syftai_space.components.settings.repository import SettingsRepository
7576
from syftai_space.components.settings.routes import build_settings_routes
7677

7778
# Import database
@@ -110,7 +111,11 @@ async def lifespan(app: FastAPI):
110111
logger.info(f"📡 Public URL: {public_url}")
111112
logger.info(f"🔗 Local URL: http://localhost:{port}")
112113
logger.info("=" * 70 + "\n")
113-
app_settings.public_url = public_url
114+
115+
# Update database via settings handler (source of truth)
116+
settings_handler = getattr(app.state, "settings_handler", None)
117+
if settings_handler:
118+
settings_handler.settings_repository.update_public_url(public_url)
114119

115120
except Exception as e:
116121
logger.error(f"⚠️ Warning: Failed to start ngrok tunnel: {e}")
@@ -243,7 +248,15 @@ async def lifespan(app: FastAPI):
243248
marketplace_repository=marketplace_repository,
244249
)
245250
tenant_handler = TenantHandler(tenant_repository)
246-
settings_handler = SettingsHandler(marketplace_handler, app_settings)
251+
252+
# Initialize settings repository and handler
253+
settings_repository = SettingsRepository(database)
254+
settings_handler = SettingsHandler(
255+
settings_repository, marketplace_handler, app_settings
256+
)
257+
258+
# Initialize settings from config on startup (env var overwrites DB if set)
259+
settings_handler.initialize_from_config()
247260

248261
# Initialize ingestion manager and handler
249262
ingestion_manager = IngestionManager(
@@ -260,6 +273,7 @@ async def lifespan(app: FastAPI):
260273
provisioner_manager = ProvisionerManager(dataset_handler)
261274
app.state.provisioner_manager = provisioner_manager
262275
app.state.ingestion_manager = ingestion_manager
276+
app.state.settings_handler = settings_handler
263277

264278
# Add tenant middleware (after CORS, before routes)
265279
app.add_middleware(TenantMiddleware, tenant_repository=tenant_repository)

0 commit comments

Comments
 (0)