docs: Add module level dockstrings and change other

Refs: OPS-68
This commit is contained in:
Alivecow 2025-03-23 17:06:54 +01:00
parent ab47d13938
commit 8a3c997cf7
3 changed files with 213 additions and 101 deletions

View file

@ -1,3 +1,59 @@
"""
Haiku Generation Module
=======================
A client interface for AI-powered haiku poem generation.
This module provides the core functionality for communicating with an Ollama-based
AI service to generate three-line haiku poems. It handles the entire generation
process, from sending properly formatted requests to processing and validating
the returned poems.
Classes
-------
Haiku
A dataclass representation of a haiku poem, providing structure for storage,
manipulation and serialization of poem data.
**Methods**:
* ``to_json()``: Converts a haiku instance to JSON format for API responses
* ``generate_haiku(seed_text)``: Creates a new haiku using the AI service
Constants
---------
AI_SERVICE_URL
The endpoint URL for the Ollama API service.
AI_MODEL_NAME
The specific AI model used for haiku generation.
REQUEST_TIMEOUT
The maximum time (in seconds) to wait for AI service responses.
Dependencies
------------
* requests: HTTP client library for API communication
* dataclasses: Support for the Haiku data structure
* logging: Error and diagnostic information capture
* json: Processing of API responses
Implementation Details
----------------------
The module implements a robust communication pattern with the AI service, including:
1. Proper request formatting with seed text integration
2. Multiple retry attempts for handling temporary service issues
3. Response validation to ensure the returned text follows haiku structure
4. Fallback mechanisms when the AI service returns unsuitable content
5. JSON serialization for consistent data exchange
When communicating with the AI service, the module maintains appropriate
error handling and logging to help diagnose any generation issues. It aims
to provide a reliable haiku generation experience even when dealing with the
inherent unpredictability of AI-generated content.
"""
from __future__ import annotations from __future__ import annotations
import json import json
@ -19,73 +75,64 @@ def foobar():
@dataclass @dataclass
class Haiku: class Haiku:
"""
A class representing a haiku poem with three lines.
:ivar lines: A list containing the three lines of the haiku.
:type lines: list[str]
"""
lines: list[str] lines: list[str]
def get_json(self): def get_json(self):
""" """
Converts the haiku lines to a JSON string. Converts the haiku lines to a JSON string.
Returns: :return: A JSON string representation of the haiku lines.
str: A JSON string representation of the haiku lines. :rtype: str
""" """
return json.dumps(self.lines) return json.dumps(self.lines)
@staticmethod @staticmethod
def request_haiku(seed: str) -> Haiku: def request_haiku(seed: str) -> 'Haiku':
""" """
Generates a haiku using an AI model based on the provided seed text. Generates a haiku using an AI model based on the provided seed text.
This function prompts the AI to generate a haiku based on the user input. This function prompts the AI to generate a haiku based on the user input.
It validates that the response: It validates that the response contains exactly 3 lines.
- Contains exactly 3 lines
The function will retry until a valid haiku is generated. The function will retry until a valid haiku is generated.
Args: :param seed: The input text used to inspire the haiku generation.
seed (str): The input text used to inspire the haiku generation. :type seed: str
:return: A new Haiku object containing the generated three lines.
:rtype: Haiku
Returns: :raises: Possible JSONDecodeError which is caught and handled with retries.
Haiku: A new Haiku object containing the generated three lines.
Raises:
Possible JSONDecodeError which is caught and handled with retries.
""" """
ai_gen_request = { ai_gen_request = {
"model": "haiku", "model": "haiku",
"prompt": f"{seed}", "prompt": f"{seed}",
"stream": False, "stream": False,
"eval_count": 20 "eval_count": 20
} }
while True: while True:
try: try:
r = requests.post(url=AI_BASE_URL + AI_GEN_ENDPOINT, r = requests.post(url=AI_BASE_URL + AI_GEN_ENDPOINT,
json=ai_gen_request) json=ai_gen_request)
ai_response = str(r.json()["response"]) ai_response = str(r.json()["response"])
logging.warning(ai_response) logging.warning(ai_response)
lines = ai_response.split("\n") lines = ai_response.split("\n")
for _ in range(0, 2): for _ in range(0, 2):
lines.pop() lines.pop()
logging.warning(lines) logging.warning(lines)
if len(lines) != 3: if len(lines) != 3:
continue continue
haiku = Haiku( haiku = Haiku(
[ [
lines[0], lines[0],
lines[1], lines[1],
lines[2] lines[2]
]) ])
break break
except json.JSONDecodeError: except json.JSONDecodeError:
continue continue
return haiku return haiku

