Skip to content

Conversation

@ShawnLord-Tovala
Copy link

Summary

This PR adds support for separate database routing for frontend and backend, enabling HardPy to work seamlessly in containerized environments where the backend and frontend (browser) need different database connection strings.

Problem

When running HardPy in Docker containers, the backend and frontend often need different database connection paths:

  • Backend (test runner) connects via Docker's internal networking (e.g., couchdb:5984)
  • Frontend (browser) must connect via host-accessible addresses (e.g., localhost:5984)

Previously, both used the same [database] config, making containerized deployments difficult.

Solution

Added an optional database_frontend configuration field that:

  • Falls back to database config when not specified (backward compatible)
  • Allows specifying a separate connection string for frontend when needed
  • Enables proper Docker/Kubernetes deployments

Changes

1. Config Enhancement (hardpy/common/config.py)

  • Added database_frontend: DatabaseConfig | None = Field(default=None)
  • Added fallback logic in model_post_init() to copy database config when database_frontend is not specified
  • Maintains full backward compatibility

2. API Update (hardpy/hardpy_panel/api.py)

  • Modified /api/couch endpoint to use database_frontend.url instead of database.url
  • Frontend already uses this endpoint via getSyncURL() in index.tsx
  • No frontend code changes needed

3. Complete Example (examples/frontend_database_routing/)

  • Working example demonstrating containerized use case
  • README explaining when and why to use this feature
  • Docker compose configuration showing typical setup
  • Sample hardpy.toml with both database configs

Backward Compatibility

Fully backward compatible - existing configs without [database_frontend] section work unchanged
All existing tests pass (214 passed, 9 deselected)
Linting clean with ruff 0.8.0

Testing

  • All 214 unit tests pass
  • Linting passes with ruff 0.8.0
  • Config fallback logic tested
  • API endpoint verified

Use Cases

  1. Docker/Kubernetes deployments - Different network paths for backend vs frontend
  2. Read replicas - Frontend connects to read-only replica for performance
  3. Network separation - Different network zones for test execution vs UI
  4. Development environments - Backend in container, frontend from host browser

Example Configuration

Minimal (uses single database for both):

[database]
user = "dev"
password = "dev"
host = "localhost"
port = 5984

With separate frontend routing:

# Backend database (Docker internal networking)
[database]
user = "dev"
password = "dev"
host = "couchdb"  # Docker service name
port = 5984

# Frontend database (host-accessible)
[database_frontend]
user = "dev"
password = "dev"
host = "localhost"  # Accessible from browser
port = 5984

If [database_frontend] is omitted, it automatically uses [database] values.

Implementation Details

The feature works through these components:

  1. Config reads TOML and sets database_frontend (or falls back to database)
  2. API endpoint /api/couch returns database_frontend.url
  3. Frontend getSyncURL() fetches from /api/couch and connects to PouchDB
  4. Backend always uses database.url for test execution

This clean separation allows different routing without duplicating code or breaking existing functionality.

ShawnLord-Tovala and others added 3 commits December 12, 2025 06:06
When database_frontend is not specified in hardpy.toml, it now
automatically falls back to using the same configuration as database.

This allows users to:
- Use a single database config by default (no database_frontend needed)
- Optionally specify a separate frontend database when needed

Implementation:
- Changed database_frontend to Optional (DatabaseConfig | None)
- Added fallback logic in model_post_init
- If database_frontend is None, copies database config values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
This example demonstrates how to configure separate database routing
for backend and frontend in containerized environments.

Use Case:
When running HardPy in Docker containers, the backend and frontend
often need different database connection strings:
- Backend connects via Docker internal networking (e.g., "couchdb:5984")
- Frontend (browser) connects via host exposed ports (e.g., "localhost:5984")

Files:
- hardpy.toml: Shows database and database_frontend configuration
- docker-compose.yaml: Example container setup demonstrating the routing
- README.md: Explains when and why to use this configuration
- test_simple.py: Basic test file
- pytest.ini: Pytest configuration for container environment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Modified the /api/couch endpoint to use database_frontend.url instead
of database.url. This ensures the frontend (browser) connects to the
correct database in containerized environments.

