engin 0.0.2__tar.gz → 0.0.4__tar.gz

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.
Files changed (78) hide show
  1. engin-0.0.4/.github/workflows/check.yaml +35 -0
  2. {engin-0.0.2 → engin-0.0.4}/.github/workflows/publish.yaml +9 -2
  3. engin-0.0.4/.readthedocs.yaml +25 -0
  4. engin-0.0.4/CHANGELOG.md +47 -0
  5. engin-0.0.4/PKG-INFO +61 -0
  6. engin-0.0.4/README.md +53 -0
  7. engin-0.0.4/docs/engin.md +1 -0
  8. engin-0.0.4/docs/index.md +29 -0
  9. engin-0.0.4/docs/js/readthedocs.js +32 -0
  10. engin-0.0.4/docs/overrides/main.html +6 -0
  11. {engin-0.0.2 → engin-0.0.4}/examples/asgi/app.py +2 -2
  12. {engin-0.0.2 → engin-0.0.4}/examples/asgi/common/db/adapaters/memory.py +1 -1
  13. {engin-0.0.2 → engin-0.0.4}/examples/asgi/common/starlette/endpoint.py +2 -3
  14. {engin-0.0.2 → engin-0.0.4}/examples/asgi/features/cats/api/get.py +2 -2
  15. {engin-0.0.2 → engin-0.0.4}/examples/asgi/features/cats/api/post.py +3 -1
  16. {engin-0.0.2 → engin-0.0.4}/examples/asgi/main.py +1 -1
  17. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/app.py +1 -1
  18. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/main.py +1 -1
  19. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/routes/cats/api.py +3 -3
  20. {engin-0.0.2 → engin-0.0.4}/examples/simple/main.py +3 -0
  21. engin-0.0.4/mkdocs.yaml +57 -0
  22. engin-0.0.4/pyproject.toml +91 -0
  23. {engin-0.0.2 → engin-0.0.4}/src/engin/_assembler.py +2 -2
  24. {engin-0.0.2 → engin-0.0.4}/src/engin/_block.py +6 -6
  25. {engin-0.0.2 → engin-0.0.4}/src/engin/_dependency.py +14 -16
  26. engin-0.0.4/src/engin/_engin.py +203 -0
  27. engin-0.0.4/src/engin/_lifecycle.py +38 -0
  28. {engin-0.0.2 → engin-0.0.4}/src/engin/ext/asgi.py +7 -7
  29. {engin-0.0.2 → engin-0.0.4}/src/engin/ext/fastapi.py +5 -9
  30. engin-0.0.4/tests/acceptance/test_error_in_shutdown.py +50 -0
  31. engin-0.0.4/tests/acceptance/test_error_in_start_up.py +45 -0
  32. engin-0.0.4/tests/conftest.py +0 -0
  33. {engin-0.0.2 → engin-0.0.4}/tests/deps.py +2 -2
  34. {engin-0.0.2 → engin-0.0.4}/tests/test_engin.py +55 -1
  35. engin-0.0.4/uv.lock +1069 -0
  36. engin-0.0.2/.github/workflows/check.yaml +0 -30
  37. engin-0.0.2/CHANGELOG.md +0 -19
  38. engin-0.0.2/PKG-INFO +0 -56
  39. engin-0.0.2/README.md +0 -48
  40. engin-0.0.2/pyproject.toml +0 -39
  41. engin-0.0.2/src/engin/_engin.py +0 -88
  42. engin-0.0.2/src/engin/_lifecycle.py +0 -21
  43. engin-0.0.2/uv.lock +0 -482
  44. {engin-0.0.2 → engin-0.0.4}/.gitignore +0 -0
  45. {engin-0.0.2 → engin-0.0.4}/LICENSE +0 -0
  46. {engin-0.0.2 → engin-0.0.4}/examples/__init__.py +0 -0
  47. {engin-0.0.2 → engin-0.0.4}/examples/asgi/__init__.py +0 -0
  48. {engin-0.0.2 → engin-0.0.4}/examples/asgi/common/__init__.py +0 -0
  49. {engin-0.0.2 → engin-0.0.4}/examples/asgi/common/db/__init__.py +0 -0
  50. {engin-0.0.2 → engin-0.0.4}/examples/asgi/common/db/adapaters/__init__.py +0 -0
  51. {engin-0.0.2 → engin-0.0.4}/examples/asgi/common/db/block.py +0 -0
  52. {engin-0.0.2 → engin-0.0.4}/examples/asgi/common/db/ports.py +0 -0
  53. {engin-0.0.2 → engin-0.0.4}/examples/asgi/common/starlette/__init__.py +0 -0
  54. {engin-0.0.2 → engin-0.0.4}/examples/asgi/features/__init__.py +0 -0
  55. {engin-0.0.2 → engin-0.0.4}/examples/asgi/features/cats/__init__.py +0 -0
  56. {engin-0.0.2 → engin-0.0.4}/examples/asgi/features/cats/api/__init__.py +0 -0
  57. {engin-0.0.2 → engin-0.0.4}/examples/asgi/features/cats/block.py +0 -0
  58. {engin-0.0.2 → engin-0.0.4}/examples/asgi/features/cats/domain.py +0 -0
  59. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/__init__.py +0 -0
  60. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/routes/__init__.py +0 -0
  61. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/routes/cats/__init__.py +0 -0
  62. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
  63. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
  64. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/routes/cats/block.py +0 -0
  65. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/routes/cats/domain.py +0 -0
  66. {engin-0.0.2 → engin-0.0.4}/examples/fastapi/routes/cats/ports.py +0 -0
  67. {engin-0.0.2 → engin-0.0.4}/examples/simple/__init__.py +0 -0
  68. {engin-0.0.2 → engin-0.0.4}/src/engin/__init__.py +0 -0
  69. {engin-0.0.2 → engin-0.0.4}/src/engin/_exceptions.py +0 -0
  70. {engin-0.0.2 → engin-0.0.4}/src/engin/_type_utils.py +0 -0
  71. {engin-0.0.2 → engin-0.0.4}/src/engin/ext/__init__.py +0 -0
  72. {engin-0.0.2 → engin-0.0.4}/src/engin/py.typed +0 -0
  73. {engin-0.0.2 → engin-0.0.4}/tests/__init__.py +0 -0
  74. /engin-0.0.2/tests/conftest.py → /engin-0.0.4/tests/acceptance/__init__.py +0 -0
  75. {engin-0.0.2 → engin-0.0.4}/tests/test_assembler.py +0 -0
  76. {engin-0.0.2 → engin-0.0.4}/tests/test_dependencies.py +0 -0
  77. {engin-0.0.2 → engin-0.0.4}/tests/test_modules.py +0 -0
  78. {engin-0.0.2 → engin-0.0.4}/tests/test_utils.py +0 -0