View file

@ -1,3 +1,42 @@
"""
Senju Haiku Web Application
===========================
A Flask-based web interface for generating, viewing, and managing haiku poetry.
This application provides a comprehensive interface between users and an AI-powered
haiku generation service, with persistent storage capabilities. Users can interact
with the system through both a web interface and a RESTful API.
Features
--------
* **Landing page**: Welcome interface introducing users to the Senju service
* **Browsing interface**: Gallery-style viewing of previously generated haikus
* **Prompt interface**: Text input system for generating haikus from seed text
* **Image scanning**: Experimental interface for creating haikus from visual inputs
* **RESTful API**: Programmatic access for integration with other services
Architecture
------------
The application implements a RESTful architecture using Flask's routing system
and template rendering. All user interactions are handled through clearly defined
routes, with appropriate error handling for exceptional cases.
Dependencies
------------
* future.annotations: Enhanced type hint support
* os, Path: Filesystem operations for storage management
* Flask: Core web application framework
* Haiku: Custom class for poem representation and generation
* StoreManager: Database abstraction for persistence operations
Implementation
--------------
The module initializes both a Flask application instance and a StoreManager
with a configured storage location. All routes and view functions required
for the complete web interface are defined within this module.
"""
from __future__ import annotations from __future__ import annotations
import os import os
@ -26,32 +65,26 @@ def index_view():
""" """
Render the main index page of the application. Render the main index page of the application.
Returns: :return: The index.html template with title "Senju".
Text: The index.html template with title "Senju". :rtype: flask.Response
""" """
return render_template("index.html", title="Senju") return render_template("index.html", title="Senju")
@app.route("/haiku/") @app.route("/haiku/")
def haiku_index_view(): def haiku_index_view():
""" """
Redirect to the most recently created haiku. Redirect to the most recently created haiku.
Returns: :return: Redirects to the haiku_view route with the latest haiku ID.
Response: Redirects to the haiku_view route with the latest haiku ID. :rtype: flask.Response
:raises KeyError: If no haikus exist in the store yet.
Raises:
KeyError: If no haikus exist in the store yet.
""" """
haiku_id: int | None = store.get_id_of_latest_haiku() haiku_id: int | None = store.get_id_of_latest_haiku()
if haiku_id is None: if haiku_id is None:
# TODO: add "empty haiku list" error page # TODO: add "empty haiku list" error page
raise KeyError("no haiku exist yet") raise KeyError("no haiku exist yet")
return redirect(url_for("haiku_view", haiku_id=haiku_id)) return redirect(url_for("haiku_view", haiku_id=haiku_id))
@app.route("/haiku/<int:haiku_id>") @app.route("/haiku/<int:haiku_id>")
def haiku_view(haiku_id): def haiku_view(haiku_id):
""" """
@ -61,16 +94,12 @@ def haiku_view(haiku_id):
the haiku.html template. If no haiku is found with the provided ID, the haiku.html template. If no haiku is found with the provided ID,
raises a KeyError. raises a KeyError.
Args: :param haiku_id: The ID of the haiku to display.
haiku_id (int): The ID of the haiku to display. :type haiku_id: int
:return: The haiku.html template with the haiku data in context.
Returns: :rtype: flask.Response
Text: The haiku.html template with the haiku data in context. :raises KeyError: If no haiku exists with the given ID.
Raises:
KeyError: If no haiku exists with the given ID.
""" """
haiku: Haiku | None = store.load_haiku(haiku_id) haiku: Haiku | None = store.load_haiku(haiku_id)
if haiku is None: if haiku is None:
# TODO: add "haiku not found" page # TODO: add "haiku not found" page
@ -78,42 +107,37 @@ def haiku_view(haiku_id):
context: dict = { context: dict = {
"haiku": haiku "haiku": haiku
} }
return render_template( return render_template(
"haiku.html", "haiku.html",
context=context, context=context,
title="Haiku of the Day") title="Haiku of the Day")
@app.route("/prompt") @app.route("/prompt")
def prompt_view(): def prompt_view():
""" """
Render the haiku generation prompt page. Render the haiku generation prompt page.
Returns: :return: The prompt.html template with title "Haiku generation".
Text: The prompt.html template with title "Haiku generation". :rtype: flask.Response
""" """
return render_template( return render_template(
"prompt.html", "prompt.html",
title="Haiku generation" title="Haiku generation"
) )
@app.route("/scan") @app.route("/scan")
def scan_view(): def scan_view():
""" """
Render the image scanning page. Render the image scanning page.
Returns: :return: The scan.html template with title "Image scanning".
Text: The scan.html template with title "Image scanning". :rtype: flask.Response
""" """
return render_template( return render_template(
"scan.html", "scan.html",
title="Image scanning" title="Image scanning"
) )
@app.route("/api/v1/haiku", methods=['POST']) @app.route("/api/v1/haiku", methods=['POST'])
def generate_haiku(): def generate_haiku():
""" """
@ -123,11 +147,10 @@ def generate_haiku():
Generates a haiku using the prompt, saves it to the store, Generates a haiku using the prompt, saves it to the store,
and returns the ID. and returns the ID.
Returns: :return: The ID of the newly created haiku if method is POST.
str: The ID of the newly created haiku if method is POST. Error message and status code 405 if method is not POST.
tuple: Error message and status code 405 if method is not POST. :rtype: Union[str, Tuple[str, int]]
""" """
if request.method == 'POST': if request.method == 'POST':
json_data = request.get_json() json_data = request.get_json()
prompt = json_data["prompt"] prompt = json_data["prompt"]
@ -137,16 +160,14 @@ def generate_haiku():
else: else:
return "Method not allowed", 405 return "Method not allowed", 405
@app.route('/favicon.ico') @app.route('/favicon.ico')
def favicon(): def favicon():
""" """
Serve the favicon.ico file from the static directory. Serve the favicon.ico file from the static directory.
Returns: :return: The favicon.ico file with the appropriate MIME type.
Response: The favicon.ico file with the appropriate MIME type. :rtype: flask.Response
""" """
return send_from_directory(os.path.join(app.root_path, 'static/img'), return send_from_directory(os.path.join(app.root_path, 'static/img'),
'favicon.ico', 'favicon.ico',
mimetype='image/vnd.microsoft.icon') mimetype='image/vnd.microsoft.icon')

