Metadata-Version: 2.4
Name: pyboot-framework
Version: 0.1.18
Summary: A lightweight, extensible Python framework for building applications with dependency injection and a modular plugin architecture.
Author: Siddharth Karn
License-Expression: MIT
Keywords: pyboot,python-framework,dependency-injection,inversion-of-control,ioc,plugin-architecture,starlette
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: loguru>=0.7.3
Dynamic: license-file

# PyBoot

PyBoot is a lightweight Python framework for inversion of control (IoC),
constructor-based dependency injection, component discovery, and extensible
plugin lifecycle management.

The core package deliberately does not create a web server or impose an
application class. Optional packages, such as
[`pyboot-web`](../plugins/pyboot_web/README.md), add infrastructure through the
plugin system.

## Requirements

- Python 3.11 or newer
- `pyboot-framework` 0.1.x

## Installation

```bash
python -m pip install pyboot-framework
```

For development from this repository:

```bash
python -m pip install -e .
```

## Quick start

Application classes must be inside the package passed to `bootstrap()`.
PyBoot imports that package and recursively scans its submodules for classes.

`app/services.py`:

```python
from pyboot.core.builtins.component import Component


@Component()
class GreetingService:
    def greet(self, name: str) -> str:
        return f"Hello, {name}!"
```

`main.py`:

```python
from pyboot import PyBoot
from app.services import GreetingService


boot = PyBoot(debug=False)
boot.bootstrap(package="app")

service = boot.get_context().ioc.get_bean(GreetingService)
print(service.greet("PyBoot"))
```

`bootstrap()` returns `None`. It initializes the context in place. Enable
framework logging with `PyBoot(debug=True)`.

## Components and dependency injection

`@Component()` marks a class for registration by the built-in component
plugin. Required constructor parameters must have type annotations:

```python
from pyboot.core.builtins.component import Component


@Component()
class UserRepository:
    def find_name(self, user_id: int) -> str:
        return {1: "Ada", 2: "Grace"}.get(user_id, "Unknown")


@Component()
class UserService:
    def __init__(self, repository: UserRepository) -> None:
        self.repository = repository

    def name_for(self, user_id: int) -> str:
        return self.repository.find_name(user_id)
```

The container resolves dependencies recursively and caches resolved objects as
singletons. Do not construct managed dependencies manually when you need their
managed instance.

```python
service = boot.get_context().ioc.get_bean(UserService)
same_service = boot.get_context().ioc.get_bean(UserService)
assert service is same_service
```

Optional constructor parameters with defaults are not resolved. A required
parameter without a type annotation raises a dependency-resolution error.
Circular dependency graphs are rejected.

## Configuration and bean factories

Use `@Configuration()` for a class containing factory methods. Every
`@Bean()` method must declare its return type; the return type is the IoC key.

```python
from pyboot.core.builtins.configuration import Bean, Configuration


class Settings:
    def __init__(self, service_name: str) -> None:
        self.service_name = service_name


@Configuration()
class ApplicationConfiguration:
    @Bean()
    def settings(self) -> Settings:
        return Settings("example-service")
```

Bean methods may also receive annotated dependencies:

```python
@Configuration()
class ClientConfiguration:
    @Bean()
    def client(self, settings: Settings) -> dict[str, str]:
        return {"service": settings.service_name}
```

## Application context

`PyBootContext` contains the discovered classes, IoC container, loaded plugin
instances, and a resource bucket:

```python
context = boot.get_context()
service = context.ioc.get_bean(UserService)

context.put_resource("service_name", "example-service")
assert context.exist_resource("service_name")
assert context.get_resource("service_name") == "example-service"

context.update_resource("service_name", "updated-service")
```

`put_resource()` rejects duplicate keys. `update_resource()` requires an
existing key. Resources are useful for sharing initialized infrastructure
between plugins.

## Plugin architecture

PyBoot discovers plugin classes from the
`pyboot.plugins` Python entry-point group. A plugin extends `PyBootPlugin`:

```python
from pyboot import PyBootPlugin
from pyboot.core.pyboot_context import PyBootContext


class MetricsPlugin(PyBootPlugin):
    def after_plugins_discovery(self, context: PyBootContext) -> None:
        context.put_resource("metrics", {"started": True})

    def on_ready(self, context: PyBootContext) -> None:
        print("PyBoot is ready")
```

Register it in the plugin package's `pyproject.toml`:

```toml
[project.entry-points."pyboot.plugins"]
metrics = "my_metrics.plugin:MetricsPlugin"
```

Install the plugin into the same environment as the application. PyBoot
creates one instance of each discovered plugin during `bootstrap()`.

The lifecycle order is:

```text
after_plugins_discovery
before_classes_discovery
after_classes_discovery
before_register
on_register
after_register
before_load
on_load
after_load
on_ready
```

Use `on_register()` to register discovered classes or bean factories. Use
`after_load()` and `on_ready()` after IoC beans have been created. Plugin
ordering is not guaranteed, so plugins should not depend on incidental
iteration order.

Retrieve a plugin by its class:

```python
from my_metrics.plugin import MetricsPlugin


metrics = boot.get_plugin(MetricsPlugin)
```

`get_plugin()` raises `LookupError` when the requested plugin was not loaded.

## Built-in plugins

The core distribution registers these entry points:

| Entry point | Responsibility |
| --- | --- |
| `configuration` | Registers `@Configuration()` classes and `@Bean()` methods |
| `component` | Registers `@Component()` classes |

Other packages can register additional entry points. For example,
`pyboot-web` registers web controllers, services, middleware, scheduling,
environment loading, and Starlette integration.

## Discovery rules

Given `bootstrap("app")`, PyBoot:

1. Loads installed `pyboot.plugins` entry points.
2. Imports `app`.
3. Walks `app` submodules with `pkgutil`.
4. Records classes defined by those modules.
5. Lets plugins register the classes they own.
6. Creates registered beans.
7. Runs the remaining lifecycle hooks.

Use normal Python packages with `__init__.py` files and keep application classes
below the scanned package. Imported classes are ignored unless they are defined
in the module currently being scanned.

## Exceptions

Common core exceptions include:

- `ClassNotRegisteredForBean`: a requested class has no registration.
- `DependencyResolutionException`: a dependency cannot be resolved.
- `CircularDependencyException`: a dependency graph contains a cycle.
- `MultipleBeanException`: multiple definitions use the same bean type.
- `KeyAlreadyInResourceBucketException`: a resource key already exists.

Import exceptions from `pyboot.core.exceptions` when application code needs to
handle them explicitly.

## Testing

Business logic remains ordinary Python and can be unit tested without
bootstrapping:

```python
def test_user_service() -> None:
    repository = UserRepository()
    service = UserService(repository)
    assert service.name_for(1) == "Ada"
```

Test wiring by bootstrapping a package and retrieving the bean from its context:

```python
def test_user_service_is_wired() -> None:
    boot = PyBoot()
    boot.bootstrap("app")
    assert boot.get_context().ioc.get_bean(UserService).name_for(1) == "Ada"
```

## Related documentation

- [Core examples](./examples.md)
- [pyboot-web README](../plugins/pyboot_web/README.md)
- [pyboot-web examples](../plugins/pyboot_web/EXAMPLES.md)

## License

PyBoot is distributed under the MIT License. See [LICENSE](./LICENSE).
