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 ID-based location querying #630

Merged
merged 6 commits into from
Jan 6, 2025
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
603 changes: 167 additions & 436 deletions Tekst-API/openapi.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Tekst-API/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "tekst"
version = "0.2.1a0"
version = "0.3.0a0"
description = "An online text research platform"
readme = "README.md"
authors = [
Expand Down
44 changes: 22 additions & 22 deletions Tekst-API/tekst/db/migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio

from collections.abc import Callable
from dataclasses import dataclass
from importlib import import_module
from pkgutil import iter_modules

Expand All @@ -11,33 +10,34 @@
from tekst.logs import log


@dataclass
class Migration:
version: str
proc: Callable[[Database], None]
MigrationsDict = dict[str, Callable[[Database], None]]


def _sort_migrations(migrs: list[Migration]) -> list[Migration]:
return sorted(migrs, key=lambda m: Version(m.version))
def _sort_migrations(migrations: MigrationsDict) -> MigrationsDict:
return {
key: migrations[key] for key in sorted(migrations, key=lambda m: Version(m))
}


def _list_migrations() -> list[Migration]:
return _sort_migrations(
[
import_module(f"{__name__}.{mod.name}").MIGRATION
for mod in iter_modules(__path__)
]
)
def _read_migrations() -> MigrationsDict:
return {
mod.name.replace("migration_", "").replace("_", "."): import_module(
f"{__name__}.{mod.name}"
).migration
for mod in iter_modules(__path__)
}


_MIGRATIONS = _list_migrations()
_MIGRATIONS = _read_migrations()


async def _is_migration_pending(db_version: str) -> bool:
if db_version is None:
log.warning("No DB version found. Has setup been run?")
return False
return bool(_MIGRATIONS and Version(db_version) < Version(_MIGRATIONS[-1].version))
return bool(
_MIGRATIONS and Version(db_version) < Version(list(_MIGRATIONS.keys())[-1])
)


async def check_db_version(db_version: str, auto_migrate: bool = False) -> None:
Expand Down Expand Up @@ -92,18 +92,18 @@ async def migrate() -> None:

# run relevant migrations
curr_db_version = db_version_before
for migration in _MIGRATIONS:
if Version(migration.version) > Version(curr_db_version):
log.debug(f"Migrating DB from {curr_db_version} to {migration.version}...")
for migration_v in _MIGRATIONS:
if Version(migration_v) > Version(curr_db_version):
log.debug(f"Migrating DB from {curr_db_version} to {migration_v}...")
try:
await migration.proc(db)
await _MIGRATIONS[migration_v](db)
except Exception as e: # pragma: no cover
log.error(
f"Failed migrating DB from {curr_db_version} "
f"to {migration.version}: {e}"
f"to {migration_v}: {e}"
)
raise e
curr_db_version = migration.version
curr_db_version = migration_v

# update DB version in state document
await state_coll.update_one(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
from tekst.db import Database
from tekst.db.migrations import Migration


async def _proc(db: Database) -> None:
async def migration(db: Database) -> None:
"""
This migration procedre is a no-op since it belongs
to the very first version handling migrations at all.
"""


MIGRATION = Migration(
version="0.1.0a0",
proc=_proc,
)
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
from tekst.db import Database
from tekst.db.migrations import Migration


async def _proc(db: Database) -> None:
async def migration(db: Database) -> None:
# rename field "custom_fonts" in "state" collection to "fonts"
await db.state.update_many({}, {"$rename": {"custom_fonts": "fonts"}})


MIGRATION = Migration(
version="0.2.0a0",
proc=_proc,
)
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
from tekst.db import Database
from tekst.db.migrations import Migration


async def _proc(db: Database) -> None:
async def migration(db: Database) -> None:
# delete platform state field "register_intro_text"
await db.state.update_many({}, {"$unset": {"register_intro_text": ""}})


MIGRATION = Migration(
version="0.2.1a0",
proc=_proc,
)
18 changes: 18 additions & 0 deletions Tekst-API/tekst/db/migrations/migration_0_3_0a0.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from tekst.db import Database


async def migration(db: Database) -> None:
# add location ID to all correction model instances
resources_by_id = {res["_id"]: res for res in await db.resources.find({}).to_list()}
async for corr in db.corrections.find({}):
res = resources_by_id[corr["resource_id"]]
loc = await db.locations.find_one(
{
"text_id": res["text_id"],
"level": res["level"],
"position": corr["position"],
}
)
await db.corrections.update_one(
{"_id": corr["_id"]}, {"$set": {"location_id": loc["_id"]}}
)
5 changes: 4 additions & 1 deletion Tekst-API/tekst/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,10 @@ def update_values(
E_400_INVALID_LEVEL = error_instance(
status_code=status.HTTP_400_BAD_REQUEST,
key="locationInvalidLevel",
msg="The level index passed does not exist in target text",
msg=(
"The level index passed is invalid or doesn't "
"match the level of a referenced object"
),
)

E_400_LOCATION_NO_LEVEL_NOR_PARENT = error_instance(
Expand Down
33 changes: 31 additions & 2 deletions Tekst-API/tekst/models/browse.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
from typing import Annotated

from beanie import PydanticObjectId
from pydantic import Field

from tekst.models.common import ModelBase
from tekst.models.location import LocationRead
from tekst.resources import AnyContentRead


class LocationData(ModelBase):
location_path: list[LocationRead] = []
contents: list[AnyContentRead] = []
location_path: Annotated[
list[LocationRead],
Field(
description="Path of locations from level 0 to this location",
),
] = []
previous_loc_id: Annotated[
PydanticObjectId | None,
Field(
description="ID of the preceding location on the same level",
alias="prev",
),
] = None
next_loc_id: Annotated[
PydanticObjectId | None,
Field(
description="ID of the subsequent location on the same level",
alias="next",
),
] = None
contents: Annotated[
list[AnyContentRead],
Field(
description="Contents of various resources on this location",
),
] = []
55 changes: 20 additions & 35 deletions Tekst-API/tekst/models/correction.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from tekst.models.common import (
DocumentBase,
ExcludeFromModelVariants,
ModelBase,
ModelFactoryMixin,
PydanticObjectId,
Expand All @@ -19,16 +20,10 @@ class Correction(ModelBase, ModelFactoryMixin):
description="ID of the resource this correction refers to",
),
]
user_id: Annotated[
location_id: Annotated[
PydanticObjectId,
Field(
description="ID of the user who created the correction note",
),
]
position: Annotated[
int,
Field(
description="Position of the content this correction refers to",
description="ID of the location this correction refers to",
),
]
note: Annotated[
Expand All @@ -43,44 +38,33 @@ class Correction(ModelBase, ModelFactoryMixin):
),
val.CleanupMultiline,
]
date: Annotated[
datetime,
Field(
description="Date when the correction was created",
),
]
location_labels: Annotated[
list[str],
Field(
description="Text location labels from root to target location",
),
]


class CorrectionCreate(ModelBase):
resource_id: Annotated[
user_id: Annotated[
PydanticObjectId,
Field(
description="ID of the resource this correction refers to",
description="ID of the user who created the correction note",
),
ExcludeFromModelVariants(create=True),
]
position: Annotated[
int,
Field(
description="Position of the content this correction refers to",
description="Position of the correction on the resource's level",
),
ExcludeFromModelVariants(create=True),
]
note: Annotated[
str,
date: Annotated[
datetime,
Field(
description="Content of the correction note",
description="Date when the correction was created",
),
StringConstraints(
min_length=1,
max_length=1000,
strip_whitespace=True,
ExcludeFromModelVariants(create=True),
]
location_labels: Annotated[
list[str],
Field(
description="Text location labels from root to target location",
),
val.CleanupMultiline,
ExcludeFromModelVariants(create=True),
]


Expand All @@ -89,8 +73,9 @@ class Settings(DocumentBase.Settings):
name = "corrections"
indexes = [
"resource_id",
"position",
"location_id",
]


CorrectionCreate = Correction.create_model()
CorrectionRead = Correction.read_model()
17 changes: 14 additions & 3 deletions Tekst-API/tekst/models/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class ResourceBase(ModelBase, ModelFactoryMixin):
),
ExcludeFromModelVariants(
update=True,
create=True,
),
] = None

Expand All @@ -163,6 +164,9 @@ class ResourceBase(ModelBase, ModelFactoryMixin):
description="Users with shared read access to this resource",
max_length=64,
),
ExcludeFromModelVariants(
create=True,
),
] = []

shared_write: Annotated[
Expand All @@ -171,6 +175,9 @@ class ResourceBase(ModelBase, ModelFactoryMixin):
description="Users with shared write access to this resource",
max_length=64,
),
ExcludeFromModelVariants(
create=True,
),
] = []

public: Annotated[
Expand All @@ -180,6 +187,7 @@ class ResourceBase(ModelBase, ModelFactoryMixin):
),
ExcludeFromModelVariants(
update=True,
create=True,
),
] = False

Expand All @@ -190,6 +198,7 @@ class ResourceBase(ModelBase, ModelFactoryMixin):
),
ExcludeFromModelVariants(
update=True,
create=True,
),
] = False

Expand Down Expand Up @@ -230,6 +239,7 @@ class ResourceBase(ModelBase, ModelFactoryMixin):
),
ExcludeFromModelVariants(
update=True,
create=True,
),
] = datetime.utcfromtimestamp(86400)

Expand Down Expand Up @@ -395,13 +405,14 @@ async def __precompute_coverage_data(self) -> None:
coverage_per_parent = {}
covered_locations_count = 0
for location in data:
loc_id = str(location["id"])
parent_id = str(location["parent_id"])
if parent_id not in coverage_per_parent:
coverage_per_parent[parent_id] = []
coverage_per_parent[parent_id].append(
{
"label": location_labels[str(location["id"])],
"position": location["position"],
"loc_id": loc_id,
"label": location_labels[loc_id],
"covered": location["covered"],
}
)
Expand Down Expand Up @@ -562,8 +573,8 @@ class ResourceReadExtras(ModelBase):


class LocationCoverage(ModelBase):
loc_id: str
label: str
position: int
covered: bool = False


Expand Down
Loading
Loading