# Configuring `pydase`

## Do I Need to Configure My `pydase` Service?

`pydase` services work out of the box without requiring any configuration. However, you
might want to change some options, such as the web server port or logging level. To 
accommodate such customizations, `pydase` allows configuration through environment 
variables - avoiding hard-coded settings in your service code.

Why should you avoid hard-coding configurations? Here are two reasons:

1. **Security**:  
    Protect sensitive information, such as usernames and passwords. By using environment
    variables, your service code can remain public while keeping private information 
    secure.

2. **Reusability**:  
    Services often need to be reused in different environments. For example, you might
    deploy multiple instances of a service (e.g., for different sensors in a lab). By 
    separating configuration from code, you can adapt the service to new requirements 
    without modifying its codebase.

Next, we’ll walk you through the environment variables `pydase` supports and provide an
example of how to separate service code from configuration.

## Configuring `pydase` Using Environment Variables

`pydase` provides the following environment variables for customization:

- **`ENVIRONMENT`**:  
  Defines the operation mode (`"development"` or `"production"`), which influences 
  behaviour such as logging (see [Logging in pydase](https://github.com/tiqi-group/pydase?tab=readme-ov-file#logging-in-pydase)).

- **`SERVICE_CONFIG_DIR`**:  
  Specifies the directory for configuration files (e.g., `web_settings.json`). Defaults
  to the `config` folder in the service root. Access this programmatically using:

    ```python
    import pydase.config
    pydase.config.ServiceConfig().config_dir
    ```

- **`SERVICE_WEB_PORT`**:  
  Defines the web server’s port. Ensure each service on the same host uses a unique 
  port. Default: `8001`.

- **`GENERATE_WEB_SETTINGS`**:  
  When `true`, generates or updates the `web_settings.json` file. Existing entries are 
  preserved, and new entries are appended.

### Configuring `pydase` via Keyword Arguments

Some settings can also be overridden directly in your service code using keyword 
arguments when initializing the server. This allows for flexibility in code-based 
configuration:

```python
import pathlib
from pydase import Server
from your_service_module import YourService

server = Server(
    YourService(),
    web_port=8080,                             # Overrides SERVICE_WEB_PORT
    config_dir=pathlib.Path("custom_config"),  # Overrides SERVICE_CONFIG_DIR
    generate_web_settings=True                 # Overrides GENERATE_WEB_SETTINGS
).run()
```

## Separating Service Code from Configuration

To decouple configuration from code, `pydase` utilizes `confz` for configuration 
management. Below is an example that demonstrates how to configure a `pydase` service 
for a sensor readout application.

### Scenario: Configuring a Sensor Service

Imagine you have multiple sensors distributed across your lab. You need to configure 
each service instance with:

1. **Hostname**: The hostname or IP address of the sensor.
2. **Authentication Token**: A token or credentials to authenticate with the sensor.
3. **Readout Interval**: A periodic interval to read sensor data and log it to a 
  database.

Given the repository structure:

```bash title="Service Repository Structure"
my_sensor
├── pyproject.toml        
├── README.md             
└── src                   
    └── my_sensor       
        ├── my_sensor.py
        ├── config.py     
        ├── __init__.py   
        └── __main__.py   
```

Your service might look like this:

### Configuration

Define the configuration using `confz`:

```python title="src/my_sensor/config.py"
import confz
from pydase.config import ServiceConfig

class MySensorConfig(confz.BaseConfig):
    instance_name: str
    hostname: str
    auth_token: str
    readout_interval_s: float

    CONFIG_SOURCES = confz.FileSource(file=ServiceConfig().config_dir / "config.yaml")
```

This class defines configurable parameters and loads values from a `config.yaml` file
located in the service’s configuration directory (which is configurable through an 
environment variable, see [above](#configuring-pydase-using-environment-variables)).  
A sample YAML file might look like this:

```yaml title="config.yaml"
instance_name: my-sensor-service-01
hostname: my-sensor-01.example.com
auth_token: my-secret-authentication-token
readout_interval_s: 5
```

### Service Implementation

Your service implementation might look like this:

```python title="src/my_sensor/my_sensor.py"
import asyncio
import http.client
import json
import logging
from typing import Any

import pydase.components
import pydase.units as u
from pydase.task.decorator import task

from my_sensor.config import MySensorConfig

logger = logging.getLogger(__name__)


class MySensor(pydase.DataService):
    def __init__(self) -> None:
        super().__init__()
        self.readout_interval_s: u.Quantity = (
            MySensorConfig().readout_interval_s * u.units.s
        )

    @property
    def hostname(self) -> str:
        """Hostname of the sensor. Read-only."""
        return MySensorConfig().hostname

    def _get_data(self) -> dict[str, Any]:
        """Fetches sensor data via an HTTP GET request. It passes the authentication 
        token as "Authorization" header."""

        connection = http.client.HTTPConnection(self.hostname, timeout=10)
        connection.request(
            "GET", "/", headers={"Authorization": MySensorConfig().auth_token}
        )
        response = connection.getresponse()
        connection.close()

        return json.loads(response.read())

    @task(autostart=True)
    async def get_and_log_sensor_values(self) -> None:
        """Periodically fetches and logs sensor data."""
        while True:
            try:
                data = self._get_data()
                # Write data to database using MySensorConfig().instance_name ...
            except Exception as e:
                logger.error(
                    "Error occurred, retrying in %s seconds. Error: %s",
                    self.readout_interval_s.m,
                    e,
                )
            await asyncio.sleep(self.readout_interval_s.m)
```

### Starting the Service

The service is launched via the `__main__.py` entry point:

```python title="src/my_sensor/__main__.py"
import pydase
from my_sensor.my_sensor import MySensor

pydase.Server(MySensor()).run()
```

You can now start the service with:

```bash
python -m my_sensor
```

This approach ensures the service is fully configured via the `config.yaml` file,
separating service logic from configuration.