The frontend already fetches the database connection through this
API endpoint (getSyncURL in index.tsx), so no frontend code changes
are needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@ShawnLord-Tovala ShawnLord-Tovala marked this pull request as ready for review December 12, 2025 06:21
@xorialexandrov xorialexandrov self-requested a review December 12, 2025 07:17
title: str = "HardPy TOML config"
tests_name: str = ""
database: DatabaseConfig = DatabaseConfig()
database_frontend: DatabaseConfig | None = Field(default=None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion for changes!

To ensure compatibility with future features, I recommend making the following slight modifications to this file and to api.py:

class DatabaseConfig(BaseModel):
    """Database configuration."""

    model_config = ConfigDict(extra="forbid")

    user: str = "dev"
    password: str = "dev"
    host: str = "localhost"
    port: int = 5984
    frontend_host: str = "localhost"
    frontend_port: int = 5984
    doc_id: str = Field(exclude=True, default="")
    url: str = Field(exclude=True, default="")
    frontend_url: str = Field(exclude=True, default="")

    def model_post_init(self, __context) -> None:  # noqa: ANN001,PYI063
        """Get database connection url."""
        self.url = self.get_url(self.host, self.port)
        self.frontend_url = self.get_url(self.frontend_host, self.frontend_port)

    def get_url(self, host: str, port: int) -> str:
        """Get database connection url.

        Returns:
            str: database connection url
        """
        credentials = f"{self.user}:{self.password}"
        uri = f"{host}:{port!s}"
        return f"http://{credentials}@{uri}/"
class HardpyConfig(BaseModel, extra="allow"):
    """HardPy configuration."""

    model_config = ConfigDict(extra="forbid")

    title: str = "HardPy TOML config"
    tests_name: str = ""
    database: DatabaseConfig = DatabaseConfig()
    frontend: FrontendConfig = FrontendConfig()
    stand_cloud: StandCloudConfig = StandCloudConfig()
    current_test_config: str = ""
    test_configs: list[TestConfig] = []

    def model_post_init(self, __context) -> None:  # noqa: ANN001,PYI063
        """Get database document name and set database_frontend fallback."""
        self.database.doc_id = self.get_doc_id()
# init_config() function

        self._config.tests_name = tests_name
        self._config.frontend.host = frontend_host
        self._config.frontend.port = frontend_port
        self._config.frontend.language = frontend_language
        self._config.database.user = database_user
        self._config.database.password = database_password
        self._config.database.host = database_host
        self._config.database.port = database_port
        self._config.database.frontend_host = database_host
        self._config.database.frontend_port = database_port
        self._config.database.doc_id = self._config.get_doc_id()
        self._config.database.url = self._config.database.get_url(
            self._config.database.host, 
            self._config.database.port,
        )
        self._config.database.frontend_url = self._config.database.get_url(
            self._config.database.frontend_host, 
            self._config.database.frontend_port,
        )
        self._config.stand_cloud.address = sc_address
        self._config.stand_cloud.connection_only = sc_connection_only
        self._config.stand_cloud.autosync = sc_autosync
        self._config.stand_cloud.api_key = sc_api_key

api.py

@app.get("/api/couch")
def couch_connection() -> dict:
    """Get couchdb connection string for frontend.

    Returns:
        dict[str, str]: couchdb connection string
    """
    config_manager = ConfigManager()

    return {
        "connection_str": config_manager.config.database.frontend_url,
    }

These changes will affect the hardpy.toml file and require a modification to the example, though not a significant one:

[database]
user = "dev"
password = "dev"
host = "couchdb"
port = 5984
frontend_host = "localhost"
frontend_port = 5984

# HardPy backend running tests
# This would be your test runner container (example only)
hardpy:
image: hardpy:latest # Your HardPy image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you provide an example of a Dockerfile for creating a Docker image to work with HardPy? You could then use it directly in the example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants