# Repository Analysis

## Repository Statistics

- **Extensions analyzed**: .py
- **Number of files analyzed**: 38
- **Total lines of code (approx)**: 2663

## Project Files

### 1. examples\caching_with_decorators\console_utils.py

- **File ID**: file_0
- **Type**: Code File
- **Line Count**: 24
- **Description**: File at examples\caching_with_decorators\console_utils.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
# examples/caching_with_decorators/console_utils.py

class Colors:
    """ANSI escape codes for colored terminal output."""
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    BLUE = '\033[94m'
    CYAN = '\033[96m'
    ENDC = '\033[0m'  # Reset to default
    BOLD = '\033[1m'

    @staticmethod
    def format(message: str, color: str = "", bold: bool = False) -> str:
        """Formats a message with optional color and bold style."""
        prefix = ""
        if bold:
            prefix += Colors.BOLD
        if color:
            prefix += color
        
        if prefix:
            return f"{prefix}{message}{Colors.ENDC}"
        return message 
```

---

### 2. examples\caching_with_decorators\decorators.py

- **File ID**: file_1
- **Type**: Code File
- **Line Count**: 44
- **Description**: File at examples\caching_with_decorators\decorators.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
# examples/caching_with_decorators/decorators.py
import time
from typing import Dict, Tuple
from wd.di import ServiceProvider # Assuming ServiceProvider is available for type hinting
from services import IExternalWeatherService

class CachingWeatherServiceDecorator(IExternalWeatherService):
    def __init__(self, inner: IExternalWeatherService, cache_duration_seconds: int = 60):
        self._inner = inner
        self._cache: Dict[str, Tuple[float, float]] = {} # Key: city, Value: (timestamp, temperature)
        self._cache_duration = cache_duration_seconds
        print(f"[CachingWeatherServiceDecorator] Initialized with cache duration: {self._cache_duration}s")

    def get_current_temperature(self, city: str) -> float:
        city_key = city.lower()
        current_time = time.time()

        if city_key in self._cache:
            timestamp, temperature = self._cache[city_key]
            if current_time - timestamp < self._cache_duration:
                print(f"[CachingWeatherServiceDecorator] Cache HIT for {city}. Temp: {temperature}°C")
                return temperature
            else:
                print(f"[CachingWeatherServiceDecorator] Cache STALE for {city}. Re-fetching...")
        else:
            print(f"[CachingWeatherServiceDecorator] Cache MISS for {city}. Fetching from real service...")

        # Cache miss or stale, fetch from inner service
        temperature = self._inner.get_current_temperature(city)
        self._cache[city_key] = (current_time, temperature)
        print(f"[CachingWeatherServiceDecorator] Cached new temperature for {city}: {temperature}°C")
        return temperature

    def clear_cache(self):
        self._cache.clear()
        print("[CachingWeatherServiceDecorator] Cache cleared.")

# Decorator Factory that creates the CachingWeatherServiceDecorator
def create_caching_weather_decorator_factory(cache_duration_seconds: int = 60):
    print(f"[Factory] Creating caching decorator factory with duration: {cache_duration_seconds}s")
    def actual_factory(provider: ServiceProvider, inner: IExternalWeatherService) -> IExternalWeatherService:
        # `provider` is available if the decorator itself needed other services, not used in this simple cache.
        return CachingWeatherServiceDecorator(inner, cache_duration_seconds)
    return actual_factory 
```

---

### 3. examples\caching_with_decorators\main.py

- **File ID**: file_2
- **Type**: Code File
- **Line Count**: 79
- **Description**: File at examples\caching_with_decorators\main.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
# examples/caching_with_decorators/main.py
import time
import random
from wd.di import ServiceCollection
from services import IExternalWeatherService, SlowExternalWeatherService
from decorators import create_caching_weather_decorator_factory
from console_utils import Colors # Import Colors from the new utility file

def main():
    print(Colors.format("--- Caching Decorator Example ---", bold=True))

    services = ServiceCollection()

    actual_slow_service = SlowExternalWeatherService()
    services.add_instance(SlowExternalWeatherService, actual_slow_service)
    services.add_instance(IExternalWeatherService, actual_slow_service)

    cache_duration = 7 # try different values to see the effect of the cache
    caching_factory = create_caching_weather_decorator_factory(cache_duration_seconds=cache_duration)
    
    services.decorate(IExternalWeatherService, caching_factory)

    provider = services.build_service_provider()

    weather_service = provider.get_service(IExternalWeatherService)
    slow_weather_service_instance_for_metrics = provider.get_service(SlowExternalWeatherService) 

    total_requests_to_weather_service = 0
    initial_api_calls = slow_weather_service_instance_for_metrics.get_api_call_count()

    def make_request(city_name: str, expected_behavior: str):
        nonlocal total_requests_to_weather_service
        total_requests_to_weather_service += 1
        
        print(f"\n{Colors.format(f'--- Requesting {city_name} ({expected_behavior}) ---', Colors.CYAN)}")
        start_time = time.perf_counter()
        temperature = weather_service.get_current_temperature(city_name)
        end_time = time.perf_counter()
        duration = end_time - start_time
        
        time_color = Colors.GREEN if duration < 0.1 else Colors.YELLOW
        temp_str = Colors.format(f"{temperature}°C", bold=True)
        duration_str = Colors.format(f"{duration:.4f}s", time_color)
        print(f"Reported temperature for {city_name}: {temp_str} (took {duration_str})")
        
        api_calls_str = Colors.format(str(slow_weather_service_instance_for_metrics.get_api_call_count()), Colors.BLUE)
        print(f"Actual API calls so far: {api_calls_str}")

    make_request("London", "should be slow, cache miss")
    make_request("London", "should be fast, from cache")
    make_request("New York", "should be slow, cache miss")

    random_sleep_duration = random.uniform(4.0, 9.0) 
    print(f"\n{Colors.format(f'--- Waiting for {random_sleep_duration:.2f} seconds (cache duration is {cache_duration}s)... ---', Colors.CYAN)}")
    time.sleep(random_sleep_duration)

    make_request("London", "cache likely expired, should be slow")
    make_request("New York", "cache might be warm or cold")

    print(f"\n{Colors.format('--- Example Complete ---', bold=True)}")

    final_api_calls = slow_weather_service_instance_for_metrics.get_api_call_count()
    api_calls_during_run = final_api_calls - initial_api_calls
    cached_requests_served = total_requests_to_weather_service - api_calls_during_run

    print(f"\n{Colors.format('--- Summary ---', bold=True)}")
    total_req_str = Colors.format(str(total_requests_to_weather_service), Colors.BLUE)
    print(f"Total requests made to weather_service: {total_req_str}")
    
    api_calls_color = Colors.YELLOW if api_calls_during_run > 0 else Colors.GREEN
    actual_api_str = Colors.format(str(api_calls_during_run), api_calls_color)
    print(f"Total actual calls to SlowExternalWeatherService API: {actual_api_str}")
    
    saved_calls_color = Colors.GREEN if cached_requests_served > 0 else Colors.YELLOW
    saved_calls_str = Colors.format(str(cached_requests_served), saved_calls_color)
    print(f"Number of requests served from cache (slow API calls saved): {saved_calls_str}")

if __name__ == "__main__":
    main() 
```

---

### 4. examples\caching_with_decorators\services.py

- **File ID**: file_3
- **Type**: Code File
- **Line Count**: 34
- **Description**: File at examples\caching_with_decorators\services.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
# examples/caching_with_decorators/services.py
import time
from abc import ABC, abstractmethod

class IExternalWeatherService(ABC):
    @abstractmethod
    def get_current_temperature(self, city: str) -> float:
        """Simulates fetching current temperature for a city."""
        pass

class SlowExternalWeatherService(IExternalWeatherService):
    """A simulated weather service that is intentionally slow."""
    def __init__(self):
        self._call_count = 0

    def get_current_temperature(self, city: str) -> float:
        self._call_count += 1
        print(f"[SlowExternalWeatherService] Fetching temperature for {city}... (Call #{self._call_count})")
        time.sleep(2) # Simulate network latency or expensive computation
        if city.lower() == "london":
            temp = 15.0 + self._call_count # Make it change slightly to show cache effect
            print(f"[SlowExternalWeatherService] Temperature for London: {temp}°C")
            return temp
        elif city.lower() == "new york":
            temp = 22.0 + self._call_count
            print(f"[SlowExternalWeatherService] Temperature for New York: {temp}°C")
            return temp
        else:
            temp = 10.0 + self._call_count
            print(f"[SlowExternalWeatherService] Temperature for {city}: {temp}°C")
            return temp

    def get_api_call_count(self) -> int:
        return self._call_count 
```

---

### 5. examples\complex_ingest\app.py

- **File ID**: file_4
- **Type**: Code File
- **Line Count**: 150
- **Description**: ====================================================================...
- **Dependencies**: None
- **Used By**: None

**Content**:
```
"""
====================================================================
 🌩️  Multi-tenant “upload” demo — WHY dependency injection helps
====================================================================

Goal
----
Pretend you run an ingestion service where *each* customer (tenant)
insists on using their own cloud storage:

    ┌────────────┐          ┌─────────┐
    │  ACME Ltd. ├──►  AWS  │   S3    │
    ├────────────┤          └─────────┘
    │ Contoso    ├──► Azure Blob Storage
    ├────────────┤
    │  Globex    ├──► Google Cloud Storage
    └────────────┘

At runtime we receive an HTTP header like ``X-Tenant-Id: ACME`` and must
upload the file to *that* tenant's backend.

Why DI instead of if/elif spaghetti?
------------------------------------
* **Pluggable backends** - adding a new customer “DigitalOcean Spaces” is one mapping
  entry, no changes elsewhere.
* **Constructor injection** - a backend can depend on *other* services
  (clock, logger, auth) and the container wires everything up.
* **Easy testing** - swap real backends for an file system mock (like this demo)by
  registering a different implementation before
  ``build_service_provider()``.

Key moving parts in this file
-----------------------------
1. ``ServiceCollection`` - registers *what* can be built.
2. ``ContextVar`` *tenant* - carries the current tenant id through async
   calls without passing it as a parameter everywhere.
3. ``_storage_factory`` - looks up the tenant's backend in the
   configuration and returns the proper concrete class.
4. **Lifetimes**:
   * ``IClock``  → **singleton** (one for the whole app)
   * ``DataIngestionService`` → **scoped** (new per request / tenant)
     ◦ **Why not singleton?**  
       A singleton would hold on to whatever backend was injected for the
       *first* request and all tenants would end up in S3 (or whichever
       came first).

Workflow
--------
* Main process builds the root provider.
* Each request:
  1. sets ``_current_tenant``,
  2. opens a *scope* (so scoped services are fresh),
  3. resolves ``DataIngestionService`` which, via the factory, gets the
     right ``IBlobStorage`` for that tenant,
  4. uploads the file,
  5. disposes the scope.

Everything here is dependency-free: the “cloud” backends live in memory,
so you can `python app.py` and see it work instantly.

"""

from contextvars import ContextVar
from typing import Dict, Callable, Type

from wd.di import ServiceCollection
from wd.di.config import Configuration, IConfiguration

from storage.abstractions import IBlobStorage, IClock
from storage.impl import UtcClock, S3Storage, AzureBlobStorage, GcsStorage
from pipeline.ingestor import DataIngestionService

# ------------------------------------------------------------------
# 1. Set up the service collection
# ------------------------------------------------------------------
services = ServiceCollection()

# Shared dependencies
services.add_singleton(IClock, UtcClock)

# DataIngestionService is *tenant-specific*: if we made it a singleton
# it would capture the first tenant's IBlobStorage instance and reuse it
# for every subsequent request. Try it out by changing it to a singleton.
services.add_scoped(DataIngestionService)

# Tenant configuration - could come from a file/env in real life
services.add_singleton_factory(
    IConfiguration,
    lambda _: Configuration(
        {
            "tenants": {
                "ACME": {"backend": "s3"},
                "Contoso": {"backend": "azure"},
                "Globex": {"backend": "gcs"},
            }
        }
    ),
)

# ContextVar that stores the tenant ID for the current scope
_current_tenant: ContextVar[str] = ContextVar("tenant")

# Runtime selector -------------------------------------------------
def _storage_factory(sp, tenant_id: str) -> IBlobStorage:
    """
    Map the tenant's configured backend to the concrete implementation.
    """
    backend = sp.get_service(IConfiguration).get(f"tenants:{tenant_id}:backend")

    mapping: Dict[str, Callable[[IClock], IBlobStorage]] = {
        "s3": lambda clock: S3Storage(clock),
        "azure": lambda clock: AzureBlobStorage(clock),
        "gcs": lambda clock: GcsStorage(clock),
    }

    if backend not in mapping:
        raise ValueError(f"Unknown backend '{backend}' for tenant {tenant_id}")

    return mapping[backend](sp.get_service(IClock))


# Register abstract factory with DI
services.add_transient_factory(
    IBlobStorage, lambda sp: _storage_factory(sp, _current_tenant.get())
)

# Build provider
provider = services.build_service_provider()

# ------------------------------------------------------------------
# 2. Request boundary helper
# ------------------------------------------------------------------
def handle_request(tenant_id: str, filename: str, data: bytes) -> None:
    """
    Simulates an HTTP request boundary: sets the tenant context, creates a
    DI scope, and processes the ingestion.
    """
    token = _current_tenant.set(tenant_id)
    with provider.create_scope() as scope:
        scope.get_service(DataIngestionService).ingest(filename, data)
    _current_tenant.reset(token)


# ------------------------------------------------------------------
# 3. Demo run
# ------------------------------------------------------------------
if __name__ == "__main__":
    handle_request("ACME", "acme/lucy.jpg", open("examples/complex_ingest/assets/lucy.jpg", "rb").read())
    handle_request("Contoso", "contoso/luna.jpg", open("examples/complex_ingest/assets/luna.jpg", "rb").read())
    handle_request("Globex", "globex/lucy.jpg", open("examples/complex_ingest/assets/lucy.jpg", "rb").read())

```