@@ -0,0 +1,35 @@
1
+ name: Check
2
+
3
+ on:
4
+ push:
5
+
6
+ jobs:
7
+ check:
8
+ name: python
9
+ runs-on: ubuntu-latest
10
+
11
+ permissions:
12
+ id-token: write
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v5
19
+ with:
20
+ enable-cache: true
21
+ cache-dependency-glob: "uv.lock"
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version-file: "pyproject.toml"
27
+
28
+ - name: Install the project
29
+ run: uv sync --all-extras --dev
30
+
31
+ - name: Quality
32
+ run: uv run poe check
33
+
34
+ - name: Test
35
+ run: uv run poe test
@@ -2,6 +2,8 @@ name: Publish
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
+ release:
6
+ types: [created]
5
7
 
6
8
  jobs:
7
9
  publish-to-pypi:
@@ -15,10 +17,15 @@ jobs:
15
17
  - uses: actions/checkout@v4
16
18
 
17
19
  - name: Install uv
18
- uses: astral-sh/setup-uv@v3
20
+ uses: astral-sh/setup-uv@v5
21
+ with:
22
+ enable-cache: true
23
+ cache-dependency-glob: "uv.lock"
19
24
 
20
25
  - name: Set up Python
21
- run: uv python install
26
+ uses: actions/setup-python@v5
27
+ with:
28
+ python-version-file: "pyproject.toml"
22
29
 
23
30
  - name: Build the Project
24
31
  run: uv build
