Skip to content

Commit dc1e82c

Browse files
committed
docs: add env management feature documentation
1 parent b1aeb2b commit dc1e82c

2 files changed

Lines changed: 206 additions & 15 deletions

File tree

Makefile

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
PROJECT_NAME = python-boilerplate
2-
DEV_TAG = dev
3-
PROD_TAG = prod
4-
DEVCONTAINER_NAME = dev
5-
PRODCONTAINER_NAME = prod
1+
DOCKER_PROJECT_NAME=python-boilerplate
2+
DOCKER_NETWORK_NAME=project-network
3+
DOCKER_DEV_IMAGE_TAG=dev
4+
DOCKER_PROD_IMAGE_TAG=prod
5+
DOCKER_DEV_CONTAINER_NAME=dev
6+
DOCKER_PROD_CONTAINER_NAME=prod
7+
68
d = docker
79
dc = docker compose
810
ur = uv run
@@ -13,7 +15,6 @@ ur = uv run
1315
init: ## Initialize the project
1416
uv sync
1517
$(ur) pre-commit install --install-hooks
16-
1718
$(ur) pre-commit autoupdate
1819

1920
##@ Local development
@@ -33,13 +34,13 @@ typecheck: ## Run the type checker
3334
$(ur) mypy --config-file=pyproject.toml ./src/
3435

3536
dev-logs: ## View development container logs
36-
$(d) logs -f $(DEVCONTAINER_NAME)
37+
$(d) logs -f $(DOCKER_DEV_CONTAINER_NAME)
3738

3839
dev-exec: ## Execute a command in the development container
39-
$(d) exec -it $(DEVCONTAINER_NAME) /bin/bash
40+
$(d) exec -it $(DOCKER_DEV_CONTAINER_NAME) /bin/bash
4041

4142
dev-bash: ## Start a bash session in the development container
42-
$(d) run --rm -it --env-file .env.development $(PROJECT_NAME):$(DEV_TAG) /bin/bash
43+
$(d) run --rm -it --env-file .env.development $(DOCKER_PROJECT_NAME):$(DOCKER_DEV_IMAGE_TAG) /bin/bash
4344

4445
dev-build: ## Build the development container
4546
cp dev.dockerignore .dockerignore
@@ -61,19 +62,19 @@ clean: ## Clean up the project (cache)
6162
##@ Production
6263
prod-build: ## Build the production Docker image
6364
cp prod.dockerignore .dockerignore
64-
$(d) build -t $(PROJECT_NAME):$(PROD_TAG) -f prod.Dockerfile .
65+
$(d) build -t $(DOCKER_PROJECT_NAME):$(DOCKER_PROD_IMAGE_TAG) -f prod.Dockerfile .
6566

6667
prod-run: ## Run the production Docker container
67-
$(d) run -d --env-file .env.production --name $(PRODCONTAINER_NAME) $(PROJECT_NAME):$(PROD_TAG)
68+
$(d) run -d --env-file .env.production --name $(DOCKER_PROD_CONTAINER_NAME) $(DOCKER_PROJECT_NAME):$(DOCKER_PROD_IMAGE_TAG)
6869

6970
prod-exec: ## Execute a command in the production container
70-
$(d) exec -it $(PRODCONTAINER_NAME) /bin/bash
71+
$(d) exec -it $(DOCKER_PROD_CONTAINER_NAME) /bin/bash
7172

7273
prod-bash: ## Start a bash session in the production container
73-
$(d) run --rm -it --entrypoint bash --env-file .env.production --name $(PRODCONTAINER_NAME) $(PROJECT_NAME):$(PROD_TAG)
74+
$(d) run --rm -it --entrypoint bash --env-file .env.production --name $(DOCKER_PROD_CONTAINER_NAME) $(DOCKER_PROJECT_NAME):$(DOCKER_PROD_IMAGE_TAG)
7475