---

### 6. examples\complex_ingest\pipeline\ingestor.py

- **File ID**: file_5
- **Type**: Code File
- **Line Count**: 14
- **Description**: Business layer that is agnostic of the concrete storage backend.
- **Dependencies**: None
- **Used By**: None

**Content**:
```
"""
Business layer that is agnostic of the concrete storage backend.
"""

from storage.abstractions import IBlobStorage


class DataIngestionService:
    def __init__(self, storage: IBlobStorage):
        self._storage = storage

    def ingest(self, filename: str, payload: bytes) -> None:
        uri = self._storage.upload(filename, payload)
        print(f"[INGEST] stored {filename} at {uri}")

```

---

### 7. examples\complex_ingest\storage\abstractions.py

- **File ID**: file_6
- **Type**: Code File
- **Line Count**: 21
- **Description**: Common interfaces that the rest of the code depends on.
- **Dependencies**: None
- **Used By**: None

**Content**:
```
"""
Common interfaces that the rest of the code depends on.
"""

from abc import ABC, abstractmethod
from typing import Protocol


class IBlobStorage(ABC):
    """Abstract storage service — backend-agnostic."""

    @abstractmethod
    def upload(self, path: str, data: bytes) -> str:
        """Uploads `data` and returns a URI to the stored object."""
        ...


class IClock(Protocol):
    """Cross-cutting dependency used for timestamping URIs."""

    def now(self): ...

```

---

### 8. examples\complex_ingest\storage\impl.py

- **File ID**: file_7
- **Type**: Code File
- **Line Count**: 88
- **Description**: Light-weight demo implementations for IBlobStorage....
- **Dependencies**: None
- **Used By**: None

**Content**:
```
# storage/impl.py
"""
Light-weight demo implementations for IBlobStorage.

* No external SDKs required.
* Files are persisted locally under ./uploads/<backend>/...
* Returned URI strings mimic the real cloud schemes (s3://, azure://, gs://).
"""

from __future__ import annotations

import datetime
import os
from pathlib import Path

from storage.abstractions import IBlobStorage, IClock


# ---------------------------------------------------------------------------
# Cross-cutting dependency
# ---------------------------------------------------------------------------

class UtcClock(IClock):
    def now(self) -> datetime.datetime:
        return datetime.datetime.utcnow()


# ---------------------------------------------------------------------------
# Helper: minimal local “object store”
# ---------------------------------------------------------------------------

def _write_file(root: str, path: str, data: bytes) -> Path:
    """
    Save *data* under ./uploads/<root>/<path> and return the Path object.
    """
    full_path = Path("examples/complex_ingest/uploads") / root / path
    full_path.parent.mkdir(parents=True, exist_ok=True)
    full_path.write_bytes(data)
    return full_path


# ---------------------------------------------------------------------------
# Concrete storage back-ends (mocked)
# ---------------------------------------------------------------------------

class S3Storage(IBlobStorage):
    """
    Pretends to be AWS S3; bucket comes from $AWS_BUCKET or defaults to 'demo'.
    """

    def __init__(self, clock: IClock):
        self._clock = clock
        self._bucket = os.getenv("AWS_BUCKET", "demo")

    def upload(self, path: str, data: bytes) -> str:
        _write_file(f"s3/{self._bucket}", path, data)
        ts = self._clock.now().isoformat()
        return f"s3://{self._bucket}/{path}?ts={ts}"


class AzureBlobStorage(IBlobStorage):
    """
    Pretends to be Azure Blob Storage; container from $AZURE_CONTAINER or 'demo'.
    """

    def __init__(self, clock: IClock):
        self._clock = clock
        self._container = os.getenv("AZURE_CONTAINER", "demo")

    def upload(self, path: str, data: bytes) -> str:
        _write_file(f"azure/{self._container}", path, data)
        ts = self._clock.now().isoformat()
        return f"azure://{self._container}/{path}?ts={ts}"


class GcsStorage(IBlobStorage):
    """
    Pretends to be Google Cloud Storage; bucket from $GCP_BUCKET or 'demo'.
    """

    def __init__(self, clock: IClock):
        self._clock = clock
        self._bucket = os.getenv("GCP_BUCKET", "demo")

    def upload(self, path: str, data: bytes) -> str:
        _write_file(f"gcs/{self._bucket}", path, data)
        ts = self._clock.now().isoformat()
        return f"gs://{self._bucket}/{path}?ts={ts}"

```

---

### 9. examples\decorator_type_hints_example.py

- **File ID**: file_8
- **Type**: Code File
- **Line Count**: 71
- **Description**: File at examples\decorator_type_hints_example.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from abc import ABC, abstractmethod
from dataclasses import dataclass
from wd.di import ServiceCollection
from wd.di.config import Configuration, IConfiguration

services = ServiceCollection()

# Define interfaces
class IEmailService(ABC):
    @abstractmethod
    def send_email(self, to: str, subject: str, body: str) -> None:
        pass

class IUserService(ABC):
    @abstractmethod
    def notify_user(self, user_id: str, message: str) -> None:
        pass



config = Configuration({
    "app": {
        "api_key": "real-secret-key"
    }
})

@dataclass
class AppConfig:
    api_key: str = "secret-key"

services.add_singleton_factory(IConfiguration, lambda _: config)
services.configure(AppConfig, section="app")

# Implementations with decorators
@services.singleton(IEmailService)
class EmailService(IEmailService):
    def send_email(self, to: str, subject: str, body: str) -> None:
        print(f"Sending email to {to}")

@services.singleton(IUserService)
class UserService(IUserService):
    def __init__(self, email_service: IEmailService):
        self.email_service = email_service

    def notify_user(self, user_id: str, message: str) -> None:
        self.email_service.send_email(f"user{user_id}@example.com", "Notification", message)



# Service without interface - register directly with the class
@services.singleton()
class LogService:
    def log(self, message: str) -> None:
        print(f"[LOG] {message}")

# Build provider
provider = services.build_service_provider()

# Get services with proper type hints
log_service = provider.get_service(LogService)
email_service = provider.get_service(IEmailService)
user_service = provider.get_service(IUserService)   
configuration = provider.get_service(IConfiguration) 


# Use the services (IDE provides code completion for all methods)
log_service.log("Application started")
email_service.send_email("test@example.com", "Test", "Hello")
user_service.notify_user("123", "Welcome!")
config_value = configuration.get("app:api_key")
log_service.log(f"Got config: {config_value}")

```

---

### 10. examples\order_processor\data\repository.py

- **File ID**: file_9
- **Type**: Code File
- **Line Count**: 10
- **Description**: File at examples\order_processor\data\repository.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from domain.interfaces import IOrderRepository
from infrastructure.logging_service import Logger

class OrderRepository(IOrderRepository):
    def __init__(self, logger: Logger):
        self.logger = logger

    def save_order(self, order):
        self.logger.log(f"Order saved: {order.order_id}")
        # In a real application, implement database save logic here.

```

---

### 11. examples\order_processor\domain\interfaces.py

- **File ID**: file_10
- **Type**: Code File
- **Line Count**: 11
- **Description**: File at examples\order_processor\domain\interfaces.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from abc import ABC, abstractmethod

class IOrderRepository(ABC):
    @abstractmethod
    def save_order(self, order):
        pass

class IOrderService(ABC):
    @abstractmethod
    def process_order(self, order):
        pass
```

---

### 12. examples\order_processor\domain\models.py

- **File ID**: file_11
- **Type**: Code File
- **Line Count**: 8
- **Description**: File at examples\order_processor\domain\models.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from dataclasses import dataclass

@dataclass
class Order:
    order_id: str
    item: str
    quantity: int
    price: float
```

---

### 13. examples\order_processor\infrastructure\config.py

- **File ID**: file_12
- **Type**: Code File
- **Line Count**: 6
- **Description**: File at examples\order_processor\infrastructure\config.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from dataclasses import dataclass

@dataclass
class AppConfig:
    debug: bool = False
    email_server: str = ""
```

---

### 14. examples\order_processor\infrastructure\logging_service.py

- **File ID**: file_13
- **Type**: Code File
- **Line Count**: 3
- **Description**: File at examples\order_processor\infrastructure\logging_service.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
class Logger:
    def log(self, message: str):
        print(f"[LOG]: {message}")
```

---

### 15. examples\order_processor\main.py

- **File ID**: file_14
- **Type**: Code File
- **Line Count**: 39
- **Description**: File at examples\order_processor\main.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from domain.interfaces import IOrderRepository, IOrderService
from wd.di import ServiceCollection
from wd.di.config import Configuration, IConfiguration
from infrastructure.config import AppConfig
from infrastructure.logging_service import Logger
from data.repository import OrderRepository
from services.order_service import OrderService
from presentation.controller import OrderController

services = ServiceCollection()

# Configure application settings
config = Configuration({
    "app": {
        "debug": True,
        "emailServer": "smtp.example.com"
    }
})
services.add_singleton_factory(IConfiguration, lambda _: config)
services.configure(AppConfig, section="app")

# Register infrastructure services
services.add_instance(Logger, Logger())

# Register data access layer (repository)
services.add_singleton(IOrderRepository, OrderRepository)

# Register business logic (order service)
services.add_singleton(IOrderService, OrderService)

# Register presentation layer (controller)
services.add_transient(OrderController)

