Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement materials database #20

Merged
merged 5 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 47 additions & 15 deletions knowledge_verificator/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from fastapi import FastAPI, Response

from knowledge_verificator.materials import Material, MaterialDatabase
from knowledge_verificator.io_handler import config
from knowledge_verificator.io_handler import get_config

endpoints = FastAPI()
material_db = MaterialDatabase(materials_dir=config.learning_materials)
ENDPOINTS = FastAPI()
MATERIAL_DB = MaterialDatabase(materials_dir=get_config().learning_materials)


def format_response(data: Any = '', message: str = '') -> dict:
Expand Down Expand Up @@ -38,7 +38,7 @@ def format_response(data: Any = '', message: str = '') -> dict:
}


@endpoints.get('/materials')
@ENDPOINTS.get('/materials')
def get_materials(
response: Response, criteria: Union[str, None] = None
) -> dict:
Expand All @@ -58,10 +58,10 @@ def get_materials(
response.status_code = 501
return format_response(message=message)
response.status_code = 200
return format_response(data=material_db.materials)
return format_response(data=MATERIAL_DB.materials)


@endpoints.get('/materials/{material_id}')
@ENDPOINTS.get('/materials/{material_id}')
def get_material(material_id: str, response: Response):
"""
Get a specific learning material.
Expand All @@ -74,7 +74,7 @@ def get_material(material_id: str, response: Response):
dict: Under `data` key, there are `material_id` and `material` keys.
"""
try:
material = material_db[material_id]
material = MATERIAL_DB[material_id]
except KeyError:
message = f'Material with id = {material_id} was not found.'
response.status_code = 404
Expand All @@ -85,10 +85,10 @@ def get_material(material_id: str, response: Response):
return format_response(data=data)


@endpoints.post('/materials')
@ENDPOINTS.post('/materials')
def add_material(material: Material, response: Response) -> dict:
"""
Endpoint to add a learning material to a database.
Endpoint to add a learning material to the database.

Args:
material (Material): Learning material to be added.
Expand All @@ -101,7 +101,7 @@ def add_material(material: Material, response: Response) -> dict:
response.status_code = 200
message = ''
try:
material_db.add_material(material=material)
MATERIAL_DB.add_material(material=material)
except (ValueError, FileExistsError) as e:
message = str(e)
response.status_code = 400
Expand All @@ -110,13 +110,15 @@ def add_material(material: Material, response: Response) -> dict:
return format_response(message=message)

data = {'material_id': material.id}
return format_response(data=data)
return format_response(
data=data, message=f'Added the material with id = {material.id}.'
)


@endpoints.delete('/materials/{material_id}')
@ENDPOINTS.delete('/materials/{material_id}')
def delete_material(material_id: str, response: Response) -> dict:
"""
Endpoint to delete a learning material.
Endpoint to delete a learning material from the database.

Args:
material_id (str): ID of the material to be removed.
Expand All @@ -127,11 +129,41 @@ def delete_material(material_id: str, response: Response) -> dict:
of the removed material.
"""
try:
material_db.delete_material(material=material_id)
MATERIAL_DB.delete_material(material=material_id)
except KeyError as e:
message = str(e)
response.status_code = 400
return format_response(message=message)

response.status_code = 200
return format_response(data=str(material_id))
return format_response(
message=f'Deleted the material with id = {material_id}.'
)


@ENDPOINTS.put('/materials')
def update_material(material: Material, response: Response) -> dict:
"""
Endpoint to update multiple attributes of a learning material
in the database.

Args:
material_id (str): ID of the material to be removed.
response (Response): Response to a request. Automatically passed.

Returns:
dict: Under `data` key, there is `material_id` key containing ID
of the removed material.
"""
try:
MATERIAL_DB.update_material(material)
except KeyError as e:
message = str(e)
response.status_code = 404
return format_response(message=message)

response.status_code = 200
return format_response(
message=f'Updated the material with id = {material.id}.',
data=MATERIAL_DB[material.id],
)
3 changes: 2 additions & 1 deletion knowledge_verificator/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from rich.text import Text

from knowledge_verificator.io_handler import logger, console, config
from knowledge_verificator.io_handler import logger, console, get_config
from knowledge_verificator.answer_chooser import AnswerChooser
from knowledge_verificator.materials import MaterialDatabase
from knowledge_verificator.nli import NaturalLanguageInference, Relation
Expand Down Expand Up @@ -45,6 +45,7 @@ def run_cli_mode():
qg_module = QuestionGeneration()
ac_module = AnswerChooser()
nli_module = NaturalLanguageInference()
config = get_config()

while True:
options = ['knowledge database', 'my own paragraph']
Expand Down
37 changes: 26 additions & 11 deletions knowledge_verificator/io_handler.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
"""
Module handling Input/Output, including parsing CLI arguments, provding
Module handling Input/Output, including parsing CLI arguments, providing
an instance of `rich` console, and an instance of a preconfigured `Logger`.
"""

from argparse import ArgumentParser
from functools import cache
from logging import Logger
from pathlib import Path
from rich.console import Console
from rich.logging import RichHandler

from knowledge_verificator.utils.configuration_parser import ConfigurationParser
from knowledge_verificator.utils.configuration_parser import (
Configuration,
ConfigurationParser,
)


def get_argument_parser() -> ArgumentParser:
Expand Down Expand Up @@ -39,16 +43,27 @@ def get_argument_parser() -> ArgumentParser:
return arg_parser


console = Console()
logger = Logger('main_logger')
@cache
def get_config() -> Configuration:
"""
Get configuration of the system.

_logging_handler = RichHandler(rich_tracebacks=True)
Returns:
Configuration: Instance of configuration.
"""
_parser = get_argument_parser()
args = _parser.parse_args()

_parser = get_argument_parser()
args = _parser.parse_args()
_configuration_parser = ConfigurationParser(configuration_file=args.config)
config = _configuration_parser.parse_configuration()

_configuratioParser = ConfigurationParser(configuration_file=args.config)
config = _configuratioParser.parse_configuration()
_logging_handler.setLevel(config.logging_level)
logger.addHandler(_logging_handler)

_logging_handler.setLevel(config.logging_level)
logger.addHandler(_logging_handler)
return config


console = Console()
logger = Logger('main_logger')

_logging_handler = RichHandler(rich_tracebacks=True)
6 changes: 3 additions & 3 deletions knowledge_verificator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from pathlib import Path
import sys

from knowledge_verificator.io_handler import config
from knowledge_verificator.io_handler import get_config
from knowledge_verificator.utils.configuration_parser import OperatingMode
from knowledge_verificator.command_line import run_cli_mode
from knowledge_verificator.backend import endpoints
from tests.model.runner import ExperimentRunner

if __name__ == '__main__':
config = get_config()
match config.mode:
case OperatingMode.EXPERIMENT:
experiment_directory = Path(config.experiment_implementation)
Expand All @@ -24,7 +24,7 @@
import uvicorn

uvicorn.run(
endpoints,
'knowledge_verificator.backend:ENDPOINTS',
host='127.0.0.1',
port=8000,
reload=(not config.production_mode),
Expand Down
93 changes: 84 additions & 9 deletions knowledge_verificator/materials.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Module with tools for managing learning material."""

from dataclasses import dataclass
from dataclasses import dataclass, field
import hashlib
import os
from pathlib import Path
import uuid

from knowledge_verificator.utils.filesystem import in_directory

Expand All @@ -14,11 +14,17 @@ class Material:
Data class representing a learning material loaded from a database.
"""

path: Path
title: str
paragraphs: list[str]
tags: list[str]
id: str = str(uuid.uuid4())
tags: list[str] = field(default_factory=list)
path: Path | None = None
id: str = ''

def __eq__(self, value: object) -> bool:
if not isinstance(value, (Material)):
return False

return value.id == self.id


class MaterialDatabase:
Expand Down Expand Up @@ -79,18 +85,22 @@ def load_material(self, path: Path) -> Material:
content = ''.join(fd.readlines()).rstrip()
paragraphs = content.split('\n\n')

return Material(
material = Material(
path=path.resolve(),
title=title,
paragraphs=paragraphs,
tags=tags,
)

self._set_id(material)
return material

def delete_material(self, material: Material | str) -> None:
"""
Remove the first material matching the provided material with its `id`.

As `id` is actually universally unique identifier it should remove one item
As `id` is actually universally unique identifier,
it should remove one item.


Args:
Expand All @@ -112,10 +122,22 @@ def delete_material(self, material: Material | str) -> None:
index = self.materials.index(material)
del self.materials[index]

def _title_to_path(self, title: str) -> Path:
title = title.replace(' ', '_')
title = title.replace('"', '')
title = title.replace("'", '')
return self.materials_dir.joinpath(title)

def _set_id(self, material: Material) -> None:
content = '\n\n'.join(material.paragraphs)
material.id = hashlib.sha256(
(material.title + content).encode(encoding='utf-8')
).hexdigest()

def add_material(self, material: Material) -> None:
"""
Add a learning material to a database, also material's its
representation in a file.
Add a learning material to a database, also create material's
representation in a file.

Args:
material (Material): Initialised learning material without
Expand All @@ -131,11 +153,18 @@ def add_material(self, material: Material) -> None:
"""
if not material.title:
raise ValueError('Title of a learning material cannot be empty.')

self._set_id(material)

if material.path is None:
material.path = self._title_to_path(material.title)

if material.path.exists():
raise FileExistsError(
'A file in the provided path already exists. '
'Choose a different filename.'
)

if not in_directory(file=material.path, directory=self.materials_dir):
raise ValueError(
f'A file {os.path.basename(material.path)}'
Expand Down Expand Up @@ -167,6 +196,52 @@ def _format_file_content(self, material: Material) -> str:
return output

def _create_file_with_material(self, material: Material) -> None:
if material.path is None:
raise ValueError(
f'Cannot create a material without a valid path. Current path: `{material.path}`.'
)
with open(material.path, 'wt', encoding='utf-8') as fd:
file_content = self._format_file_content(material=material)
fd.write(file_content)

def update_material(
self, material: Material, ignore_empty: bool = True
) -> None:
"""
Update an existing learning material.


Args:
material (Material): Instance of learning material from the database.
ignore_empty (bool, optional): Do not update attribute with the
value equal to `None`. Defaults to True.

Raises:
KeyError: Raised if the learning material is not
present in the database.
"""
if material not in self.materials:
raise KeyError(
f'Cannot update non-existent material: {str(material)}.'
)

index = self.materials.index(material)
old_path = self.materials[index].path

for _, attribute in material.__dataclass_fields__.items():
field_name = attribute.name
value = getattr(material, field_name)

if value is None and ignore_empty:
continue

index = self.materials.index(material)
original_material = self.materials[index]

setattr(original_material, field_name, value)

# If path is missing (not provided), use the old path.
if not material.path:
material.path = old_path
# Override a file with old material with the updated one.
self._create_file_with_material(material)
1 change: 0 additions & 1 deletion knowledge_verificator/utils/configuration_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ def __init__(
self.__setattr__(attribute, value)

# Convert to a proper datatypes.
self.learning_materials: Path = Path(kwargs['learning_materials'])
self.mode: OperatingMode = OperatingMode(kwargs['mode'])
self.experiment_implementation = Path(
kwargs['experiment_implementation']
Expand Down
Loading
Loading