Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2025-03-20

### Added

- Initial release of the configuration library
- ConfigContract abstract base class defining the configuration interface
- Environment enum with DEVELOPMENT, STAGING, and PRODUCTION options
- LogLevel enum mapping to standard Python logging levels
- Settings class using Pydantic for environment variable loading
- Support for .env file configuration
- Type-safe configuration with validation
- Helper methods for environment checking
84 changes: 79 additions & 5 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,17 @@

## Overview

ConfigCore
ConfigCore provides a minimal, type-safe foundation for building configuration
systems in Python applications. Rather than being a complete configuration
solution, it defines contracts and base classes that can be extended to create
application-specific configuration systems.

The library focuses on:

- πŸ”„ Providing a standardized way to handle environment selection
- πŸ“Š Managing log levels consistently across projects
- 🧩 Offering a base Settings class for extension
- πŸ“ Using Pydantic for type validation and .env file loading

## Setup and installation

Expand All @@ -24,18 +34,82 @@ ConfigCore

### Installation

1. Clone the repository

2. Install dependencies
```sh
make init
```

## Usage

### Extending the Base Settings

ConfigCore is designed to be extended in your projects. Here's how to use it:

```python
from configcore import Settings
from pydantic import Field


class MyAppSettings(Settings):
"""Application-specific settings extending the base Settings class."""

# Add your application-specific settings
api_url: str = Field(default="https://api.example.com")
max_retry_count: int = Field(default=3, ge=1)
timeout_seconds: float = Field(default=30.0)

# Add custom methods as needed
def get_api_timeout(self) -> float:
if self.is_env_production():
return self.timeout_seconds
return self.timeout_seconds * 2
```

### Using Your Configuration

```python
# In your application
settings = MyAppSettings() # Loads from environment variables / .env

# Access standard settings from the base class
env = settings.get_environment() # Returns Environment enum
log_level = settings.get_log_level() # Returns LogLevel enum
is_prod = settings.is_env_production() # Convenience method

# Access your custom settings
api_url = settings.api_url
timeout = settings.get_api_timeout()
```

## Development

### Code Formatting and Linting

### Environment Variables
This project uses ruff for formatting and linting:

| Variable | Description | Required | Default Value | Possible Values |
|----------|-------------|----------|---------------|-----------------|
```sh
# Format code
make format

# Run linters
make lint
```

### Running Tests

```sh
# Run tests with coverage
make test
```

### Environment Variables

### Architecture
| Variable | Description | Default Value | Possible Values |
|-------------|-------------------------|---------------|---------------------------------------|
| ENVIRONMENT | Application environment | PRODUCTION | DEVELOPMENT, STAGING, PRODUCTION |
| LOG_LEVEL | Logging level | INFO | DEBUG, INFO, WARNING, ERROR, CRITICAL |

## Contributing

Expand Down
3 changes: 3 additions & 0 deletions docs/api/environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Environment

::: src.configcore.environment.Environment
3 changes: 3 additions & 0 deletions docs/api/loglevel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# LogLevel

::: src.configcore.loglevel.LogLevel
3 changes: 3 additions & 0 deletions docs/api/settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Settings

::: src.configcore.settings.Settings
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "configcore"
version = "0.1.0"
description = "ConfigCore"
description = "A flexible, type-safe configuration management library for Python applications."
authors = [
{ name = "Inokufu", email = "contact@inokufu.com" }
]
Expand All @@ -10,6 +10,7 @@ dependencies = [
"mkdocstrings-python~=1.16.6",
"mkdocs-material~=9.6.9",
"griffe-inherited-docstrings~=1.1.1",
"pydantic-settings~=2.8.1",
]
readme = "docs/README.md"
requires-python = ">= 3.12"
Expand Down
16 changes: 16 additions & 0 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# universal: false

-e file:.
annotated-types==0.7.0
# via pydantic
babel==2.17.0
# via mkdocs-material
backrefs==5.8
Expand Down Expand Up @@ -100,6 +102,12 @@ pluggy==1.5.0
# via diff-cover
# via pytest
pre-commit==4.2.0
pydantic==2.11.2
# via pydantic-settings
pydantic-core==2.33.1
# via pydantic
pydantic-settings==2.8.1
# via configcore
pygments==2.19.1
# via diff-cover
# via mkdocs-material
Expand All @@ -113,6 +121,8 @@ pytest-cov==6.0.0
pytest-mock==3.14.0
python-dateutil==2.9.0.post0
# via ghp-import
python-dotenv==1.1.0
# via pydantic-settings
pyyaml==6.0.2
# via mkdocs
# via mkdocs-get-deps
Expand All @@ -125,6 +135,12 @@ requests==2.32.3
# via mkdocs-material
six==1.17.0
# via python-dateutil
typing-extensions==4.13.1
# via pydantic
# via pydantic-core
# via typing-inspection
typing-inspection==0.4.0
# via pydantic
urllib3==2.3.0
# via requests
virtualenv==20.30.0
Expand Down
16 changes: 16 additions & 0 deletions requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# universal: false