# Build the service provider
provider = services.build_service_provider()

# Resolve the controller and simulate an order submission
controller = provider.get_service(OrderController)
controller.submit_order("order001", "Widget", 5, 19.99)

```

---

### 16. examples\order_processor\presentation\controller.py

- **File ID**: file_15
- **Type**: Code File
- **Line Count**: 10
- **Description**: File at examples\order_processor\presentation\controller.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from domain.models import Order
from domain.interfaces import IOrderService

class OrderController:
    def __init__(self, order_service: IOrderService):
        self.order_service = order_service

    def submit_order(self, order_id, item, quantity, price):
        order = Order(order_id=order_id, item=item, quantity=quantity, price=price)
        self.order_service.process_order(order)

```

---

### 17. examples\order_processor\services\order_service.py

- **File ID**: file_16
- **Type**: Code File
- **Line Count**: 14
- **Description**: File at examples\order_processor\services\order_service.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from domain.interfaces import IOrderService, IOrderRepository
from domain.models import Order
from infrastructure.logging_service import Logger

class OrderService(IOrderService):
    def __init__(self, repository: IOrderRepository, logger: Logger):
        self.repository = repository
        self.logger = logger

    def process_order(self, order: Order):
        self.logger.log(f"Processing order: {order.order_id}")
        # Business logic: validate order, process payment, etc.
        self.repository.save_order(order)
        self.logger.log(f"Order processed: {order.order_id}")

```

---

### 18. examples\type_hints_example.py

- **File ID**: file_17
- **Type**: Code File
- **Line Count**: 41
- **Description**: File at examples\type_hints_example.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from wd.di import ServiceCollection

services = ServiceCollection()

# Define interfaces and implementations
class IEmailService:
    def send_email(self, to: str, subject: str, body: str) -> None:
        pass

class EmailService(IEmailService):
    def send_email(self, to: str, subject: str, body: str) -> None:
        print(f"\nSending email to {to}")
        print(f"Subject: {subject}")
        print(f"Body: {body}")
        print("Email sent successfully\n")

# UserService depends on IEmailService
class UserService:
    def __init__(self, email_service: IEmailService):
        self.email_service = email_service

    def notify_user(self, user_id: str, message: str) -> None:
        self.email_service.send_email(f"user{user_id}@example.com", "Notification", message)

# Register services
services.add_singleton(IEmailService, EmailService)
services.add_singleton(UserService)

# Build provider
# Services will be resolved and injected into services 
# like EmailService into UserService
provider = services.build_service_provider()

# Get services with proper type hints
email_service = provider.get_service(IEmailService)
user_service = provider.get_service(UserService)

# IDE will provide code completion for these methods
# No need to cast or use Any
email_service.send_email("test@example.com", "Test", "Hello")
user_service.notify_user("123", "Welcome!")

```

---

### 19. src\wd\di\__init__.py

- **File ID**: file_18
- **Type**: Code File
- **Line Count**: 51
- **Description**: File at src\wd\di\__init__.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from importlib.metadata import PackageNotFoundError, version


from wd.di.service_collection import ServiceCollection
from wd.di.container import ServiceProvider, Scope
from wd.di.exceptions import InvalidOperationError, CircularDecoratorError
from wd.di.lifetimes import ServiceLifetime
from wd.di.config import IConfiguration, Options, OptionsBuilder

from wd.di.middleware import (
    IMiddleware,
    MiddlewarePipeline,
    LoggingMiddleware,
    ValidationMiddleware,
    CachingMiddleware,
    ExceptionHandlerMiddleware,
)
from wd.di.middleware_di import create_application_builder

__all__ = [
    "ServiceCollection",
    "create_service_collection",
    "ServiceProvider",
    "Scope",
    "InvalidOperationError",
    "CircularDecoratorError",
    "ServiceLifetime",
    "IConfiguration",
    "Options",
    "OptionsBuilder",
    "IMiddleware",
    "MiddlewarePipeline",
    "LoggingMiddleware",
    "ValidationMiddleware",
    "CachingMiddleware",
    "ExceptionHandlerMiddleware",
    "create_application_builder",
]

try:
    __version__ = version("wd-di")
except PackageNotFoundError:
    __version__ = "0.1.1"


# Attach extension methods
ServiceCollection.create_application_builder = create_application_builder

def create_service_collection() -> ServiceCollection:
    """Return a brand-new, empty service collection."""
    return ServiceCollection()

```

---

### 20. src\wd\di\config.py

- **File ID**: file_19
- **Type**: Code File
- **Line Count**: 116
- **Description**: File at src\wd\di\config.py
- **Dependencies**: None
- **Used By**:
  - file_26

**Content**:
```
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, TypeVar, Generic, Type
from dataclasses import dataclass
import json
import os

T = TypeVar("T")


class IConfiguration(ABC):
    @abstractmethod
    def get(self, key: str) -> Any:
        pass

    @abstractmethod
    def get_section(self, section: str) -> "IConfiguration":
        pass


class Configuration(IConfiguration):
    def __init__(self, data: Dict[str, Any]):
        self._data = data

    def get(self, key: str) -> Any:
        keys = key.split(":")
        current = self._data
        for k in keys:
            if not isinstance(current, dict) or k not in current:
                return None
            current = current[k]
        return current

    def get_section(self, section: str) -> "IConfiguration":
        value = self.get(section)
        if isinstance(value, dict):
            return Configuration(value)
        return Configuration({})


class ConfigurationBuilder:
    def __init__(self):
        self._sources: Dict[str, Any] = {}

    def add_json_file(self, path: str) -> "ConfigurationBuilder":
        if os.path.exists(path):
            with open(path, "r") as f:
                self._sources.update(json.load(f))
        return self

    def add_env_variables(self, prefix: str = "") -> "ConfigurationBuilder":
        for key, value in os.environ.items():
            if prefix and not key.startswith(prefix):
                continue
            if prefix:
                key = key[len(prefix) :]
            self._sources[key] = value
        return self

    def add_dictionary(self, dictionary: Dict[str, Any]) -> "ConfigurationBuilder":
        self._sources.update(dictionary)
        return self

    def build(self) -> IConfiguration:
        return Configuration(self._sources)


@dataclass
class ConfigureOptions:
    section: str


class Options(Generic[T]):
    def __init__(self, value: T):
        self._value = value

    @property
    def value(self) -> T:
        return self._value


class OptionsBuilder(Generic[T]):
    def __init__(self, options_type: Type[T]):
        self._options_type = options_type
        self._configuration: Optional[IConfiguration] = None
        self._section: Optional[str] = None

    def bind_configuration(
        self, configuration: IConfiguration, section: Optional[str] = None
    ) -> "OptionsBuilder[T]":
        self._configuration = configuration
        self._section = section
        return self

    def build(self) -> T:
        if self._configuration is None:
            return self._options_type()

        config_section = self._configuration
        if self._section:
            config_section = self._configuration.get_section(self._section)

        instance = self._options_type()
        config_dict = {}

        if hasattr(config_section, "_data"):
            config_dict = config_section._data

        # Convert camelCase to snake_case for property names
        for key, value in config_dict.items():
            snake_key = "".join(
                ["_" + c.lower() if c.isupper() else c for c in key]
            ).lstrip("_")
            if hasattr(instance, snake_key):
                setattr(instance, snake_key, value)

        return instance

```

---

### 21. src\wd\di\container.py

- **File ID**: file_20
- **Type**: Code File
- **Line Count**: 311
- **Description**: Runtime *service provider* for **wd-di** (decorator-aware, backward-compatible)....
- **Dependencies**:
  - file_21
  - file_23
  - file_22
- **Used By**:
  - file_21
  - file_26

**Content**:
```
"""Runtime *service provider* for **wd-di** (decorator-aware, backward-compatible).

This module keeps 100% API compatibility with the pre-decorator version while
adding

* first-class **decorator folding** (see :pyclass:`ServiceDescriptor`) and
* task-local **circular-decorator detection**.

Back-compat shims
-----------------
* ``ServiceProvider.__init__`` now accepts **either** the historical
  ``Dict[Type, ServiceDescriptor]`` **or** a ``List[ServiceDescriptor]`` (the
  format emitted by the new :pyclass:`~wd.di.service_collection.ServiceCollection`).
* A lightweight :pyclass:`Scope` alias mirrors the previous subclass so import
  paths like ``from wd.di.container import Scope`` keep working.  A scoped
  provider supports ``dispose()``, ``close()`` semantics and can be used as a
  context manager (``with services.create_scope() as scope: ...``).
"""

from __future__ import annotations

import contextvars
import inspect
import threading
from typing import Any, Dict, List, Mapping, Sequence, Type, TypeVar, Union, overload, get_type_hints

from .descriptors import ServiceDescriptor
from .exceptions import CircularDecoratorError, InvalidOperationError
from .lifetimes import ServiceLifetime

__all__ = ["ServiceProvider", "Scope"]

T = TypeVar("T")

# --------------------------------------------------------------------------- #
# Task-local resolution stack (dependency & decorator cycles)
# --------------------------------------------------------------------------- #

_resolution_stack: contextvars.ContextVar[List[str]] = contextvars.ContextVar(
    "wd_di_resolution_stack", default=[]
)