@@ -0,0 +1,25 @@
1
+ # Read the Docs configuration file
2
+ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3
+
4
+ # Required
5
+ version: 2
6
+
7
+ # Set the OS, Python version, and other tools you might need
8
+ build:
9
+ os: ubuntu-24.04
10
+ tools:
11
+ python: "3.13"
12
+ jobs:
13
+ create_environment:
14
+ - asdf plugin add uv
15
+ - asdf install uv latest
16
+ - asdf global uv latest
17
+ - uv venv
18
+ install:
19
+ - uv sync --group docs
20
+ build:
21
+ html:
22
+ - NO_COLOR=1 uv run mkdocs build -f mkdocs.yaml --strict --site-dir $READTHEDOCS_OUTPUT/html
23
+
24
+ mkdocs:
25
+ configuration: mkdocs.yaml
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+
9
+ ## [0.0.4] - 2025-01-27
10
+
11
+ ### Changed
12
+
13
+ - Invocations, startups tasks and shutdown tasks are now all run sequentially.
14
+ - Improved error handling, if an Invocation errors, or a Lifecycle startup tasks errors
15
+ then the Engin will exit. Whilst errors in shutdown tasks are logged and ignored.
16
+ - Improved error messaging when Invocations or Lifecycle tasks error.
17
+ - Removed non-public methods from the Lifecycle class, and renamed `register_context` to
18
+ `append`.
19
+
20
+
21
+ ## [0.0.3] - 2025-01-15
22
+
23
+ ### Added
24
+
25
+ - Blocks can now provide options via the `options` class variable. This allows packaged
26
+ Blocks to easily expose Providers and Invocations as normal functions whilst allowing
27
+ them to be part of a Block as well. This makes usage of the Block optional which makes
28
+ it more flexible for end users.
29
+ - Added missing type hints and enabled mypy strict mode.
30
+
31
+ ### Fixed
32
+
33
+ - Engin now performs Lifecycle shutdown.
34
+
35
+
36
+ ## [0.0.2] - 2025-01-10
37
+
38
+ ### Added
39
+
40
+ - The `ext` sub-package is now explicitly exported in the package `__init__.py`
41
+
42
+
43
+ ## [0.0.1] - 2024-12-12
44
+
45
+ ### Added
46
+
47
+ - Initial release
engin-0.0.4/PKG-INFO ADDED
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: engin
3
+ Version: 0.0.4
4
+ Summary: An async-first modular application framework
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # Engin 🏎️
10
+
11
+ Engin is a zero-dependency application framework for modern Python.
12
+
13
+ **Documentation**: https://engin.readthedocs.io/
14
+
15
+ ## Features ✨
16
+
17
+ - **Dependency Injection** - Engin includes a fully-featured Dependency Injection system,
18
+ powered by type hints.
19
+ - **Lifecycle Management** - Engin provides a simple & portable approach for attaching
20
+ startup and shutdown tasks to the application's lifecycle.
21
+ - **Code Reuse** - Engin's modular components, called Blocks, work great as distributed
22
+ packages allowing zero boiler-plate code reuse across multiple applications. Perfect for
23
+ maintaining many services across your organisation.
24
+ - **Ecosystem Compatability** - Engin ships with integrations for popular frameworks that
25
+ provide their own Dependency Injection, for example FastAPI, allowing you to integrate
26
+ Engin into existing code bases incrementally.
27
+ - **Async Native**: Engin is an async framework, meaning first class support for async
28
+ dependencies. However Engin will happily run synchronous code as well.
29
+
30
+ ## Installation
31
+
32
+ Engin is available on PyPI, install using your favourite dependency manager:
33
+
34
+ - **pip**:`pip install engin`
35
+ - **poetry**: `poetry add engin`
36
+ - **uv**: `uv add engin`
37
+
38
+ ## Getting Started
39
+
40
+ A minimal example:
41
+
42
+ ```python
43
+ import asyncio
44
+
45
+ from httpx import AsyncClient
46
+
47
+ from engin import Engin, Invoke, Provide
48
+
49
+
50
+ def httpx_client() -> AsyncClient:
51
+ return AsyncClient()
52
+
53
+
54
+ async def main(http_client: AsyncClient) -> None:
55
+ print(await http_client.get("https://httpbin.org/get"))
56
+
57
+ engin = Engin(Provide(httpx_client), Invoke(main))
58
+
59
+ asyncio.run(engin.run())
60
+ ```
61
+
engin-0.0.4/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Engin 🏎️
2
+
3
+ Engin is a zero-dependency application framework for modern Python.
4
+
5
+ **Documentation**: https://engin.readthedocs.io/
6
+
7
+ ## Features ✨
8
+
9
+ - **Dependency Injection** - Engin includes a fully-featured Dependency Injection system,
10
+ powered by type hints.
11
+ - **Lifecycle Management** - Engin provides a simple & portable approach for attaching
12
+ startup and shutdown tasks to the application's lifecycle.
13
+ - **Code Reuse** - Engin's modular components, called Blocks, work great as distributed
14
+ packages allowing zero boiler-plate code reuse across multiple applications. Perfect for
15
+ maintaining many services across your organisation.
16
+ - **Ecosystem Compatability** - Engin ships with integrations for popular frameworks that
17
+ provide their own Dependency Injection, for example FastAPI, allowing you to integrate
18
+ Engin into existing code bases incrementally.
19
+ - **Async Native**: Engin is an async framework, meaning first class support for async
20
+ dependencies. However Engin will happily run synchronous code as well.
21
+
22
+ ## Installation
23
+
24
+ Engin is available on PyPI, install using your favourite dependency manager:
25
+
26
+ - **pip**:`pip install engin`
27
+ - **poetry**: `poetry add engin`
28
+ - **uv**: `uv add engin`
29
+
30
+ ## Getting Started
31
+
32
+ A minimal example:
33
+
34
+ ```python
35
+ import asyncio
36
+
37
+ from httpx import AsyncClient
38
+
39
+ from engin import Engin, Invoke, Provide
40
+
41
+
42
+ def httpx_client() -> AsyncClient:
43
+ return AsyncClient()
44
+
45
+
46
+ async def main(http_client: AsyncClient) -> None:
47
+ print(await http_client.get("https://httpbin.org/get"))
48
+
49
+ engin = Engin(Provide(httpx_client), Invoke(main))
50
+
51
+ asyncio.run(engin.run())
52
+ ```
53
+
@@ -0,0 +1 @@
1
+ ::: engin.Engin
@@ -0,0 +1,29 @@
1
+ # Introduction
2
+
3
+ Engin is a zero-dependency application framework for modern Python.
4
+
5
+ Engin is inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx) and the
6
+ [Injector framework for Python](https://github.com/python-injector/injector).
7
+
8
+ ## Why use Engin?
9
+
10
+ - **Dependency Injection** - Engin includes a fully-featured Dependency Injection system,
11
+ powered by type hints.
12
+ - **Lifecycle Management** - Engin provides a simple & portable approach for attaching
13
+ startup and shutdown tasks to the application's lifecycle.
14
+ - **Code Reuse** - Engin's modular components, called Blocks, work great as distributed
15
+ packages allowing zero boiler-plate code reuse across multiple applications. Perfect for
16
+ maintaining many services across your organisation.
17
+ - **Ecosystem Compatability** - Engin ships with integrations for popular frameworks that
18
+ provide their own Dependency Injection, for example FastAPI, allowing you to integrate
19
+ Engin into existing code bases incrementally.
20
+ - **Async Native**: Engin is an async framework, meaning first class support for async
21
+ dependencies. However Engin will happily run synchronous code as well.
22
+
23
+ ## Installation
24
+
25
+ Engin is available on PyPI, install using your favourite dependency manager:
26
+
27
+ - **pip**:`pip install engin`
28
+ - **poetry**: `poetry add engin`
29
+ - **uv**: `uv add engin`
@@ -0,0 +1,32 @@
1
+ document.addEventListener("DOMContentLoaded", function(event) {
2
+ // Trigger Read the Docs' search addon instead of Material MkDocs default
3
+ document.querySelector(".md-search__input").addEventListener("focus", (e) => {
4
+ const event = new CustomEvent("readthedocs-search-show");
5
+ document.dispatchEvent(event);
6
+ });
7
+ });
8
+
9
+ // Use CustomEvent to generate the version selector
10
+ document.addEventListener(
11
+ "readthedocs-addons-data-ready",
12
+ function (event) {
13
+ const config = event.detail.data();
14
+ const versioning = `
15
+ <div class="md-version">
16
+ <button class="md-version__current" aria-label="Select version">
17
+ ${config.versions.current.slug}
18
+ </button>
19
+
20
+ <ul class="md-version__list">
21
+ ${ config.versions.active.map(
22
+ (version) => `
23
+ <li class="md-version__item">
24
+ <a href="${ version.urls.documentation }" class="md-version__link">
25
+ ${ version.slug }
26
+ </a>
27
+ </li>`).join("\n")}
28
+ </ul>
29
+ </div>`;
30
+
31
+ document.querySelector(".md-header__topic").insertAdjacentHTML("beforeend", versioning);
32
+ });
@@ -0,0 +1,6 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block site_meta %}
4
+ {{ super() }}
5
+ <meta name="readthedocs-addons-api-version" content="1" />
6
+ {% endblock %}
@@ -5,8 +5,8 @@ from starlette.requests import Request
5
5
  from starlette.responses import JSONResponse, Response
