diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 58d128985..7d22838b1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,10 +28,16 @@ jobs: - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + + - name: Install Rust + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source $HOME/.cargo/env - name: Install id: install run: | + pip install --upgrade pip pip install poetry poetry install --no-root @@ -85,4 +91,4 @@ jobs: push: true tags: | ebl.badw.de/ebl-api:master - ${{format('ebl.badw.de/ebl-api:master.{0}', github.run_number)}} + ${{format('ebl.badw.de/ebl-api:master.{0}', github.run_number)}} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index da651d466..9c6d2cb9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM pypy:3.9-7.3.10 +RUN pip install --upgrade pip +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" RUN pip install poetry EXPOSE 8000 @@ -16,3 +19,4 @@ COPY ./docs ./docs RUN chmod -R a-wx ./docs CMD ["poetry", "run", "waitress-serve", "--port=8000", "--connection-limit=500", "--call", "ebl.app:get_app"] + diff --git a/ebl/app.py b/ebl/app.py index d90037892..849d9cc08 100644 --- a/ebl/app.py +++ b/ebl/app.py @@ -43,6 +43,7 @@ from ebl.signs.infrastructure.mongo_sign_repository import MongoSignRepository from ebl.signs.web.bootstrap import create_signs_routes from ebl.afo_register.web.bootstrap import create_afo_register_routes +from ebl.dossiers.web.bootstrap import create_dossiers_routes from ebl.transliteration.application.parallel_line_injector import ParallelLineInjector from ebl.transliteration.infrastructure.mongo_parallel_repository import ( MongoParallelRepository, @@ -50,6 +51,9 @@ from ebl.afo_register.infrastructure.mongo_afo_register_repository import ( MongoAfoRegisterRepository, ) +from ebl.dossiers.infrastructure.mongo_dossiers_repository import ( + MongoDossiersRepository, +) from ebl.users.domain.user import Guest from ebl.users.infrastructure.auth0 import Auth0Backend from ebl.fragmentarium.infrastructure.mongo_findspot_repository import ( @@ -99,6 +103,7 @@ def create_context(): annotations_repository=MongoAnnotationsRepository(database), lemma_repository=MongoLemmaRepository(database), afo_register_repository=MongoAfoRegisterRepository(database), + dossiers_repository=MongoDossiersRepository(database), findspot_repository=MongoFindspotRepository(database), custom_cache=custom_cache, cache=cache, @@ -128,6 +133,7 @@ def create_app(context: Context, issuer: str = "", audience: str = ""): create_lemmatization_routes(api, context) create_markup_routes(api, context) create_afo_register_routes(api, context) + create_dossiers_routes(api, context) return api diff --git a/ebl/chronology/chronology.py b/ebl/chronology/chronology.py index b685af73c..1b2225033 100644 --- a/ebl/chronology/chronology.py +++ b/ebl/chronology/chronology.py @@ -6,7 +6,7 @@ @attr.s(auto_attribs=True, frozen=True) class King: - order_global: int + order_global: float group_with: int dynasty_number: str dynasty_name: str @@ -30,7 +30,7 @@ def find_king_by_name(self, king_name: str) -> Optional[King]: class KingSchema(Schema): - order_global = fields.Integer(data_key="orderGlobal") + order_global = fields.Float(data_key="orderGlobal") group_with = fields.Integer( data_key="groupWith", allow_none=True, load_default=None ) diff --git a/ebl/context.py b/ebl/context.py index ab8681eec..c43cc9df8 100644 --- a/ebl/context.py +++ b/ebl/context.py @@ -29,6 +29,7 @@ from ebl.fragmentarium.infrastructure.mongo_findspot_repository import ( MongoFindspotRepository, ) +from ebl.dossiers.application.dossiers_repository import DossiersRepository @attr.s(auto_attribs=True, frozen=True) @@ -53,6 +54,7 @@ class Context: cache: Cache parallel_line_injector: ParallelLineInjector afo_register_repository: AfoRegisterRepository + dossiers_repository: DossiersRepository def get_bibliography(self): return Bibliography(self.bibliography_repository, self.changelog) diff --git a/ebl/dossiers/application/dossiers_repository.py b/ebl/dossiers/application/dossiers_repository.py new file mode 100644 index 000000000..61e431429 --- /dev/null +++ b/ebl/dossiers/application/dossiers_repository.py @@ -0,0 +1,14 @@ +from typing import Sequence +from abc import ABC, abstractmethod + +from ebl.dossiers.domain.dossier_record import ( + DossierRecord, +) + + +class DossiersRepository(ABC): + @abstractmethod + def query_by_ids(self, ids: Sequence[str]) -> Sequence[DossierRecord]: ... + + @abstractmethod + def create(self, dossier_record: DossierRecord) -> str: ... diff --git a/ebl/dossiers/domain/dossier_record.py b/ebl/dossiers/domain/dossier_record.py new file mode 100644 index 000000000..f93943937 --- /dev/null +++ b/ebl/dossiers/domain/dossier_record.py @@ -0,0 +1,19 @@ +import attr +from typing import Sequence, Optional + +from ebl.common.domain.provenance import Provenance +from ebl.fragmentarium.domain.fragment import Script +from ebl.bibliography.domain.reference import ReferenceType + + +@attr.s(frozen=True, auto_attribs=True) +class DossierRecord: + id: str + description: Optional[str] = None + is_approximate_date: bool = False + year_range_from: Optional[int] = None + year_range_to: Optional[int] = None + related_kings: Sequence[float] = [] + provenance: Optional[Provenance] = None + script: Optional[Script] = None + references: Sequence[ReferenceType] = [] diff --git a/ebl/dossiers/infrastructure/mongo_dossiers_repository.py b/ebl/dossiers/infrastructure/mongo_dossiers_repository.py new file mode 100644 index 000000000..724d50b2b --- /dev/null +++ b/ebl/dossiers/infrastructure/mongo_dossiers_repository.py @@ -0,0 +1,59 @@ +from typing import Sequence +from marshmallow import Schema, fields, post_load, EXCLUDE +from pymongo.database import Database +from ebl.mongo_collection import MongoCollection +from ebl.dossiers.domain.dossier_record import ( + DossierRecord, +) +from ebl.dossiers.application.dossiers_repository import DossiersRepository +from ebl.common.domain.provenance import Provenance +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema +from ebl.schemas import NameEnumField +from ebl.bibliography.domain.reference import ReferenceType + +COLLECTION = "dossier" + +provenance_field = fields.Function( + lambda object_: getattr(object_.provenance, "long_name", None), + lambda value: Provenance.from_name(value) if value else None, + allow_none=True, +) + + +class DossierRecordSchema(Schema): + class Meta: + unknown = EXCLUDE + + id = fields.String(required=True, unique=True, data_key="_id") + description = fields.String(load_default=None) + is_approximate_date = fields.Boolean( + data_key="isApproximateDate", load_default=False + ) + year_range_from = fields.Integer( + data_key="yearRangeFrom", allow_none=True, load_default=None + ) + year_range_to = fields.Integer( + data_key="yearRangeTo", allow_none=True, load_default=None + ) + related_kings = fields.List( + fields.Float(), data_key="relatedKings", load_default=list + ) + provenance = provenance_field + script = fields.Nested(ScriptSchema, allow_none=True, load_default=None) + references = fields.List(NameEnumField(ReferenceType), load_default=list) + + @post_load + def make_record(self, data, **kwargs): + return DossierRecord(**data) + + +class MongoDossiersRepository(DossiersRepository): + def __init__(self, database: Database): + self._collection = MongoCollection(database, COLLECTION) + + def query_by_ids(self, ids: Sequence[str]) -> Sequence[DossierRecord]: + cursor = self._collection.find_many({"_id": {"$in": ids}}) + return DossierRecordSchema(many=True).load(cursor) + + def create(self, dossier_record: DossierRecord) -> str: + return self._collection.insert_one(DossierRecordSchema().dump(dossier_record)) diff --git a/ebl/dossiers/web/bootstrap.py b/ebl/dossiers/web/bootstrap.py new file mode 100644 index 000000000..5a4133168 --- /dev/null +++ b/ebl/dossiers/web/bootstrap.py @@ -0,0 +1,11 @@ +import falcon +from ebl.context import Context + +from ebl.dossiers.web.dossier_records import ( + DossiersResource, +) + + +def create_dossiers_routes(api: falcon.App, context: Context): + dossier_resourse = DossiersResource(context.dossiers_repository) + api.add_route("/dossiers", dossier_resourse) diff --git a/ebl/dossiers/web/dossier_records.py b/ebl/dossiers/web/dossier_records.py new file mode 100644 index 000000000..6bf3fe0c5 --- /dev/null +++ b/ebl/dossiers/web/dossier_records.py @@ -0,0 +1,24 @@ +from falcon import Request, Response +from ebl.errors import NotFoundError +from marshmallow import EXCLUDE + +from ebl.dossiers.application.dossiers_repository import DossiersRepository +from ebl.dossiers.infrastructure.mongo_dossiers_repository import ( + DossierRecordSchema, +) + + +class DossiersResource: + def __init__(self, _dossiersRepository: DossiersRepository): + self._dossiersRepository = _dossiersRepository + + def on_get(self, req: Request, resp: Response) -> None: + try: + dossiers = self._dossiersRepository.query_by_ids( + req.params["ids"].split(",") + ) + except ValueError as error: + raise NotFoundError( + f"No dossier records matching {str(req.params)} found." + ) from error + resp.media = DossierRecordSchema(unknown=EXCLUDE, many=True).dump(dossiers) diff --git a/ebl/fragmentarium/application/annotations_schema.py b/ebl/fragmentarium/application/annotations_schema.py index 3fb29ca24..32f670bd5 100644 --- a/ebl/fragmentarium/application/annotations_schema.py +++ b/ebl/fragmentarium/application/annotations_schema.py @@ -1,7 +1,7 @@ from marshmallow import Schema, fields, post_load, post_dump import pydash from ebl.fragmentarium.application.cropped_sign_image import CroppedSignSchema -from ebl.fragmentarium.application.fragment_schema import ScriptSchema +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema from ebl.fragmentarium.domain.annotation import ( Geometry, AnnotationData, diff --git a/ebl/fragmentarium/application/fragment_fields_schemas.py b/ebl/fragmentarium/application/fragment_fields_schemas.py new file mode 100644 index 000000000..496b405be --- /dev/null +++ b/ebl/fragmentarium/application/fragment_fields_schemas.py @@ -0,0 +1,164 @@ +import pydash +from marshmallow import Schema, fields, post_dump, post_load, EXCLUDE +from ebl.fragmentarium.domain.folios import Folio, Folios +from ebl.fragmentarium.domain.fragment import ( + Introduction, + Notes, + Measure, + UncuratedReference, + Script, + DossierReference, +) +from ebl.fragmentarium.domain.record import Record, RecordEntry, RecordType +from ebl.schemas import ValueEnumField +from ebl.transliteration.application.note_line_part_schemas import ( + OneOfNoteLinePartSchema, +) +from ebl.common.domain.period import Period, PeriodModifier +from ebl.fragmentarium.domain.fragment_external_numbers import ExternalNumbers + + +class MeasureSchema(Schema): + value = fields.Float(load_default=None) + note = fields.String(load_default=None) + + @post_load + def make_measure(self, data, **kwargs): + return Measure(**data) + + @post_dump + def filter_none(self, data, **kwargs): + return pydash.omit_by(data, pydash.is_none) + + +class RecordEntrySchema(Schema): + user = fields.String(required=True) + type = ValueEnumField(RecordType, required=True) + date = fields.String(required=True) + + @post_load + def make_record_entry(self, data, **kwargs): + return RecordEntry(**data) + + +class RecordSchema(Schema): + entries = fields.Nested(RecordEntrySchema, many=True, required=True) + + @post_load + def make_record(self, data, **kwargs): + return Record(tuple(data["entries"])) + + +class FolioSchema(Schema): + name = fields.String(required=True) + number = fields.String(required=True) + + @post_load + def make_record_entry(self, data, **kwargs): + return Folio(**data) + + +class FoliosSchema(Schema): + entries = fields.Nested(FolioSchema, many=True, required=True) + + @post_load + def make_folio(self, data, **kwargs): + return Folios(tuple(data["entries"])) + + +class UncuratedReferenceSchema(Schema): + document = fields.String(required=True) + pages = fields.List(fields.Integer(), required=True) + + @post_load + def make_uncurated_reference(self, data, **kwargs): + data["pages"] = tuple(data["pages"]) + return UncuratedReference(**data) + + +class MarkupTextSchema(Schema): + text = fields.String(required=True) + parts = fields.List(fields.Nested(OneOfNoteLinePartSchema), required=True) + + +class IntroductionSchema(MarkupTextSchema): + @post_load + def make_introduction(self, data, **kwargs) -> Introduction: + return Introduction(data["text"], tuple(data["parts"])) + + +class NotesSchema(MarkupTextSchema): + @post_load + def make_notes(self, data, **kwargs) -> Notes: + return Notes(data["text"], tuple(data["parts"])) + + +class ScriptSchema(Schema): + class Meta: + unknown = EXCLUDE + + period = fields.Function( + lambda script: script.period.long_name, + lambda value: Period.from_name(value), + required=True, + ) + period_modifier = ValueEnumField( + PeriodModifier, required=True, data_key="periodModifier" + ) + uncertain = fields.Boolean(load_default=None) + sort_key = fields.Function( + lambda script: script.period.sort_key, data_key="sortKey", dump_only=True + ) + + @post_load + def make_script(self, data, **kwargs) -> Script: + return Script(**data) + + +class ExternalNumbersSchema(Schema): + cdli_number = fields.String(load_default="", data_key="cdliNumber") + bm_id_number = fields.String(load_default="", data_key="bmIdNumber") + archibab_number = fields.String(load_default="", data_key="archibabNumber") + bdtns_number = fields.String(load_default="", data_key="bdtnsNumber") + chicago_isac_number = fields.String(load_default="", data_key="chicagoIsacNumber") + ur_online_number = fields.String(load_default="", data_key="urOnlineNumber") + hilprecht_jena_number = fields.String( + load_default="", data_key="hilprechtJenaNumber" + ) + hilprecht_heidelberg_number = fields.String( + load_default="", data_key="hilprechtHeidelbergNumber" + ) + metropolitan_number = fields.String(load_default="", data_key="metropolitanNumber") + yale_peabody_number = fields.String(load_default="", data_key="yalePeabodyNumber") + louvre_number = fields.String(load_default="", data_key="louvreNumber") + dublin_tcd_number = fields.String(load_default="", data_key="dublinTcdNumber") + alalah_hpm_number = fields.String(load_default="", data_key="alalahHpmNumber") + philadelphia_number = fields.String(load_default="", data_key="philadelphiaNumber") + australianinstituteofarchaeology_number = fields.String( + load_default="", data_key="australianinstituteofarchaeologyNumber" + ) + achemenet_number = fields.String(load_default="", data_key="achemenetNumber") + nabucco_number = fields.String(load_default="", data_key="nabuccoNumber") + oracc_numbers = fields.List( + fields.String(), load_default=(), data_key="oraccNumbers" + ) + seal_numbers = fields.List(fields.String(), load_default=(), data_key="sealNumbers") + + @post_load + def make_external_numbers(self, data, **kwargs) -> ExternalNumbers: + data["oracc_numbers"] = tuple(data["oracc_numbers"]) + data["seal_numbers"] = tuple(data["seal_numbers"]) + return ExternalNumbers(**data) + + @post_dump + def omit_empty_numbers(self, data, **kwargs): + return pydash.omit_by(data, pydash.is_empty) + + +class DossierReferenceSchema(Schema): + dossierId = fields.String(required=True) + isUncertain = fields.Boolean(load_default=False) + + @post_load + def make_dossier_reference(self, data, **kwargs) -> DossierReference: + return DossierReference(**data) diff --git a/ebl/fragmentarium/application/fragment_info_schema.py b/ebl/fragmentarium/application/fragment_info_schema.py index 6abde4f0e..737fa1076 100644 --- a/ebl/fragmentarium/application/fragment_info_schema.py +++ b/ebl/fragmentarium/application/fragment_info_schema.py @@ -5,7 +5,7 @@ ApiReferenceSchema, ) from ebl.common.application.schemas import AccessionSchema -from ebl.fragmentarium.application.fragment_schema import ScriptSchema +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema from ebl.fragmentarium.application.genre_schema import GenreSchema from ebl.fragmentarium.domain.fragment_infos_pagination import FragmentInfosPagination from ebl.transliteration.application.museum_number_schema import MuseumNumberSchema diff --git a/ebl/fragmentarium/application/fragment_schema.py b/ebl/fragmentarium/application/fragment_schema.py index 57946d483..1e2ed80e2 100644 --- a/ebl/fragmentarium/application/fragment_schema.py +++ b/ebl/fragmentarium/application/fragment_schema.py @@ -1,171 +1,37 @@ import pydash from ebl.schemas import NameEnumField -from marshmallow import Schema, fields, post_dump, post_load, EXCLUDE +from marshmallow import Schema, fields, post_dump, post_load from ebl.fragmentarium.domain.museum import Museum from ebl.bibliography.application.reference_schema import ReferenceSchema from ebl.common.application.schemas import AccessionSchema -from ebl.common.domain.period import Period, PeriodModifier from ebl.fragmentarium.application.archaeology_schemas import ArchaeologySchema from ebl.fragmentarium.application.genre_schema import GenreSchema from ebl.transliteration.application.museum_number_schema import MuseumNumberSchema -from ebl.fragmentarium.domain.folios import Folio, Folios from ebl.fragmentarium.domain.fragment import ( Fragment, Introduction, Notes, - Measure, Script, - UncuratedReference, ) from ebl.fragmentarium.domain.fragment_external_numbers import ExternalNumbers from ebl.fragmentarium.domain.line_to_vec_encoding import LineToVecEncoding -from ebl.fragmentarium.domain.record import Record, RecordEntry, RecordType from ebl.schemas import ResearchProjectField, ScopeField, ValueEnumField -from ebl.transliteration.application.note_line_part_schemas import ( - OneOfNoteLinePartSchema, -) from ebl.transliteration.application.text_schema import TextSchema from ebl.fragmentarium.application.joins_schema import JoinsSchema from ebl.fragmentarium.domain.joins import Joins from ebl.fragmentarium.domain.date import DateSchema from ebl.fragmentarium.application.colophon_schema import ColophonSchema - - -class MeasureSchema(Schema): - value = fields.Float(load_default=None) - note = fields.String(load_default=None) - - @post_load - def make_measure(self, data, **kwargs): - return Measure(**data) - - @post_dump - def filter_none(self, data, **kwargs): - return pydash.omit_by(data, pydash.is_none) - - -class RecordEntrySchema(Schema): - user = fields.String(required=True) - type = ValueEnumField(RecordType, required=True) - date = fields.String(required=True) - - @post_load - def make_record_entry(self, data, **kwargs): - return RecordEntry(**data) - - -class RecordSchema(Schema): - entries = fields.Nested(RecordEntrySchema, many=True, required=True) - - @post_load - def make_record(self, data, **kwargs): - return Record(tuple(data["entries"])) - - -class FolioSchema(Schema): - name = fields.String(required=True) - number = fields.String(required=True) - - @post_load - def make_record_entry(self, data, **kwargs): - return Folio(**data) - - -class FoliosSchema(Schema): - entries = fields.Nested(FolioSchema, many=True, required=True) - - @post_load - def make_folio(self, data, **kwargs): - return Folios(tuple(data["entries"])) - - -class UncuratedReferenceSchema(Schema): - document = fields.String(required=True) - pages = fields.List(fields.Integer(), required=True) - - @post_load - def make_uncurated_reference(self, data, **kwargs): - data["pages"] = tuple(data["pages"]) - return UncuratedReference(**data) - - -class MarkupTextSchema(Schema): - text = fields.String(required=True) - parts = fields.List(fields.Nested(OneOfNoteLinePartSchema), required=True) - - -class IntroductionSchema(MarkupTextSchema): - @post_load - def make_introduction(self, data, **kwargs) -> Introduction: - return Introduction(data["text"], tuple(data["parts"])) - - -class NotesSchema(MarkupTextSchema): - @post_load - def make_notes(self, data, **kwargs) -> Notes: - return Notes(data["text"], tuple(data["parts"])) - - -class ScriptSchema(Schema): - class Meta: - unknown = EXCLUDE - - period = fields.Function( - lambda script: script.period.long_name, - lambda value: Period.from_name(value), - required=True, - ) - period_modifier = ValueEnumField( - PeriodModifier, required=True, data_key="periodModifier" - ) - uncertain = fields.Boolean(load_default=None) - sort_key = fields.Function( - lambda script: script.period.sort_key, data_key="sortKey", dump_only=True - ) - - @post_load - def make_script(self, data, **kwargs) -> Script: - return Script(**data) - - -class ExternalNumbersSchema(Schema): - cdli_number = fields.String(load_default="", data_key="cdliNumber") - bm_id_number = fields.String(load_default="", data_key="bmIdNumber") - archibab_number = fields.String(load_default="", data_key="archibabNumber") - bdtns_number = fields.String(load_default="", data_key="bdtnsNumber") - chicago_isac_number = fields.String(load_default="", data_key="chicagoIsacNumber") - ur_online_number = fields.String(load_default="", data_key="urOnlineNumber") - hilprecht_jena_number = fields.String( - load_default="", data_key="hilprechtJenaNumber" - ) - hilprecht_heidelberg_number = fields.String( - load_default="", data_key="hilprechtHeidelbergNumber" - ) - metropolitan_number = fields.String(load_default="", data_key="metropolitanNumber") - yale_peabody_number = fields.String(load_default="", data_key="yalePeabodyNumber") - louvre_number = fields.String(load_default="", data_key="louvreNumber") - dublin_tcd_number = fields.String(load_default="", data_key="dublinTcdNumber") - alalah_hpm_number = fields.String(load_default="", data_key="alalahHpmNumber") - philadelphia_number = fields.String(load_default="", data_key="philadelphiaNumber") - australianinstituteofarchaeology_number = fields.String( - load_default="", data_key="australianinstituteofarchaeologyNumber" - ) - achemenet_number = fields.String(load_default="", data_key="achemenetNumber") - nabucco_number = fields.String(load_default="", data_key="nabuccoNumber") - oracc_numbers = fields.List( - fields.String(), load_default=(), data_key="oraccNumbers" - ) - seal_numbers = fields.List(fields.String(), load_default=(), data_key="sealNumbers") - - @post_load - def make_external_numbers(self, data, **kwargs) -> ExternalNumbers: - data["oracc_numbers"] = tuple(data["oracc_numbers"]) - data["seal_numbers"] = tuple(data["seal_numbers"]) - return ExternalNumbers(**data) - - @post_dump - def omit_empty_numbers(self, data, **kwargs): - return pydash.omit_by(data, pydash.is_empty) +from ebl.fragmentarium.application.fragment_fields_schemas import ( + MeasureSchema, + RecordSchema, + FoliosSchema, + UncuratedReferenceSchema, + IntroductionSchema, + NotesSchema, + ScriptSchema, + ExternalNumbersSchema, + DossierReferenceSchema, +) class FragmentSchema(Schema): @@ -219,6 +85,7 @@ class FragmentSchema(Schema): ) archaeology = fields.Nested(ArchaeologySchema, allow_none=True, default=None) colophon = fields.Nested(ColophonSchema, allow_none=True, default=None) + dossiers = fields.Nested(DossierReferenceSchema, many=True, default=[]) @post_load def make_fragment(self, data, **kwargs): diff --git a/ebl/fragmentarium/application/line_to_vec_ranking_schema.py b/ebl/fragmentarium/application/line_to_vec_ranking_schema.py index 60c569046..0eb1be1ce 100644 --- a/ebl/fragmentarium/application/line_to_vec_ranking_schema.py +++ b/ebl/fragmentarium/application/line_to_vec_ranking_schema.py @@ -1,6 +1,6 @@ from marshmallow import Schema, fields, pre_dump from ebl.fragmentarium.application.line_to_vec import LineToVecScore -from ebl.fragmentarium.application.fragment_schema import ScriptSchema +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema from ebl.fragmentarium.domain.fragment import Script diff --git a/ebl/fragmentarium/domain/fragment.py b/ebl/fragmentarium/domain/fragment.py index 6ff5dd440..0a5cc3019 100644 --- a/ebl/fragmentarium/domain/fragment.py +++ b/ebl/fragmentarium/domain/fragment.py @@ -99,6 +99,12 @@ def abbreviation(self) -> str: return self.period.value[1] +@attr.s(auto_attribs=True, frozen=True) +class DossierReference: + dossierId: str + isUncertain: bool = False + + @attr.s(auto_attribs=True, frozen=True) class Fragment(FragmentExternalNumbers): number: MuseumNumber @@ -131,6 +137,7 @@ class Fragment(FragmentExternalNumbers): archaeology: Optional[Archaeology] = None colophon: Optional[Colophon] = None external_numbers: ExternalNumbers = ExternalNumbers() + dossiers: Sequence[str] = [] @property def is_lowest_join(self) -> bool: diff --git a/ebl/fragmentarium/infrastructure/mongo_fragment_repository_get_extended.py b/ebl/fragmentarium/infrastructure/mongo_fragment_repository_get_extended.py index b4c3ffd99..6084f29c3 100644 --- a/ebl/fragmentarium/infrastructure/mongo_fragment_repository_get_extended.py +++ b/ebl/fragmentarium/infrastructure/mongo_fragment_repository_get_extended.py @@ -9,7 +9,7 @@ from ebl.fragmentarium.infrastructure.mongo_fragment_repository_base import ( MongoFragmentRepositoryBase, ) -from ebl.fragmentarium.application.fragment_schema import ScriptSchema +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema from ebl.transliteration.domain.museum_number import MuseumNumber from ebl.transliteration.application.museum_number_schema import MuseumNumberSchema from ebl.fragmentarium.domain.date import Date, DateSchema diff --git a/ebl/fragmentarium/web/fragment_script.py b/ebl/fragmentarium/web/fragment_script.py index 12ecb29d3..3cfc18ff9 100644 --- a/ebl/fragmentarium/web/fragment_script.py +++ b/ebl/fragmentarium/web/fragment_script.py @@ -6,7 +6,7 @@ from ebl.fragmentarium.application.fragment_updater import FragmentUpdater from ebl.fragmentarium.web.dtos import create_response_dto, parse_museum_number from ebl.users.web.require_scope import require_scope -from ebl.fragmentarium.application.fragment_schema import ScriptSchema +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema class FragmentScriptResource: diff --git a/ebl/tests/conftest.py b/ebl/tests/conftest.py index 3eca9f194..6b809a59c 100644 --- a/ebl/tests/conftest.py +++ b/ebl/tests/conftest.py @@ -84,6 +84,9 @@ from ebl.afo_register.infrastructure.mongo_afo_register_repository import ( MongoAfoRegisterRepository, ) +from ebl.dossiers.infrastructure.mongo_dossiers_repository import ( + MongoDossiersRepository, +) from ebl.users.domain.user import Guest, User from ebl.users.infrastructure.auth0 import Auth0User from ebl.fragmentarium.web.annotations import AnnotationResource @@ -434,6 +437,11 @@ def user() -> User: ) +@pytest.fixture +def dossiers_repository(database): + return MongoDossiersRepository(database) + + @pytest.fixture def context( ebl_ai_client, @@ -451,6 +459,7 @@ def context( annotations_repository, lemma_repository, afo_register_repository, + dossiers_repository, findspot_repository, user, parallel_line_injector, @@ -473,6 +482,7 @@ def context( annotations_repository=annotations_repository, lemma_repository=lemma_repository, afo_register_repository=afo_register_repository, + dossiers_repository=dossiers_repository, findspot_repository=findspot_repository, cache=Cache({"CACHE_TYPE": "null"}), custom_cache=ChapterCache(mongo_cache_repository), diff --git a/ebl/tests/dossiers/test_dossier.py b/ebl/tests/dossiers/test_dossier.py new file mode 100644 index 000000000..f88f0d7bb --- /dev/null +++ b/ebl/tests/dossiers/test_dossier.py @@ -0,0 +1,82 @@ +import pytest +from ebl.dossiers.domain.dossier_record import ( + DossierRecord, +) +from ebl.dossiers.infrastructure.mongo_dossiers_repository import ( + DossierRecordSchema, +) +from ebl.tests.factories.dossier import DossierRecordFactory +from ebl.fragmentarium.domain.fragment import Script +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema +from ebl.common.domain.provenance import Provenance + + +@pytest.fixture +def dossier_record(): + return DossierRecordFactory.build() + + +def test_dossier_record_creation( + dossier_record: DossierRecord, +) -> None: + assert dossier_record.id is not None + assert isinstance(dossier_record.description, (str, type(None))) + assert isinstance(dossier_record.is_approximate_date, (bool, type(None))) + assert isinstance(dossier_record.year_range_from, (float, int, type(None))) + assert isinstance(dossier_record.year_range_to, (float, int, type(None))) + assert isinstance(dossier_record.related_kings, (list, type(None))) + assert isinstance(dossier_record.provenance, (Provenance, type(None))) + assert isinstance(dossier_record.script, (Script, type(None))) + assert isinstance(dossier_record.references, (list, type(None))) + + +def test_dossier_record_defaults() -> None: + blank_dossier_record = DossierRecord("test id") + + assert blank_dossier_record.id == "test id" + assert blank_dossier_record.description is None + assert blank_dossier_record.is_approximate_date is False + assert blank_dossier_record.year_range_from is None + assert blank_dossier_record.year_range_to is None + assert blank_dossier_record.related_kings == [] + assert blank_dossier_record.provenance is None + assert blank_dossier_record.script is None + assert blank_dossier_record.references == [] + + +def test_dossier_record_to_dict( + dossier_record: DossierRecord, +) -> None: + assert DossierRecordSchema().dump(dossier_record) == { + "_id": dossier_record.id, + "description": dossier_record.description, + "isApproximateDate": dossier_record.is_approximate_date, + "yearRangeFrom": dossier_record.year_range_from, + "yearRangeTo": dossier_record.year_range_to, + "relatedKings": dossier_record.related_kings, + "provenance": dossier_record.provenance.long_name + if dossier_record.provenance + else None, + "script": ScriptSchema().dump(dossier_record.script), + "references": [ + str(reference).replace("ReferenceType.", "") + for reference in dossier_record.references + ], + } + + +def test_dossier_record_from_dict( + dossier_record: DossierRecord, +) -> None: + serialized_data = DossierRecordSchema().dump(dossier_record) + deserialized_object = DossierRecordSchema().load(serialized_data) + + assert deserialized_object.id == dossier_record.id + assert deserialized_object.description == dossier_record.description + assert deserialized_object.is_approximate_date == dossier_record.is_approximate_date + assert deserialized_object.year_range_from == dossier_record.year_range_from + assert deserialized_object.year_range_to == dossier_record.year_range_to + assert deserialized_object.related_kings == dossier_record.related_kings + assert deserialized_object.provenance == dossier_record.provenance + assert deserialized_object.script == dossier_record.script + assert deserialized_object.references == dossier_record.references diff --git a/ebl/tests/dossiers/test_dossiers_repository.py b/ebl/tests/dossiers/test_dossiers_repository.py new file mode 100644 index 000000000..48d31df52 --- /dev/null +++ b/ebl/tests/dossiers/test_dossiers_repository.py @@ -0,0 +1,10 @@ +from ebl.tests.factories.dossier import DossierRecordFactory +from ebl.dossiers.application.dossiers_repository import DossiersRepository + + +def test_query_by_ids(dossiers_repository: DossiersRepository): + dossier_record = DossierRecordFactory.build() + dossiers_repository.create(dossier_record) + dossiers_repository.create(DossierRecordFactory.build()) + + assert dossiers_repository.query_by_ids([dossier_record.id]) == [dossier_record] diff --git a/ebl/tests/dossiers/test_dossiers_route.py b/ebl/tests/dossiers/test_dossiers_route.py new file mode 100644 index 000000000..c20342f77 --- /dev/null +++ b/ebl/tests/dossiers/test_dossiers_route.py @@ -0,0 +1,48 @@ +import falcon +import pytest +from ebl.dossiers.domain.dossier_record import DossierRecord +from ebl.tests.factories.dossier import ( + DossierRecordFactory, +) +from ebl.dossiers.application.dossiers_repository import ( + DossiersRepository, +) +from ebl.dossiers.infrastructure.mongo_dossiers_repository import ( + DossierRecordSchema, +) + + +@pytest.fixture +def dossier_record() -> DossierRecord: + return DossierRecordFactory.build() + + +@pytest.fixture +def another_dossier_record() -> DossierRecord: + return DossierRecordFactory.build() + + +@pytest.fixture +def unrelated_dossier_record() -> DossierRecord: + return DossierRecordFactory.build() + + +def test_fetch_dossier_record_route( + dossier_record, + another_dossier_record, + unrelated_dossier_record, + dossiers_repository: DossiersRepository, + client, +) -> None: + dossiers_repository.create(dossier_record) + dossiers_repository.create(another_dossier_record) + dossiers_repository.create(unrelated_dossier_record) + get_result = client.simulate_get( + "/dossiers", + params={"ids": ",".join([dossier_record.id, another_dossier_record.id])}, + ) + + assert get_result.status == falcon.HTTP_OK + assert sorted(get_result.json, key=lambda r: r["_id"]) == DossierRecordSchema( + many=True + ).dump(sorted([dossier_record, another_dossier_record], key=lambda r: r.id)) diff --git a/ebl/tests/factories/dossier.py b/ebl/tests/factories/dossier.py new file mode 100644 index 000000000..c159deb27 --- /dev/null +++ b/ebl/tests/factories/dossier.py @@ -0,0 +1,32 @@ +import factory +from random import randint +from ebl.dossiers.domain.dossier_record import ( + DossierRecord, +) +from ebl.common.domain.provenance import Provenance +from ebl.tests.factories.fragment import ScriptFactory +from ebl.chronology.chronology import chronology +from ebl.bibliography.domain.reference import ReferenceType + + +class DossierRecordFactory(factory.Factory): + class Meta: + model = DossierRecord + + id = factory.Faker("word") + description = factory.Faker("sentence") + is_approximate_date = factory.Faker("boolean") + year_range_from = factory.Maybe("is_approximate_date", randint(-2500, -400), None) + year_range_to = factory.Maybe( + "is_approximate_date", + factory.LazyAttribute(lambda obj: obj.year_range_from + randint(0, 500)), + None, + ) + related_kings = factory.LazyAttribute( + lambda _: [chronology.kings[i].order_global for i in range(randint(0, 10))] + ) + provenance = factory.fuzzy.FuzzyChoice(set(Provenance) - {Provenance.STANDARD_TEXT}) + script = factory.SubFactory(ScriptFactory) + references = factory.LazyAttribute( + lambda _: list({list(ReferenceType)[i] for i in range(randint(1, 6))}) + ) diff --git a/ebl/tests/factories/fragment.py b/ebl/tests/factories/fragment.py index fdcdfd7de..c34da02e1 100644 --- a/ebl/tests/factories/fragment.py +++ b/ebl/tests/factories/fragment.py @@ -19,7 +19,9 @@ Notes, Script, UncuratedReference, + DossierReference, ) + from ebl.fragmentarium.domain.fragment_external_numbers import ExternalNumbers from ebl.fragmentarium.domain.line_to_vec_encoding import LineToVecEncoding from ebl.transliteration.domain.museum_number import MuseumNumber @@ -204,6 +206,14 @@ class Meta: ) +class FragmentDossierReferenceFactory(factory.Factory): + class Meta: + model = DossierReference + + dossierId = factory.Faker("word") + isUncertain = factory.Faker("boolean") + + class FragmentFactory(factory.Factory): class Meta: model = Fragment @@ -237,6 +247,12 @@ class Meta: projects = (ResearchProject.CAIC, ResearchProject.ALU_GENEVA, ResearchProject.AMPS) archaeology = factory.SubFactory(ArchaeologyFactory) colophon = factory.SubFactory(ColophonFactory) + dossiers = factory.List( + [ + factory.SubFactory(FragmentDossierReferenceFactory) + for _ in range(random.randint(0, 4)) + ] + ) class InterestingFragmentFactory(FragmentFactory): diff --git a/ebl/tests/fragmentarium/test_dtos.py b/ebl/tests/fragmentarium/test_dtos.py index 9ecb534fe..0f7ac727c 100644 --- a/ebl/tests/fragmentarium/test_dtos.py +++ b/ebl/tests/fragmentarium/test_dtos.py @@ -17,13 +17,14 @@ LemmatizedFragmentFactory, ) from ebl.transliteration.application.text_schema import TextSchema -from ebl.fragmentarium.application.fragment_schema import ( +from ebl.fragmentarium.application.fragment_fields_schemas import ( ExternalNumbersSchema, - JoinsSchema, IntroductionSchema, NotesSchema, ScriptSchema, + DossierReferenceSchema, ) +from ebl.fragmentarium.application.joins_schema import JoinsSchema from ebl.fragmentarium.application.colophon_schema import ColophonSchema from ebl.fragmentarium.domain.date import DateSchema from ebl.fragmentarium.domain.joins import Joins @@ -121,6 +122,10 @@ def expected_dto(lemmatized_fragment, has_photo): "archaeology": ArchaeologySchema().dump(lemmatized_fragment.archaeology), "colophon": ColophonSchema().dump(lemmatized_fragment.colophon), "authorizedScopes": [], + "dossiers": [ + DossierReferenceSchema().dump(dossier) + for dossier in lemmatized_fragment.dossiers + ], }, pydash.is_none, ) diff --git a/ebl/tests/fragmentarium/test_fragment_script_route.py b/ebl/tests/fragmentarium/test_fragment_script_route.py index 4017b56ac..7acde4937 100644 --- a/ebl/tests/fragmentarium/test_fragment_script_route.py +++ b/ebl/tests/fragmentarium/test_fragment_script_route.py @@ -2,7 +2,7 @@ import pytest import json from ebl.common.domain.period import Period, PeriodModifier -from ebl.fragmentarium.application.fragment_schema import ScriptSchema +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema from ebl.fragmentarium.web.dtos import create_response_dto from ebl.tests.factories.fragment import FragmentFactory, ScriptFactory diff --git a/ebl/tests/fragmentarium/test_line_to_vec_ranking_schema.py b/ebl/tests/fragmentarium/test_line_to_vec_ranking_schema.py index d75a71d18..8eaf636c6 100644 --- a/ebl/tests/fragmentarium/test_line_to_vec_ranking_schema.py +++ b/ebl/tests/fragmentarium/test_line_to_vec_ranking_schema.py @@ -5,7 +5,7 @@ ) from ebl.transliteration.domain.museum_number import MuseumNumber from ebl.tests.factories.fragment import ScriptFactory -from ebl.fragmentarium.application.fragment_schema import ScriptSchema +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema SCRIPT = ScriptFactory.build() diff --git a/ebl/tests/fragmentarium/test_script_schema.py b/ebl/tests/fragmentarium/test_script_schema.py index eace5b0d2..8c39c9e1d 100644 --- a/ebl/tests/fragmentarium/test_script_schema.py +++ b/ebl/tests/fragmentarium/test_script_schema.py @@ -1,5 +1,5 @@ import pytest -from ebl.fragmentarium.application.fragment_schema import ScriptSchema +from ebl.fragmentarium.application.fragment_fields_schemas import ScriptSchema from ebl.fragmentarium.domain.fragment import Script from ebl.common.domain.period import Period, PeriodModifier