class ServiceProvider:  # noqa: D101 – public runtime DI container
    # NOTE: 'services' can be a mapping (old API) **or** a list (new API)
    def __init__(
        self,
        services: Union[Sequence[ServiceDescriptor[Any]], Mapping[Type[Any], ServiceDescriptor[Any]]],
        *,
        _root: "ServiceProvider | None" = None,
    ) -> None:
        # ------------------------------------------------------------------ #
        # Root vs scope initialisation
        # ------------------------------------------------------------------ #
        if _root is None:  # root provider (publicly constructed)
            self._root: "ServiceProvider" = self

            # Accept both old dict-based and new list-based descriptor stores.
            if isinstance(services, Mapping):
                self._descriptors: Dict[Type[Any], ServiceDescriptor[Any]] = dict(services)
            else:
                self._descriptors = {d.service_type: d for d in services}

            self._singleton_cache: Dict[Type[Any], Any] = {}
            self._singleton_lock = threading.RLock()
        else:  # scoped provider (private constructor via create_scope)
            self._root = _root
            # Re-use the root's descriptors & singleton cache.
            self._descriptors = _root._descriptors
            self._singleton_cache = _root._singleton_cache
            self._singleton_lock = _root._singleton_lock

        # Each scope (including root) gets its *own* scoped cache & disposables.
        self._scoped_cache: Dict[Type[Any], Any] = {}
        self._disposables: List[Any] = []

    # ------------------------------------------------------------------ #
    # Public API – resolve service
    # ------------------------------------------------------------------ #
    @overload
    def get_service(self, service_type: Type[T]) -> T: ...

    @overload
    def get_service(self, service_type: Type[Any]) -> Any: ...

    def get_service(self, service_type: Type[Any]):  # type: ignore[override]
        """Resolve *service_type*, creating & caching it as needed."""
        desc = self._descriptors.get(service_type)
        if desc is None:
            raise KeyError(f"No service registered for type {service_type!r}.")

        # ---------- fast path: cache lookup ----------
        if desc.lifetime is ServiceLifetime.SINGLETON:
            try:
                return self._singleton_cache[service_type]
            except KeyError:
                pass  # miss → build
        elif desc.lifetime is ServiceLifetime.SCOPED:
            # Check if attempting to resolve a scoped service from the root provider
            if self._root is self: # We are the root provider
                raise InvalidOperationError(
                    "Cannot resolve scoped service from the root provider. "
                    "Please create a scope using 'create_scope()' and resolve it from the scope."
                )
            try:
                return self._scoped_cache[service_type]
            except KeyError:
                pass
        # Transient → always build.

        # ---------- circular dependency guard ----------
        stack = _resolution_stack.get()
        frame = _key(service_type)

        if frame in stack:
            idx_frame_in_stack = stack.index(frame)
            
            # Check if this is a decorator-induced cycle:
            # Service S is being resolved, its decorator D is invoked, and D (or its dependencies)
            # tries to resolve S again.
            # Stack would be: [..., S_key, D_key_for_S, ... (possibly other things if D calls other services)],
            # and now 'frame' (S_key) is being requested again.
            # 'desc' is the ServiceDescriptor for 'service_type' (whose key is 'frame').
            if desc and desc.decorators and (idx_frame_in_stack + 1) < len(stack):
                item_after_frame_in_stack = stack[idx_frame_in_stack + 1]
                # These are the keys of decorators registered for the current service 'frame'.
                decorator_keys_for_this_service = {_key(deco_factory) for deco_factory in desc.decorators}
                
                if item_after_frame_in_stack in decorator_keys_for_this_service:
                    # The cycle is: frame -> item_after_frame (a decorator for frame) -> ... -> frame (current request)
                    # This indicates the decorator 'item_after_frame_in_stack' (or something it called)
                    # is trying to resolve 'frame' again.
                    cycle_path = stack[idx_frame_in_stack:] + [frame]
                    raise CircularDecoratorError(cycle_path)
            
            # If not a decorator cycle identified above, then it's a general circular dependency.
            cycle_path = stack[idx_frame_in_stack:] + [frame]
            raise RuntimeError("Circular dependency detected: " + " -> ".join(cycle_path))

        stack.append(frame)
        _resolution_stack.set(stack)
        try:
            instance = self._create_instance(desc)
        finally:
            stack.pop()

        # ---------- lifetime caching ----------
        if desc.lifetime is ServiceLifetime.SINGLETON:
            with self._singleton_lock:
                if service_type not in self._singleton_cache:  # double-checked
                    self._singleton_cache[service_type] = instance
        elif desc.lifetime is ServiceLifetime.SCOPED:
            self._scoped_cache[service_type] = instance
            self._try_register_disposable(instance)
        # Transient → caller owns the object.

        return instance  # type: ignore[return-value]

    # ------------------------------------------------------------------ #
    # Scoping
    # ------------------------------------------------------------------ #
    def create_scope(self) -> "ServiceProvider":
        """Return a *scoped* provider that shares singletons with the root."""
        return ServiceProvider([], _root=self._root)

    # ------------------------------------------------------------------ #
    # Disposal (scoped provider)
    # ------------------------------------------------------------------ #
    def dispose(self):  # noqa: D401 – verb form mirrors old API
        """Dispose all tracked scoped instances (close / dispose)."""
        for inst in self._disposables:
            if hasattr(inst, "dispose") and callable(inst.dispose):
                try:
                    inst.dispose()
                except Exception as exc:
                    print(f"Error disposing {inst}: {exc}")
            elif hasattr(inst, "close") and callable(inst.close):
                try:
                    inst.close()
                except Exception as exc:
                    print(f"Error closing {inst}: {exc}")
        self._disposables.clear()
        self._scoped_cache.clear()

    # Context-manager support (for 'with' blocks)
    def __enter__(self):  # noqa: D401 – magic method
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):  # noqa: D401 – magic method
        self.dispose()

    # ------------------------------------------------------------------ #
    # Internal helpers
    # ------------------------------------------------------------------ #
    def _create_instance(self, desc: ServiceDescriptor[Any]) -> Any:
        """Instantiate *desc* and apply registered decorators."""
        # Build inner service
        if desc.factory is not None:
            inner = desc.factory(self)
        else:  # constructor injection via type hints
            impl = desc.implementation_type
            assert impl is not None
            inner = self._construct_via_type_hints(impl)

        # Apply decorators (outermost == last registered)
        if desc.decorators:
            stack = _resolution_stack.get()
            for deco in reversed(desc.decorators):
                deco_frame = _key(deco)
                if deco_frame in stack:
                    cycle = stack[stack.index(deco_frame) :] + [deco_frame]
                    raise CircularDecoratorError(cycle)

                stack.append(deco_frame)
                _resolution_stack.set(stack)
                try:
                    inner = deco(self, inner)
                finally:
                    stack.pop()

        return inner

    # -- helper: constructor injection ---------------------------------- #
    def _construct_via_type_hints(self, cls: Type[Any]) -> Any:
        # Get the constructor, __init__ might be inherited from object
        # For dataclasses or classes without explicit __init__, inspect.signature directly on class might be better
        # but __init__ is standard.
        constructor_to_inspect = cls.__init__
        if constructor_to_inspect is object.__init__ and not hasattr(cls, '__init__'): # Handle classes with no explicit __init__
             # If it's object.__init__ and there's no __init__ on the class itself (e.g. simple class Foo: pass)
             # then there are no parameters to inject.
             if not [p for p_name, p in inspect.signature(cls).parameters.items() if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]:
                 return cls()


        sig = inspect.signature(constructor_to_inspect)
        
        # Resolve type hints, providing globals and locals from the class's module
        # to help resolve forward references.
        try:
            # Get the module where the class `cls` is defined.
            module = inspect.getmodule(cls)
            if module is None:
                # Fallback if module cannot be determined (e.g. dynamically created classes)
                # This might limit forward reference resolution.
                resolved_hints = get_type_hints(constructor_to_inspect)
            else:
                resolved_hints = get_type_hints(constructor_to_inspect, module.__dict__)
        except NameError as e:
            # This is where the test expects the failure for unresolved forward reference in a cycle
            stack = _resolution_stack.get() # Get current resolution stack for error message
            # Ensure the current class being constructed is part of the reported stack if not already
            cls_key = _key(cls)
            if not stack or stack[-1] != cls_key: # Add if not already the last one
                effective_stack_for_error = stack + [cls_key]
            else:
                effective_stack_for_error = stack
            
            raise RuntimeError(
                f"Failed to resolve dependencies for {cls.__qualname__} "
                f"due to NameError (potential forward reference in a circular dependency): {e}. "
                f"Resolution stack: {effective_stack_for_error}"
            ) from e

        kwargs: Dict[str, Any] = {}
        for name, param in sig.parameters.items():
            if name == "self":
                continue
            
            actual_param_type = resolved_hints.get(name)

            if actual_param_type is None:
                # Parameter not in resolved_hints, could be *args, **kwargs, or missing annotation
                if param.kind == inspect.Parameter.VAR_POSITIONAL or \
                   param.kind == inspect.Parameter.VAR_KEYWORD:
                    continue # Skip *args, **kwargs
                raise TypeError(
                    f"Cannot resolve constructor parameter '{name}' for {cls.__qualname__}; "
                    f"missing type annotation or type could not be resolved."
                )
            
            kwargs[name] = self.get_service(actual_param_type)
        return cls(**kwargs)

    # -- helper: register disposables ----------------------------------- #
    def _try_register_disposable(self, instance: Any) -> None:
        if hasattr(instance, "dispose") and callable(instance.dispose):
            self._disposables.append(instance)
        elif hasattr(instance, "close") and callable(instance.close):
            self._disposables.append(instance)


# ---------------------------------------------------------------------- #
# Helper – make a readable stack entry name
# ---------------------------------------------------------------------- #

def _key(obj: object) -> str:
    if isinstance(obj, type):
        return obj.__qualname__
    if hasattr(obj, "__qualname__"):
        return obj.__qualname__  # type: ignore[attr-defined]
    if hasattr(obj, "__name__"):
        return obj.__name__  # type: ignore[attr-defined]
    return repr(obj)


# ---------------------------------------------------------------------- #
# Aliases – keep old import paths working
# ---------------------------------------------------------------------- #

Scope = ServiceProvider  # backward-compat

```

---

### 22. src\wd\di\descriptors.py

- **File ID**: file_21
- **Type**: Code File
- **Line Count**: 130
- **Description**: Service descriptors for wd-di....
- **Dependencies**:
  - file_23
  - file_20
- **Used By**:
  - file_20
  - file_26

**Content**:
```
"""Service descriptors for wd-di.

This module defines :class:`ServiceDescriptor`, the immutable value object that stores
all metadata about a service registration. The container folds those factories
outside-in when it builds the final object, thereby enabling first-class decorator
support (see acceptance criteria A1–A4).
"""

from __future__ import annotations

import inspect
from dataclasses import dataclass, field
from typing import Any, Callable, List, Optional, TypeVar, Generic, TYPE_CHECKING

from .lifetimes import ServiceLifetime

if TYPE_CHECKING:  # pragma: no cover
    # Forward import to avoid a circular reference on runtime.
    from .container import ServiceProvider

T = TypeVar("T")
DecoratorFactory = Callable[["ServiceProvider", Any], Any]
"""Callable that takes the current :class:`~wd.di.container.ServiceProvider` and the
*inner* instance and returns a (possibly) wrapped instance.  The return type is *Any*
on purpose to allow the decorator to change the runtime subtype while still respecting
the *semantic* contract of *T* at the call site.
"""


@dataclass(frozen=True, slots=True)
class ServiceDescriptor(Generic[T]):
    """Immutable description of a service registration.

    Attributes
    ----------
    service_type:
        The *abstraction* that clients request—usually an interface or base class.
    implementation_type:
        The concrete class that will be instantiated to satisfy the registration.
        Mutually exclusive with *factory*.
    factory:
        A callable that builds the instance given the current service provider.
        Mutually exclusive with *implementation_type*.
    lifetime:
        Controls how long the resulting object lives (transient, scoped, singleton).
    decorators:
        An *ordered* list of :data:`DecoratorFactory` objects to be applied to the
        instance after construction but **before** lifetime caching.
    """

    service_type: type[T]
    implementation_type: Optional[type] = None
    factory: Optional[Callable[["ServiceProvider"], T]] = None
    lifetime: ServiceLifetime = ServiceLifetime.TRANSIENT
    decorators: List[DecoratorFactory] = field(default_factory=list, repr=False, compare=False)

    # --------------------------------------------------------------------- #
    # Factory helpers
    # --------------------------------------------------------------------- #
    @classmethod
    def transient(
        cls,
        service_type: type[T],
        implementation_type: Optional[type] = None,
        factory: Optional[Callable[["ServiceProvider"], T]] = None,
    ) -> "ServiceDescriptor[T]":
        return cls(service_type, implementation_type, factory, ServiceLifetime.TRANSIENT)

    @classmethod
    def scoped(
        cls,
        service_type: type[T],
        implementation_type: Optional[type] = None,
        factory: Optional[Callable[["ServiceProvider"], T]] = None,
    ) -> "ServiceDescriptor[T]":
        return cls(service_type, implementation_type, factory, ServiceLifetime.SCOPED)

    @classmethod
    def singleton(
        cls,
        service_type: type[T],
        implementation_type: Optional[type] = None,
        factory: Optional[Callable[["ServiceProvider"], T]] = None,
    ) -> "ServiceDescriptor[T]":
        return cls(service_type, implementation_type, factory, ServiceLifetime.SINGLETON)

    # ------------------------------------------------------------------ #
    # Mutation helpers (return a *new* instance to keep immutability)
    # ------------------------------------------------------------------ #
    def with_decorator(self, decorator: DecoratorFactory) -> "ServiceDescriptor[T]":
        """Return a new descriptor with *decorator* appended to the chain.

        The method does **not** modify the original object (dataclass is frozen)
        but returns a new instance that shares all existing attributes.
        """
        return ServiceDescriptor(
            self.service_type,
            self.implementation_type,
            self.factory,
            self.lifetime,
            [*self.decorators, decorator],
        )

    # ------------------------------------------------------------------ #
    # Validation
    # ------------------------------------------------------------------ #
    def __post_init__(self) -> None:
        if (self.implementation_type is None) == (self.factory is None):
            raise ValueError(
                "Exactly one of 'implementation_type' or 'factory' must be provided."
            )

        if self.decorators:
            # Ensure user did not accidentally pass a non-callable.
            for deco in self.decorators:
                if not callable(deco):
                    raise TypeError(
                        f"Decorator {deco!r} registered for {self.service_type.__name__} " 
                        "is not callable."
                    )

        if (
            self.implementation_type is not None
            and inspect.isabstract(self.implementation_type)
        ):
            raise TypeError(
                "implementation_type cannot be abstract; register the concrete class instead"
            )