6
6
  from starlette.routing import Mount, Route
7
7
 
8
- from engin import Block, invoke, provide
9
- from engin.extensions.asgi import ASGIType
8
+ from engin import Block, provide
9
+ from engin.ext.asgi import ASGIType
10
10
 
11
11
 
12
12
  class AppConfig(BaseSettings):
@@ -5,7 +5,7 @@ from examples.asgi.common.db.ports import DatabaseInterface
5
5
 
6
6
  class InMemoryDatabase(DatabaseInterface):
7
7
  def __init__(self) -> None:
8
- self._data = {}
8
+ self._data: dict[str, Any] = {}
9
9
 
10
10
  def get(self, key: str) -> Any:
11
11
  return self._data.get(key)
@@ -1,5 +1,4 @@
1
1
  from abc import ABC, abstractmethod
2
- from collections.abc import Iterable
3
2
  from typing import ClassVar
4
3
 
5
4
  from starlette.exceptions import HTTPException
@@ -14,7 +13,7 @@ class Endpoint(ABC):
14
13
  HTTPEndpoint does not allow you to control class initialisation.
15
14
  """
16
15
 
17
- ALLOWED_METHODS: ClassVar[Iterable[str]] = (
16
+ ALLOWED_METHODS: ClassVar[list[str]] = [
18
17
  "GET",
19
18
  "HEAD",
20
19
  "POST",
@@ -22,7 +21,7 @@ class Endpoint(ABC):
22
21
  "PATCH",
23
22
  "DELETE",
24
23
  "OPTIONS",
25
- )
24
+ ]
26
25
 
27
26
  @abstractmethod
28
27
  async def exec(self, request: Request) -> Response: ...
@@ -1,4 +1,4 @@
1
- import json
1
+ from typing import ClassVar
2
2
 
3
3
  from starlette.requests import Request
4
4
  from starlette.responses import JSONResponse, Response
@@ -9,7 +9,7 @@ from examples.asgi.features.cats.domain import Cat
9
9
 
10
10
 
11
11
  class GetCatEndpoint(Endpoint):
12
- ALLOWED_METHODS = ("GET",)
12
+ ALLOWED_METHODS: ClassVar[list[str]] = ["GET"]
13
13
 
14
14
  def __init__(self, db: DatabaseInterface) -> None:
15
15
  self._db = db
@@ -1,3 +1,5 @@
1
+ from typing import ClassVar
2
+
1
3
  from pydantic import ValidationError
2
4
  from starlette.requests import Request
3
5
  from starlette.responses import Response
@@ -8,7 +10,7 @@ from examples.asgi.features.cats.domain import Cat
8
10
 
9
11
 
10
12
  class PostCatEndpoint(Endpoint):
11
- ALLOWED_METHODS = ("POST",)
13
+ ALLOWED_METHODS: ClassVar[list[str]] = ["POST"]
12
14
 
13
15
  def __init__(self, db: DatabaseInterface) -> None:
14
16
  self._db = db
@@ -3,7 +3,7 @@ import logging
3
3
  import uvicorn
4
4
 
5
5
  from engin import Supply
6
- from engin.extensions.asgi import ASGIEngin
6
+ from engin.ext.asgi import ASGIEngin
7
7
  from examples.asgi.app import AppBlock, AppConfig
8
8
  from examples.asgi.common.db.block import DatabaseBlock
9
9
  from examples.asgi.features.cats.block import CatBlock
@@ -19,7 +19,7 @@ class AppBlock(Block):
19
19
 
20
20
  @invoke
21
21
  def add_health_endpoint(self, app: FastAPI) -> None:
22
- async def health():
22
+ async def health() -> dict[str, bool]:
23
23
  return {"ok": True}
24
24
 
25
25
  app.add_api_route(path="/health", endpoint=health)
@@ -3,7 +3,7 @@ import logging
3
3
  import uvicorn
4
4
 
5
5
  from engin import Supply
6
- from engin.extensions.fastapi import FastAPIEngin
6
+ from engin.ext.fastapi import FastAPIEngin
7
7
  from examples.fastapi.app import AppBlock, AppConfig
8
8
  from examples.fastapi.routes.cats.block import CatBlock
9
9
 
@@ -26,17 +26,17 @@ class CatPostModel(BaseModel):
26
26
 
27
27
 
28
28
  @router.post("/")
29
- async def get_cat(
29
+ async def post_cat(
30
30
  cat: CatPostModel,
31
31
  repository: Annotated[CatRepository, Inject(CatRepository)],
32
32
  ) -> int:
33
33
  cat_id = repository.next_id()
34
- cat = Cat(
34
+ cat_domain = Cat(
35
35
  id=cat_id,
36
36
  name=cat.name,
37
37
  personality=cat.personality,
38
38
  age=cat.age,
39
39
  breed=cat.breed,
40
40
  )
41
- repository.set(cat=cat)
41
+ repository.set(cat=cat_domain)
42
42
  return cat_id
@@ -1,9 +1,12 @@
1
1
  import asyncio
2
+ import logging
2
3
 
3
4
  from httpx import AsyncClient
4
5
 
5
6
  from engin import Engin, Invoke, Provide
6
7
 
8
+ logging.basicConfig(level=logging.DEBUG)
9
+
7
10
 
8
11
  def new_httpx_client() -> AsyncClient:
9
12
  return AsyncClient()
@@ -0,0 +1,57 @@
1
+ site_name: Engin
2
+ site_description: A lightweight application framework for modern Python.
3
+ site_url: !ENV READTHEDOCS_CANONICAL_URL
4
+
5
+ theme:
6
+ name: 'material'
7
+ custom_dir: 'docs/overrides'
8
+ palette:
9
+ - scheme: 'default'
10
+ media: '(prefers-color-scheme: light)'
11
+ toggle:
12
+ icon: 'material/lightbulb'
13
+ name: "Switch to dark mode"
14
+ - scheme: 'slate'
15
+ media: '(prefers-color-scheme: dark)'
16
+ primary: 'blue'
17
+ toggle:
18
+ icon: 'material/lightbulb-outline'
19
+ name: 'Switch to light mode'
20
+
21
+ repo_name: invokermain/engin
22
+ repo_url: https://github.com/invokermain/engin/
23
+ edit_uri: ""
24
+
25
+ extra_javascript:
26
+ - js/readthedocs.js
27
+
28
+ plugins:
29
+ - mkdocstrings:
30
+ default_handler: python
31
+ handlers:
32
+ python:
33
+ paths: [.]
34
+ options:
35
+ members_order: source
36
+ separate_signature: true
37
+ filters: ["!^_"]
38
+ docstring_options:
39
+ ignore_init_summary: true
40
+ merge_init_into_class: true
41
+ show_signature_annotations: true
42
+ signature_crossrefs: true
43
+ import:
44
+ - url: https://docs.python.org/3/objects.inv
45
+ domains: [py, std]
46
+
47
+ watch:
48
+ - src
49
+
50
+ markdown_extensions:
51
+ - pymdownx.highlight:
52
+ anchor_linenums: true
53
+ line_spans: __span
54
+ pygments_lang_class: true
55
+ - pymdownx.inlinehilite
56
+ - pymdownx.snippets
57
+ - pymdownx.superfences
@@ -0,0 +1,91 @@
1
+ [project]
2
+ name = "engin"
3
+ version = "0.0.4"
4
+ description = "An async-first modular application framework"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = []
8
+
9
+ [build-system]
10
+ requires = ["hatchling"]
11
+ build-backend = "hatchling.build"
12
+
13
+
14
+ [tool.uv]
15
+ dev-dependencies = [
16
+ "fastapi>=0.115.6",
17
+ "httpx>=0.27.2",
18
+ "mypy>=1",
19
+ "poethepoet>=0.32.1",
20
+ "pydantic-settings>=2.6.0",
21
+ "pydantic>=2.9.2",
22
+ "pytest-asyncio>=0.24.0",
23
+ "pytest>=8",
24
+ "ruff>=0",
25
+ "starlette>=0.39.2",
26
+ "uvicorn>=0.31.1",
27
+ ]
28
+
29
+
30
+ [dependency-groups]
31
+ docs = [
32
+ "mkdocs-material>=9.5.50",
33
+ "mkdocstrings[python]>=0.27.0",
34
+ ]
35
+
36
+
37
+ [tool.ruff]
38
+ line-length = 95
39
+ target-version = "py310"
40
+
41
+
42
+ [tool.ruff.lint]
43
+ select = [
44
+ "ANN", "ASYNC", "B", "C4", "DTZ", "E", "F", "I", "INP", "LOG", "PERF", "PIE", "PT",
45
+ "PTH", "Q", "UP", "R", "RUF", "S", "SIM", "TCH", "T20", "W"
46
+ ]
47
+ ignore = [
48
+ "ANN401",
49
+ "PERF203", # `try`-`except` within a loop incurs performance overhead
50
+ "RET505", # Unnecessary `else` after `return` statement
51
+ "RET506", # Unnecessary `else` after `raise` statement
52
+ ]
53
+
54
+ [tool.ruff.lint.per-file-ignores]
55
+ "**/src/*" = ["PT"]
56
+ "**/tests/*" = ["S", "ANN"]
57
+ "**/examples/*" = ["T201"]
58
+
59
+
60
+ [tool.pytest.ini_options]
61
+ log_cli = true
62
+ log_cli_level = "DEBUG"
63
+ asyncio_mode = "auto"
64
+ asyncio_default_fixture_loop_scope = "session"
65
+
66
+
67
+ [tool.mypy]
68
+ strict = true
69
+ disable_error_code = [
70
+ "type-arg", # allow generic types without type arguments
71
+ ]
72
+
73
+
74
+ [tool.poe.tasks]
75
+ format.default_item_type = "cmd"
76
+ format.sequence = [
77
+ "ruff format src tests examples",
78
+ "ruff check --select I --fix src tests examples"
79
+ ]
80
+
81
+ check.default_item_type = "cmd"
82
+ check.sequence = [
83
+ "ruff format --check src tests examples",
84
+ "ruff check src tests examples",
85
+ "mypy src examples",
86
+ ]
87
+
88
+ fix.default_item_type = "cmd"
89
+ fix.sequence = ["ruff check src tests --fix"]
90
+
91
+ test = "pytest -s tests"
@@ -4,7 +4,7 @@ from collections import defaultdict
4
4
  from collections.abc import Collection, Iterable
5
5
  from dataclasses import dataclass
6
6
  from inspect import BoundArguments, Signature
7
- from typing import Any, Generic, TypeVar
7
+ from typing import Any, Generic, TypeVar, cast
8
8
 
9
9
  from engin._dependency import Dependency, Provide, Supply
10
10
  from engin._exceptions import AssemblyError
@@ -113,7 +113,7 @@ class Assembler:
113
113
  async def get(self, type_: type[T]) -> T:
114
114
  type_id = type_id_of(type_)
115
115
  if type_id in self._dependencies:
116
- return self._dependencies[type_id]
116
+ return cast(T, self._dependencies[type_id])
117
117
  if type_id.multi:
118
118
  out = []
119
119
  for provider in self._multiproviders[type_id]:
@@ -1,29 +1,29 @@
1
1
  import inspect
2
2
  from collections.abc import Iterable, Iterator
3
+ from typing import ClassVar
3
4
 
4
5
  from engin._dependency import Func, Invoke, Provide
5
6
 
6
7
 
7
8
  def provide(func: Func) -> Func:
8
- func._opt = Provide(func) # type: ignore[union-attr]
9
+ func._opt = Provide(func) # type: ignore[attr-defined]
9
10
  return func
10
11
 
11
12
 
12
13
  def invoke(func: Func) -> Func:
13
- func._opt = Invoke(func) # type: ignore[union-attr]
14
+ func._opt = Invoke(func) # type: ignore[attr-defined]
14
15
  return func
15
16
 
16
17
 
17
18
  class Block(Iterable[Provide | Invoke]):
18
- _name: str
19
- _options: list[Provide | Invoke]
19
+ options: ClassVar[list[Provide | Invoke]] = []
20
20
 
21
21
  def __init__(self, /, block_name: str | None = None) -> None:
22
- self._options: list[Provide | Invoke] = []
22
+ self._options: list[Provide | Invoke] = self.options[:]
23
23
  self._name = block_name or f"{type(self).__name__}"
24
24
  for _, method in inspect.getmembers(self):
25
25
  if opt := getattr(method, "_opt", None):
26
- if not isinstance(opt, (Provide, Invoke)):
26
+ if not isinstance(opt, Provide | Invoke):
27
27
  raise RuntimeError("Block option is not an instance of Provide or Invoke")
28
28
  opt.set_block_name(self._name)
29
29
  self._options.append(opt)