7576
prod-logs: ## View production container logs
76-
$(d) logs -f $(PRODCONTAINER_NAME)
77+
$(d) logs -f $(DOCKER_PROD_CONTAINER_NAME)
7778

7879
##@ Git
7980
commit: ## Do commit with conventional commit message
@@ -90,4 +91,4 @@ serve: ## Serve the documentation locally
9091
help: ## Show this help message
9192
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
9293

93-
.PHONY: commit bump help run test lint format typecheck dev-logs dev-exec dev-bash dev-build dev-up dev-stop dev-down clean prod-build prod-run
94+
.PHONY: commit bump help run test lint format typecheck dev-logs dev-exec dev-bash dev-build dev-up dev-stop dev-down clean prod-build prod-run

docs/features/environment.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Environment Management
2+
3+
Managing environment variables and configuration files is crucial for building robust and flexible Python applications.
4+
5+
This template supports **stage-based environment configuration** (development and production) using the `APP_STAGE`
6+
variable, and loads the appropriate `.env.*` file automatically.
7+
8+
---
9+
10+
## How Environment Loading Works in This Project
11+
12+
### **Stage-based Environment Selection**
13+
14+
- The environment is controlled by the `APP_STAGE` variable, which can be set to values like `development` or `production`.
15+
- Depending on `APP_STAGE`, the app loads the correct environment file:
16+
- `APP_STAGE=development` → loads `.env.development`
17+
- `APP_STAGE=production` → loads `.env.production`
18+
19+
---
20+
21+
### **Where is this implemented?**
22+
23+
Environment loading logic is implemented in `src/config/env.py`.
24+
25+
??? example "Key logic from your code:"
26+
27+
```python title="src/config/env.py"
28+
import os
29+
from pathlib import Path
30+
from typing import Final
31+
32+
from environs import Env
33+
34+
ABS_PATH: Final[Path] = Path(__file__).resolve().parent.parent.parent
35+
36+
APP_STAGE: Final[str] = os.getenv("APP_STAGE", "development")
37+
38+
ENV_FILE_MAP: Final[dict[str, Path]] = {
39+
"development": ABS_PATH / ".env.development",
40+
"production": ABS_PATH / ".env.production",
41+
}
42+
43+
ENV_PATH: Final[Path] = ENV_FILE_MAP[APP_STAGE]
44+
45+
# or you can use this way to load the environment file:
46+
# ENV_PATH: Final[Path] = ABS_PATH / f".env.{APP_STAGE}"
47+
48+
if not ENV_PATH.exists():
49+
raise FileNotFoundError(f"Environment file not found: {ENV_PATH}")
50+
51+
env = Env()
52+
env.read_env(ENV_PATH)
53+
```
54+
55+
- This code checks the value of `APP_STAGE`, determines the corresponding `.env.*` file, and loads it using [environs](https://github.com/sloria/environs).
56+
- If the file does not exist, it raises an error and your application run failed.
57+
58+
---
59+
60+
### **How to use it**
61+
62+
!!! example "!"
63+
64+
=== "Manually"
65+
- To switch environments, just set `APP_STAGE` before running your app:
66+
```bash
67+
export APP_STAGE=production
68+
make run
69+
```
70+
- Or, pass it inline:
71+
```bash
72+
APP_STAGE=production make run
73+
```
74+
75+
=== "Docker"
76+
- If you run your app in Docker, you can set `APP_STAGE` in your `docker-compose.yml` or `Dockerfile`:
77+
=== "docker-compose.yml"
78+
```yaml
79+
environment:
80+
- APP_STAGE=production
81+
```
82+
=== "Dockerfile"
83+
```dockerfile
84+
ENV APP_STAGE=production
85+
```
86+
87+
!!! warning "Important"
88+
- You needn't set `APP_STAGE` variable if you use `development` stage, it automatically will load `.env.development` file, but make sure if your file exists.
89+
90+
---
91+
92+
## Alternative: Using Pydantic Settings
93+
94+
While the current approach is simple and robust, modern Python projects often use **Pydantic** (and [pydantic-settings](https://docs.pydantic.dev/latest/usage/pydantic_settings/)) for type-safe, validated environment management.
95+
96+
!!! warning "Dependency"
97+
- If you want to use Pydantic Settings, you need to install `pydantic-settings` package:
98+
```bash
99+
uv add pydantic-settings
100+
```
101+
102+
??? example "Example Implementation with Pydantic:"
103+
104+
```python title="src/config/env.py"
105+
"""Environment configuration module for loading environment variables."""
106+
107+
import os
108+
from pathlib import Path
109+
from typing import Final
110+
111+
from pydantic import field_validator
112+
from pydantic_core.core_schema import FieldValidationInfo
113+
from pydantic_settings import BaseSettings, SettingsConfigDict
114+
115+
ABS_PATH: Final[Path] = Path(__file__).resolve().parent.parent.parent
116+
117+
APP_STAGE: Final[str] = os.getenv("APP_STAGE", "development")
118+
119+
120+
class BaseEnvSettings(BaseSettings):
121+
"""
122+
Environment configuration settings.
123+
124+
This class loads environment variables from a specified file based on the application stage.
125+
""" # noqa: E501
126+
127+
ENV_PATH: Path = ABS_PATH / f".env.{APP_STAGE}"
128+
129+
@field_validator("ENV_PATH", mode="before")
130+
@classmethod
131+
def check_env_path_exists(cls, v: Path, info: FieldValidationInfo) -> Path:
132+
"""
133+
Validates that the provided environment file path exists.
134+
135+
Args:
136+
v (Path): The path to the environment file.
137+
info (FieldValidationInfo): Additional validation information.
138+
139+
Raises:
140+
FileNotFoundError: If the environment file does not exist.
141+
142+
Returns:
143+
Path: The validated environment file path.
144+
"""
145+
146+
if not v.exists():
147+
raise FileNotFoundError(
148+
f"Environment file not found: {cls.ENV_PATH}")
149+
return v
150+
151+
model_config = SettingsConfigDict(
152+
env_file=ENV_PATH,
153+
env_file_encoding="utf-8",
154+
env_nested_delimiter="__",
155+
extra="ignore",
156+
case_sensitive=True
157+
)
158+
159+
160+
class Settings(BaseEnvSettings):
161+
"""Application settings."""
162+
163+
YOUR_VARIABLE: str # Example variable, add your own
164+
165+
env = Settings()
166+
167+
```
168+
169+
## Description of environment variables used in this project
170+
171+
- `APP_STAGE` - The current application stage, which determines which environment file to load (e.g., `development`, `production`).
172+
173+
- `DOCKER_PROJECT_NAME` - The name of your project, used for Docker image naming and container management.
174+
- `DOCKER_NETWORK_NAME` - The name of the Docker network your containers will use.
175+
- `DOCKER_IMAGE_TAG` - The tag for your Docker image, typically the version or stage (e.g., `latest`, `dev`, `prod`).
176+
- `DOCKER_CONTAINER_NAME` - The name of the Docker container, which can be used to easily identify and manage it.
177+
178+
!!! note "Note"
179+
- All these docker variables are used in `docker-compose.yml` and `Makefile` for creating a Docker image and running a container
180+
- In `Makefile` these variables are defined directly in the file, they are not loaded from the .env files.
181+
182+
183+
**More about Docker and his variables you can see in [Docker section](./docker.md)**
184+
185+
**More about Makefile you can see in [Makefile section](./makefile.md)**
186+
187+
## References
188+
189+
- [environs](https://github.com/sloria/environs)
190+
- [Pydantic Settings documentation](https://docs.pydantic.dev/latest/usage/pydantic_settings/)

0 commit comments

Comments
 (0)