__all__ = ["DecoratorFactory", "ServiceDescriptor"]

```

---

### 23. src\wd\di\exceptions.py

- **File ID**: file_22
- **Type**: Code File
- **Line Count**: 59
- **Description**: Custom exception hierarchy for **wd-di**....
- **Dependencies**: None
- **Used By**:
  - file_20
  - file_26

**Content**:
```
"""Custom exception hierarchy for **wd-di**.

The container uses a handful of domain-specific errors to give end-users clear,
actionable feedback instead of generic `RuntimeError`s.

Public classes
--------------
* :class:`InvalidOperationError` – raised when the caller performs an action
  that is not allowed in the current container state, e.g. mutating a
  :class:`~wd.di.service_collection.ServiceCollection` *after* the provider has
  been built.
* :class:`CircularDecoratorError` – raised at provider-build time when two or
  more decorators form a cycle (A ↔ B ↔ … ↔ A).  The error message includes the
  discovered chain to help users debug the registration.

Both errors inherit from :class:`RuntimeError` so existing `except` blocks that
catch generic runtime container issues continue to work.
"""

from __future__ import annotations

from typing import Sequence

__all__ = [
    "InvalidOperationError",
    "CircularDecoratorError",
]


class InvalidOperationError(RuntimeError):
    """Raised when an operation is not valid for the object’s state."""


class CircularDecoratorError(RuntimeError):
    """Raised when a cycle in the decorator chain is detected.

    Parameters
    ----------
    chain:
        The list of decorator callables/classes involved in the cycle, in the
        order they were traversed.
    """

    def __init__(self, chain: Sequence[str | type | object]):
        pretty_chain = " \u2192 ".join(_name(c) for c in chain)  # arrows between names
        super().__init__(f"Circular decorator chain detected: {pretty_chain}")
        self.chain = list(chain)


# ---------------------------------------------------------------------- #
# Helper – derive a human-readable name for a decorator object
# ---------------------------------------------------------------------- #

def _name(obj: object) -> str:  # pragma: no cover – pure formatting
    if hasattr(obj, "__qualname__"):
        return obj.__qualname__  # type: ignore[attr-defined]
    if hasattr(obj, "__name__"):
        return obj.__name__  # type: ignore[attr-defined]
    return repr(obj)
```

---

### 24. src\wd\di\lifetimes.py

- **File ID**: file_23
- **Type**: Code File
- **Line Count**: 7
- **Description**: File at src\wd\di\lifetimes.py
- **Dependencies**: None
- **Used By**:
  - file_21
  - file_20
  - file_26

**Content**:
```
from enum import Enum


class ServiceLifetime(Enum):
    TRANSIENT = 1
    SINGLETON = 2
    SCOPED = 3

```

---

### 25. src\wd\di\middleware.py

- **File ID**: file_24
- **Type**: Code File
- **Line Count**: 119
- **Description**: File at src\wd\di\middleware.py
- **Dependencies**: None
- **Used By**:
  - file_25

**Content**:
```
from abc import ABC, abstractmethod
from typing import Any, Callable, List, Optional, TypeVar

T = TypeVar("T")
TNext = Callable[[], Any]
TMiddleware = Callable[[T, TNext], Any]


class IMiddleware(ABC):
    """Base interface for middleware components."""

    @abstractmethod
    async def invoke(self, context: T, next: TNext) -> Any:
        """Process the context and call the next middleware in the pipeline."""
        pass


class MiddlewarePipeline:
    """Manages the middleware pipeline execution."""

    def __init__(self):
        self._middleware: List[TMiddleware[T]] = []

    def use(self, middleware: TMiddleware[T]) -> "MiddlewarePipeline":
        """Add a middleware to the pipeline."""
        self._middleware.append(middleware)
        return self

    def use_middleware(
        self, middleware_class: type, instance: Optional[Any] = None
    ) -> "MiddlewarePipeline":
        """Add a middleware class to the pipeline."""

        def adapter(context: T, next: TNext) -> Any:
            nonlocal instance
            if instance is None:
                instance = middleware_class()
            return instance.invoke(context, next)

        self._middleware.append(adapter)
        return self

    async def execute(self, context: T) -> Any:
        """Execute the middleware pipeline."""
        if not self._middleware:
            return None

        index = 0

        async def invoke_next() -> Any:
            nonlocal index
            if index < len(self._middleware):
                middleware = self._middleware[index]
                index += 1
                return await middleware(context, invoke_next)
            return None

        return await invoke_next()


class ExceptionHandlerMiddleware(IMiddleware):
    """Built-in middleware for handling exceptions in the pipeline."""

    async def invoke(self, context: T, next: TNext) -> Any:
        try:
            return await next()
        except Exception as e:
            # Log the exception or handle it as needed
            raise


class LoggingMiddleware(IMiddleware):
    """Built-in middleware for logging pipeline execution."""

    def __init__(self, logger: Optional[Callable[[str], None]] = None):
        self._logger = logger or print

    async def invoke(self, context: T, next: TNext) -> Any:
        self._logger(f"Executing pipeline with context: {context}")
        try:
            result = await next()
            self._logger(f"Pipeline execution completed with result: {result}")
            return result
        except Exception as e:
            self._logger(f"Pipeline execution failed: {str(e)}")
            raise


class ValidationMiddleware(IMiddleware):
    """Built-in middleware for context validation."""

    def __init__(self, validator: Callable[[T], bool]):
        self._validator = validator

    async def invoke(self, context: T, next: TNext) -> Any:
        if not self._validator(context):
            raise ValueError(f"Invalid context: {context}")
        return await next()


class CachingMiddleware(IMiddleware):
    """Built-in middleware for caching pipeline results."""

    def __init__(self):
        self._cache = {}

    async def invoke(self, context: T, next: TNext) -> Any:
        # Use context as cache key if it's hashable
        try:
            cache_key = hash(str(context.__dict__))
            if cache_key in self._cache:
                return self._cache[cache_key]

            result = await next()
            self._cache[cache_key] = result
            return result
        except (TypeError, AttributeError):
            # If context is not hashable or has no __dict__, skip caching
            return await next()

```

---

### 26. src\wd\di\middleware_di.py

- **File ID**: file_25
- **Type**: Code File
- **Line Count**: 91
- **Description**: File at src\wd\di\middleware_di.py
- **Dependencies**:
  - file_24
  - file_26
- **Used By**: None

**Content**:
```
from typing import Any, Callable, Optional, TypeVar
from .middleware import IMiddleware, MiddlewarePipeline, LoggingMiddleware
from .service_collection import ServiceCollection

T = TypeVar("T")
TNext = Callable[[], Any] # Explicitly define TNext for clarity in the adapter

# Forward declaration for type hinting if ServiceCollection is used in type hints
# before its full definition in this file or if it's a circular dependency concern.
# However, in this case, ServiceCollection is imported and used as a type hint
# for the constructor argument, which is standard.
# class ServiceCollection: # This is a forward reference / type stub for the real one
#     pass                 # that will be passed in. # REMOVE STUB

class MiddlewareBuilder:
    """Builder for configuring middleware in the DI container."""

    def __init__(self, services: ServiceCollection):
        self._services = services
        self._pipeline = MiddlewarePipeline()

    def use(
        self, middleware: Callable[[T, TNext], Any] # Use TNext here
    ) -> "MiddlewareBuilder":
        """Add a middleware function to the pipeline."""
        self._pipeline.use(middleware)
        return self

    def use_middleware(self, middleware_type: type) -> "MiddlewareBuilder":
        """Add a middleware class to the pipeline."""
        # Register the middleware type with DI using the provided services instance
        if middleware_type == LoggingMiddleware:
            self._services.add_transient_factory(
                middleware_type, lambda _: LoggingMiddleware() # Assuming LoggingMiddleware can be default constructed or gets deps
            )
        else:
            # Ensure the middleware itself is registered as transient so each resolution gets a new one if needed by its design
            self._services.add_transient(middleware_type)

        # This adapter will be called by the MiddlewarePipeline for each execution
        def adapter_for_pipeline(context: T, next_func: TNext) -> Any:
            # Build provider and resolve middleware instance ONCE PER EXECUTION of this middleware in the pipeline
            # This uses the ServiceCollection available at the time ApplicationBuilder.build() will be called (or later if services are added)
            # which might still be an issue if services are added *after* ApplicationBuilder.build() and before request.
            # However, the more common pattern is that the main service provider is built once.
            # If the goal is to use THE final application provider, that would need to be passed around differently.
            # For now, this builds from the _services collection held by MiddlewareBuilder, which is typically the main app collection.
            provider = self._services.build_service_provider() 
            instance = provider.get_service(middleware_type)
            return instance.invoke(context, next_func) # type: ignore

        self._pipeline.use(adapter_for_pipeline) # Use the generic .use() method
        return self

    def build(self) -> MiddlewarePipeline:
        """Build the middleware pipeline."""
        return self._pipeline


class ApplicationBuilder:
    """Builder for configuring the application with middleware support."""

    def __init__(self, services: ServiceCollection):
        self._services = services

    def configure_middleware(
        self, configure: Callable[["MiddlewareBuilder"], None]
    ) -> "ApplicationBuilder":
        """Configure the middleware pipeline."""
        builder = MiddlewareBuilder(self._services)
        configure(builder)
        pipeline = builder.build()

        self._services.add_singleton_factory(MiddlewarePipeline, lambda _: pipeline)
        return self

    def build(self):
        """Build the application."""
        # The ServiceProvider built here is the one that should ideally be used by middleware if they need the "final" app state.
        # The current MiddlewareBuilder approach builds a new provider each time from self._services.
        # This is a complex interaction. The user's suggestion improves it from config-time to per-request, which is better.
        return self._services.build_service_provider()


def create_application_builder(services: ServiceCollection) -> ApplicationBuilder:
    """Create an application builder for the service collection."""
    return ApplicationBuilder(services)


# Attach the extension method to ServiceCollection - This is done in __init__.py now
# ServiceCollection.create_application_builder = create_application_builder

```

---

### 27. src\wd\di\service_collection.py

- **File ID**: file_26
- **Type**: Code File
- **Line Count**: 267
- **Description**: Service registration API for **wd-di** (decorator‑aware)....
- **Dependencies**:
  - file_19
  - file_21
  - file_20
  - file_23
  - file_22
- **Used By**:
  - file_25

**Content**:
```
"""Service registration API for **wd-di** (decorator‑aware).

This module exposes the fluent DSL that users employ at *composition‑root* time
to wire up their application.  It now supports *decorators* via
:meth:`ServiceCollection.decorate` while **retaining every public helper** that
existed in previous releases:

* `add_instance`, `add_*` for class registrations, `add_*_factory` for factory
  registrations
* `configure` to bind strongly‑typed *Options* objects from an
  `IConfiguration` source
* Python‑level class decorators: `@services.singleton`, `@services.scoped`,
  `@services.transient`

No user code must change; the new API is purely additive.
"""

