Dependencies¶
Ergate has a very similar dependency injection system to FastAPI, which is designed to be very simple to use, and to make it very easy for any developer to integrate other components with their application.
What is dependency injection?¶
Dependency injection is a way through which your code declares what dependencies it needs to run, and where your code doesn't explicitly create that dependency when it needs to use it, but rather lets the underlying system/application (in this case, Ergate) automatically create those dependencies for it.
Dependency injection is an extremely powerful tool, since it allows you to do things like easily having common shared logic in your code and easily sharing common dependencies across different surfaces of your application, amongst other things; all of it while reducing code repetition.
Creating a dependency¶
To use dependencies in Ergate, you'll need a function that yield
s a dependency. It's important to note that the function must yield
one time only. As an example, let's create a dependency that returns a datetime
object representing the current time.
from collections.abc import Generator
from datetime import datetime
def create_current_time() -> Generator[datetime, None, None]:
yield datetime.now()
Info
Ergate uses contextmanagers under the hood, so any function that works with them can be used as a dependency.
Using a dependency¶
Now, to use this dependency from within a step, you must first understand the use of an Annotated
type. To put it simply, the Annotated
type is a way of adding metadata to a type annotation. In this case, the metadata we want to add to our type is that it's a dependency, and that it's generated by a certain callable.
To do so, you need to use the Depends
class from Ergate, which takes a callable as its first argument. Let's modify the workflow from the previous section to use the dependency we just created.
from datetime import datetime
from typing import Annotated
from ergate import Depends, Workflow
from my_dependency import create_current_time
workflow = Workflow(unique_name="my_first_workflow")
@workflow.step
def step_1(
input_value: int,
now: Annotated[
datetime, # (1)!
Depends(create_current_time), # (2)!
],
) -> int:
print(f"Hello, I am step 1 and I've received {input_value} at {now}")
return input_value + 1
@workflow.step
def step_2(input_value: int) -> None:
print(f"Hello, I am step 2 and I've received {input_value}")
- The first argument is the actual type of the
now
argument. This is what type checkers will see it as. - Any subsequent arguments are the metadata we want to add to the type. In this case, we're saying that
now
is a dependency that is generated by thecreate_current_time
callable.
Dependency arguments¶
Dependencies can take any arguments that workflow steps can take. This means that they can also "see" the input values and they can make use of other dependencies too, and same goes for those other subdependencies, and so on to infinity and beyooond.
And now, time for another challenge! Can you modify the create_current_time
dependency to receive the current timestamp from another dependency and then yield a datetime
object created with it. Give it a try and then check our solution below!
Solution
from collections.abc import Generator
from datetime import datetime
def create_current_timestamp() -> Generator[float, None, None]:
yield datetime.now().timestamp()
def create_current_time(
timestamp: Annotated[
float,
Depends(create_current_timestamp),
]
) -> Generator[datetime, None, None]:
yield datetime.fromtimestamp(timestamp)