engin 0.0.14__tar.gz → 0.0.15__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.
- {engin-0.0.14 → engin-0.0.15}/CHANGELOG.md +21 -4
- {engin-0.0.14 → engin-0.0.15}/PKG-INFO +1 -1
- engin-0.0.15/docs/concepts/lifecycle.md +100 -0
- {engin-0.0.14 → engin-0.0.15}/docs/concepts/providers.md +36 -23
- {engin-0.0.14 → engin-0.0.15}/mkdocs.yaml +0 -1
- {engin-0.0.14 → engin-0.0.15}/pyproject.toml +1 -1
- {engin-0.0.14 → engin-0.0.15}/src/engin/_assembler.py +29 -25
- {engin-0.0.14 → engin-0.0.15}/src/engin/_block.py +23 -7
- {engin-0.0.14 → engin-0.0.15}/src/engin/_dependency.py +56 -18
- {engin-0.0.14 → engin-0.0.15}/src/engin/_engin.py +9 -5
- {engin-0.0.14 → engin-0.0.15}/src/engin/ext/asgi.py +1 -1
- {engin-0.0.14 → engin-0.0.15}/src/engin/ext/fastapi.py +3 -2
- {engin-0.0.14 → engin-0.0.15}/tests/test_assembler.py +11 -11
- {engin-0.0.14 → engin-0.0.15}/tests/test_block.py +9 -3
- {engin-0.0.14 → engin-0.0.15}/tests/test_dependencies.py +38 -0
- {engin-0.0.14 → engin-0.0.15}/uv.lock +13 -13
- engin-0.0.14/docs/concepts/lifecycle.md +0 -0
- engin-0.0.14/docs/guides/dependency_injection.md +0 -4
- {engin-0.0.14 → engin-0.0.15}/.github/workflows/check.yaml +0 -0
- {engin-0.0.14 → engin-0.0.15}/.github/workflows/publish.yaml +0 -0
- {engin-0.0.14 → engin-0.0.15}/.gitignore +0 -0
- {engin-0.0.14 → engin-0.0.15}/.readthedocs.yaml +0 -0
- {engin-0.0.14 → engin-0.0.15}/LICENSE +0 -0
- {engin-0.0.14 → engin-0.0.15}/README.md +0 -0
- {engin-0.0.14 → engin-0.0.15}/docs/concepts/engin.md +0 -0
- {engin-0.0.14 → engin-0.0.15}/docs/concepts/invocations.md +0 -0
- {engin-0.0.14 → engin-0.0.15}/docs/getting-started.md +0 -0
- {engin-0.0.14 → engin-0.0.15}/docs/guides/fastapi-graph.png +0 -0
- {engin-0.0.14 → engin-0.0.15}/docs/guides/fastapi.md +0 -0
- {engin-0.0.14 → engin-0.0.15}/docs/index.md +0 -0
- {engin-0.0.14 → engin-0.0.15}/docs/js/readthedocs.js +0 -0
- {engin-0.0.14 → engin-0.0.15}/docs/overrides/main.html +0 -0
- {engin-0.0.14 → engin-0.0.15}/docs/reference.md +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/app.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/common/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/common/db/block.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/features/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/asgi/main.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/app.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/main.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/routes/cats/api.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/routes/cats/block.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/simple/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/examples/simple/main.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/_cli/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/_cli/_graph.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/_cli/_utils.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/_exceptions.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/_graph.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/_introspect.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/_lifecycle.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/_option.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/_type_utils.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/ext/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/src/engin/py.typed +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/acceptance/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/acceptance/test_error_in_shutdown.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/acceptance/test_error_in_start_up.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/acceptance/test_fastapi.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/cli/__init__.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/cli/test_graph.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/conftest.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/deps.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/test_engin.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/test_graph.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/test_lifecycle.py +0 -0
- {engin-0.0.14 → engin-0.0.15}/tests/test_utils.py +0 -0
@@ -6,25 +6,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
8
|
|
9
|
+
## [0.0.15] - 2025-03-25
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
|
13
|
+
- `Provide` & `Supply` will now raise an error if overriding an existing provider from the
|
14
|
+
same package. This is to prevent accidental overrides. Users can explicitly allow
|
15
|
+
overrides by specifying the `override` parameter when defining the provider
|
16
|
+
`Provide(..., override=True)` or `@provide(override=True)`.
|
17
|
+
- Lifecycle startup tasks will now timeout after 15 seconds and raise an error.
|
18
|
+
- Assembler's `get` method has been renamed to `build`.
|
19
|
+
- Supply's `type_hint` parameter has been renamed to `as_type`.
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
|
23
|
+
- `Assembler` would occasionally fail to call all multiproviders due to inconsistent
|
24
|
+
ordering.
|
25
|
+
|
26
|
+
|
9
27
|
## [0.0.14] - 2025-03-23
|
10
28
|
|
11
29
|
### Added
|
12
30
|
|
13
31
|
- `LifecycleHook` class to help build simple lifecycles with a start and stop call.
|
14
|
-
- `TypeId` now attempts to introspect type aliases, this is experimental and currently on
|
15
|
-
used to enrich error messages.
|
16
32
|
|
17
33
|
### Changed
|
18
34
|
|
19
35
|
- `engin-graph` has been replaced by `engin graph`.
|
20
|
-
- Engin now uses `typer` for an improved cli experience.
|
21
|
-
- The package now has an extra `cli` which must be installed to use the developer cli.
|
36
|
+
- Engin now uses `typer` for an improved cli experience. Note the package now has an extra `cli` which must be installed to use the cli.
|
22
37
|
- `Assembler.add(...)` does not error when adding already registered providers.
|
23
38
|
- Use a more performant algorithm for inspecting frame stack.
|
24
39
|
|
25
40
|
### Fixed
|
26
41
|
|
27
42
|
- `ASGIEngin` now properly surfaces startup errors.
|
43
|
+
- `Engin.run()` doing a double shutdown.
|
44
|
+
|
28
45
|
|
29
46
|
## [0.0.13] - 2025-03-22
|
30
47
|
|
@@ -0,0 +1,100 @@
|
|
1
|
+
from contextlib import asynccontextmanager
|
2
|
+
|
3
|
+
# Lifecycle
|
4
|
+
|
5
|
+
Certain types of object naturally have some form of startup and shutdown behaviour
|
6
|
+
associated with them, these steps need to be tied the lifecycle of the application itself
|
7
|
+
in order to be useful. For example, a database connection manager might want to fill its
|
8
|
+
connection pool on startup, and gracefully release the connections on shutdown.
|
9
|
+
|
10
|
+
Doing this yourself can be tricky and is application dependent: most will not have any
|
11
|
+
special support for this and will expect you to manage your lifecycle concerns in your
|
12
|
+
entrypoint function, leading to unwieldy code in larger applications, whilst other
|
13
|
+
types application might expected you to translate the lifecyle tasks into something they
|
14
|
+
offer, e.g. an ASGI server would expect you to manage this via its lifespan. In both cases
|
15
|
+
you end up managing lifecycle in a completely different place to where you declare your
|
16
|
+
objects, which make the codebase more complicated to understand.
|
17
|
+
|
18
|
+
Luckily, engin makes declaring lifecycle tasks a breeze, and it can be done in the same
|
19
|
+
provider that build your object keeping your code nicely collocated.
|
20
|
+
|
21
|
+
## The Lifecycle type
|
22
|
+
|
23
|
+
Engin automatically provides a special type called `Lifecycle` that can be used like any
|
24
|
+
other provided type. This type allows you to register lifecycle tasks with the Engin which
|
25
|
+
will automatically be run as part of your application lifecycle.
|
26
|
+
|
27
|
+
## Registering lifecycle tasks
|
28
|
+
|
29
|
+
There are a few different ways to declare and register your lifecycle tasks, they all do
|
30
|
+
the same thing, so which one to use depends on whichever is easiest for your specific
|
31
|
+
lifecycle tasks.
|
32
|
+
|
33
|
+
### 1. Existing context manager
|
34
|
+
|
35
|
+
If your type exposes a context manager interface to handle its lifecycle, registering it
|
36
|
+
is as easy as calling `lifecycle.append(...)`, this works for sync and async context
|
37
|
+
managers.
|
38
|
+
|
39
|
+
Let's look at an example using `httpx.AsyncClient`:
|
40
|
+
|
41
|
+
```python
|
42
|
+
from engin import Lifecycle
|
43
|
+
from httpx import AsyncClient
|
44
|
+
|
45
|
+
|
46
|
+
def httpx_client(lifecycle: Lifecycle) -> AsyncClient:
|
47
|
+
client = AsyncClient()
|
48
|
+
lifecycle.append(client) # register the lifecycle tasks
|
49
|
+
return client
|
50
|
+
```
|
51
|
+
|
52
|
+
### 2. Explict startup & shutdown methods
|
53
|
+
|
54
|
+
If your type exposes meathods that must be called as part of the lifecycle, e.g. `start()`
|
55
|
+
& `stop()`, then `lifecycle.hook(on_start=..., on_stop=...)` is the way.
|
56
|
+
|
57
|
+
Let's look at an example using `piccolo.engine.PostgresEngin`:
|
58
|
+
|
59
|
+
```python
|
60
|
+
from engin import Lifecycle
|
61
|
+
from piccolo.engine import PostgresEngine
|
62
|
+
|
63
|
+
def postgres_engine(lifecyle: Lifecycle) -> PostgresEngine:
|
64
|
+
db_engine = PostgresEngine(...) # fill in actual connection details
|
65
|
+
|
66
|
+
lifecyle.hook(
|
67
|
+
on_start=db_engine.start_connection_pool(),
|
68
|
+
on_stop=db_engine.close_connection_pool(),
|
69
|
+
)
|
70
|
+
|
71
|
+
return db_engine
|
72
|
+
```
|
73
|
+
|
74
|
+
### 3. Custom context managers
|
75
|
+
|
76
|
+
For more advanced use cases you can always define your own context manager.
|
77
|
+
|
78
|
+
In this example assume that `worker.run()` will not return to us when we await it, and
|
79
|
+
therefore we want to manage it as a task.
|
80
|
+
|
81
|
+
|
82
|
+
```python
|
83
|
+
import asyncio
|
84
|
+
from engin import Lifecycle
|
85
|
+
from some_package import BlockingAsyncWorker
|
86
|
+
|
87
|
+
def blocking_worker(lifecycle: Lifecycle) -> BlockingWorker:
|
88
|
+
worker = BlockingAsyncWorker()
|
89
|
+
|
90
|
+
@asynccontextmanager
|
91
|
+
async def worker_lifecycle() -> AsyncIterator[None]:
|
92
|
+
task = asyncio.create_task(worker.run())
|
93
|
+
yield None
|
94
|
+
worker.stop()
|
95
|
+
del task
|
96
|
+
|
97
|
+
lifecycle.append(worker_lifecycle())
|
98
|
+
|
99
|
+
return worker
|
100
|
+
```
|
@@ -17,17 +17,19 @@ class: `Provide`.
|
|
17
17
|
```python
|
18
18
|
from engin import Engin, Provide
|
19
19
|
|
20
|
+
|
20
21
|
# define our constructor
|
21
22
|
def string_factory() -> str:
|
22
|
-
|
23
|
+
return "hello"
|
24
|
+
|
23
25
|
|
24
26
|
# register it as a provider with the Engin
|
25
27
|
engin = Engin(Provide(string_factory))
|
26
28
|
|
27
29
|
# construct the string
|
28
|
-
a_string = await engin.assembler.
|
30
|
+
a_string = await engin.assembler.build(str)
|
29
31
|
|
30
|
-
print(a_string)
|
32
|
+
print(a_string) # hello
|
31
33
|
```
|
32
34
|
|
33
35
|
Providers can be asynchronous as well, this factory function would work exactly the same
|
@@ -45,27 +47,31 @@ Providers that construct more interesting objects generally require their own pa
|
|
45
47
|
```python
|
46
48
|
from engin import Engin, Provide
|
47
49
|
|
50
|
+
|
48
51
|
class Greeter:
|
49
52
|
def __init__(self, greeting: str) -> None:
|
50
53
|
self._greeting = greeting
|
51
|
-
|
54
|
+
|
52
55
|
def greet(self, name: str) -> None:
|
53
56
|
print(f"{self._greeting}, {name}!")
|
54
|
-
|
57
|
+
|
58
|
+
|
55
59
|
# define our constructors
|
56
60
|
def string_factory() -> str:
|
57
|
-
|
61
|
+
return "hello"
|
62
|
+
|
58
63
|
|
59
64
|
def greeter_factory(greeting: str) -> Greeter:
|
60
65
|
return Greeter(greeting=greeting)
|
61
66
|
|
67
|
+
|
62
68
|
# register them as providers with the Engin
|
63
69
|
engin = Engin(Provide(string_factory), Provide(greeter_factory))
|
64
70
|
|
65
71
|
# construct the Greeter
|
66
|
-
greeter = await engin.assembler.
|
72
|
+
greeter = await engin.assembler.build(Greeter)
|
67
73
|
|
68
|
-
greeter.greet("Bob")
|
74
|
+
greeter.greet("Bob") # hello, Bob!
|
69
75
|
```
|
70
76
|
|
71
77
|
|
@@ -81,19 +87,21 @@ from engin import Engin, Provide
|
|
81
87
|
|
82
88
|
# define our constructors
|
83
89
|
def string_factory() -> str:
|
84
|
-
|
90
|
+
return "hello"
|
91
|
+
|
85
92
|
|
86
93
|
def evil_factory() -> int:
|
87
94
|
raise RuntimeError("I have ruined your plans")
|
88
95
|
|
96
|
+
|
89
97
|
# register them as providers with the Engin
|
90
98
|
engin = Engin(Provide(string_factory), Provide(evil_factory))
|
91
99
|
|
92
100
|
# this will not raise an error
|
93
|
-
await engin.assembler.
|
101
|
+
await engin.assembler.build(str)
|
94
102
|
|
95
103
|
# this will raise an error
|
96
|
-
await engin.assembler.
|
104
|
+
await engin.assembler.build(int)
|
97
105
|
```
|
98
106
|
|
99
107
|
|
@@ -109,20 +117,23 @@ To turn a factory into a multiprovider, simply return a list:
|
|
109
117
|
```python
|
110
118
|
from engin import Engin, Provide
|
111
119
|
|
120
|
+
|
112
121
|
# define our constructors
|
113
122
|
def animal_names_factory() -> list[str]:
|
114
|
-
|
123
|
+
return ["cat", "dog"]
|
124
|
+
|
115
125
|
|
116
126
|
def other_animal_names_factory() -> list[str]:
|
117
|
-
|
127
|
+
return ["horse", "cow"]
|
128
|
+
|
118
129
|
|
119
130
|
# register them as providers with the Engin
|
120
131
|
engin = Engin(Provide(animal_names_factory), Provide(other_animal_names_factory))
|
121
132
|
|
122
133
|
# construct the list of strings
|
123
|
-
animal_names = await engin.assembler.
|
134
|
+
animal_names = await engin.assembler.build(list[str])
|
124
135
|
|
125
|
-
print(animal_names)
|
136
|
+
print(animal_names) # ["cat", "dog", "horse", "cow"]
|
126
137
|
```
|
127
138
|
|
128
139
|
|
@@ -133,25 +144,28 @@ Providers of the same type can be discriminated using annotations.
|
|
133
144
|
```python
|
134
145
|
from engin import Engin, Provide
|
135
146
|
from typing import Annotated
|
136
|
-
|
147
|
+
|
148
|
+
|
137
149
|
# define our constructors
|
138
150
|
def greeting_factory() -> Annotated[str, "greeting"]:
|
139
|
-
|
151
|
+
return "hello"
|
152
|
+
|
140
153
|
|
141
154
|
def name_factory() -> Annotated[str, "name"]:
|
142
155
|
return "Jelena"
|
143
156
|
|
157
|
+
|
144
158
|
# register them as providers with the Engin
|
145
159
|
engin = Engin(Provide(greeting_factory), Provide(name_factory))
|
146
160
|
|
147
161
|
# this will return "hello"
|
148
|
-
await engin.assembler.
|
162
|
+
await engin.assembler.build(Annotated[str, "greeting"])
|
149
163
|
|
150
164
|
# this will return "Jelena"
|
151
|
-
await engin.assembler.
|
165
|
+
await engin.assembler.build(Annotated[str, "name"])
|
152
166
|
|
153
167
|
# N.B. this will raise an error!
|
154
|
-
await engin.assembler.
|
168
|
+
await engin.assembler.build(str)
|
155
169
|
```
|
156
170
|
|
157
171
|
|
@@ -162,7 +176,6 @@ provided type is automatically inferred.
|
|
162
176
|
|
163
177
|
For example the first example on this page could be rewritten as:
|
164
178
|
|
165
|
-
|
166
179
|
```python
|
167
180
|
from engin import Engin, Supply
|
168
181
|
|
@@ -170,7 +183,7 @@ from engin import Engin, Supply
|
|
170
183
|
engin = Engin(Supply("hello"))
|
171
184
|
|
172
185
|
# construct the string
|
173
|
-
a_string = await engin.assembler.
|
186
|
+
a_string = await engin.assembler.build(str)
|
174
187
|
|
175
|
-
print(a_string)
|
188
|
+
print(a_string) # hello
|
176
189
|
```
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import asyncio
|
2
2
|
import logging
|
3
3
|
from collections import defaultdict
|
4
|
-
from collections.abc import
|
4
|
+
from collections.abc import Iterable
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from inspect import BoundArguments, Signature
|
7
7
|
from typing import Any, Generic, TypeVar, cast
|
@@ -39,7 +39,7 @@ class Assembler:
|
|
39
39
|
A container for Providers that is responsible for building provided types.
|
40
40
|
|
41
41
|
The Assembler acts as a cache for previously built types, meaning repeat calls
|
42
|
-
to `
|
42
|
+
to `build` will produce the same value.
|
43
43
|
|
44
44
|
Examples:
|
45
45
|
```python
|
@@ -47,7 +47,7 @@ class Assembler:
|
|
47
47
|
return "foo"
|
48
48
|
|
49
49
|
a = Assembler([Provide(build_str)])
|
50
|
-
await a.
|
50
|
+
await a.build(str)
|
51
51
|
```
|
52
52
|
"""
|
53
53
|
|
@@ -85,17 +85,15 @@ class Assembler:
|
|
85
85
|
bound_args=await self._bind_arguments(dependency.signature),
|
86
86
|
)
|
87
87
|
|
88
|
-
async def
|
88
|
+
async def build(self, type_: type[T]) -> T:
|
89
89
|
"""
|
90
|
-
|
90
|
+
Build the type from Assembler's factories.
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
If the
|
92
|
+
If the type has been built previously the value will be cached and will return the
|
93
|
+
same instance.
|
96
94
|
|
97
95
|
Args:
|
98
|
-
type_: the type of the desired value.
|
96
|
+
type_: the type of the desired value to build.
|
99
97
|
|
100
98
|
Raises:
|
101
99
|
LookupError: When no provider is found for the given type.
|
@@ -180,31 +178,37 @@ class Assembler:
|
|
180
178
|
del self._assembled_outputs[type_id]
|
181
179
|
self._providers[type_id] = provider
|
182
180
|
|
183
|
-
def _resolve_providers(self, type_id: TypeId) ->
|
181
|
+
def _resolve_providers(self, type_id: TypeId) -> Iterable[Provide]:
|
182
|
+
"""
|
183
|
+
Resolves the chain of providers required to satisfy the provider of a given type.
|
184
|
+
Ordering of the return value is very important!
|
185
|
+
|
186
|
+
# TODO: performance optimisation, do not recurse for already satisfied providers?
|
187
|
+
"""
|
184
188
|
if type_id.multi:
|
185
|
-
|
189
|
+
root_providers = self._multiproviders.get(type_id)
|
186
190
|
else:
|
187
|
-
|
188
|
-
|
191
|
+
root_providers = [provider] if (provider := self._providers.get(type_id)) else None
|
192
|
+
|
193
|
+
if not root_providers:
|
189
194
|
if type_id.multi:
|
190
195
|
LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
|
191
|
-
|
196
|
+
root_providers = [(Supply([], as_type=list[type_id.type]))] # type: ignore[name-defined]
|
192
197
|
# store default to prevent the warning appearing multiple times
|
193
|
-
self._multiproviders[type_id] =
|
198
|
+
self._multiproviders[type_id] = root_providers
|
194
199
|
else:
|
195
200
|
available = sorted(str(k) for k in self._providers)
|
196
201
|
msg = f"Missing Provider for type '{type_id}', available: {available}"
|
197
202
|
raise LookupError(msg)
|
198
203
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
return {*required_providers, *providers}
|
204
|
+
# providers that must be satisfied to satisfy the root level providers
|
205
|
+
yield from (
|
206
|
+
child_provider
|
207
|
+
for root_provider in root_providers
|
208
|
+
for root_provider_param in root_provider.parameter_types
|
209
|
+
for child_provider in self._resolve_providers(root_provider_param)
|
210
|
+
)
|
211
|
+
yield from root_providers
|
208
212
|
|
209
213
|
async def _satisfy(self, target: TypeId) -> None:
|
210
214
|
for provider in self._resolve_providers(target):
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import inspect
|
2
|
-
from collections.abc import Iterable, Sequence
|
2
|
+
from collections.abc import Callable, Iterable, Sequence
|
3
3
|
from itertools import chain
|
4
4
|
from typing import TYPE_CHECKING, ClassVar
|
5
5
|
|
@@ -10,20 +10,36 @@ if TYPE_CHECKING:
|
|
10
10
|
from engin._engin import Engin
|
11
11
|
|
12
12
|
|
13
|
-
def provide(
|
13
|
+
def provide(
|
14
|
+
func_: Func | None = None, *, override: bool = False
|
15
|
+
) -> Func | Callable[[Func], Func]:
|
14
16
|
"""
|
15
17
|
A decorator for defining a Provider in a Block.
|
16
18
|
"""
|
17
|
-
func._opt = Provide(func) # type: ignore[attr-defined]
|
18
|
-
return func
|
19
19
|
|
20
|
+
def _inner(func: Func) -> Func:
|
21
|
+
func._opt = Provide(func, override=override) # type: ignore[attr-defined]
|
22
|
+
return func
|
20
23
|
|
21
|
-
|
24
|
+
if func_ is None:
|
25
|
+
return _inner
|
26
|
+
else:
|
27
|
+
return _inner(func_)
|
28
|
+
|
29
|
+
|
30
|
+
def invoke(func_: Func | None = None) -> Func | Callable[[Func], Func]:
|
22
31
|
"""
|
23
32
|
A decorator for defining an Invocation in a Block.
|
24
33
|
"""
|
25
|
-
|
26
|
-
|
34
|
+
|
35
|
+
def _inner(func: Func) -> Func:
|
36
|
+
func._opt = Invoke(func) # type: ignore[attr-defined]
|
37
|
+
return func
|
38
|
+
|
39
|
+
if func_ is None:
|
40
|
+
return _inner
|
41
|
+
else:
|
42
|
+
return _inner(func_)
|
27
43
|
|
28
44
|
|
29
45
|
class Block(Option):
|
@@ -30,11 +30,11 @@ def _noop(*args: Any, **kwargs: Any) -> None: ...
|
|
30
30
|
|
31
31
|
|
32
32
|
class Dependency(ABC, Option, Generic[P, T]):
|
33
|
-
def __init__(self, func: Func[P, T]
|
33
|
+
def __init__(self, func: Func[P, T]) -> None:
|
34
34
|
self._func = func
|
35
35
|
self._is_async = iscoroutinefunction(func)
|
36
36
|
self._signature = inspect.signature(self._func)
|
37
|
-
self._block_name =
|
37
|
+
self._block_name: str | None = None
|
38
38
|
|
39
39
|
source_frame = get_first_external_frame()
|
40
40
|
self._source_package = cast("str", source_frame.frame.f_globals["__package__"])
|
@@ -88,9 +88,6 @@ class Dependency(ABC, Option, Generic[P, T]):
|
|
88
88
|
def signature(self) -> Signature:
|
89
89
|
return self._signature
|
90
90
|
|
91
|
-
def set_block_name(self, name: str) -> None:
|
92
|
-
self._block_name = name
|
93
|
-
|
94
91
|
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
95
92
|
if self._is_async:
|
96
93
|
return await cast("Awaitable[T]", self._func(*args, **kwargs))
|
@@ -117,8 +114,8 @@ class Invoke(Dependency):
|
|
117
114
|
```
|
118
115
|
"""
|
119
116
|
|
120
|
-
def __init__(self, invocation: Func[P, T]
|
121
|
-
super().__init__(func=invocation
|
117
|
+
def __init__(self, invocation: Func[P, T]) -> None:
|
118
|
+
super().__init__(func=invocation)
|
122
119
|
|
123
120
|
def apply(self, engin: "Engin") -> None:
|
124
121
|
engin._invocations.append(self)
|
@@ -134,9 +131,9 @@ class Entrypoint(Invoke):
|
|
134
131
|
Entrypoints are a short hand for no-op Invocations that can be used to
|
135
132
|
"""
|
136
133
|
|
137
|
-
def __init__(self, type_: type[Any]
|
134
|
+
def __init__(self, type_: type[Any]) -> None:
|
138
135
|
self._type = type_
|
139
|
-
super().__init__(invocation=_noop
|
136
|
+
super().__init__(invocation=_noop)
|
140
137
|
|
141
138
|
@property
|
142
139
|
def parameter_types(self) -> list[TypeId]:
|
@@ -155,8 +152,17 @@ class Entrypoint(Invoke):
|
|
155
152
|
|
156
153
|
|
157
154
|
class Provide(Dependency[Any, T]):
|
158
|
-
def __init__(self, builder: Func[P, T],
|
159
|
-
|
155
|
+
def __init__(self, builder: Func[P, T], *, override: bool = False) -> None:
|
156
|
+
"""
|
157
|
+
Provide a type via a builder or factory function.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
builder: the builder function that returns the type.
|
161
|
+
override: allow this provider to override existing providers from the same
|
162
|
+
package.
|
163
|
+
"""
|
164
|
+
super().__init__(func=builder)
|
165
|
+
self._override = override
|
160
166
|
self._is_multi = typing.get_origin(self.return_type) is list
|
161
167
|
|
162
168
|
# Validate that the provider does to depend on its own output value, as this will
|
@@ -198,10 +204,28 @@ class Provide(Dependency[Any, T]):
|
|
198
204
|
return self._is_multi
|
199
205
|
|
200
206
|
def apply(self, engin: "Engin") -> None:
|
207
|
+
type_id = self.return_type_id
|
201
208
|
if self.is_multiprovider:
|
202
|
-
engin._multiproviders[
|
203
|
-
|
204
|
-
|
209
|
+
engin._multiproviders[type_id].append(self)
|
210
|
+
return
|
211
|
+
|
212
|
+
if type_id not in engin._providers:
|
213
|
+
engin._providers[type_id] = self
|
214
|
+
return
|
215
|
+
|
216
|
+
existing_provider = engin._providers[type_id]
|
217
|
+
is_same_package = existing_provider.source_package == self.source_package
|
218
|
+
|
219
|
+
# overwriting a dependency from the same package must be explicit
|
220
|
+
if is_same_package and not self._override:
|
221
|
+
msg = (
|
222
|
+
f"Provider '{self.name}' is implicitly overriding "
|
223
|
+
f"'{existing_provider.name}', if this is intended specify "
|
224
|
+
"`override=True` for the overriding Provider"
|
225
|
+
)
|
226
|
+
raise RuntimeError(msg)
|
227
|
+
|
228
|
+
engin._providers[type_id] = self
|
205
229
|
|
206
230
|
def __hash__(self) -> int:
|
207
231
|
return hash(self.return_type_id)
|
@@ -212,13 +236,27 @@ class Provide(Dependency[Any, T]):
|
|
212
236
|
|
213
237
|
class Supply(Provide, Generic[T]):
|
214
238
|
def __init__(
|
215
|
-
self, value: T, *,
|
239
|
+
self, value: T, *, as_type: type | None = None, override: bool = False
|
216
240
|
) -> None:
|
241
|
+
"""
|
242
|
+
Supply a value.
|
243
|
+
|
244
|
+
This is a shorthand which under the hood creates a Provider with a noop factory
|
245
|
+
function.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
value: the value to Supply
|
249
|
+
as_type: allows you to specify the provided type, useful for type erasing,
|
250
|
+
e.g. Supply a concrete value but specify it as an interface or other
|
251
|
+
abstraction.
|
252
|
+
override: allow this provider to override existing providers from the same
|
253
|
+
package.
|
254
|
+
"""
|
217
255
|
self._value = value
|
218
|
-
self._type_hint =
|
256
|
+
self._type_hint = as_type
|
219
257
|
if self._type_hint is not None:
|
220
|
-
self._get_val.__annotations__["return"] =
|
221
|
-
super().__init__(builder=self._get_val,
|
258
|
+
self._get_val.__annotations__["return"] = as_type
|
259
|
+
super().__init__(builder=self._get_val, override=override)
|
222
260
|
|
223
261
|
@property
|
224
262
|
def return_type(self) -> type[T]:
|
@@ -84,7 +84,7 @@ class Engin:
|
|
84
84
|
self._run_task: Task | None = None
|
85
85
|
|
86
86
|
self._providers: dict[TypeId, Provide] = {
|
87
|
-
TypeId.from_type(Engin): Supply(self,
|
87
|
+
TypeId.from_type(Engin): Supply(self, as_type=Engin)
|
88
88
|
}
|
89
89
|
self._multiproviders: dict[TypeId, list[Provide]] = defaultdict(list)
|
90
90
|
self._invocations: list[Invoke] = []
|
@@ -132,14 +132,18 @@ class Engin:
|
|
132
132
|
LOG.error(f"invocation '{name}' errored, exiting", exc_info=err)
|
133
133
|
return
|
134
134
|
|
135
|
-
lifecycle = await self._assembler.
|
135
|
+
lifecycle = await self._assembler.build(Lifecycle)
|
136
136
|
|
137
137
|
try:
|
138
138
|
for hook in lifecycle.list():
|
139
|
-
await self._exit_stack.enter_async_context(hook)
|
139
|
+
await asyncio.wait_for(self._exit_stack.enter_async_context(hook), timeout=15)
|
140
140
|
except Exception as err:
|
141
|
-
|
142
|
-
|
141
|
+
if isinstance(err, TimeoutError):
|
142
|
+
msg = "lifecycle startup task timed out after 15s, exiting"
|
143
|
+
else:
|
144
|
+
msg = "lifecycle startup task errored, exiting"
|
145
|
+
LOG.error(msg, exc_info=err)
|
146
|
+
await self._shutdown()
|
143
147
|
return
|
144
148
|
|
145
149
|
LOG.info("startup complete")
|
@@ -50,7 +50,7 @@ class ASGIEngin(Engin, ASGIType):
|
|
50
50
|
await self._asgi_app(scope, receive, send)
|
51
51
|
|
52
52
|
async def _startup(self) -> None:
|
53
|
-
self._asgi_app = await self._assembler.
|
53
|
+
self._asgi_app = await self._assembler.build(self._asgi_type)
|
54
54
|
await self.start()
|
55
55
|
|
56
56
|
def graph(self) -> list[Node]:
|
@@ -58,7 +58,7 @@ def Inject(interface: type[T]) -> Depends:
|
|
58
58
|
assembler: Assembler = conn.app.state.assembler
|
59
59
|
except AttributeError:
|
60
60
|
raise RuntimeError("Assembler is not attached to Application state") from None
|
61
|
-
return await assembler.
|
61
|
+
return await assembler.build(interface)
|
62
62
|
|
63
63
|
dep = Depends(inner)
|
64
64
|
dep.__engin__ = True # type: ignore[attr-defined]
|
@@ -143,7 +143,8 @@ class APIRouteDependency(Dependency):
|
|
143
143
|
"""
|
144
144
|
Warning: this should never be constructed in application code.
|
145
145
|
"""
|
146
|
-
super().__init__(_noop
|
146
|
+
super().__init__(_noop)
|
147
|
+
self._block_name = wraps.block_name
|
147
148
|
self._wrapped = wraps
|
148
149
|
self._route = route
|
149
150
|
self._signature = inspect.signature(route.endpoint)
|
@@ -60,18 +60,18 @@ def test_assembler_with_duplicate_provider_errors():
|
|
60
60
|
async def test_assembler_get():
|
61
61
|
assembler = Assembler([Provide(make_int), Provide(make_many_int)])
|
62
62
|
|
63
|
-
assert await assembler.
|
64
|
-
assert await assembler.
|
63
|
+
assert await assembler.build(int)
|
64
|
+
assert await assembler.build(list[int])
|
65
65
|
|
66
66
|
|
67
67
|
async def test_assembler_with_unknown_type_raises_lookup_error():
|
68
68
|
assembler = Assembler([])
|
69
69
|
|
70
70
|
with pytest.raises(LookupError):
|
71
|
-
await assembler.
|
71
|
+
await assembler.build(str)
|
72
72
|
|
73
73
|
with pytest.raises(LookupError):
|
74
|
-
await assembler.
|
74
|
+
await assembler.build(list[str])
|
75
75
|
|
76
76
|
with pytest.raises(LookupError):
|
77
77
|
await assembler.assemble(Entrypoint(str))
|
@@ -87,10 +87,10 @@ async def test_assembler_with_erroring_provider_raises_provider_error():
|
|
87
87
|
assembler = Assembler([Provide(make_str), Provide(make_many_str)])
|
88
88
|
|
89
89
|
with pytest.raises(ProviderError):
|
90
|
-
await assembler.
|
90
|
+
await assembler.build(str)
|
91
91
|
|
92
92
|
with pytest.raises(ProviderError):
|
93
|
-
await assembler.
|
93
|
+
await assembler.build(list[str])
|
94
94
|
|
95
95
|
|
96
96
|
async def test_annotations():
|
@@ -103,10 +103,10 @@ async def test_annotations():
|
|
103
103
|
assembler = Assembler([Provide(make_str_1), Provide(make_str_2)])
|
104
104
|
|
105
105
|
with pytest.raises(LookupError):
|
106
|
-
await assembler.
|
106
|
+
await assembler.build(str)
|
107
107
|
|
108
|
-
assert await assembler.
|
109
|
-
assert await assembler.
|
108
|
+
assert await assembler.build(Annotated[str, "1"]) == "bar"
|
109
|
+
assert await assembler.build(Annotated[str, "2"]) == "foo"
|
110
110
|
|
111
111
|
|
112
112
|
async def test_assembler_has():
|
@@ -153,8 +153,8 @@ async def test_assembler_add_overrides():
|
|
153
153
|
assembler = Assembler([])
|
154
154
|
assembler.add(Provide(return_one))
|
155
155
|
|
156
|
-
assert await assembler.
|
156
|
+
assert await assembler.build(int) == 1
|
157
157
|
|
158
158
|
assembler.add(Provide(return_two))
|
159
159
|
|
160
|
-
assert await assembler.
|
160
|
+
assert await assembler.build(int) == 2
|
@@ -8,12 +8,18 @@ def test_block():
|
|
8
8
|
return 3
|
9
9
|
|
10
10
|
@invoke
|
11
|
-
def invoke_square(self, some: int) -> None:
|
12
|
-
|
11
|
+
def invoke_square(self, some: int) -> None: ...
|
12
|
+
|
13
|
+
@provide()
|
14
|
+
def provide_str(self) -> str:
|
15
|
+
return "3"
|
16
|
+
|
17
|
+
@invoke()
|
18
|
+
def invoke_str(self, some: str) -> None: ...
|
13
19
|
|
14
20
|
my_block = MyBlock()
|
15
21
|
|
16
22
|
options = list(my_block._method_options())
|
17
|
-
assert len(options) == 2
|
18
23
|
|
24
|
+
assert len(options) == 4
|
19
25
|
assert Engin(my_block)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from typing import Annotated
|
2
|
+
from unittest.mock import Mock
|
2
3
|
|
3
4
|
import pytest
|
4
5
|
|
@@ -91,3 +92,40 @@ def test_provider_cannot_depend_on_self():
|
|
91
92
|
|
92
93
|
with pytest.raises(ValueError, match="return type"):
|
93
94
|
Provide(invalid_provider_2)
|
95
|
+
|
96
|
+
|
97
|
+
def test_provides_implicit_overrides():
|
98
|
+
provide_a = Provide(make_int)
|
99
|
+
provide_b = Provide(make_int)
|
100
|
+
|
101
|
+
engin = Mock()
|
102
|
+
engin._providers = {}
|
103
|
+
|
104
|
+
provide_a.apply(engin)
|
105
|
+
|
106
|
+
with pytest.raises(RuntimeError, match="implicit"):
|
107
|
+
provide_b.apply(engin)
|
108
|
+
|
109
|
+
|
110
|
+
def test_provides_explicit_overrides_allowed():
|
111
|
+
provide_a = Provide(make_int)
|
112
|
+
provide_b = Provide(make_int, override=True)
|
113
|
+
|
114
|
+
engin = Mock()
|
115
|
+
engin._providers = {}
|
116
|
+
|
117
|
+
provide_a.apply(engin)
|
118
|
+
provide_b.apply(engin)
|
119
|
+
|
120
|
+
|
121
|
+
def test_provides_implicit_overrides_allowed_when_3rd_party():
|
122
|
+
provide_a = Provide(make_int)
|
123
|
+
provide_b = Provide(make_int)
|
124
|
+
|
125
|
+
provide_a._source_package = "foo"
|
126
|
+
|
127
|
+
engin = Mock()
|
128
|
+
engin._providers = {}
|
129
|
+
|
130
|
+
provide_a.apply(engin)
|
131
|
+
provide_b.apply(engin)
|
@@ -206,7 +206,7 @@ toml = [
|
|
206
206
|
|
207
207
|
[[package]]
|
208
208
|
name = "engin"
|
209
|
-
version = "0.0.
|
209
|
+
version = "0.0.15"
|
210
210
|
source = { editable = "." }
|
211
211
|
|
212
212
|
[package.optional-dependencies]
|
@@ -273,16 +273,16 @@ wheels = [
|
|
273
273
|
|
274
274
|
[[package]]
|
275
275
|
name = "fastapi"
|
276
|
-
version = "0.115.
|
276
|
+
version = "0.115.12"
|
277
277
|
source = { registry = "https://pypi.org/simple" }
|
278
278
|
dependencies = [
|
279
279
|
{ name = "pydantic" },
|
280
280
|
{ name = "starlette" },
|
281
281
|
{ name = "typing-extensions" },
|
282
282
|
]
|
283
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
283
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 }
|
284
284
|
wheels = [
|
285
|
-
{ url = "https://files.pythonhosted.org/packages/b3/
|
285
|
+
{ url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 },
|
286
286
|
]
|
287
287
|
|
288
288
|
[[package]]
|
@@ -580,7 +580,7 @@ python = [
|
|
580
580
|
|
581
581
|
[[package]]
|
582
582
|
name = "mkdocstrings-python"
|
583
|
-
version = "1.16.
|
583
|
+
version = "1.16.8"
|
584
584
|
source = { registry = "https://pypi.org/simple" }
|
585
585
|
dependencies = [
|
586
586
|
{ name = "griffe" },
|
@@ -588,9 +588,9 @@ dependencies = [
|
|
588
588
|
{ name = "mkdocstrings" },
|
589
589
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
590
590
|
]
|
591
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
591
|
+
sdist = { url = "https://files.pythonhosted.org/packages/8e/b8/62190ea298fdb1e84670ef548590748c633ab4e05b35bcf902e89f2f28c6/mkdocstrings_python-1.16.8.tar.gz", hash = "sha256:9453ccae69be103810c1cf6435ce71c8f714ae37fef4d87d16aa92a7c800fe1d", size = 205119 }
|
592
592
|
wheels = [
|
593
|
-
{ url = "https://files.pythonhosted.org/packages/
|
593
|
+
{ url = "https://files.pythonhosted.org/packages/67/d0/ef6e82f7a68c7ac02e1a01815fbe88773f4f9e40728ed35bd1664a5d76f2/mkdocstrings_python-1.16.8-py3-none-any.whl", hash = "sha256:211b7aaf776cd45578ecb531e5ad0d3a35a8be9101a6bfa10de38a69af9d8fd8", size = 124116 },
|
594
594
|
]
|
595
595
|
|
596
596
|
[[package]]
|
@@ -851,14 +851,14 @@ wheels = [
|
|
851
851
|
|
852
852
|
[[package]]
|
853
853
|
name = "pytest-asyncio"
|
854
|
-
version = "0.
|
854
|
+
version = "0.26.0"
|
855
855
|
source = { registry = "https://pypi.org/simple" }
|
856
856
|
dependencies = [
|
857
857
|
{ name = "pytest" },
|
858
858
|
]
|
859
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
859
|
+
sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 }
|
860
860
|
wheels = [
|
861
|
-
{ url = "https://files.pythonhosted.org/packages/
|
861
|
+
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 },
|
862
862
|
]
|
863
863
|
|
864
864
|
[[package]]
|
@@ -900,11 +900,11 @@ wheels = [
|
|
900
900
|
|
901
901
|
[[package]]
|
902
902
|
name = "python-dotenv"
|
903
|
-
version = "1.0
|
903
|
+
version = "1.1.0"
|
904
904
|
source = { registry = "https://pypi.org/simple" }
|
905
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
905
|
+
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
|
906
906
|
wheels = [
|
907
|
-
{ url = "https://files.pythonhosted.org/packages/
|
907
|
+
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
|
908
908
|
]
|
909
909
|
|
910
910
|
[[package]]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|