Problema de injeção de dependência com FastAPI em Python

Oct 05 2020

Dia bom! Diga-me como você pode resolver o seguinte problema em Python + FastAPI.

Existe um projeto de teste:

app / main.py - main file
app / routes / users.py -set of api methods
app / repos / factory.py - repository factory
app / repos / user_repository.py - repositories
app / handlers / factory.py - handler factory
app / handlers / users.py - handlers
app / domain / user.py - data class

A estrutura principal e das rotas é a mesma do exemplo https://fastapi.tiangolo.com/tutorial/bigger-applications/

No arquivo routes / users.py :

from fastapi import APIRouter, Depends
from ..handlers import factory

router = APIRouter()

@router.get("/users/", tags=["users"])
def read_users(handler=Depends(factory.get_handler)):
    return handler.get_all()

Em handlers / factory.py :

from fastapi import Depends
from .users import UserHandler1

def get_handler(handler=Depends(UserHandler1)):
    return handler

Em handlers / users.py :

from fastapi import Depends
from ..repos import factory

class UserHandler1:
    def __init__(self):
        pass

    def get_all(self, repo=Depends(factory.get_repo)):
        return repo.get_all()

repos / factory.py :

from fastapi import Depends
from ..repos.user_repository import UserRepository

def get_repo(repo=Depends(UserRepository)):
    return repo

repos / user_repository.py :

from ..domain.user import User

class UserRepository:
    def __init__(self):
        pass

    def get_all(self):
        return [User(1, 'A'), User(2, 'B'), User(3, 'C')]

domínio / usuário.py :

class User:
    id: int
    name: str

    def __init__(self, id, name):
        self.id = id
        self.name = name

Em seguida, executo o servidor hypercorn: app.main:app --reloadtente chamar o método api: http://127.0.0.1:8000/users/E recebo o erro AttributeError:'Depends' object has no attribute 'get_all'

Se você remover a camada de manipuladores e fizer isso, tudo funcionará.

rotas / users.py:

from fastapi import APIRouter, Depends
from ..repos import factory

router = APIRouter()

@router.get("/users/", tags=["users"])
def read_users(repo=Depends(factory.get_repo)):
    return repo.get_all()
It also works if you completely remove all Depends and create
UserRepository and UserHandler1 directly in factories.

Pergunta 1: Como uso "Depends" neste caso e por que não funciona?

Em geral, a fábrica não parece uma boa solução para este problema. Eu vi um exemplo de implementação de DI usando herança múltipla, mas para mim é o mesmo que o método de fábrica. Também tentei usar a biblioteca Pinject, mas ela requer a construção inicial de um gráfico, que precisa ser salvo em algum lugar para ser acessado nos manipuladores de API.

Questão 2 (mais importante): como a injeção de dependência pode ser aplicada neste caso?

Respostas

1 lsabi Oct 05 2020 at 12:37

Conforme observado nos comentários, uma dependência pode ser qualquer coisa que possa ser chamada e, portanto, uma classe também. A única ressalva no último caso é que a classe será apenas inicializada (ou seja, apenas a função init (..) será chamada).

Então, para ter uma classe como dependência, como no exemplo de https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/#shortcutvocê só precisa chamar as funções de destino dentro do init e definir os valores como atributos da classe.

from ..domain.user import User

class UserRepository:
    def __init__(self):
        self.get_all()

    def get_all(self):
        self.users = [User(1, 'A'), User(2, 'B'), User(3, 'C')]


from fastapi import Depends
from ..repos.user_repository import UserRepository

def get_repo(repo=Depends(UserRepository)):
    print(repo.users) # This will print the list of users
    return repo

QUESTÃO 2

NB

Esta é uma questão de modelagem. Aqui, proponho o que acredito ser adequado do meu ponto de vista. Não precisa necessariamente ser a abordagem melhor ou mais simples.

Respondendo a sua segunda pergunta, eu não aconselharia essas dependências complexas. Se as dependências estiverem no nível do roteador, você pode simplesmente adicioná-las ao roteador, usando o parâmetro depends=[...]e fornecendo uma lista de classes / funções de dependência.

Como alternativa, você pode declarar todas as dependências como parâmetros de função do nó de extremidade, como fez para a fábrica. Esse método pode fazer com que grandes pedaços de código sejam copiados e colados, portanto, aconselho a abordagem acima.

Se você precisar processar os parâmetros de dados, adicione-os à solicitação e os acesse de dentro do terminal. Consulte FastAPI obter ID do usuário da chave de API para um exemplo mínimo.