pico-boot 0.1.0.post0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pico_boot/__init__.py ADDED
@@ -0,0 +1,121 @@
1
+ import inspect
2
+ import os
3
+ from importlib import import_module
4
+ from importlib.metadata import entry_points
5
+ from types import ModuleType
6
+ from typing import Any, Iterable, List, TYPE_CHECKING, Union
7
+
8
+ KeyT = Union[str, type]
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Dict, Optional, Tuple
12
+ from pico_ioc import PicoContainer, ContextConfig
13
+ from pico_ioc import init as init, ContainerObserver
14
+ else:
15
+ from typing import Dict, Optional, Tuple
16
+ import logging
17
+ from pico_ioc import PicoContainer, ContextConfig
18
+ from pico_ioc import init as _ioc_init, ContainerObserver
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ def _to_module_list(modules: Union[Any, Iterable[Any]]) -> List[Any]:
23
+ if isinstance(modules, Iterable) and not isinstance(modules, (str, bytes)):
24
+ return list(modules)
25
+ return [modules]
26
+
27
+ def _import_module_like(obj: Any) -> ModuleType:
28
+ if isinstance(obj, ModuleType):
29
+ return obj
30
+ if isinstance(obj, str):
31
+ return import_module(obj)
32
+ module_name = getattr(obj, "__module__", None) or getattr(obj, "__name__", None)
33
+ if not module_name:
34
+ raise ImportError(f"Cannot determine module for object {obj!r}")
35
+ return import_module(module_name)
36
+
37
+ def _normalize_modules(raw: Iterable[Any]) -> List[ModuleType]:
38
+ seen: set[str] = set()
39
+ result: List[ModuleType] = []
40
+ for item in raw:
41
+ m = _import_module_like(item)
42
+ name = m.__name__
43
+ if name not in seen:
44
+ seen.add(name)
45
+ result.append(m)
46
+ return result
47
+
48
+ def _harvest_scanners(modules: List[ModuleType]) -> list:
49
+ scanners: list = []
50
+ for m in modules:
51
+ module_scanners = getattr(m, "PICO_SCANNERS", None)
52
+ if module_scanners:
53
+ scanners.extend(module_scanners)
54
+ return scanners
55
+
56
+ def _load_plugin_modules(group: str = "pico_boot.modules") -> List[ModuleType]:
57
+ eps = entry_points()
58
+ if hasattr(eps, "select"):
59
+ selected = eps.select(group=group)
60
+ else: # legacy API
61
+ selected = [ep for ep in eps if ep.group == group]
62
+
63
+ seen: set[str] = set()
64
+ modules: List[ModuleType] = []
65
+
66
+ for ep in selected:
67
+ try:
68
+ if ep.module in ("pico_ioc", "pico_boot"):
69
+ continue
70
+ m = import_module(ep.module)
71
+ except Exception as exc:
72
+ logger.warning(
73
+ "Failed to load pico-boot plugin entry point '%s' (%s): %s",
74
+ ep.name,
75
+ ep.module,
76
+ exc,
77
+ )
78
+ continue
79
+
80
+ name = m.__name__
81
+ if name not in seen:
82
+ seen.add(name)
83
+ modules.append(m)
84
+
85
+ return modules
86
+
87
+ _IOC_INIT_SIG = inspect.signature(_ioc_init)
88
+
89
+ def init(*args: Any, **kwargs: Any) -> PicoContainer:
90
+ bound = _IOC_INIT_SIG.bind(*args, **kwargs)
91
+ bound.apply_defaults()
92
+
93
+ base_modules = _normalize_modules(_to_module_list(bound.arguments["modules"]))
94
+
95
+ auto_flag = os.getenv("PICO_BOOT_AUTO_PLUGINS", "true").lower()
96
+ auto_plugins = auto_flag not in ("0", "false", "no")
97
+
98
+ if auto_plugins:
99
+ plugin_modules = _load_plugin_modules()
100
+ all_modules = _normalize_modules(list(base_modules) + plugin_modules)
101
+ else:
102
+ all_modules = base_modules
103
+
104
+ bound.arguments["modules"] = all_modules
105
+
106
+ harvested = _harvest_scanners(all_modules)
107
+ if harvested:
108
+ existing = bound.arguments.get("custom_scanners") or []
109
+ bound.arguments["custom_scanners"] = list(existing) + harvested
110
+
111
+ return _ioc_init(*bound.args, **bound.kwargs)
112
+
113
+ init.__signature__ = _IOC_INIT_SIG
114
+
115
+ __all__ = [
116
+ "init",
117
+ "PicoContainer",
118
+ "ContextConfig",
119
+ "ContainerObserver",
120
+ ]
121
+
pico_boot/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = '0.1.0.post0'
@@ -0,0 +1,387 @@
1
+ Metadata-Version: 2.4
2
+ Name: pico-boot
3
+ Version: 0.1.0.post0
4
+ Summary: Pico-IOC integration layer for ecosystem modules. Auto-discovers DI modules via entry points.
5
+ Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 David Perez
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/dperezcabrera/pico-boot
29
+ Project-URL: Repository, https://github.com/dperezcabrera/pico-boot
30
+ Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-boot/issues
31
+ Keywords: ioc,di,dependency injection,boot,inversion of control,asyncio,plugins,bootstrap
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Programming Language :: Python :: 3.14
40
+ Classifier: License :: OSI Approved :: MIT License
41
+ Classifier: Operating System :: OS Independent
42
+ Requires-Python: >=3.11
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Requires-Dist: pico-ioc>=2.2.0
46
+ Provides-Extra: async
47
+ Requires-Dist: asyncpg>=0.29.0; extra == "async"
48
+ Provides-Extra: test
49
+ Requires-Dist: pytest>=8; extra == "test"
50
+ Requires-Dist: pytest-asyncio>=0.23.5; extra == "test"
51
+ Requires-Dist: pytest-cov>=5; extra == "test"
52
+ Dynamic: license-file
53
+
54
+ # Pico-Boot
55
+
56
+ [![PyPI](https://img.shields.io/pypi/v/pico-boot.svg)](https://pypi.org/project/pico-boot/)
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
58
+ ![CI (tox matrix)](https://github.com/dperezcabrera/pico-boot/actions/workflows/ci.yml/badge.svg)
59
+ [![codecov](https://codecov.io/gh/dperezcabrera/pico-boot/branch/main/graph/badge.svg)](https://codecov.io/gh/dperezcabrera/pico-boot)
60
+ [![Docs](https://img.shields.io/badge/Docs-pico--stack-blue?style=flat&logo=readthedocs&logoColor=white)](https://dperezcabrera.github.io/pico-boot/)
61
+
62
+ **Zero-configuration bootstrap for the Pico ecosystem.**
63
+
64
+ Pico-Boot is a thin orchestration layer over [pico-ioc](https://github.com/dperezcabrera/pico-ioc) that provides:
65
+
66
+ - **Auto-discovery of plugins** via Python entry points
67
+ - **Custom scanner harvesting** from loaded modules
68
+
69
+ > 🐍 Requires Python 3.11+
70
+
71
+ ---
72
+
73
+ ## When to Use Pico-Boot vs Pico-IoC
74
+
75
+ | Use Case | Recommendation |
76
+ |----------|----------------|
77
+ | Simple app, manual control | Use `pico-ioc` directly |
78
+ | Multiple pico-* integrations (fastapi, sqlalchemy, celery) | Use `pico-boot` |
79
+ | Want zero-config plugin discovery | Use `pico-boot` |
80
+
81
+ ---
82
+
83
+ ## Installation
84
+
85
+ ```bash
86
+ pip install pico-boot
87
+ ```
88
+
89
+ This automatically installs `pico-ioc` as a dependency.
90
+
91
+ ---
92
+
93
+ ## Quick Start
94
+
95
+ ### Basic Usage
96
+
97
+ ```python
98
+ # app.py
99
+ from pico_ioc import component, provides
100
+ from pico_boot import init
101
+
102
+ @component
103
+ class Database:
104
+ def query(self) -> str:
105
+ return "data"
106
+
107
+ @component
108
+ class UserService:
109
+ def __init__(self, db: Database):
110
+ self.db = db
111
+
112
+ # pico-boot's init() replaces pico-ioc's init()
113
+ # It auto-discovers plugins and harvests custom scanners
114
+ container = init(modules=[__name__])
115
+
116
+ service = container.get(UserService)
117
+ print(service.db.query())
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Features
123
+
124
+ ### 1. Plugin Auto-Discovery
125
+
126
+ When you install a pico-* integration package, it automatically registers itself:
127
+
128
+ ```bash
129
+ pip install pico-fastapi pico-sqlalchemy pico-celery
130
+ ```
131
+
132
+ ```python
133
+ from pico_boot import init
134
+
135
+ # All installed pico-* plugins are automatically loaded!
136
+ container = init(modules=["myapp"])
137
+ ```
138
+
139
+ No need to explicitly import or configure each integration.
140
+
141
+ ### 2. Custom Scanner Harvesting
142
+
143
+ Modules can expose custom component scanners via `PICO_SCANNERS`:
144
+
145
+ ```python
146
+ # my_plugin/__init__.py
147
+ from pico_ioc import CustomScanner
148
+
149
+ class MyScanner(CustomScanner):
150
+ def scan(self, module):
151
+ # Custom component discovery logic
152
+ pass
153
+
154
+ PICO_SCANNERS = [MyScanner()]
155
+ ```
156
+
157
+ Pico-Boot automatically collects and applies these scanners.
158
+
159
+ ---
160
+
161
+ ## Environment Variables
162
+
163
+ | Variable | Default | Description |
164
+ |----------|---------|-------------|
165
+ | `PICO_BOOT_AUTO_PLUGINS` | `true` | Enable/disable plugin auto-discovery |
166
+
167
+ ### Disabling Auto-Discovery
168
+
169
+ ```bash
170
+ export PICO_BOOT_AUTO_PLUGINS=false
171
+ ```
172
+
173
+ Useful for testing or when you want explicit control.
174
+
175
+ ---
176
+
177
+ ## Creating a Pico-Boot Plugin
178
+
179
+ To make your library discoverable by pico-boot, add an entry point in `pyproject.toml`:
180
+
181
+ ```toml
182
+ [project.entry-points."pico_boot.modules"]
183
+ my_library = "my_library"
184
+ ```
185
+
186
+ ### Example: Creating a Redis Integration
187
+
188
+ ```python
189
+ # pico_redis/__init__.py
190
+ import redis
191
+ from pico_ioc import provides, configured
192
+ from dataclasses import dataclass
193
+
194
+ @configured(prefix="redis")
195
+ @dataclass
196
+ class RedisConfig:
197
+ url: str = "redis://localhost:6379/0"
198
+
199
+ @provides(redis.Redis)
200
+ def build_redis(config: RedisConfig) -> redis.Redis:
201
+ return redis.Redis.from_url(config.url)
202
+ ```
203
+
204
+ ```toml
205
+ # pyproject.toml
206
+ [project.entry-points."pico_boot.modules"]
207
+ pico_redis = "pico_redis"
208
+ ```
209
+
210
+ Now any app using pico-boot will automatically have Redis available!
211
+
212
+ For a complete guide, see the [Creating Plugins](https://dperezcabrera.github.io/pico-boot/creating-plugins/) documentation.
213
+
214
+ ---
215
+
216
+ ## Complete Example
217
+
218
+ ### Project Structure
219
+
220
+ ```
221
+ myapp/
222
+ ├── application.yaml
223
+ ├── main.py
224
+ ├── config.py
225
+ ├── services.py
226
+ └── repositories.py
227
+ ```
228
+
229
+ ### application.yaml
230
+
231
+ ```yaml
232
+ database:
233
+ url: postgresql://localhost/myapp
234
+
235
+ redis:
236
+ url: redis://localhost:6379/0
237
+
238
+ app:
239
+ name: My Application
240
+ debug: false
241
+ ```
242
+
243
+ ### config.py
244
+
245
+ ```python
246
+ from dataclasses import dataclass
247
+ from pico_ioc import configured
248
+
249
+ @configured(prefix="database")
250
+ @dataclass
251
+ class DatabaseConfig:
252
+ url: str
253
+
254
+ @configured(prefix="redis")
255
+ @dataclass
256
+ class RedisConfig:
257
+ url: str
258
+
259
+ @configured(prefix="app")
260
+ @dataclass
261
+ class AppConfig:
262
+ name: str
263
+ debug: bool = False
264
+ ```
265
+
266
+ ### services.py
267
+
268
+ ```python
269
+ from pico_ioc import component
270
+ from .config import AppConfig
271
+
272
+ @component
273
+ class GreetingService:
274
+ def __init__(self, config: AppConfig):
275
+ self.app_name = config.name
276
+
277
+ def greet(self, user: str) -> str:
278
+ return f"Welcome to {self.app_name}, {user}!"
279
+ ```
280
+
281
+ ### main.py
282
+
283
+ ```python
284
+ from pico_ioc import configuration, YamlSource, EnvSource
285
+ from pico_boot import init
286
+ from .services import GreetingService
287
+
288
+ def main():
289
+ # Load configuration via pico-ioc, let pico-boot discover plugins
290
+ config = configuration(
291
+ YamlSource("application.yaml"),
292
+ EnvSource()
293
+ )
294
+ container = init(modules=["myapp.config", "myapp.services"], config=config)
295
+
296
+ service = container.get(GreetingService)
297
+ print(service.greet("Alice"))
298
+
299
+ container.shutdown()
300
+
301
+ if __name__ == "__main__":
302
+ main()
303
+ ```
304
+
305
+ ### Running
306
+
307
+ ```bash
308
+ $ python -m myapp.main
309
+ Welcome to My Application, Alice!
310
+ ```
311
+
312
+ ### Override with Environment Variables
313
+
314
+ ```bash
315
+ $ APP_NAME="Production App" python -m myapp.main
316
+ Welcome to Production App, Alice!
317
+ ```
318
+
319
+ ---
320
+
321
+ ## API Reference
322
+
323
+ ### `init(*args, **kwargs) -> PicoContainer`
324
+
325
+ Drop-in replacement for `pico_ioc.init()` with additional features:
326
+
327
+ - Auto-discovers plugins from `pico_boot.modules` entry points
328
+ - Harvests custom scanners (`PICO_SCANNERS`) from loaded modules
329
+
330
+ All parameters from `pico_ioc.init()` are supported:
331
+
332
+ ```python
333
+ container = init(
334
+ modules=["myapp"], # Modules to scan
335
+ config=my_config, # Optional: custom ContextConfig
336
+ profiles=["prod"], # Optional: active profiles
337
+ overrides={Service: Mock()}, # Optional: test overrides
338
+ observers=[MyObserver()], # Optional: container observers
339
+ custom_scanners=[], # Optional: additional scanners
340
+ )
341
+ ```
342
+
343
+ ---
344
+
345
+ ## Documentation
346
+
347
+ Full documentation is available at [dperezcabrera.github.io/pico-boot](https://dperezcabrera.github.io/pico-boot/).
348
+
349
+ - [Getting Started](https://dperezcabrera.github.io/pico-boot/getting-started/)
350
+ - [Configuration](https://dperezcabrera.github.io/pico-boot/configuration/)
351
+ - [Creating Plugins](https://dperezcabrera.github.io/pico-boot/creating-plugins/)
352
+ - [API Reference](https://dperezcabrera.github.io/pico-boot/api-reference/)
353
+ - [Ecosystem](https://dperezcabrera.github.io/pico-boot/ecosystem/)
354
+
355
+ ---
356
+
357
+ ## Ecosystem
358
+
359
+ Pico-Boot works with these integration packages:
360
+
361
+ | Package | Description |
362
+ |---------|-------------|
363
+ | [pico-ioc](https://github.com/dperezcabrera/pico-ioc) | Core DI container |
364
+ | [pico-fastapi](https://github.com/dperezcabrera/pico-fastapi) | FastAPI integration |
365
+ | [pico-sqlalchemy](https://github.com/dperezcabrera/pico-sqlalchemy) | SQLAlchemy integration |
366
+ | [pico-celery](https://github.com/dperezcabrera/pico-celery) | Celery integration |
367
+ | [pico-pydantic](https://github.com/dperezcabrera/pico-pydantic) | Pydantic validation |
368
+
369
+ ---
370
+
371
+ ## Development
372
+
373
+ ```bash
374
+ # Run tests
375
+ pip install tox
376
+ tox
377
+
378
+ # Build documentation locally
379
+ pip install -r docs/requirements.txt
380
+ mkdocs serve
381
+ ```
382
+
383
+ ---
384
+
385
+ ## License
386
+
387
+ MIT - [LICENSE](./LICENSE)
@@ -0,0 +1,7 @@
1
+ pico_boot/__init__.py,sha256=kiTzllt7y4VcLwTB-xo8VNvwoEkVvSHeM4A_jy-I0RA,3957
2
+ pico_boot/_version.py,sha256=0iP1THdPq83rbUsMGDOrAIfUyC5BhxyeOWsGdqdnFSY,28
3
+ pico_boot-0.1.0.post0.dist-info/licenses/LICENSE,sha256=j86ePhARUMlHapXj0uGGdmnRTholb6VMoORCv8jkGdU,1068
4
+ pico_boot-0.1.0.post0.dist-info/METADATA,sha256=kr6Kf_u8a_1gCmH9k5_48_sfPuJy0GlnJ6BVUicfNWQ,10238
5
+ pico_boot-0.1.0.post0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ pico_boot-0.1.0.post0.dist-info/top_level.txt,sha256=XKFMUvxblFAJ8HJIToiDbAt5wyzp1xIRcTz0__-0gMo,10
7
+ pico_boot-0.1.0.post0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 David Perez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pico_boot