Metadata-Version: 2.4
Name: getit-python
Version: 1.0.0
Summary: Production-grade service locator and dependency injection container for Python with thread-safe singletons, lazy loading, and factory patterns
Author-email: Aziz Al-Soufi <azizalsoufi.inbox@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/AzizAl-Soufi/get_it
Project-URL: Documentation, https://github.com/AzizAl-Soufi/get_it#readme
Project-URL: Repository, https://github.com/AzizAl-Soufi/get_it.git
Project-URL: Issues, https://github.com/AzizAl-Soufi/get_it/issues
Project-URL: Changelog, https://github.com/AzizAl-Soufi/get_it/releases
Keywords: getit,get-it,get_it,dependency-injection,service-locator,di-container,ioc,singleton,lazy-loading,factory-pattern,thread-safe,decorators
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Systems Administration
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: isort>=5.0; extra == "dev"
Requires-Dist: flake8>=6.0; extra == "dev"
Requires-Dist: pylint>=2.0; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx>=5.0; extra == "docs"
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
Dynamic: license-file
Dynamic: requires-python

# GetIt: Production-Grade Service Locator & Dependency Injection Container

[![Python Version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

**GetIt** is a thread-safe, production-ready service locator and dependency injection container for Python. It provides elegant, decoupled architecture through declarative registration, multiple resolution strategies, and automatic lifecycle management—all without the boilerplate.

Designed for WSGI/ASGI applications, microservices, and complex Python projects that need flexible, maintainable dependency management.

## Features

- **Thread-Safe:** Double-checked locking guarantees singletons instantiate exactly once under concurrent load
- **Multiple Lifecycles:** Singleton, Lazy Singleton, and Factory patterns for flexible dependency management
- **Zero-Import Resolution:** Fetch dependencies at runtime without circular imports
- **Hybrid Resolution APIs:** Class-based, dot-notation, and explicit getter syntaxes
- **Declarative Registration:** Use `@Singleton`, `@LazySingleton`, `@Factory` decorators at point of definition
- **Full Type Hints:** Complete type annotations with `@overload` for IDE autocomplete in VS Code and PyCharm
- **Production Logging:** Structured logging for observability and debugging
- **Comprehensive Error Handling:** Custom exceptions with actionable error messages

---

## Quick Start

### Installation

```bash
pip install getit-python
```

### Basic Example

```python
from get_it import GetIt, singleton

@singleton
class Service:
    def sync(self):
        print("Syncing.....")

# Global resolution
service = GetIt.get(Service)
service.sync()  # Output: Syncing.....
```

---

## Complete Usage Guide

### 1. Defining Services with Decorators

Services are registered where they're defined, keeping code organized and discoverable.

#### Singleton (Eager Initialization)
Instantiated immediately at decorator evaluation time. Use for lightweight config or static resources.

```python
from get_it import GetIt, singleton

@singleton
class AppConfig:
    def __init__(self):
        self.debug = True
        self.host = "localhost"
        self.port = 8000
        print("[CONFIG] Initialized at import time")

# Automatically registered and instantiated
config = GetIt.get(AppConfig)
print(config.host)  # "localhost"
```

#### Lazy Singleton (Deferred Initialization)
Instantiated only on first access. Perfect for expensive operations like database connections.

```python
from get_it import GetIt, LazySingleton

@LazySingleton
class DatabaseService:
    def __init__(self):
        print("[DB] Connecting to database...")
        self.connected = True

    def query(self, sql: str):
        return f"Executing: {sql}"

# Not instantiated yet
print("App starting...")

# First access triggers initialization
db = GetIt.get(DatabaseService)  # Prints: [DB] Connecting to database...
result = db.query("SELECT * FROM users")
```

#### Factory (Fresh Instance Per Request)
Creates a new instance every time. Ideal for request-scoped state or temporary objects.

```python
from get_it import GetIt, Factory

@Factory
class RequestContext:
    def __init__(self):
        self.request_id = id(self)
        self.user_data = {}

# Each call gets a fresh instance
ctx1 = GetIt.get(RequestContext)
ctx2 = GetIt.get(RequestContext)

assert ctx1 is not ctx2  # Different instances
assert ctx1.request_id != ctx2.request_id
```

---

### 2. Resolution Strategies

GetIt supports multiple ways to resolve dependencies. Choose the pattern that best fits your codebase.

#### Strategy A: Dot-Notation (Cleanest)
Flutter-style syntax. Resolves by class name matching.

```python
from get_it import GetIt, LazySingleton

get_it = GetIt()

@LazySingleton
class DatabaseService:
    def connect(self) -> None:
        print("--> Establishing Database Connection...")

@LazySingleton
class UserRepository:
    def get_user(self, user_id: int):
        return {"id": user_id, "name": "Alice"}

# Dot-notation resolution
db: DatabaseService = get_it.DatabaseService
db.connect()

repo: UserRepository = get_it.UserRepository
user = repo.get_user(1)
print(user)
```

#### Strategy B: Instance Call
Pass the type directly to the locator instance.

```python
from get_it import GetIt, LazySingleton

get_it = GetIt()

@LazySingleton
class CacheService:
    def set(self, key: str, value: str):
        print(f"Cache: {key} = {value}")

# Instance call syntax
cache = get_it(CacheService)
cache.set("user_123", "cached_data")
```

#### Strategy C: Metaclass Resolution
Call the class directly for explicit type-based resolution.

```python
from get_it import GetIt, LazySingleton

@LazySingleton
class AuthService:
    def verify_token(self, token: str) -> bool:
        return len(token) > 10

# Metaclass resolution
auth = GetIt(AuthService)
is_valid = auth.verify_token("my_token_12345")
print(f"Valid: {is_valid}")
```

#### Strategy D: Explicit Get
Standard, unambiguous method for any situation.

```python
from get_it import GetIt, LazySingleton

@LazySingleton
class MailService:
    def send(self, to: str, message: str):
        print(f"Email to {to}: {message}")

# Explicit get
mail = GetIt.get(MailService)
mail.send("user@example.com", "Hello!")
```

#### Complete Example with All Strategies

```python
from get_it import GetIt, LazySingleton

get_it = GetIt()

@LazySingleton
class DatabaseService:
    def connect(self) -> None:
        print("--> Establishing Database Connection...")

def handle_user_login():
    # Strategy A: Dot-notation (cleanest)
    db_a: DatabaseService = get_it.DatabaseService
    db_a.connect()

    # Strategy B: Instance call
    db_b = get_it(DatabaseService)
    db_b.connect()

    # Strategy C: Metaclass call
    db_c = GetIt(DatabaseService)
    db_c.connect()

    # Strategy D: Explicit getter
    db_d = GetIt.get(DatabaseService)
    db_d.connect()
    
    # All resolve to the same cached instance
    assert db_a is db_b is db_c is db_d

handle_user_login()
```

---

### 3. Real-World Web Framework Integration

#### Flask Example

```python
from flask import Flask, jsonify
from get_it import GetIt, LazySingleton

app = Flask(__name__)
get_it = GetIt()

@LazySingleton
class UserDatabase:
    def __init__(self):
        print("[INIT] Connecting to PostgreSQL...")
        # self.connection = psycopg2.connect(...)

    def get_user(self, user_id: int):
        return {"id": user_id, "name": "Alice", "email": "alice@example.com"}

@LazySingleton
class AuthService:
    def __init__(self):
        self.db = get_it.UserDatabase

    def authenticate(self, user_id: int):
        user = self.db.get_user(user_id)
        return bool(user)

@app.route("/users/<int:user_id>")
def get_user(user_id: int):
    auth = GetIt.get(AuthService)
    if auth.authenticate(user_id):
        user = auth.db.get_user(user_id)
        return jsonify(user)
    return jsonify({"error": "Unauthorized"}), 401

if __name__ == "__main__":
    app.run(debug=True)
```

#### FastAPI Example

```python
from fastapi import FastAPI, HTTPException
from get_it import GetIt, LazySingleton

app = FastAPI()
get_it = GetIt()

@LazySingleton
class UserService:
    async def get_user(self, user_id: int):
        # Simulated database call
        return {"id": user_id, "name": "Bob", "email": "bob@example.com"}

@app.get("/users/{user_id}")
async def read_user(user_id: int):
    service = GetIt.get(UserService)
    user = await service.get_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user
```

---

### 4. Manual Registration

For runtime registration or testing scenarios:

```python
from get_it import GetIt

class Logger:
    def log(self, msg: str):
        print(f"[LOG] {msg}")

# Manual registration
logger = Logger()
GetIt.register_singleton(logger)

# Resolution
retrieved_logger = GetIt.get(Logger)
retrieved_logger.log("Hello from manual registration!")

# Factory registration
GetIt.register_factory(Logger, lambda: Logger())

# Lazy singleton registration
GetIt.register_lazy_singleton(Logger, lambda: Logger())
```

---

### 5. Testing with GetIt

Isolate tests by resetting the container and registering mocks:

```python
import pytest
from get_it import GetIt

class UserRepository:
    def get_user(self, user_id: int):
        # Real implementation
        return {"id": user_id, "name": "Real DB"}

class UserService:
    def __init__(self):
        self.repo = GetIt.get(UserRepository)

    def fetch_user(self, user_id: int):
        return self.repo.get_user(user_id)

@pytest.fixture
def clean_di():
    """Clean GetIt before and after each test."""
    GetIt.reset()
    yield
    GetIt.reset()

@pytest.fixture
def mock_user_repo(clean_di):
    """Provide a mock repository for testing."""
    class MockRepository:
        def get_user(self, user_id: int):
            return {"id": user_id, "name": "Mock User", "test": True}

    mock = MockRepository()
    GetIt.register_singleton(mock)
    return mock

def test_user_service_with_mock(mock_user_repo):
    service = UserService()
    user = service.fetch_user(1)
    
    assert user["name"] == "Mock User"
    assert user["test"] is True
```

---

## Lifecycle Reference

| Lifecycle | When Initialized | When Destroyed | Best For |
|-----------|------------------|----------------|----------|
| **Singleton** | At decoration/registration time | Application lifetime | Static config, caches, shared state |
| **LazySingleton** | On first access | Application lifetime | Database connections, API clients, services |
| **Factory** | On every access | Immediately after use | Request-scoped state, temporary objects |

---

## Exception Handling

GetIt provides specific exceptions for precise error handling:

```python
from get_it import (
    GetIt,
    DependencyNotRegisteredError,
    InvalidDependencyTypeError,
    LazySingleton
)

@LazySingleton
class Service:
    pass

try:
    # This will raise DependencyNotRegisteredError
    GetIt.get(str)
except DependencyNotRegisteredError as e:
    print(f"Not registered: {e}")

try:
    # This will raise InvalidDependencyTypeError
    GetIt.register_lazy_singleton("not_a_type", lambda: None)
except InvalidDependencyTypeError as e:
    print(f"Invalid type: {e}")
```

---

## Performance Characteristics

| Operation | Complexity | Notes |
|-----------|-----------|-------|
| `GetIt.get(Type)` singleton | O(1) | Direct dict lookup, no lock |
| `GetIt.get(Type)` lazy (first) | O(1) + factory | Uses double-checked locking |
| `GetIt.get(Type)` lazy (cached) | O(1) | Direct dict lookup, no lock |
| `GetIt.get(Type)` factory | O(1) + factory | Creates new instance |
| `get_it.ServiceName` | O(n) | Linear search by name, n=registered types |

**Tip:** For performance-critical paths, prefer explicit type-based resolution (`GetIt.get(Type)`) over dot-notation to avoid the O(n) name lookup.

---

## Best Practices

### Do
- Register services at their definition point using decorators
- Use `LazySingleton` for expensive operations (database, HTTP clients)
- Use `Factory` for request-scoped or stateful objects
- Type-hint all resolved variables for IDE autocomplete
- Call `GetIt.reset()` in test teardown to prevent test pollution
- Use logging to debug dependency resolution (enable DEBUG logs)

### Don't
- Create circular dependencies (register A that depends on B that depends on A)
- Block on expensive I/O in `@singleton` constructors (use `@LazySingleton` instead)
- Resolve dependencies in module-level code (defer to runtime)
- Mix service registration across multiple files without clear organization
- Register services inside request handlers (register during app startup)

---

## Comparison with Alternatives

| Feature | GetIt | dependency-injector | injector | manual DI |
|---------|-------|---------------------|----------|-----------|
| Thread Safety | Yes (RLock) | Yes | Partial | N/A |
| Lazy Singletons | Yes | Yes | Yes | Manual |
| Factory Pattern | Yes | Yes | Yes | Manual |
| Dot-Notation | Yes | No | No | N/A |
| Type Hints | Full | Partial | Partial | Full |
| Decorator Support | Yes | Yes | Yes | No |
| Learning Curve | Low | Medium | Low | High |
| Circular Import Safe | Yes | Yes | Yes | No |

---

## Contributing

Contributions are welcome! Please ensure:
- All tests pass: `python3 -B test_get_it.py`
- Code is type-checked: `mypy get_it.py`
- Follow PEP 8 style guidelines

---

## License

MIT License - see LICENSE file for details

---

## Changelog

### v1.0.0 (2026-06-18)
- Initial release
- Thread-safe singleton, lazy singleton, and factory patterns
- Comprehensive type hints and error handling
- Full documentation and test coverage