from __future__ import annotations

from typing import Any, Callable, Iterable, List, Optional, Type, TypeVar, Generic, TYPE_CHECKING, overload

from .descriptors import DecoratorFactory, ServiceDescriptor
from .lifetimes import ServiceLifetime
from .exceptions import InvalidOperationError
from .config import IConfiguration, Options, OptionsBuilder

if TYPE_CHECKING:  # pragma: no cover
    from .container import ServiceProvider

__all__ = ["ServiceCollection"]

T = TypeVar("T")
S = TypeVar("S")  # service/abstraction type for decorator overloads


class ServiceCollection(Generic[T]):
    """Collects :class:`~wd.di.descriptors.ServiceDescriptor` objects and builds a provider."""

    # ------------------------------------------------------------------ #
    # Construction
    # ------------------------------------------------------------------ #
    def __init__(self, descriptors: Optional[Iterable[ServiceDescriptor[Any]]] = None):
        self._services: List[ServiceDescriptor[Any]] = list(descriptors) if descriptors else []
        self._is_built: bool = False

    # ------------------------------------------------------------------ #
    # Private helpers
    # ------------------------------------------------------------------ #
    def _ensure_not_built(self) -> None:
        if self._is_built:
            raise InvalidOperationError(
                "Cannot modify ServiceCollection after ServiceProvider has been built."
            )

    def _add(
        self,
        lifetime: ServiceLifetime,
        service_type: Type[Any],
        implementation_type: Optional[Type[Any]] = None,
        factory: Optional[Callable[["ServiceProvider"], Any]] = None,
    ) -> "ServiceCollection":
        """Create & store a :class:`ServiceDescriptor` (internal helper).

        If the caller omits both *implementation_type* and *factory* we assume a
        *self‑binding* registration (``implementation_type = service_type``) to
        preserve legacy behaviour.
        """
        self._ensure_not_built()

        if implementation_type is None and factory is None:
            implementation_type = service_type

        descriptor = ServiceDescriptor(
            service_type,
            implementation_type,
            factory,
            lifetime,
        )
        self._services.append(descriptor)
        return self  # fluent API

    # ------------------------------------------------------------------ #
    # Public API – registrations (class or factory)
    # ------------------------------------------------------------------ #
    def add_transient(
        self,
        service_type: Type[Any],
        implementation_type: Optional[Type[Any]] = None,
        factory: Optional[Callable[["ServiceProvider"], Any]] = None,
    ) -> "ServiceCollection":
        return self._add(ServiceLifetime.TRANSIENT, service_type, implementation_type, factory)

    def add_scoped(
        self,
        service_type: Type[Any],
        implementation_type: Optional[Type[Any]] = None,
        factory: Optional[Callable[["ServiceProvider"], Any]] = None,
    ) -> "ServiceCollection":
        return self._add(ServiceLifetime.SCOPED, service_type, implementation_type, factory)

    def add_singleton(
        self,
        service_type: Type[Any],
        implementation_type: Optional[Type[Any]] = None,
        factory: Optional[Callable[["ServiceProvider"], Any]] = None,
    ) -> "ServiceCollection":
        return self._add(ServiceLifetime.SINGLETON, service_type, implementation_type, factory)

    # ------------------------------------------------------------------ #
    # Convenience – direct instance
    # ------------------------------------------------------------------ #
    def add_instance(self, service_type: Type[Any], instance: Any) -> "ServiceCollection":
        """Register an *already‑constructed* singleton instance."""
        self._ensure_not_built()

        def _factory(_: "ServiceProvider") -> Any:  # noqa: D401 – simple stub
            return instance

        descriptor = ServiceDescriptor(
            service_type,
            factory=_factory,
            lifetime=ServiceLifetime.SINGLETON,
        )
        self._services.append(descriptor)
        return self

    # ------------------------------------------------------------------ #
    # Convenience – explicit factory overloads
    # ------------------------------------------------------------------ #
    def add_transient_factory(
        self,
        service_type: Type[Any],
        factory: Callable[["ServiceProvider"], Any],
    ) -> "ServiceCollection":
        return self._add(ServiceLifetime.TRANSIENT, service_type, factory=factory)

    def add_scoped_factory(
        self,
        service_type: Type[Any],
        factory: Callable[["ServiceProvider"], Any],
    ) -> "ServiceCollection":
        return self._add(ServiceLifetime.SCOPED, service_type, factory=factory)

    def add_singleton_factory(
        self,
        service_type: Type[Any],
        factory: Callable[["ServiceProvider"], Any],
    ) -> "ServiceCollection":
        return self._add(ServiceLifetime.SINGLETON, service_type, factory=factory)

    # ------------------------------------------------------------------ #
    # Options / configuration support
    # ------------------------------------------------------------------ #
    def configure(self, options_type: Type[T], *, section: Optional[str] = None) -> "ServiceCollection":
        """Bind *options_type* from the shared :pydata:`IConfiguration` instance.

        ``services.configure(MySettings)`` automatically registers
        ``Options[MySettings]`` as a singleton whose value is built by binding
        *MySettings* against the configuration tree at provider‑build time.
        """
        self._ensure_not_built()

        def _factory(sp: "ServiceProvider") -> Options[T]:  # runtime generic
            try:
                configuration = sp.get_service(IConfiguration)
            except Exception as exc:  # pragma: no cover – defensive
                raise Exception("IConfiguration service not registered") from exc

            builder = OptionsBuilder(options_type)
            builder.bind_configuration(configuration, section)
            return Options(builder.build())

        # Register an *open‑generic* Options[...] singleton.
        self.add_singleton_factory(Options[options_type], _factory)  # type: ignore[index]
        return self

    # ------------------------------------------------------------------ #
    # Python‑level decorators – syntactic sugar
    # ------------------------------------------------------------------ #
    # Singleton -------------------------------------------------------- #
    @overload
    def singleton(self) -> Callable[[Type[T]], Type[T]]: ...

    @overload
    def singleton(self, service_type: Type[S]) -> Callable[[Type[T]], Type[T]]: ...

    def singleton(self, service_type: Optional[Type[S]] = None) -> Callable[[Type[T]], Type[T]]:  # type: ignore[override]
        def _decorator(impl: Type[T]) -> Type[T]:
            if service_type is None:
                self.add_singleton(impl, impl)
            else:
                self.add_singleton(service_type, impl)  # type: ignore[arg-type]
            return impl

        return _decorator

    # Scoped ----------------------------------------------------------- #
    @overload
    def scoped(self) -> Callable[[Type[T]], Type[T]]: ...

    @overload
    def scoped(self, service_type: Type[S]) -> Callable[[Type[T]], Type[T]]: ...

    def scoped(self, service_type: Optional[Type[S]] = None) -> Callable[[Type[T]], Type[T]]:  # type: ignore[override]
        def _decorator(impl: Type[T]) -> Type[T]:
            if service_type is None:
                self.add_scoped(impl, impl)
            else:
                self.add_scoped(service_type, impl)  # type: ignore[arg-type]
            return impl

        return _decorator

    # Transient -------------------------------------------------------- #
    @overload
    def transient(self) -> Callable[[Type[T]], Type[T]]: ...

    @overload
    def transient(self, service_type: Type[S]) -> Callable[[Type[T]], Type[T]]: ...

    def transient(self, service_type: Optional[Type[S]] = None) -> Callable[[Type[T]], Type[T]]:  # type: ignore[override]
        def _decorator(impl: Type[T]) -> Type[T]:
            if service_type is None:
                self.add_transient(impl, impl)
            else:
                self.add_transient(service_type, impl)  # type: ignore[arg-type]
            return impl

        return _decorator

    # ------------------------------------------------------------------ #
    # Public API – decorator support (service *wrappers*)
    # ------------------------------------------------------------------ #
    def decorate(self, service_type: Type[Any], decorator: DecoratorFactory) -> "ServiceCollection":
        self._ensure_not_built()

        for idx, descriptor in enumerate(self._services):
            if descriptor.service_type is service_type:
                self._services[idx] = descriptor.with_decorator(decorator)
                break
        else:  # pragma: no cover
            raise KeyError(
                f"No service registered for {service_type.__name__!s}; cannot apply decorator."
            )

        return self

    # ------------------------------------------------------------------ #
    # Build provider
    # ------------------------------------------------------------------ #
    def build_service_provider(self) -> "ServiceProvider":
        from .container import ServiceProvider  # local import avoids heavy cost when only building collections

        self._is_built = True
        return ServiceProvider(self._services)

    # ------------------------------------------------------------------ #
    # Introspection helpers
    # ------------------------------------------------------------------ #
    def __iter__(self):
        return iter(self._services)

    def __len__(self) -> int:  # type: ignore[override]
        return len(self._services)

    def __repr__(self) -> str:  # pragma: no cover
        return f"<ServiceCollection services={len(self)} built={self._is_built}>"

```

---

### 28. tests\__init__.py

- **File ID**: file_27
- **Type**: Code File
- **Line Count**: 1
- **Description**: File at tests\__init__.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```


```

---

### 29. tests\test_add_instance_roundtrip.py

- **File ID**: file_28
- **Type**: Code File
- **Line Count**: 33
- **Description**: Ensures that ServiceCollection.add_instance registers an already...
- **Dependencies**: None
- **Used By**: None

**Content**:
```
"""
Ensures that ServiceCollection.add_instance registers an already
constructed object exactly once and that the very *same* object is
returned from any provider or scope.
"""

from wd.di import ServiceCollection

class Logger:
    def __init__(self):
        self.lines = []

    def log(self, msg: str) -> None:
        self.lines.append(msg)

def test_add_instance_returns_same_object_everywhere():
    services = ServiceCollection()

    logger = Logger()
    services.add_instance(Logger, logger)

    provider = services.build_service_provider()

    # root access
    assert provider.get_service(Logger) is logger

    # scoped access – must still be the very same object
    with provider.create_scope() as scope:
        assert scope.get_service(Logger) is logger

    # sanity: logger is functional
    logger.log("hello")
    assert logger.lines == ["hello"]

```

---

### 30. tests\test_circular_dependency.py

- **File ID**: file_29
- **Type**: Code File
- **Line Count**: 31
- **Description**: File at tests\test_circular_dependency.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
import pytest
# from wd.di.service_collection import ServiceCollection # Old import path
from wd.di import ServiceCollection # Correct import path

def test_circular_dependency_detection():
    services = ServiceCollection() # Instantiate locally

    # Define two classes that depend on each other
    class ServiceA:
        def __init__(self, service_b: "ServiceB"):
            self.service_b = service_b

    class ServiceB:
        def __init__(self, service_a: ServiceA):
            self.service_a = service_a

    services.add_transient(ServiceA)
    services.add_transient(ServiceB)

    provider = services.build_service_provider()

    # Updated to expect the NameError that occurs first when forward references
    # are involved in a circular dependency.
    expected_pattern = (
        r"Failed to resolve dependencies for .*ServiceA.* "
        r"due to NameError \(potential forward reference in a circular dependency\): "
        r"name 'ServiceB' is not defined. "
        r"Resolution stack: \[.*ServiceA.*\]"
    )
    with pytest.raises(Exception, match=expected_pattern):
        provider.get_service(ServiceA)

```

---

### 31. tests\test_config.py

- **File ID**: file_30
- **Type**: Code File
- **Line Count**: 133
- **Description**: File at tests\test_config.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
import pytest
from dataclasses import dataclass
from wd.di.config import (
    Configuration,
    ConfigurationBuilder,
    Options,
    OptionsBuilder,
    IConfiguration,
)
from wd.di import ServiceCollection