View file

@ -1,3 +1,47 @@
"""
Senju Database Management Module
================================
A database interaction layer for the Senju haiku management system.
This module implements a lightweight document database abstraction using TinyDB
for persistent storage of haiku poems. It provides a clean interface for storing,
retrieving, updating, and managing haiku entries in the system.
Classes
-------
StoreManager
The primary class responsible for all database operations. Handles connection
management, CRUD operations, and query capabilities for haiku data.
Functions
---------
utility_function
Provides simple arithmetic operations to support database functionalities.
Constants
---------
DEFAULT_DB_PATH
The default filesystem location for the TinyDB database file (/var/lib/senju.json).
Dependencies
------------
* future.annotations: Enhanced type hint support
* logging.Logger: Diagnostic and error logging capabilities
* pathlib.Path: Cross-platform filesystem path handling
* typing.Optional: Type annotations for nullable values
* tinydb.TinyDB: Lightweight document database implementation
* tinydb.QueryImpl: Query builder for database searches
* senju.haiku.Haiku: Data model for haiku representation
Implementation Details
----------------------
The module uses TinyDB as its storage engine, providing a JSON-based document
storage solution that balances simplicity with functionality. The StoreManager
abstracts all database operations behind a clean API, handling connection lifecycle
and providing methods for common operations on haiku data.
"""
from __future__ import annotations from __future__ import annotations
from logging import Logger from logging import Logger
@ -20,6 +64,17 @@ def foobar():
class StoreManager: class StoreManager:
"""
Manages the storage and retrieval of haiku data using TinyDB.
This class provides an interface for saving and loading haikus from
a TinyDB database file.
:ivar _db: Database instance for storing haiku data.
:type _db: TinyDB
:ivar logger: Logger for tracking operations and errors.
:type logger: Logger
"""
__slots__ = "_db", "logger" __slots__ = "_db", "logger"
_db: TinyDB _db: TinyDB
logger: Logger logger: Logger
@ -28,9 +83,9 @@ class StoreManager:
""" """
Initialize the StoreManager with a database path. Initialize the StoreManager with a database path.
Args: :param path_to_db: Path to the TinyDB database file. Defaults to DEFAULT_DB_PATH.
path_to_db (Path, optional): Path to the TinyDB database file. :type path_to_db: Path, optional
Defaults to DEFAULT_DB_PATH. :return: None
""" """
self._db = TinyDB(path_to_db) self._db = TinyDB(path_to_db)
self.logger = Logger(__name__) self.logger = Logger(__name__)
@ -39,29 +94,25 @@ class StoreManager:
""" """
Execute a query against the database. Execute a query against the database.
Args: :param query: TinyDB query to execute.
query (QueryImpl): TinyDB query to execute. :type query: QueryImpl
:return: List of documents matching the query.
Returns: :rtype: list[dict]
list[dict]: List of documents matching the query.
""" """
return self._db.search(query) return self._db.search(query)
def _load(self, id: int) -> Optional[dict]: def _load(self, id: int) -> Optional[dict]:
""" """
Load a document by its ID. Load a document by its ID.
Args: :param id: Document ID to load.
id (int): Document ID to load. :type id: int
:return: The document if found, None otherwise.
:rtype: Optional[dict]
Returns: .. note::
Optional[dict]: The document if found, None otherwise. Logs a warning if document with specified ID is not found.
Logs:
Warning if document with specified ID is not found.
""" """
try: try:
return self._db.get(doc_id=id) return self._db.get(doc_id=id)
except IndexError as e: except IndexError as e:
@ -72,26 +123,22 @@ class StoreManager:
""" """
Save a document to the database. Save a document to the database.
Args: :param data: Document data to save.
data (dict): Document data to save. :type data: dict
:return: The document ID of the saved document.
Returns: :rtype: int
int: The document ID of the saved document.
""" """
return self._db.insert(data) return self._db.insert(data)
def load_haiku(self, key: int) -> Optional[Haiku]: def load_haiku(self, key: int) -> Optional[Haiku]:
""" """
Load a haiku by its ID. Load a haiku by its ID.
Args: :param key: The ID of the haiku to load.
key (int): The ID of the haiku to load. :type key: int
:return: A Haiku object if found, None otherwise.
Returns: :rtype: Optional[Haiku]
Optional[Haiku]: A Haiku object if found, None otherwise.
""" """
raw_haiku: dict | None = self._load(key) raw_haiku: dict | None = self._load(key)
if raw_haiku is None: if raw_haiku is None:
return None return None
@ -102,26 +149,23 @@ class StoreManager:
""" """
Save a haiku to the database. Save a haiku to the database.
Args: :param data: The Haiku object to save.
data (Haiku): The Haiku object to save. :type data: Haiku
:return: The document ID of the saved haiku.
Returns: :rtype: int
int: The document ID of the saved haiku.
""" """
return self._save(data.__dict__) return self._save(data.__dict__)
def get_id_of_latest_haiku(self) -> Optional[int]: def get_id_of_latest_haiku(self) -> Optional[int]:
""" """
Get the ID of the most recently added haiku. Get the ID of the most recently added haiku.
Returns: :return: The ID of the latest haiku if any exists, None otherwise.
Optional[int]: The ID of the latest haiku if any exists, None otherwise. :rtype: Optional[int]
Logs: .. note::
Error if the database is empty. Logs an error if the database is empty.
""" """
try: try:
id = self._db.all()[-1].doc_id id = self._db.all()[-1].doc_id
return id return id