Skip to content

Commit

Permalink
Support config for multiple environments
Browse files Browse the repository at this point in the history
This commit adds support for specifying configurations for multiple
environments in the `toml` configuration file. The active enviroment
is specified in the `PROTEAN_ENV` enviroment variable.
  • Loading branch information
subhashb committed Jul 25, 2024
1 parent 7c720ab commit dd1201e
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 101 deletions.
235 changes: 214 additions & 21 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,157 @@
# Configuration

## Primary Configuration Attributes
Protean's configuration is specified in a `toml` file. A `domain.toml` file is
generated when you initialize a protean application with the
[`new`](./cli/new.md) command.

### `environment`
The configuration can be placed in a file named `.domain.toml` or `domain.toml`,
or can even leverage existing `pyproject.toml`. Protean looks for these files
- in that order - when the domain object is initialized.

The `environment` attribute specifies the current running environment of the
application. By default, the environment is `development`. The current
environment can be gathered from an environment variable called `PROTEAN_ENV`.
A sample configuration file is below:

Protean recognizes `development` and `production` environments, but additional
environments such as `pre-prod`, `staging`, and `testing` can be specified as
needed.
```toml
debug = true
testing = true
secret_key = "tvTpk3PAfkGr5x9!2sFU%XpW7bR8cwKA"
identity_strategy = "uuid"
identity_type = "string"
event_processing = "sync"
command_processing = "sync"

[databases.default]
provider = "memory"

[databases.memory]
provider = "memory"

[brokers.default]
provider = "inline"

[caches.default]
provider = "memory"

[event_store]
provider = "memory"

[custom]
foo = "bar"

[staging]
event_processing = "async"
command_processing = "sync"

[staging.databases.default]
provider = "sqlite"
database_url = "sqlite:///test.db"

[staging.brokers.default]
provider = "redis"
URI = "redis://staging.example.com:6379/2"
TTL = 300

[staging.custom]
foo = "qux"

[prod]
event_processing = "async"
command_processing = "async"

[prod.databases.default]
provider = "postgresql"
database_url = "postgresql://postgres:postgres@localhost:5432/postgres"

- **Default Value**: `development`
- **Environment Variable**: `PROTEAN_ENV`
- **Examples**:
- `development`
- `production`
- `pre-prod`
- `staging`
- `testing`
[prod.brokers.default]
provider = "redis"
URI = "redis://prod.example.com:6379/2"
TTL = 30

[prod.event_store]
provider = "message_db"
database_uri = "postgresql://message_store@localhost:5433/message_store"

[prod.custom]
foo = "quux"
```

## Basic Configuration Parameters

### `debug`

Specifies if the application is running in debug mode.

***Do not enable debug mode when deploying in production.***

Default: `False`

### `testing`

Enable testing mode. Exceptions are propagated rather than handled by the
domain’s error handlers. Extensions may also change their behavior to
facilitate easier testing. You should enable this in your own tests.

Default: `False`

### `secret_key`

## Domain Configuration Attributes
A secret key that will be used for security related needs by extensions or your
application. It should be a long random `bytes` or `str`.

You can generate a secret key with the following command:

```shell
> python -c 'import secrets; print(secrets.token_hex())'
c4bf0121035265bf44657217c33a7d041fe9e505961fc7da5d976aa0eaf5cf94
```

***Do not reveal the secret key when posting questions or committing code.***

### `identity_strategy`