@dataclass
class DatabaseOptions:
    connection_string: str = ""
    max_connections: int = 10


@dataclass
class AppSettings:
    name: str = ""
    version: str = ""


def test_configuration_get():
    config = Configuration(
        {
            "database": {
                "connectionString": "test-connection",
                "maxConnections": 5,
            },
            "app": {"name": "TestApp", "version": "1.0.0"},
        }
    )

    assert config.get("database:connectionString") == "test-connection"
    assert config.get("app:name") == "TestApp"
    assert config.get("nonexistent") is None
    assert config.get("database:nonexistent") is None


def test_configuration_get_section():
    config = Configuration(
        {
            "database": {
                "connectionString": "test-connection",
                "maxConnections": 5,
            }
        }
    )

    section = config.get_section("database")
    assert section.get("connectionString") == "test-connection"
    assert section.get("maxConnections") == 5


def test_configuration_builder():
    builder = ConfigurationBuilder()
    config = (
        builder.add_dictionary(
            {
                "database": {
                    "connectionString": "test-connection",
                    "maxConnections": 5,
                }
            }
        )
        .add_dictionary({"app": {"name": "TestApp"}})
        .build()
    )

    assert config.get("database:connectionString") == "test-connection"
    assert config.get("app:name") == "TestApp"


def test_options_builder():
    config = Configuration(
        {"database": {"connectionString": "test-connection", "maxConnections": 5}}
    )

    builder = OptionsBuilder(DatabaseOptions)
    options = builder.bind_configuration(config, "database").build()

    assert options.connection_string == "test-connection"
    assert options.max_connections == 5


def test_options_di_integration():
    services = ServiceCollection()

    config = Configuration(
        {
            "database": {"connectionString": "test-connection", "maxConnections": 5},
            "app": {"name": "TestApp", "version": "1.0.0"},
        }
    )

    services.add_singleton_factory(IConfiguration, lambda sp: config)
    services.configure(DatabaseOptions, section="database")
    services.configure(AppSettings, section="app")

    provider = services.build_service_provider()

    db_options = provider.get_service(Options[DatabaseOptions])
    app_options = provider.get_service(Options[AppSettings])

    assert db_options.value.connection_string == "test-connection"
    assert db_options.value.max_connections == 5
    assert app_options.value.name == "TestApp"
    assert app_options.value.version == "1.0.0"


def test_options_without_configuration():
    services = ServiceCollection()

    services.configure(DatabaseOptions)
    provider = services.build_service_provider()

    with pytest.raises(Exception, match="Configuration service not registered"):
        provider.get_service(Options[DatabaseOptions])


def test_options_missing_section():
    services = ServiceCollection()

    config = Configuration({})
    services.add_singleton_factory(IConfiguration, lambda sp: config)
    services.configure(DatabaseOptions, section="database")
    provider = services.build_service_provider()

    db_options = provider.get_service(Options[DatabaseOptions])
    assert db_options.value.connection_string == ""
    assert db_options.value.max_connections == 10

```

---

### 32. tests\test_decorators.py

- **File ID**: file_31
- **Type**: Code File
- **Line Count**: 26
- **Description**: File at tests\test_decorators.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
from wd.di import ServiceCollection

services = ServiceCollection()

@services.transient()
class FooService:
    def do_something(self):
        print("FooService doing something!")


@services.singleton()
class BarService:
    def __init__(self, foo_service: FooService):
        self.foo_service = foo_service

    def do_something_else(self):
        print("BarService doing something else!")
        self.foo_service.do_something()


def test_decorators():
    service_provider = services.build_service_provider()
    bar_service = service_provider.get_service(BarService)
    bar_service.do_something_else()

    assert bar_service is not None

```

---

### 33. tests\test_di.py

- **File ID**: file_32
- **Type**: Code File
- **Line Count**: 65
- **Description**: File at tests\test_di.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
# tests/test_di.py

from typing import assert_type
# from wd.di.service_collection import ServiceCollection # Old import path
from wd.di import ServiceCollection # Correct import path


# Define test services - these are fine at module level
class IService:
    def execute(self):
        pass


class ServiceA(IService):
    def execute(self):
        return "ServiceA"


class ServiceB(IService):
    def __init__(self, service_a: ServiceA):
        self.service_a = service_a

    def execute(self):
        return f"ServiceB depends on {self.service_a.execute()}"


def test_transient_services():
    services = ServiceCollection() # Instantiate locally
    services.add_transient(ServiceA)
    services.add_transient(IService, ServiceB)

    provider = services.build_service_provider()
    service1 = provider.get_service(IService)
    service2 = provider.get_service(IService)

    assert service1 is not service2
    assert service1.execute() == "ServiceB depends on ServiceA"

def test_transient_services_alt():
    services = ServiceCollection() # Instantiate locally
    services.add_transient(ServiceA)
    services.add_transient(IService, ServiceB)

    provider = services.build_service_provider()
    service1 = provider.get_service(IService)
    service2 = provider.get_service(ServiceA)

    assert service1 is not service2
    assert service1.execute() == "ServiceB depends on ServiceA"


def test_singleton_services():
    services = ServiceCollection() # Instantiate locally
    services.add_singleton(ServiceA)
    services.add_singleton(IService, ServiceB)

    provider = services.build_service_provider()
    service1 = provider.get_service(IService)
    service2 = provider.get_service(IService)


    assert_type(service1, IService)
    assert service1 is service2
    assert isinstance(service1, IService) and isinstance(service1, ServiceB)
    assert isinstance(service1, IService) and not isinstance(service1, ServiceA)
```

---

### 34. tests\test_middleware.py

- **File ID**: file_33
- **Type**: Code File
- **Line Count**: 206
- **Description**: File at tests\test_middleware.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
import pytest
from dataclasses import dataclass
from typing import List, Optional
from wd.di.middleware import (
    IMiddleware,
    MiddlewarePipeline,
    LoggingMiddleware,
    ValidationMiddleware,
    CachingMiddleware,
)
from wd.di import ServiceCollection


@dataclass
class RequestContext:
    path: str
    value: Optional[str] = None


class HelperTestMiddleware(IMiddleware):
    def __init__(self):
        self.executed = False
        self.context_path = None

    async def invoke(self, context: RequestContext, next):
        self.executed = True
        self.context_path = context.path
        return await next()


class ModifyingMiddleware(IMiddleware):
    async def invoke(self, context: RequestContext, next):
        context.value = "modified"
        return await next()


class ResultMiddleware(IMiddleware):
    async def invoke(self, context: RequestContext, next):
        await next()
        return "result"


@pytest.mark.asyncio
async def test_middleware_pipeline_execution():
    pipeline = MiddlewarePipeline()
    middleware = HelperTestMiddleware()
    pipeline.use_middleware(HelperTestMiddleware, instance=middleware)

    context = RequestContext(path="/test")
    await pipeline.execute(context)

    assert middleware.executed
    assert middleware.context_path == "/test"


@pytest.mark.asyncio
async def test_middleware_order():
    executed_order: List[str] = []

    class FirstMiddleware(IMiddleware):
        async def invoke(self, context, next):
            executed_order.append("first")
            return await next()

    class SecondMiddleware(IMiddleware):
        async def invoke(self, context, next):
            executed_order.append("second")
            return await next()

    pipeline = MiddlewarePipeline()
    pipeline.use_middleware(FirstMiddleware)
    pipeline.use_middleware(SecondMiddleware)

    await pipeline.execute(RequestContext(path="/test"))
    assert executed_order == ["first", "second"]


@pytest.mark.asyncio
async def test_middleware_modification():
    pipeline = MiddlewarePipeline()
    pipeline.use_middleware(ModifyingMiddleware)

    context = RequestContext(path="/test")
    await pipeline.execute(context)

    assert context.value == "modified"


@pytest.mark.asyncio
async def test_middleware_result():
    pipeline = MiddlewarePipeline()
    pipeline.use_middleware(ResultMiddleware)

    result = await pipeline.execute(RequestContext(path="/test"))
    assert result == "result"


@pytest.mark.asyncio
async def test_validation_middleware():
    def validator(context: RequestContext) -> bool:
        return context.path.startswith("/")

    pipeline = MiddlewarePipeline()
    pipeline.use_middleware(lambda: ValidationMiddleware(validator))

    # Valid path should work
    await pipeline.execute(RequestContext(path="/test"))

    # Invalid path should raise ValueError
    with pytest.raises(ValueError):
        await pipeline.execute(RequestContext(path="invalid"))


@pytest.mark.asyncio
async def test_caching_middleware():
    executed_count = 0

    class CountingMiddleware(IMiddleware):
        async def invoke(self, context, next):
            nonlocal executed_count
            executed_count += 1
            return "result"

    pipeline = MiddlewarePipeline()
    pipeline.use_middleware(CachingMiddleware)
    pipeline.use_middleware(CountingMiddleware)

    context = RequestContext(path="/test")

    # First call should execute the pipeline
    result1 = await pipeline.execute(context)
    assert result1 == "result"
    assert executed_count == 1

    # Second call should use cached result
    result2 = await pipeline.execute(context)
    assert result2 == "result"
    assert executed_count == 1  # Count shouldn't increase


@pytest.mark.asyncio
async def test_logging_middleware():
    logs = []
    logger = lambda msg: logs.append(msg)

    pipeline = MiddlewarePipeline()
    pipeline.use_middleware(lambda: LoggingMiddleware(logger))
    pipeline.use_middleware(ResultMiddleware)

    await pipeline.execute(RequestContext(path="/test"))

    assert len(logs) == 2
    assert "Executing pipeline" in logs[0]
    assert "completed with result" in logs[1]


@pytest.mark.asyncio
async def test_di_integration():
    services = ServiceCollection()

    app = services.create_application_builder()

    app.configure_middleware(
        lambda builder: (
            builder.use_middleware(LoggingMiddleware)
            .use_middleware(HelperTestMiddleware)
            .use_middleware(ResultMiddleware)
        )
    )

    provider = app.build()

    pipeline = provider.get_service(MiddlewarePipeline)

    result = await pipeline.execute(RequestContext(path="/test"))

    assert result == "result"


@pytest.mark.asyncio
async def test_di_middleware_dependencies():
    services = ServiceCollection()

    class DependentMiddleware(IMiddleware):
        def __init__(self, test_middleware: HelperTestMiddleware):
            self.test_middleware = test_middleware

        async def invoke(self, context, next):
            await self.test_middleware.invoke(context, next)
            return "dependent_result"

    app = services.create_application_builder()

    app.configure_middleware(
        lambda builder: (
            builder.use_middleware(HelperTestMiddleware).use_middleware(DependentMiddleware)
        )
    )

    provider = app.build()

    pipeline = provider.get_service(MiddlewarePipeline)

    result = await pipeline.execute(RequestContext(path="/test"))

    assert result == "dependent_result"

```

---

### 35. tests\test_runtime_factory_with_contextvar.py

- **File ID**: file_34
- **Type**: Code File
- **Line Count**: 90
- **Description**: Full reproduction of the complex multi-tenant storage scenario, proving...
- **Dependencies**: None
- **Used By**: None

