Crie uma Cocktail API com o Beanie e o MongoDB
Avalie esse Tutorial
Tenho uma coleção do MongoDB contendo receitas de coquetéis que criei durante a pandemia.
Recentemente, estou tentando construir uma API sobre ela usando algumas tecnologias que conheço bem. Não estava muito satisfeito com os resultados. Escrever código para transformar o BSON que sai do MongoDB em JSON adequado é relativamente trabalhoso. Eu queria algo mais declarativo, mas a minha tentativa mais recente – uma mash-up de Flask, MongoEngine e Marshmallow– parecia desajeitada e repetitiva. Eu estava prestes a começar a experimentar a construção de minha própria estrutura declarativa, quando me deparei com uma introdução a um novo MongoDB ODM chamado Beanie. Parecia exatamente o que eu estava procurando.
O código usado nesta publicação se baseia em grande parte na postagem de Beanie com link acima. Eu o personalizei de acordo com minhas necessidades e adicionei um endpoint extra que usa o MongoDB Atlas Search para fornecer preenchimento automático para uma GUI que estou planejando criar no futuro.
Tenho uma coleção de documentos que se parece um pouco com isto:
1 { 2 "_id": "5f7daa158ec9dfb536781b0a", 3 "name": "Hunter's Moon", 4 "ingredients": [ 5 { 6 "name": "Vermouth", 7 "quantity": { 8 "quantity": "25", 9 "unit": "ml" 10 } 11 }, 12 { 13 "name": "Maraschino Cherry", 14 "quantity": { 15 "quantity": "15", 16 "unit": "ml" 17 } 18 }, 19 { 20 "name": "Sugar Syrup", 21 "quantity": { 22 "quantity": "10", 23 "unit": "ml" 24 } 25 }, 26 { 27 "name": "Lemonade", 28 "quantity": { 29 "quantity": "100", 30 "unit": "ml" 31 } 32 }, 33 { 34 "name": "Blackberries", 35 "quantity": { 36 "quantity": "2", 37 "unit": null 38 } 39 } 40 ] 41 }
A promessa do Beanie e da FastAPI – apenas para criar um modelo para esses dados e fazer com que ele traduza automaticamente os tipos de campos complicados, como
ObjectId
e Date
entre a representação BSON e JSON – era muito tentadora, então eu chamei um novo projeto Python e defini meu esquema em um submódulo de modelos da seguinte forma:1 class Cocktail(Document): 2 class Settings: 3 name = "recipes" 4 5 name: str 6 ingredients: List["Ingredient"] 7 instructions: List[str] 8 9 10 class Ingredient(BaseModel): 11 name: str 12 quantity: Optional["IngredientQuantity"] 13 14 15 class IngredientQuantity(BaseModel): 16 quantity: Optional[str] 17 unit: Optional[str] 18 19 20 class IngredientAggregation(BaseModel): 21 """ A model for an ingredient count. """ 22 23 id: str = Field(None, alias="_id") 24 total: int
Fiquei feliz em ver que eu poderia definir uma classe interna
Settings
e substituir o nome da coleção. Era um recurso que eu achava que deveria estar lá, mas não tinha certeza se estaria.A outra coisa que foi um pouco trabalhosa foi fazer
Cocktail
se referir a Ingredient
, que não estava definido até então. Felizmente, posso apenas usar o nome da turma em uma string e as coisas serão coladas mais tarde.O pacotebeanieCocktails, definido no arquivo
__init__.py
, contém principalmente código para inicializar FastAPI, Motor eBeanie:1 # ... some code skipped 2 3 async def app_lifespan(app: FastAPI): 4 # startup code goes here: 5 client: AsyncIOMotorClient = AsyncIOMotorClient( 6 Settings().mongodb_url, 7 ) 8 await init_beanie(client.get_default_database(), document_models=[Cocktail]) 9 app.include_router(cocktail_router, prefix="/v1") 10 11 yield 12 13 # shutdown code goes here: 14 client.close() 15 16 app = FastAPI(lifespan=app_lifespan) 17 18 # I'm using pydantic_settings to load the MongoDB connection string from an environment variable, 19 # or it defaults to a local installation. 20 class Settings(BaseSettings): 21 mongodb_url: str = "mongodb://localhost:27017/cocktails"
O código acima define um manipulador de vida útil para a inicialização do aplicativo FastAPI. Ele se conecta ao MongoDB, configura o Goanie com a conexão do banco de dados e fornece o modelo
Cocktail
que usarei para o Goanie.A última linha adiciona o
cocktail_router
ao FastAPI. É um APIRouter
definido no submódulo derotas .Agora é hora de mostrar o arquivo de rotas. Foi nele em que passei a maior parte do meu tempo. Fiquei impressionado com a rapidez com que consegui desenvolver os pontos de extremidade da API.
1 # ... imports skipped 2 3 cocktail_router = APIRouter()
O
cocktail_router
é responsável por rotear caminhos de URL para diferentes manipuladores de função que fornecerão dados a serem renderizados como JSON. Provavelmente, o manipulador mais simples é:1 2 async def list_cocktails(): 3 return await Cocktail.find_all().to_list()
Esse manipulador tira o máximo proveito desses fatos: a FastAPI renderizará automaticamente as instâncias do Pydantic como JSON; e os modelos Beanie
Document
são definidos usando o Pydantic.Cocktail.find_all()
retorna um iterador sobre todos os documentos Cocktail
na coleção recipes
. A FastAPI não pode lidar diretamente com esses iteradores, portanto, a sequência é convertida em uma lista usando o método to_list()
.1 just run
Caso contrário, você pode executá-lo diretamente:
1 uvicorn beaniecocktails:app --reload
E então você pode testar o ponto de extremidade apontando seu navegador para "http://localhost:8000/v1/cocktails/".
Um ponto de extremidade semelhante para apenas um único coquetel é perfeitamente encapsulado por dois métodos: um para pesquisar um documento por
_id
e gerar um erro "404 Não Encontrado" se ele não existir e um manipulador para rotear a solicitação HTTP. Os dois são perfeitamente colados usando a declaração Depends
que converte o cocktail_id
fornecido em uma instância Cocktail
carregada.1 async def get_cocktail(cocktail_id: PydanticObjectId) -> Cocktail: 2 """ Helper function to look up a cocktail by id """ 3 4 cocktail = await Cocktail.get(cocktail_id) 5 if cocktail is None: 6 raise HTTPException(status_code=404, detail="Cocktail not found") 7 return cocktail 8 9 10 async def get_cocktail_by_id(cocktail: Cocktail = Depends(get_cocktail)): 11 return cocktail
Agora, vamos ao que eu realmente gostaria de ver no Beanie: sua integração com a framework de agregação do MongoDB. Os pipelines de agregação podem remodelar documentos por meio de projeção ou agrupamento, e o Beanie permite que os documentos resultantes sejam mapeados para uma subclasse
BaseModel
Pydantic.Usando essa técnica, pode ser adicionado um ponto de extremidade que forneça um índice de todos os componentes e o número de coquetéis que cada um aparece em:
1 # models.py: 2 3 class IngredientAggregation(BaseModel): 4 """ A model for an ingredient count. """ 5 6 id: str = Field(None, alias="_id") 7 total: int 8 9 # routes.py: 10 11 12 async def list_ingredients(): 13 """ Group on each ingredient name and return a list of `IngredientAggregation`s. """ 14 15 return await Cocktail.aggregate( 16 aggregation_query=[ 17 {"$unwind": "$ingredients"}, 18 {"$group": {"_id": "$ingredients.name", "total": {"$sum": 1}}}, 19 {"$sort": {"_id": 1}}, 20 ], 21 projection_model=IngredientAggregation, 22 ).to_list()
1 [ 2 {"_id":"7-Up","total":1}, 3 {"_id":"Amaretto","total":2}, 4 {"_id":"Angostura Bitters","total":1}, 5 {"_id":"Apple schnapps","total":1}, 6 {"_id":"Applejack","total":1}, 7 {"_id":"Apricot brandy","total":1}, 8 {"_id":"Bailey","total":1}, 9 {"_id":"Baileys irish cream","total":1}, 10 {"_id":"Bitters","total":3}, 11 {"_id":"Blackberries","total":1}, 12 {"_id":"Blended whiskey","total":1}, 13 {"_id":"Bourbon","total":1}, 14 {"_id":"Bourbon Whiskey","total":1}, 15 {"_id":"Brandy","total":7}, 16 {"_id":"Butterscotch schnapps","total":1}, 17 ]
Curti tanto esse recurso que decidi usá-lo junto com o MongoDB Atlas Search, que fornece pesquisa de texto gratuita em coleções do MongoDB, para implementar um endpoint de preenchimento automático.
A primeira etapa foi adicionar um índice de pesquisa na coleção
recipes
, na interface web do MongoDB Atlas:Eu precisei adicionar o campo
name
como um tipo de campo "autocomplete".Esperei que o índice terminasse de ser criado, o que não demorou muito, pois não é uma coleção muito grande. Então eu estava pronto para gravar meu endpoint de preenchimento automático:
1 2 async def cocktail_autocomplete(fragment: str): 3 """ Return an array of cocktail names matched from a string fragment. """ 4 5 return [ 6 c["name"] 7 for c in await Cocktail.aggregate( 8 aggregation_query=[ 9 { 10 "$search": { 11 "autocomplete": { 12 "query": fragment, 13 "path": "name", 14 } 15 } 16 } 17 ] 18 ).to_list() 19 ]
O estágio de agregação do
$search
utiliza especificamente um índice de pesquisa. Neste caso, estou usando o tipo autocomplete
, para corresponder ao tipo de índice que criei no campo name
. Como eu queria que a resposta fosse a mais leve possível, estou assumindo a serialização para JSON, extraindo o nome de cada instância Cocktail
e apenas retornando uma lista de strings.Os resultados são ótimos!
Abordando meu navegador em http://localhost:8000/v1/Cocktail_autocomplete?fragment=fi me dá
["Imperial Fizz","Vodka Fizz"]
e http://localhost:8000/v1/Cocktail_autocomplete?fragment= Ma me dá ["Manhattan","Espresso Martini"]
.A próxima etapa é construir um frontend do React, para que eu possa realmente chamar isso de aplicativo FARM Stack.
Fiquei realmente impressionado com a rapidez com que consegui colocar tudo isso em funcionamento. O tratamento de instâncias
ObjectId
era totalmente invisível, graças ao tipo PydanticObjectId
do Beanie, e vi outro código de exemplo que mostra como os valores BSON Date
são igualmente bem tratados.Preciso ver como posso criar alguma funcionalidade HATEOAS nos endpoints, com entidades vinculadas a suas URL canônicas. A paginação também é algo que será importante à medida que minha coleção crescer, mas acho que já sei como lidar com isso..
Espero que tenha gostado deste rápido resumo da minha primeira experiência usando o Beanie. Na próxima vez que estiver construindo uma API no MongoDB, recomendo que você experimente!
Se esta foi sua primeira exposição à Estrutura de agregação, eu realmente recomendável que você leia nossa documentação sobre esse recurso poderoso do MongoDB. Ou, se você realmente quiser colocar a mão na massa, por que não conferir nosso cursogratuito de Agregação da MongoDB University?
Se tiver dúvidas, acesse o site da nossa comunidade de desenvolvedores, no qual os engenheiros e a comunidade do MongoDB ajudarão você a desenvolver sua próxima grande ideia com o MongoDB.