The default strategy to use to generate an identity value. Can be overridden
at the [`Auto`](./domain-definition/fields/simple-fields.md#auto) field level.

Supported options are `uuid` and `function`.

If the `identity_strategy` is chosen to be a `function`, `identity_function`
has to be mandatorily specified during domain object initialization.

Default: `uuid`

### `identity_type`

The type of the identity value. Can be overridden
at the [`Auto`](./domain-definition/fields/simple-fields.md#auto) field level.

Supported options are `integer`, `string`, and `uuid`.

Default: `string`

### `command_processing`

Whether to process commands synchronously or asynchronously.

Supported options are `sync` and `async`.

Default: `sync`

### `event_processing`

Whether to process events synchronously or asynchronously.

Supported options are `sync` and `async`.

Default: `sync`

### `snapshot_threshold`

The threshold number of aggregate events after which a snapshot is created to
optimize performance.

Applies only when aggregates are event sourced.

Default: `10`

## Adapter Configuration

### `databases`
Expand All @@ -65,8 +178,7 @@ by the `default` key, and is used when you do not specify a database name when
accessing the domain.

The only other database defined by default is `memory`, which is the in-memory
stub database provider. You can name all other database definitions as
necessary.
stub database provider.

The persistence store defined here is then specified in the `provider` key of
aggregates and entities to assign them a specific database.
Expand All @@ -81,18 +193,67 @@ class User:
1. `sqlite` is the key of the database definition in the `[databases.sqlite]`
section.

### `cache`
Read more in [Adapters → Database](../adapters/database/index.md) section.

### `caches`

This section holds definitions for cache infrastructure.

```toml
[caches.default]
provider = "memory"

[caches.redis]
provider = "redis"
URI = "redis://127.0.0.1:6379/2"
TTL = 300
```

Default provider: `memory`

Read more in [Adapters → Cache](../adapters/cache/index.md) section.

### `broker`

This section holds configurations for message brokers.

```toml
[brokers.default]
provider = "memory"

[brokers.redis]
provider = "redis"
URI = "redis://127.0.0.1:6379/0"
IS_ASYNC = true
```

Default provider: `memory`

Read more in [Adapters → Broker](../adapters/broker/index.md) section.

### `event_store`

The event store that stores event and command messages is defined in this
section.

```toml
[event_store]
provider = "message_db"
database_uri = "postgresql://message_store@localhost:5433/message_store"
```

Note that there can only be only event store defined per domain.

Default provider: `memory`

Read more in [Adapters → Event Store](../adapters/eventstore/index.md) section.

## Custom Attributes

Custom attributes can be defined in toml under the `[custom]` section (or
`[tool.protean.custom]` if you are leveraging the `pyproject.toml` file).

Custom attributes are also made available on the domain object directly.
Custom attributes are also made available as domain attributes.

```toml hl_lines="5"
debug = true
Expand All @@ -111,3 +272,35 @@ Out[2]: 'bar'
In [3]: domain.FOO
Out[3]: 'bar'
```

## Multiple Environments

Most applications need more than one configuration. At the very least, there
should be separate configurations for production and for local development.
The `toml` configuration file can hold configurations for different
environments.

The current environment is gathered from an environment variable named
`PROTEAN_ENV`.

The string specified in `PROTEAN_ENV` is used as a qualifier in the
configuration.

```toml hl_lines="4 8"
[databases.default]
provider = "memory"

[staging.databases.default]
provider = "sqlite"
database_url = "sqlite:///test.db"

[prod.databases.default]
provider = "postgresql"
database_url = "postgresql://postgres:postgres@localhost:5432/postgres"
```

Protean has a default configuration with memory stubs that is overridden
by configurations in the `toml` file, which can further be over-ridden by an
environment-specific configuration, as seen above. There are two environment
specific settings above for databases - an `sqlite` db configuration for
`staging` and a `postgresql` db configuration for `prod`.
2 changes: 1 addition & 1 deletion docs/guides/domain-definition/fields/simple-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ value is expected to be generated by the database at the time of persistence.
Cross-check with the specific adapter's documentation and your database
to confirm if the database supports this functionality.

- **`identity_strategy`**: The strategy to use to generate the identity value.
- **`identity_strategy`**: The strategy to use to generate an identity value.
If not provided, the strategy defined at the domain level is used.

- **`identity_function`**: A function that is used to generate the identity
Expand Down
2 changes: 1 addition & 1 deletion src/protean/adapters/email/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self, domain):

def _initialize_email_providers(self):
"""Read config file and initialize email providers"""
configured_email_providers = self.domain.config["EMAIL_PROVIDERS"]
configured_email_providers = self.domain.config["email_providers"]

Check warning on line 16 in src/protean/adapters/email/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/protean/adapters/email/__init__.py#L16

Added line #L16 was not covered by tests
email_provider_objects = {}

if configured_email_providers and isinstance(configured_email_providers, dict):
Expand Down
53 changes: 4 additions & 49 deletions src/protean/domain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,56 +42,13 @@

from .config import Config2, ConfigAttribute
from .context import DomainContext, _DomainContextGlobals
from .helpers import get_debug_flag, get_env

logger = logging.getLogger(__name__)

# a singleton sentinel value for parameter defaults
_sentinel = object()


def _default_config():
"""Return the default configuration for a Protean application.
This is placed in a separate function because we want to be absolutely
sure that we are using a copy of the defaults when we manipulate config
directly in tests. Housing it within the main `Domain` class can
potentially lead to issues because the config can be overwritten by accident.
"""
from protean.utils import IdentityStrategy, IdentityType

return {
"env": None,
"debug": None,
"secret_key": None,
"identity_strategy": IdentityStrategy.UUID.value,
"identity_type": IdentityType.STRING.value,
"databases": {
"default": {"provider": "memory"},
"memory": {"provider": "memory"},
},
"event_processing": EventProcessing.ASYNC.value,
"command_processing": CommandProcessing.ASYNC.value,
"event_store": {
"provider": "memory",
},
"caches": {
"default": {
"provider": "memory",
"TTL": 300,
}
},
"brokers": {"default": {"provider": "inline"}},
"EMAIL_PROVIDERS": {
"default": {
"provider": "protean.adapters.DummyEmailProvider",
"DEFAULT_FROM_EMAIL": "[email protected]",
},
},
"SNAPSHOT_THRESHOLD": 10,
}


class Domain:
"""The domain object is a one-stop gateway to:
Expand Down Expand Up @@ -121,7 +78,7 @@ class Domain:
#:
#: **Do not enable development when deploying in production.**
#:
#: Default: ``'production'``
#: Default: ``'development'``
env = ConfigAttribute("env")

#: The testing flag. Set this to ``True`` to enable the test mode of
Expand Down Expand Up @@ -310,6 +267,7 @@ def _traverse(self):
directories_to_traverse = [str(root_dir)] # Include root directory

# Identify subdirectories that have a toml file
# And ignore them from traversal
files_to_check = ["domain.toml", ".domain.toml", "pyproject.toml"]
for subdirectory in subdirectories:
subdirectory_path = os.path.join(root_dir, subdirectory)
Expand Down Expand Up @@ -360,13 +318,10 @@ def _initialize(self):

def load_config(self, load_toml=True):
"""Load configuration from dist or a .toml file."""
defaults = _default_config()
defaults["env"] = get_env()
defaults["debug"] = get_debug_flag()
if load_toml:
config = Config2.load_from_path(self.root_path, defaults)
config = Config2.load_from_path(self.root_path)
else:
config = Config2.load_from_dict(defaults)
config = Config2.load_from_dict()

# Load Constants
if "custom" in config:
Expand Down
Loading

0 comments on commit dd1201e

Please sign in to comment.