Implement task API with input validation; implement error handling; refactoring
This commit is contained in:
parent
19d264a03b
commit
50aff05614
24 changed files with 612 additions and 131 deletions
|
|
@ -1,5 +1,5 @@
|
|||
from tofu_api.common.rest import BaseBlueprint
|
||||
from .tasks import TaskApiBlueprint
|
||||
from .tasks import TaskBlueprint
|
||||
|
||||
|
||||
class TofuApiBlueprint(BaseBlueprint):
|
||||
|
|
@ -12,4 +12,4 @@ class TofuApiBlueprint(BaseBlueprint):
|
|||
url_prefix = '/api'
|
||||
|
||||
def init_blueprint(self) -> None:
|
||||
self.register_blueprint(TaskApiBlueprint(self.app))
|
||||
self.register_blueprint(TaskBlueprint(self.app))
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
from .task_api import TaskApiBlueprint
|
||||
from .task_blueprint import TaskBlueprint
|
||||
from .task_handler import TaskHandler
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
from flask import jsonify
|
||||
from flask.views import MethodView
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from tofu_api.common.rest import BaseBlueprint
|
||||
from tofu_api.models import Task
|
||||
|
||||
|
||||
class TaskApiBlueprint(BaseBlueprint):
|
||||
"""
|
||||
Blueprint for the tasks REST API.
|
||||
"""
|
||||
|
||||
# Blueprint settings
|
||||
name = 'rest_api_tasks'
|
||||
import_name = __name__
|
||||
url_prefix = '/tasks'
|
||||
|
||||
def init_blueprint(self) -> None:
|
||||
"""
|
||||
Register URL rules.
|
||||
"""
|
||||
db_session = self.app.dependencies.get_db_session()
|
||||
|
||||
self.add_url_rule(
|
||||
'',
|
||||
view_func=self.create_view_func(TaskCollectionView, db_session=db_session),
|
||||
methods=['GET', 'POST'],
|
||||
)
|
||||
self.add_url_rule(
|
||||
'/<int:task_id>',
|
||||
view_func=self.create_view_func(TaskItemView, db_session=db_session),
|
||||
methods=['GET', 'PATCH', 'DELETE'],
|
||||
)
|
||||
|
||||
|
||||
class TaskBaseView(MethodView):
|
||||
"""
|
||||
Base class for view classes for the `/tasks` endpoint.
|
||||
"""
|
||||
|
||||
# TODO: Use a handler class instead of accessing the database session directly
|
||||
db_session: Session
|
||||
|
||||
def __init__(self, *, db_session: Session):
|
||||
self.db_session = db_session
|
||||
|
||||
|
||||
class TaskCollectionView(TaskBaseView):
|
||||
"""
|
||||
View class for `/tasks` endpoint.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Get list of all tasks.
|
||||
"""
|
||||
task_list = self.db_session.query(Task).all()
|
||||
return jsonify({
|
||||
'count': len(task_list),
|
||||
'items': [task.to_dict() for task in task_list],
|
||||
}), 200
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
Create a new task.
|
||||
"""
|
||||
# TODO: Parse request data and create real data
|
||||
new_task = Task(
|
||||
title='Do stuff'
|
||||
)
|
||||
self.db_session.add(new_task)
|
||||
self.db_session.commit()
|
||||
return jsonify(new_task.to_dict()), 201
|
||||
|
||||
|
||||
class TaskItemView(TaskBaseView):
|
||||
"""
|
||||
View class for `/tasks/<int:task_id>` endpoint.
|
||||
"""
|
||||
|
||||
def get(self, task_id: int):
|
||||
"""
|
||||
Get a single task by ID.
|
||||
"""
|
||||
task = self.db_session.query(Task).get(task_id)
|
||||
if task is None:
|
||||
raise NotFound(f'Task with ID {task_id} not found!')
|
||||
return jsonify(task.to_dict()), 200
|
||||
|
||||
def patch(self, task_id: int):
|
||||
"""
|
||||
Update a single task by ID.
|
||||
"""
|
||||
# TODO: Implement
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, task_id: int):
|
||||
"""
|
||||
Delete a single task by ID.
|
||||
"""
|
||||
# TODO: Implement
|
||||
raise NotImplementedError
|
||||
30
tofu_api/api/tasks/task_blueprint.py
Normal file
30
tofu_api/api/tasks/task_blueprint.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from tofu_api.common.rest import BaseBlueprint
|
||||
from .task_views import TaskCollectionView, TaskItemView
|
||||
|
||||
|
||||
class TaskBlueprint(BaseBlueprint):
|
||||
"""
|
||||
Blueprint for the tasks REST API.
|
||||
"""
|
||||
|
||||
# Blueprint settings
|
||||
name = 'rest_api_tasks'
|
||||
import_name = __name__
|
||||
url_prefix = '/tasks'
|
||||
|
||||
def init_blueprint(self) -> None:
|
||||
"""
|
||||
Register URL rules.
|
||||
"""
|
||||
task_handler = self.app.dependencies.get_task_handler()
|
||||
|
||||
self.add_url_rule(
|
||||
'',
|
||||
view_func=TaskCollectionView.as_view(task_handler=task_handler),
|
||||
methods=['GET', 'POST'],
|
||||
)
|
||||
self.add_url_rule(
|
||||
'/<int:task_id>',
|
||||
view_func=TaskItemView.as_view(task_handler=task_handler),
|
||||
methods=['GET', 'PATCH', 'DELETE'],
|
||||
)
|
||||
50
tofu_api/api/tasks/task_handler.py
Normal file
50
tofu_api/api/tasks/task_handler.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from tofu_api.models import Task
|
||||
from tofu_api.repositories import TaskRepository
|
||||
from .validators import TaskCreateData, TaskUpdateData
|
||||
|
||||
|
||||
class TaskHandler:
|
||||
"""
|
||||
Handles operations on tasks.
|
||||
"""
|
||||
|
||||
task_repository: TaskRepository
|
||||
|
||||
def __init__(self, *, task_repository: TaskRepository):
|
||||
self.task_repository = task_repository
|
||||
|
||||
def fetch_task(self, task_id: int) -> Task:
|
||||
"""
|
||||
Fetches a single task by its ID from the database.
|
||||
Raises an ObjectNotFoundError if the task was not found.
|
||||
"""
|
||||
return self.task_repository.fetch_by_id(task_id)
|
||||
|
||||
def fetch_all_tasks(self) -> list[Task]:
|
||||
"""
|
||||
Fetches a list of all tasks.
|
||||
"""
|
||||
return self.task_repository.fetch_all()
|
||||
|
||||
def create_task(self, create_data: TaskCreateData) -> Task:
|
||||
"""
|
||||
Creates a new task, saves it to the database and returns it.
|
||||
"""
|
||||
task = Task()
|
||||
task.update_from(create_data)
|
||||
self.task_repository.save_resource(task)
|
||||
return task
|
||||
|
||||
def update_task(self, task: Task, update_data: TaskUpdateData) -> Task:
|
||||
"""
|
||||
Updates a Task object with new data.
|
||||
"""
|
||||
task.update_from(update_data)
|
||||
self.task_repository.save_resource(task)
|
||||
return task
|
||||
|
||||
def delete_task(self, task: Task) -> None:
|
||||
"""
|
||||
Deletes a task from the database.
|
||||
"""
|
||||
self.task_repository.delete_resource(task)
|
||||
89
tofu_api/api/tasks/task_views.py
Normal file
89
tofu_api/api/tasks/task_views.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
from flask import jsonify
|
||||
from validataclass.validators import DataclassValidator
|
||||
|
||||
from tofu_api.common.rest import BaseMethodView
|
||||
from .task_handler import TaskHandler
|
||||
from .validators import TaskCreateData, TaskUpdateData
|
||||
|
||||
|
||||
class TaskBaseView(BaseMethodView):
|
||||
"""
|
||||
Base class for view classes for the `/tasks` endpoint.
|
||||
"""
|
||||
|
||||
task_handler: TaskHandler
|
||||
|
||||
def __init__(self, *, task_handler: TaskHandler):
|
||||
self.task_handler = task_handler
|
||||
|
||||
|
||||
class TaskCollectionView(TaskBaseView):
|
||||
"""
|
||||
View class for `/api/tasks` endpoint.
|
||||
"""
|
||||
|
||||
# Validators
|
||||
task_create_validator = DataclassValidator(TaskCreateData)
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Get list of all tasks.
|
||||
"""
|
||||
task_list = self.task_handler.fetch_all_tasks()
|
||||
return jsonify({
|
||||
'items': [task.to_dict() for task in task_list],
|
||||
'total_count': len(task_list),
|
||||
}), 200
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
Create a new task.
|
||||
"""
|
||||
# Parse request data
|
||||
create_data: TaskCreateData = self.validate_request_data(self.task_create_validator)
|
||||
|
||||
# Create new task
|
||||
new_task = self.task_handler.create_task(create_data)
|
||||
|
||||
# Return new task as JSON
|
||||
return jsonify(new_task.to_dict()), 201
|
||||
|
||||
|
||||
class TaskItemView(TaskBaseView):
|
||||
"""
|
||||
View class for `/api/tasks/<int:task_id>` endpoint.
|
||||
"""
|
||||
|
||||
# Validators
|
||||
task_update_validator = DataclassValidator(TaskUpdateData)
|
||||
|
||||
def get(self, task_id: int):
|
||||
"""
|
||||
Get a single task by ID.
|
||||
"""
|
||||
task = self.task_handler.fetch_task(task_id)
|
||||
return jsonify(task.to_dict()), 200
|
||||
|
||||
def patch(self, task_id: int):
|
||||
"""
|
||||
Update a single task by ID.
|
||||
"""
|
||||
# Parse request data
|
||||
update_data: TaskUpdateData = self.validate_request_data(self.task_update_validator)
|
||||
|
||||
# Fetch task and update
|
||||
task = self.task_handler.fetch_task(task_id)
|
||||
task = self.task_handler.update_task(task, update_data)
|
||||
|
||||
# Return updated task as JSON
|
||||
return jsonify(task.to_dict()), 200
|
||||
|
||||
def delete(self, task_id: int):
|
||||
"""
|
||||
Delete a single task by ID.
|
||||
"""
|
||||
# Fetch task and delete
|
||||
task = self.task_handler.fetch_task(task_id)
|
||||
self.task_handler.delete_task(task)
|
||||
|
||||
return self.empty_response()
|
||||
23
tofu_api/api/tasks/validators.py
Normal file
23
tofu_api/api/tasks/validators.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from validataclass.dataclasses import Default, DefaultUnset, ValidataclassMixin, validataclass
|
||||
from validataclass.helpers import OptionalUnset
|
||||
from validataclass.validators import StringValidator
|
||||
|
||||
|
||||
@validataclass
|
||||
class TaskCreateData(ValidataclassMixin):
|
||||
"""
|
||||
Dataclass for "create task" request data.
|
||||
"""
|
||||
|
||||
title: str = StringValidator(min_length=1, max_length=200)
|
||||
description: str = StringValidator(max_length=2000), Default('')
|
||||
|
||||
|
||||
@validataclass
|
||||
class TaskUpdateData(TaskCreateData):
|
||||
"""
|
||||
Dataclass for "update task" request data.
|
||||
"""
|
||||
|
||||
title: OptionalUnset[str] = DefaultUnset
|
||||
description: OptionalUnset[str] = DefaultUnset
|
||||
Loading…
Add table
Add a link
Reference in a new issue