Send Index Requests
Let's add the API for indexing books, then we can put in some test data for later use.
Move domain/model to books/domain/model.
Create books/domain/gateway/book_manager.py:
from abc import ABC, abstractmethod
from ..model import Book
class BookManager(ABC):
@abstractmethod
def index_book(self, b: Book) -> str:
pass
Again, we‘re using 4 Layers Architecture Pattern. It will pay off when your project grows bigger and bigger. Read more here.
Add books/domain/gateway/__init__.py:
from .book_manager import BookManager
Create books/infrastructure/search/es.py:
from dataclasses import asdict
from elasticsearch import Elasticsearch
from ...domain.gateway import BookManager
from ...domain.model import Book
INDEX_BOOK = "book_idx"
class ElasticSearchEngine(BookManager):
def __init__(self, address: str, page_size: int):
self.page_size = page_size
self.client = Elasticsearch(address)
def index_book(self, b: Book) -> str:
result = self.client.index(index=INDEX_BOOK, document=asdict(b))
return result['_id']
By default, Elasticsearch allows you to index documents into an index that doesn‘t exist yet. When you index a document into a non-existent index, Elasticsearch will dynamically create the index for you with default settings. This can be convenient for certain use cases where you don‘t want to manage index creation explicitly.
Install yaml
dependencies:
pip3 install PyYAML
Create books/infrastructure/config/config.py:
from dataclasses import dataclass
import yaml
@dataclass
class SearchConfig:
address: str
@dataclass
class ApplicationConfig:
page_size: int
@dataclass
class Config:
app: ApplicationConfig
search: SearchConfig
def parseConfig(filename: str) -> Config:
with open(filename, 'r') as f:
data = yaml.safe_load(f)
return Config(
ApplicationConfig(**data['app']),
SearchConfig(**data['search'])
)
Add books/infrastructure/config/__init__.py:
from .config import Config, parseConfig
Create config.yml:
app:
page_size: 10
search:
address: "http://localhost:9200"
Caution: Do not directly track your config.yml file with Git, as this could potentially leak sensitive data. If necessary, only track the template of the configuration file.
e.g.app: page_size: 10 search: address: ""
Create books/application/executor/book_operator.py:
from ...domain.model import Book
from ...domain.gateway import BookManager
class BookOperator():
def __init__(self, book_manager: BookManager):
self.book_manager = book_manager
def create_book(self, b: Book) -> str:
return self.book_manager.index_book(b)
Remember to create __init__.py files for all the sub-folders.
Create books/application/wire_helper.py:
from ..domain.gateway import BookManager
from ..infrastructure.config import Config
from ..infrastructure.search import ElasticSearchEngine
class WireHelper:
def __init__(self, engine: ElasticSearchEngine):
self.engine = engine
@classmethod
def new(cls, c: Config):
es = ElasticSearchEngine(c.search.address, c.app.page_size)
return cls(es)
def book_manager(self) -> BookManager:
return self.engine
Create books/adapter/router.py:
import logging
from fastapi import FastAPI, HTTPException
from ..application.executor import BookOperator
from ..application import WireHelper
from ..domain.model import Book
class RestHandler:
def __init__(self, logger: logging.Logger, book_operator: BookOperator):
self._logger = logger
self.book_operator = book_operator
def create_book(self, b: Book):
try:
return self.book_operator.create_book(b)
except Exception as e:
self._logger.error(f"Failed to create: {e}")
raise HTTPException(status_code=400, detail="Failed to create")
def make_router(app: FastAPI, wire_helper: WireHelper):
rest_handler = RestHandler(
logging.getLogger("lr-full-text"),
BookOperator(wire_helper.book_manager())
)
@app.get("/")
async def welcome():
return {"status": "ok"}
@app.post("/books")
async def create_book(b: Book):
book_id = rest_handler.create_book(b)
return {"id": book_id}
Replace main.py with the following code:
from fastapi import FastAPI
from books.adapter import make_router
from books.application import WireHelper
from books.infrastructure.config import parseConfig
CONFIG_FILENAME = "config.yml"
c = parseConfig(CONFIG_FILENAME)
wire_helper = WireHelper.new(c)
app = FastAPI()
make_router(app, wire_helper)
Run the server again and try it with curl
:
uvicorn web.main:app --reload
Example request:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Da Vinci Code","author":"Dan Brown","published_at":"2003-03-18","content":"In the Louvre, a curator is found dead. Next to his body, an enigmatic message. It is the beginning of a race to discover the truth about the Holy Grail."}' \
http://localhost:8000/books
Example response:
{"id":"Te9HCo8BPyexJHELQDO_"}
Put in more test data
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Harry Potter and the Philosopher\u0027s Stone","author":"J.K. Rowling","published_at":"1997-06-26","content":"A young boy discovers he is a wizard and begins his education at Hogwarts School of Witchcraft and Wizardry, where he uncovers the mystery of the Philosopher‘s Stone."}' \
http://localhost:8000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"To Kill a Mockingbird","author":"Harper Lee","published_at":"1960-07-11","content":"Set in the American South during the Great Depression, the novel explores themes of racial injustice and moral growth through the eyes of young Scout Finch."}' \
http://localhost:8000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Lord of the Rings","author":"J.R.R. Tolkien","published_at":"1954-07-29","content":"A hobbit named Frodo Baggins embarks on a perilous journey to destroy a powerful ring and save Middle-earth from the Dark Lord Sauron."}' \
http://localhost:8000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Catcher in the Rye","author":"J.D. Salinger","published_at":"1951-07-16","content":"Holden Caulfield narrates his experiences in New York City after being expelled from prep school, grappling with themes of alienation, identity, and innocence."}' \
http://localhost:8000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Alchemist","author":"Paulo Coelho","published_at":"1988-01-01","content":"Santiago, a shepherd boy, travels from Spain to Egypt in search of a treasure buried near the Pyramids. Along the way, he learns about the importance of following one‘s dreams."}' \
http://localhost:8000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Hunger Games","author":"Suzanne Collins","published_at":"2008-09-14","content":"In a dystopian future, teenagers are forced to participate in a televised death match called the Hunger Games. Katniss Everdeen volunteers to take her sister‘s place and becomes a symbol of rebellion."}' \
http://localhost:8000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"1984","author":"George Orwell","published_at":"1949-06-08","content":"Winston Smith lives in a totalitarian society ruled by the Party led by Big Brother. He rebels against the oppressive regime but ultimately succumbs to its control."}' \
http://localhost:8000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Girl with the Dragon Tattoo","author":"Stieg Larsson","published_at":"2005-08-01","content":"Journalist Mikael Blomkvist and hacker Lisbeth Salander investigate the disappearance of a young woman from a wealthy family, uncovering dark secrets and corruption."}' \
http://localhost:8000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Gone Girl","author":"Gillian Flynn","published_at":"2012-06-05","content":"On their fifth wedding anniversary, Nick Dunne‘s wife, Amy, disappears. As the media circus ensues and suspicions mount, Nick finds himself in a whirlwind of deception and betrayal."}' \
http://localhost:8000/books