diff --git a/README.md b/README.md index 23fbc4c..bea184d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ The application allows you to automate the process of generating a bibliography Supported citation styles: - ГОСТ Р 7.0.5-2008 +- American Psychological Association ## Installation diff --git a/src/formatters/models.py b/src/formatters/models.py index c9236ca..46c75f0 100644 --- a/src/formatters/models.py +++ b/src/formatters/models.py @@ -54,7 +54,6 @@ class InternetResourceModel(BaseModel): class ArticlesCollectionModel(BaseModel): - """ Модель сборника статей: @@ -78,3 +77,85 @@ class ArticlesCollectionModel(BaseModel): publishing_house: str year: int = Field(..., gt=0) pages: str + + +class AbstractModel(BaseModel): + """ + Модель автореферата: + + .. code-block:: + + ArticlesCollectionModel( + author="Иванов И.М.", + abstract_title="Наука как искусство", + author_status="д-р. / канд.", + science_field="экон.", + specialty_code="01.01.01", + city="Спб.", + year=2020, + pages="199", + ) + """ + + author: str + abstract_title: str + author_status: str + science_field: str + specialty_code: str + city: str + year: int = Field(..., gt=0) + pages: str + + +class RegulationModel(BaseModel): + """ + Модель нормативного акта: + + .. code-block:: + + ArticlesCollectionModel( + regulation_title="Наука как искусство", + source="Конституция Российской Федерации", + publishing_source="Парламентская газета", + regulation_id="1234-56", + acceptance_date="1/1/2000", + publishing_year="2020", + publishing_source_id="5", + publishing_article_id="15", + modification_date="9/11/2002" + ) + """ + + regulation_title: str + source: str + publishing_source: str + regulation_id: str + acceptance_date: str + publishing_year: int = Field(..., gt=0) + publishing_source_id: str + publishing_article_id: str + modification_date: str + + +class NewsPaperModel(BaseModel): + """ + Модель газеты: + + .. code-block:: + + NewsPaperModel( + article_title="Наука как искусство", + authors="Иванов И.М., Петров С.Н.", + news_title="Южный Урал", + publishing_year="1980", + publishing_date="01.10", + publishing_number="5" + ) + """ + + article_title: str + authors: str + news_title: str + publishing_year: int = Field(..., gt=0) + publishing_date: str + publishing_number: int = Field(..., gt=0) diff --git a/src/formatters/styles/apa.py b/src/formatters/styles/apa.py new file mode 100644 index 0000000..a6aa604 --- /dev/null +++ b/src/formatters/styles/apa.py @@ -0,0 +1,115 @@ +""" +Стиль цитирования по American Psychological Association +""" +from string import Template + +from pydantic import BaseModel + +from formatters.models import BookModel, InternetResourceModel, NewsPaperModel +from formatters.styles.base import BaseCitationStyle +from logger import get_logger + +logger = get_logger(__name__) + + +class APABook(BaseCitationStyle): + """ + Форматирование для книг. + """ + + data: BookModel + + @property + def template(self) -> Template: + return Template("$authors ($year). $title. $publishing_house.") + + def substitute(self) -> str: + logger.info('Форматирование книги "%s" ...', self.data.title) + + return self.template.substitute( + authors=self.data.authors, + title=self.data.title, + publishing_house=self.data.publishing_house, + year=self.data.year, + ) + + +class APAInternetResource(BaseCitationStyle): + """ + Форматирование для интернет-ресурсов. + """ + + data: InternetResourceModel + + @property + def template(self) -> Template: + return Template("$article ($access_date) $website $link") + + def substitute(self) -> str: + logger.info('Форматирование интернет-ресурса "%s" ...', self.data.article) + + return self.template.substitute( + article=self.data.article, + website=self.data.website, + link=self.data.link, + access_date=self.data.access_date, + ) + + +class APANewsPaperResource(BaseCitationStyle): + """ + Форматирование для газеты. + """ + + data: NewsPaperModel + + @property + def template(self) -> Template: + return Template( + "$authors ($publishing_year, $publishing_date). $article_title. $news_title." + ) + + def substitute(self) -> str: + logger.info('Форматирование газеты "%s" ...', self.data.article_title) + + return self.template.substitute( + article_title=self.data.article_title, + authors=self.data.authors, + news_title=self.data.news_title, + publishing_year=self.data.publishing_year, + publishing_date=self.data.publishing_date, + ) + + +class APACitationFormatter: + """ + Базовый класс для итогового форматирования списка источников. + """ + + formatters_map = { + BookModel.__name__: APABook, + InternetResourceModel.__name__: APAInternetResource, + NewsPaperModel.__name__: APANewsPaperResource, + } + + def __init__(self, models: list[BaseModel]) -> None: + """ + Конструктор. + :param models: Список объектов для форматирования + """ + + formatted_items = [] + for model in models: + formatter = self.formatters_map.get(type(model).__name__) + if formatter is not None: + formatted_items.append(formatter(model)) # type: ignore + + self.formatted_items = formatted_items + + def format(self) -> list[BaseCitationStyle]: + """ + Форматирование списка источников. + :return: + """ + + return sorted(self.formatted_items, key=lambda item: item.formatted) diff --git a/src/formatters/styles/gost.py b/src/formatters/styles/gost.py index b237f8a..b21b8e5 100644 --- a/src/formatters/styles/gost.py +++ b/src/formatters/styles/gost.py @@ -5,11 +5,16 @@ from pydantic import BaseModel -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AbstractModel, + RegulationModel, +) from formatters.styles.base import BaseCitationStyle from logger import get_logger - logger = get_logger(__name__) @@ -27,7 +32,6 @@ def template(self) -> Template: ) def substitute(self) -> str: - logger.info('Форматирование книги "%s" ...', self.data.title) return self.template.substitute( @@ -64,7 +68,6 @@ def template(self) -> Template: ) def substitute(self) -> str: - logger.info('Форматирование интернет-ресурса "%s" ...', self.data.article) return self.template.substitute( @@ -89,7 +92,6 @@ def template(self) -> Template: ) def substitute(self) -> str: - logger.info('Форматирование сборника статей "%s" ...', self.data.article_title) return self.template.substitute( @@ -103,6 +105,68 @@ def substitute(self) -> str: ) +class GOSTAbtract(BaseCitationStyle): + """ + Форматирование для автореферата. + """ + + data: AbstractModel + + @property + def template(self) -> Template: + return Template( + "$author $abstract_title: автореф. дис. ... $author_status $science_field наук: $specialty_code. $city, " + "$year. $pages с." + ) + + def substitute(self) -> str: + logger.info('Форматирование автореферата "%s" ...', self.data.abstract_title) + + return self.template.substitute( + author=self.data.author, + abstract_title=self.data.abstract_title, + author_status=self.data.author_status, + science_field=self.data.science_field, + specialty_code=self.data.specialty_code, + city=self.data.city, + year=self.data.year, + pages=self.data.pages, + ) + + +class GOSTRegulation(BaseCitationStyle): + """ + Форматирование для нормативного акта. + """ + + data: RegulationModel + + @property + def template(self) -> Template: + return Template( + '$source "$regulation_title" от $acceptance_date № $regulation_id // $publishing_source. ' + "$publishing_year г. № $publishing_source_id. Ст. $publishing_article_id с изм. и допол. в ред. от " + "$modification_date" + ) + + def substitute(self) -> str: + logger.info( + 'Форматирование нормативного акта "%s" ...', self.data.regulation_title + ) + + return self.template.substitute( + regulation_title=self.data.regulation_title, + source=self.data.source, + publishing_source=self.data.publishing_source, + regulation_id=self.data.regulation_id, + acceptance_date=self.data.acceptance_date, + publishing_year=self.data.publishing_year, + publishing_source_id=self.data.publishing_source_id, + publishing_article_id=self.data.publishing_article_id, + modification_date=self.data.modification_date, + ) + + class GOSTCitationFormatter: """ Базовый класс для итогового форматирования списка источников. @@ -112,6 +176,8 @@ class GOSTCitationFormatter: BookModel.__name__: GOSTBook, InternetResourceModel.__name__: GOSTInternetResource, ArticlesCollectionModel.__name__: GOSTCollectionArticle, + AbstractModel.__name__: GOSTAbtract, + RegulationModel.__name__: GOSTRegulation, } def __init__(self, models: list[BaseModel]) -> None: @@ -123,7 +189,9 @@ def __init__(self, models: list[BaseModel]) -> None: formatted_items = [] for model in models: - formatted_items.append(self.formatters_map.get(type(model).__name__)(model)) # type: ignore + formatter = self.formatters_map.get(type(model).__name__) + if formatter is not None: + formatted_items.append(formatter(model)) # type: ignore self.formatted_items = formatted_items diff --git a/src/main.py b/src/main.py old mode 100644 new mode 100755 index 7a9fa8e..f1242bb --- a/src/main.py +++ b/src/main.py @@ -5,6 +5,7 @@ import click +from formatters.styles.apa import APACitationFormatter from formatters.styles.gost import GOSTCitationFormatter from logger import get_logger from readers.reader import SourcesReader @@ -25,6 +26,19 @@ class CitationEnum(Enum): APA = "apa" # American Psychological Association +style_map = { + CitationEnum.APA.name: APACitationFormatter, + CitationEnum.GOST.name: GOSTCitationFormatter, +} + + +def format_by_style(citation: str, models: list, path_output: str) -> None: + logger.info(style_map[citation]) + formatted_models = tuple(str(item) for item in style_map[citation](models).format()) + logger.info(f"Генерация выходного файла в формате ${citation}...") + Renderer(formatted_models).render(path_output) + + @click.command() @click.option( "--citation", @@ -77,12 +91,7 @@ def process_input( ) models = SourcesReader(path_input).read() - formatted_models = tuple( - str(item) for item in GOSTCitationFormatter(models).format() - ) - - logger.info("Генерация выходного файла ...") - Renderer(formatted_models).render(path_output) + format_by_style(citation, models, path_output) logger.info("Команда успешно завершена.") diff --git a/src/readers/reader.py b/src/readers/reader.py index 9007a80..820a514 100644 --- a/src/readers/reader.py +++ b/src/readers/reader.py @@ -7,11 +7,17 @@ import openpyxl from openpyxl.workbook import Workbook -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AbstractModel, + RegulationModel, + NewsPaperModel, +) from logger import get_logger from readers.base import BaseReader - logger = get_logger(__name__) @@ -90,6 +96,86 @@ def attributes(self) -> dict: } +class AbstractReader(BaseReader): + """ + Чтение модели автореферата. + """ + + @property + def model(self) -> Type[AbstractModel]: + return AbstractModel + + @property + def sheet(self) -> str: + return "Автореферат" + + @property + def attributes(self) -> dict: + return { + "author": {0: str}, + "abstract_title": {1: str}, + "author_status": {2: str}, + "science_field": {3: str}, + "specialty_code": {4: str}, + "city": {5: str}, + "year": {6: int}, + "pages": {7: str}, + } + + +class RegulationReader(BaseReader): + """ + Чтение модели нормативного акта. + """ + + @property + def model(self) -> Type[RegulationModel]: + return RegulationModel + + @property + def sheet(self) -> str: + return " Закон, нормативный акт и т.п." + + @property + def attributes(self) -> dict: + return { + "source": {0: str}, + "regulation_title": {1: str}, + "acceptance_date": {2: date}, + "regulation_id": {3: str}, + "publishing_source": {4: str}, + "publishing_year": {5: int}, + "publishing_source_id": {6: str}, + "publishing_article_id": {7: str}, + "modification_date": {8: date}, + } + + +class NewsPaperReader(BaseReader): + """ + Чтение модели газеты. + """ + + @property + def model(self) -> Type[NewsPaperModel]: + return NewsPaperModel + + @property + def sheet(self) -> str: + return "Статья из газеты" + + @property + def attributes(self) -> dict: + return { + "authors": {0: str}, + "article_title": {1: str}, + "news_title": {2: str}, + "publishing_year": {3: int}, + "publishing_date": {4: str}, + "publishing_number": {5: int}, + } + + class SourcesReader: """ Чтение из источника данных. @@ -100,6 +186,9 @@ class SourcesReader: BookReader, InternetResourceReader, ArticlesCollectionReader, + AbstractReader, + RegulationReader, + NewsPaperReader, ] def __init__(self, path: str) -> None: diff --git a/src/tests/conftest.py b/src/tests/conftest.py index ac5c9aa..7335b83 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -3,7 +3,14 @@ """ import pytest -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AbstractModel, + RegulationModel, + NewsPaperModel, +) @pytest.fixture @@ -58,3 +65,62 @@ def articles_collection_model_fixture() -> ArticlesCollectionModel: year=2020, pages="25-30", ) + + +@pytest.fixture +def abstract_model_fixture() -> AbstractModel: + """ + Фикстура модели автореферата. + + :return: AbstractModel + """ + + return AbstractModel( + author="Иванов И.М.", + abstract_title="Наука как искусство", + author_status="д-р. / канд.", + science_field="экон.", + specialty_code="01.01.01", + city="Спб.", + year=2020, + pages="199", + ) + + +@pytest.fixture +def regulation_model_fixture() -> RegulationModel: + """ + Фикстура модели нормативного акта. + + :return: RegulationModel + """ + + return RegulationModel( + regulation_title="Наука как искусство", + source="Конституция Российской Федерации", + publishing_source="Парламентская газета", + regulation_id="1234-56", + acceptance_date="1/1/2000", + publishing_year="2020", + publishing_source_id="5", + publishing_article_id="15", + modification_date="9/11/2002", + ) + + +@pytest.fixture +def newspaper_model_fixture() -> NewsPaperModel: + """ + Фикстура модели газеты. + + :return: NewsPaperModel + """ + + return NewsPaperModel( + article_title="Наука как искусство", + authors="Иванов И.М.", + news_title="Южный Урал", + publishing_year="2020", + publishing_date="01.10", + publishing_number="5", + ) diff --git a/src/tests/formatters/test_apa.py b/src/tests/formatters/test_apa.py new file mode 100644 index 0000000..e00516b --- /dev/null +++ b/src/tests/formatters/test_apa.py @@ -0,0 +1,91 @@ +""" +Тестирование функций оформления списка источников по APA. +""" + +from formatters.base import BaseCitationFormatter +from formatters.models import ( + BookModel, + InternetResourceModel, + NewsPaperModel, +) +from formatters.styles.apa import APABook, APAInternetResource, APANewsPaperResource + + +class TestAPA: + """ + Тестирование оформления списка источников согласно APA + """ + + def test_book(self, book_model_fixture: BookModel) -> None: + """ + Тестирование форматирования книги. + + :param BookModel book_model_fixture: Фикстура модели книги + :return: + """ + + model = APABook(book_model_fixture) + + assert ( + model.formatted + == "Иванов И.М., Петров С.Н. (2020). Наука как искусство. Просвещение." + ) + + def test_internet_resource( + self, internet_resource_model_fixture: InternetResourceModel + ) -> None: + """ + Тестирование форматирования интернет-ресурса. + + :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса + :return: + """ + + model = APAInternetResource(internet_resource_model_fixture) + + assert ( + model.formatted + == "Наука как искусство (01.01.2021) Ведомости https://www.vedomosti.ru" + ) + + def test_news_paper(self, newspaper_model_fixture: NewsPaperModel) -> None: + """ + Тестирование форматирования газеты. + + :param ArticlesCollectionModel newspaper_model_fixture: Фикстура модели газеты + :return: + """ + + model = APANewsPaperResource(newspaper_model_fixture) + + assert ( + model.formatted + == "Иванов И.М. (2020, 01.10). Наука как искусство. Южный Урал." + ) + + def test_citation_formatter( + self, + book_model_fixture: BookModel, + internet_resource_model_fixture: InternetResourceModel, + newspaper_model_fixture: NewsPaperModel, + ) -> None: + """ + Тестирование функции итогового форматирования списка источников. + + :param BookModel book_model_fixture: Фикстура модели книги + :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса + :param ArticlesCollectionModel newspaper_model_fixture: Фикстура модели газеты + :return: + """ + + models = [ + APABook(book_model_fixture), + APAInternetResource(internet_resource_model_fixture), + APANewsPaperResource(newspaper_model_fixture), + ] + result = BaseCitationFormatter(models).format() + + # тестирование сортировки списка источников + assert result[0] == models[2] + assert result[1] == models[0] + assert result[2] == models[1] diff --git a/src/tests/formatters/test_gost.py b/src/tests/formatters/test_gost.py index c93e1e7..befda4a 100644 --- a/src/tests/formatters/test_gost.py +++ b/src/tests/formatters/test_gost.py @@ -3,8 +3,20 @@ """ from formatters.base import BaseCitationFormatter -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel -from formatters.styles.gost import GOSTBook, GOSTInternetResource, GOSTCollectionArticle +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AbstractModel, + RegulationModel, +) +from formatters.styles.gost import ( + GOSTBook, + GOSTInternetResource, + GOSTCollectionArticle, + GOSTAbtract, + GOSTRegulation, +) class TestGOST: @@ -61,11 +73,45 @@ def test_articles_collection( == "Иванов И.М., Петров С.Н. Наука как искусство // Сборник научных трудов. – СПб.: АСТ, 2020. – С. 25-30." ) + def test_abstract(self, abstract_model_fixture: AbstractModel) -> None: + """ + Тестирование форматирования автореферата. + + :param AbstractModel abstract_model_fixture: Фикстура модели автореферата + :return: + """ + + model = GOSTAbtract(abstract_model_fixture) + + assert ( + model.formatted + == "Иванов И.М. Наука как искусство: автореф. дис. ... д-р. / канд. экон. наук: 01.01.01. Спб., " + "2020. 199 с." + ) + + def test_regulation(self, regulation_model_fixture: RegulationModel) -> None: + """ + Тестирование форматирования нормативного акта. + + :param RegulationModel regulation_model_fixture: Фикстура модели нормативного акта + :return: + """ + + model = GOSTRegulation(regulation_model_fixture) + + assert ( + model.formatted + == 'Конституция Российской Федерации "Наука как искусство" от 1/1/2000 № 1234-56 // Парламентская ' + "газета. 2020 г. № 5. Ст. 15 с изм. и допол. в ред. от 9/11/2002" + ) + def test_citation_formatter( self, book_model_fixture: BookModel, internet_resource_model_fixture: InternetResourceModel, articles_collection_model_fixture: ArticlesCollectionModel, + abstract_model_fixture: AbstractModel, + regulation_model_fixture: RegulationModel, ) -> None: """ Тестирование функции итогового форматирования списка источников. @@ -73,6 +119,8 @@ def test_citation_formatter( :param BookModel book_model_fixture: Фикстура модели книги :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса :param ArticlesCollectionModel articles_collection_model_fixture: Фикстура модели сборника статей + :param AbstractModel abstract_model_fixture: Фикстура модели автореферата + :param RegulationModel regulation_model_fixture: Фикстура модели нормативного акта :return: """ @@ -80,10 +128,14 @@ def test_citation_formatter( GOSTBook(book_model_fixture), GOSTInternetResource(internet_resource_model_fixture), GOSTCollectionArticle(articles_collection_model_fixture), + GOSTAbtract(abstract_model_fixture), + GOSTRegulation(regulation_model_fixture), ] result = BaseCitationFormatter(models).format() # тестирование сортировки списка источников - assert result[0] == models[2] - assert result[1] == models[0] - assert result[2] == models[1] + assert result[0] == models[3] + assert result[1] == models[2] + assert result[2] == models[0] + assert result[3] == models[4] + assert result[4] == models[1] diff --git a/src/tests/readers/test_readers.py b/src/tests/readers/test_readers.py index 67d863b..de99c35 100644 --- a/src/tests/readers/test_readers.py +++ b/src/tests/readers/test_readers.py @@ -5,12 +5,22 @@ import pytest -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AbstractModel, + RegulationModel, + NewsPaperModel, +) from readers.reader import ( BookReader, SourcesReader, InternetResourceReader, ArticlesCollectionReader, + AbstractReader, + RegulationReader, + NewsPaperReader, ) from settings import TEMPLATE_FILE_PATH @@ -104,6 +114,86 @@ def test_articles_collection(self, workbook: Any) -> None: # проверка общего количества атрибутов assert len(model_type.schema().get("properties", {}).keys()) == 7 + def test_abstract(self, workbook: Any) -> None: + """ + Тестирование чтения автореферата. + + :param workbook: Объект тестовой рабочей книги. + """ + + models = AbstractReader(workbook).read() + + assert len(models) == 1 + model = models[0] + + model_type = AbstractModel + + assert isinstance(model, model_type) + assert model.author == "Иванов И.М." + assert model.abstract_title == "Наука как искусство" + assert model.author_status == "д-р. / канд." + assert model.science_field == "экон." + assert model.specialty_code == "01.01.01" + assert model.city == "СПб." + assert model.year == 2020 + assert model.pages == "199" + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 8 + + def test_regulation(self, workbook: Any) -> None: + """ + Тестирование чтения нормативного акта. + + :param workbook: Объект тестовой рабочей книги. + """ + + models = RegulationReader(workbook).read() + + assert len(models) == 1 + model = models[0] + + model_type = RegulationModel + + assert isinstance(model, model_type) + assert model.regulation_title == "Наука как искусство" + assert model.source == "Конституция Российской Федерации" + assert model.publishing_source == "Парламентская газета" + assert model.regulation_id == "1234-56" + assert model.acceptance_date == "01.01.2000" + assert model.publishing_year == 2020 + assert model.publishing_source_id == "5" + assert model.publishing_article_id == "15" + assert model.modification_date == "11.09.2002" + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 9 + + def test_newspaper(self, workbook: Any) -> None: + """ + Тестирование чтения газеты. + + :param workbook: Объект тестовой рабочей книги. + """ + + models = NewsPaperReader(workbook).read() + + assert len(models) == 1 + model = models[0] + + model_type = NewsPaperModel + + assert isinstance(model, model_type) + assert model.article_title == "Наука как искусство" + assert model.authors == "Иванов И.М., Петров С.Н." + assert model.news_title == "Южный Урал" + assert model.publishing_year == 1980 + assert model.publishing_date == "01.10" + assert model.publishing_number == 5 + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 6 + def test_sources_reader(self) -> None: """ Тестирование функции чтения всех моделей из источника. @@ -111,7 +201,7 @@ def test_sources_reader(self) -> None: models = SourcesReader(TEMPLATE_FILE_PATH).read() # проверка общего считанного количества моделей - assert len(models) == 8 + assert len(models) == 11 # проверка наличия всех ожидаемых типов моделей среди типов считанных моделей model_types = {model.__class__.__name__ for model in models} @@ -119,4 +209,7 @@ def test_sources_reader(self) -> None: BookModel.__name__, InternetResourceModel.__name__, ArticlesCollectionModel.__name__, + AbstractModel.__name__, + RegulationModel.__name__, + NewsPaperModel.__name__, }