-e file:.
annotated-types==0.7.0
# via pydantic
babel==2.17.0
# via mkdocs-material
backrefs==5.8
Expand Down Expand Up @@ -76,13 +78,21 @@ pathspec==0.12.1
# via mkdocs
platformdirs==4.3.7
# via mkdocs-get-deps
pydantic==2.11.2
# via pydantic-settings
pydantic-core==2.33.1
# via pydantic
pydantic-settings==2.8.1
# via configcore
pygments==2.19.1
# via mkdocs-material
pymdown-extensions==10.14.3
# via mkdocs-material
# via mkdocstrings
python-dateutil==2.9.0.post0
# via ghp-import
python-dotenv==1.1.0
# via pydantic-settings
pyyaml==6.0.2
# via mkdocs
# via mkdocs-get-deps
Expand All @@ -94,6 +104,12 @@ requests==2.32.3
# via mkdocs-material
six==1.17.0
# via python-dateutil
typing-extensions==4.13.1
# via pydantic
# via pydantic-core
# via typing-inspection
typing-inspection==0.4.0
# via pydantic
urllib3==2.3.0
# via requests
watchdog==6.0.0
Expand Down
14 changes: 13 additions & 1 deletion src/configcore/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
"""Package init file."""
"""Config package for configuration management in Python apps."""

from .contract import ConfigContract
from .environment import Environment
from .loglevel import LogLevel
from .settings import Settings

__all__ = [
"ConfigContract",
"Environment",
"LogLevel",
"Settings",
]
1 change: 0 additions & 1 deletion src/configcore/__main__.py

This file was deleted.

34 changes: 34 additions & 0 deletions src/configcore/contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from abc import ABC, abstractmethod

from .environment import Environment
from .loglevel import LogLevel


class ConfigContract(ABC):
"""Abstract base class defining the contract for configuration management."""

@abstractmethod
def get_log_level(self) -> LogLevel:
"""Get the log level.

Returns:
LogLevel: The log level.
"""
raise NotImplementedError

@abstractmethod
def get_environment(self) -> Environment:
"""Get the current environment.

Returns:
Environment: The current environment.
"""
raise NotImplementedError

def is_env_production(self) -> bool:
"""Check if the current environment is Production.

Returns:
bool: True if current environment is Production.
"""
return self.get_environment() == Environment.PRODUCTION
12 changes: 12 additions & 0 deletions src/configcore/environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from enum import Enum, auto


class Environment(Enum):
"""Represents the different environments in which the application can run.

DEVELOPMENT, STAGING or PRODUCTION.
"""

DEVELOPMENT = auto()
STAGING = auto()
PRODUCTION = auto()
34 changes: 34 additions & 0 deletions src/configcore/loglevel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging
from enum import IntEnum
from typing import Self


class LogLevel(IntEnum):
"""Represents the different log levels."""

DEBUG = logging.DEBUG
INFO = logging.INFO
WARNING = logging.WARNING
ERROR = logging.ERROR
CRITICAL = logging.CRITICAL

@classmethod
def from_str(cls, value: str) -> Self:
"""Create a LogLevel from a string, case-insensitive.

Args:
value: String representation of the log level.

Returns:
LogLevel enum value.

Raises:
ValueError: If the string doesn't match any log level.
"""
try:
return cls[value.upper()]
except KeyError as e:
valid_values = [e.name for e in cls]
raise ValueError(
f"Invalid log level '{value}'. Must be one of: {valid_values}",
) from e
36 changes: 36 additions & 0 deletions src/configcore/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Annotated, override

from pydantic import BeforeValidator, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

from .contract import ConfigContract
from .environment import Environment
from .loglevel import LogLevel


class Settings(BaseSettings, ConfigContract):
"""Application settings loaded from environment variables, via Pydantic model."""

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)

environment: Annotated[
Environment,
BeforeValidator(lambda v: Environment[v.upper()] if isinstance(v, str) else v),
] = Field(default=Environment.PRODUCTION.value)

log_level: Annotated[
LogLevel,
BeforeValidator(lambda v: LogLevel.from_str(v) if isinstance(v, str) else v),
] = Field(default=LogLevel.INFO.name)

@override
def get_environment(self) -> Environment:
return self.environment

@override
def get_log_level(self) -> LogLevel:
return self.log_level
Loading