**Content**:
```
"""
Full reproduction of the complex multi-tenant storage scenario, proving
that:

* each scope gets the right backend according to `tenant_id`
* IConfiguration (singleton) can be resolved inside the transient
  factory without circular-dependency issues
"""

from contextvars import ContextVar
from typing import Dict

from wd.di import ServiceCollection
from wd.di.config import IConfiguration, Configuration

# ── Abstractions & fake back-ends ----------------------------------

class IBlobStorage:
    def upload(self, path: str, data: bytes) -> str: ...

class InMemoryStore(IBlobStorage):
    def __init__(self, scheme: str):
        self.scheme = scheme
        self.objects: Dict[str, bytes] = {}

    def upload(self, path: str, data: bytes) -> str:
        self.objects[path] = data
        return f"{self.scheme}://bucket/{path}"

# ── Tenant-aware factory -------------------------------------------

_current_tenant: ContextVar[str] = ContextVar("tenant")

def storage_factory(sp, tenant_id: str) -> IBlobStorage:
    backend = sp.get_service(IConfiguration).get(
        f"tenants:{tenant_id}:backend"
    )
    mapping = {
        "s3":    lambda: InMemoryStore("s3"),
        "azure": lambda: InMemoryStore("azure"),
        "gcs":   lambda: InMemoryStore("gs"),
    }
    return mapping[backend]()

# ── Worker that depends on the storage -----------------------------

class DataIngestionService:
    def __init__(self, storage: IBlobStorage):
        self.storage = storage

    def ingest(self, file: str):
        return self.storage.upload(file, b"...")

# ── Test ------------------------------------------------------------

def test_tenant_specific_backend_via_contextvar_and_factory():
    services = ServiceCollection()

    services.add_singleton_factory(
        IConfiguration,
        lambda _:
            Configuration(
                {
                    "tenants": {
                        "ACME":    {"backend": "s3"},
                        "Contoso": {"backend": "azure"},
                        "Globex":  {"backend": "gcs"},
                    }
                }
            ),
    )

    services.add_scoped(DataIngestionService)  # per request
    services.add_transient_factory(
        IBlobStorage, lambda sp: storage_factory(sp, _current_tenant.get())
    )

    provider = services.build_service_provider()

    def run_for(tenant, file):
        token = _current_tenant.set(tenant)
        try:
            with provider.create_scope() as scope:
                return scope.get_service(DataIngestionService).ingest(file)
        finally:
            _current_tenant.reset(token)

    assert run_for("ACME",    "a.bin").startswith("s3://")
    assert run_for("Contoso", "b.jpg").startswith("azure://")
    assert run_for("Globex",  "c.pdf").startswith("gs://")

```

---

### 36. tests\test_scope_singleton_interop.py

- **File ID**: file_35
- **Type**: Code File
- **Line Count**: 42
- **Description**: Verifies that resolving a root-level singleton from inside a scope...
- **Dependencies**: None
- **Used By**: None

**Content**:
```
"""
Verifies that resolving a root-level singleton from inside a scope
*does not* trip the circular-dependency guard and that the singleton is
shared across scopes.
"""

from wd.di import ServiceCollection
from wd.di.config import Configuration, IConfiguration

# ── Sample services -------------------------------------------------

class RootSingleton:
    def __init__(self):
        self.counter = 0

class ScopedWorker:
    def __init__(self, singleton: RootSingleton):
        # Touch the singleton to prove we can use it
        singleton.counter += 1
        self.singleton = singleton

# ── Test ------------------------------------------------------------

def test_singleton_resolved_from_scope_without_cycle():
    services = ServiceCollection()
    # root singleton comes from normal registration
    services.add_singleton(RootSingleton)
    # register scoped worker that *depends on* the singleton
    services.add_scoped(ScopedWorker)

    provider = services.build_service_provider()

    # Use two independent scopes
    for _ in range(2):
        with provider.create_scope() as scope:
            worker = scope.get_service(ScopedWorker)
            assert isinstance(worker, ScopedWorker)
            # each scope sees the *same* singleton object
            assert worker.singleton is provider.get_service(RootSingleton)

    # singleton's counter was incremented twice (once per scope)
    assert provider.get_service(RootSingleton).counter == 2

```

---

### 37. tests\test_scoped.py

- **File ID**: file_36
- **Type**: Code File
- **Line Count**: 91
- **Description**: File at tests\test_scoped.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
import pytest
# from wd.di.service_collection import ServiceCollection # Old import path
from wd.di import ServiceCollection # Correct import path

def test_scoped_service_from_root_fails():
    services = ServiceCollection() # Instantiate locally
    # Define a simple scoped service.
    class ScopedService:
        pass

    services.add_scoped(ScopedService)
    provider = services.build_service_provider()

    # Attempting to resolve a scoped service directly from the root provider should fail.
    with pytest.raises(Exception, match="Cannot resolve scoped service from the root provider"):
        provider.get_service(ScopedService)

def test_scoped_service_same_instance_in_scope():
    services = ServiceCollection() # Instantiate locally
    # Define a simple scoped service.
    class ScopedService:
        pass

    services.add_scoped(ScopedService)
    provider = services.build_service_provider()

    # Within the same scope, multiple resolutions should yield the same instance.
    with provider.create_scope() as scope:
        instance1 = scope.get_service(ScopedService)
        instance2 = scope.get_service(ScopedService)
        assert instance1 is instance2

def test_scoped_service_different_instances_in_different_scopes():
    services = ServiceCollection() # Instantiate locally
    # Define a simple scoped service.
    class ScopedService:
        pass

    services.add_scoped(ScopedService)
    provider = services.build_service_provider()

    # Different scopes should produce different instances.
    with provider.create_scope() as scope1:
        instance1 = scope1.get_service(ScopedService)
    with provider.create_scope() as scope2:
        instance2 = scope2.get_service(ScopedService)
    assert instance1 is not instance2

def test_disposable_service_is_disposed():
    services = ServiceCollection() # Instantiate locally
    # Define a disposable service that implements a dispose method.
    class DisposableService:
        def __init__(self):
            self.is_disposed = False

        def dispose(self):
            self.is_disposed = True

    services.add_scoped(DisposableService)
    provider = services.build_service_provider()
    disposable_instance = None

    # Within the scope, the instance is not disposed.
    with provider.create_scope() as scope:
        disposable_instance = scope.get_service(DisposableService)
        assert not disposable_instance.is_disposed

    # Exiting the scope should trigger disposal.
    assert disposable_instance.is_disposed

def test_close_method_is_called_for_disposable():
    services = ServiceCollection() # Instantiate locally
    # Define a disposable service that uses a close method.
    class DisposableService:
        def __init__(self):
            self.is_closed = False

        def close(self):
            self.is_closed = True

    services.add_scoped(DisposableService)
    provider = services.build_service_provider()
    disposable_instance = None

    # Within the scope, the instance is not closed.
    with provider.create_scope() as scope:
        disposable_instance = scope.get_service(DisposableService)
        assert not disposable_instance.is_closed

    # Exiting the scope should trigger the close method.
    assert disposable_instance.is_closed

```

---

### 38. tests\test_service_decorators.py

- **File ID**: file_37
- **Type**: Code File
- **Line Count**: 127
- **Description**: File at tests\test_service_decorators.py
- **Dependencies**: None
- **Used By**: None

**Content**:
```
import pytest

from wd.di import ServiceCollection, ServiceProvider
from wd.di.exceptions import CircularDecoratorError, InvalidOperationError

# --- Test Services ---

class IMessageHandler:
    def handle(self, message: str) -> str:
        raise NotImplementedError

class ConcreteMessageHandler(IMessageHandler):
    def handle(self, message: str) -> str:
        return f"ConcreteHandler: {message}"

# --- Decorator Factories ---

def UppercaseDecoratorFactory(sp: ServiceProvider, inner: IMessageHandler) -> IMessageHandler:
    return UppercaseDecorator(inner)

class UppercaseDecorator(IMessageHandler):
    def __init__(self, inner: IMessageHandler):
        self.inner = inner
    def handle(self, message: str) -> str:
        return self.inner.handle(message).upper()

def ExclamationDecoratorFactory(sp: ServiceProvider, inner: IMessageHandler) -> IMessageHandler:
    return ExclamationDecorator(inner)

class ExclamationDecorator(IMessageHandler):
    def __init__(self, inner: IMessageHandler):
        self.inner = inner
    def handle(self, message: str) -> str:
        return f"{self.inner.handle(message)}!"

def create_wrapping_decorator_factory(prefix: str, suffix: str):
    def factory(sp: ServiceProvider, inner: IMessageHandler) -> IMessageHandler:
        return WrappingDecorator(inner, prefix, suffix)
    return factory

class WrappingDecorator(IMessageHandler):
    def __init__(self, inner: IMessageHandler, prefix: str, suffix: str):
        self.inner = inner
        self.prefix = prefix
        self.suffix = suffix
    def handle(self, message: str) -> str:
        return f"{self.prefix}{self.inner.handle(message)}{self.suffix}"

# --- Tests ---

def test_single_decorator_applied():
    services = ServiceCollection()
    services.add_transient(IMessageHandler, ConcreteMessageHandler)
    services.decorate(IMessageHandler, UppercaseDecoratorFactory)
    
    provider = services.build_service_provider()
    handler = provider.get_service(IMessageHandler)
    
    result = handler.handle("hello")
    assert result == "CONCRETEHANDLER: HELLO"

def test_multiple_decorators_applied_in_order():
    services = ServiceCollection()
    services.add_transient(IMessageHandler, ConcreteMessageHandler)
    services.decorate(IMessageHandler, UppercaseDecoratorFactory)
    services.decorate(IMessageHandler, ExclamationDecoratorFactory)
    
    provider = services.build_service_provider()
    handler = provider.get_service(IMessageHandler)
    
    result = handler.handle("hello")
    assert result == "CONCRETEHANDLER: HELLO!"

def test_decorator_with_parameters_from_factory():
    services = ServiceCollection()
    services.add_transient(IMessageHandler, ConcreteMessageHandler)
    
    prefix_suffix_decorator_factory = create_wrapping_decorator_factory(prefix="[INFO] ", suffix=" ...done")
    services.decorate(IMessageHandler, prefix_suffix_decorator_factory)
    
    provider = services.build_service_provider()
    handler = provider.get_service(IMessageHandler)
    
    result = handler.handle("processing event")
    assert result == "[INFO] ConcreteHandler: processing event ...done"

def test_decorate_unregistered_service_raises_keyerror():
    services = ServiceCollection()
    with pytest.raises(KeyError, match=r"No service registered for IMessageHandler; cannot apply decorator\."):
        services.decorate(IMessageHandler, UppercaseDecoratorFactory)

def test_decorate_after_provider_built_raises_invalidoperationerror():
    services = ServiceCollection()
    services.add_transient(IMessageHandler, ConcreteMessageHandler)
    provider = services.build_service_provider()
    with pytest.raises(InvalidOperationError, match="Cannot modify ServiceCollection after ServiceProvider has been built."):
        services.decorate(IMessageHandler, UppercaseDecoratorFactory)

# --- Circular Decorator Detection Test ---

def _test_key(obj: object) -> str:
    if isinstance(obj, type):
        return obj.__qualname__
    if hasattr(obj, "__qualname__"):
        return obj.__qualname__
    if hasattr(obj, "__name__"):
        return obj.__name__
    return repr(obj)

def test_circular_decorator_dependency_detected():
    services = ServiceCollection()

    def recursive_decorator_factory(sp: ServiceProvider, inner: IMessageHandler) -> IMessageHandler:
        sp.get_service(IMessageHandler) 
        return inner

    services.add_transient(IMessageHandler, ConcreteMessageHandler)
    services.decorate(IMessageHandler, recursive_decorator_factory)

    provider = services.build_service_provider()

    with pytest.raises(CircularDecoratorError) as excinfo:
        provider.get_service(IMessageHandler)
    
    expected_deco_key_name = _test_key(recursive_decorator_factory)
    assert expected_deco_key_name in str(excinfo.value)
    assert "Circular decorator chain detected" in str(excinfo.value) 
```

---

## Summary

- **Total Files**: 38
- **Code Files**: 38
- **Regular Files**: 0
- **Total Lines of Code**: 2663
