Skip to content

Commit

Permalink
Implement package creation 🎁
Browse files Browse the repository at this point in the history
  • Loading branch information
marksparkza committed Apr 7, 2024
1 parent afe6d57 commit 92e6817
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 38 deletions.
22 changes: 19 additions & 3 deletions odp/api/lib/schema.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Any

from fastapi import HTTPException
from jschon import JSONSchema, URI
from jschon import JSON, JSONSchema, URI
from sqlalchemy import select
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY

from odp.api.models import RecordModelIn, TagInstanceModelIn
from odp.api.models import PackageModelIn, RecordModelIn, TagInstanceModelIn
from odp.const.db import SchemaType
from odp.db import Session
from odp.db.models import Schema, Tag, Vocabulary
Expand All @@ -29,8 +31,22 @@ async def get_vocabulary_schema(vocabulary_id: str) -> JSONSchema:
return schema_catalog.get_schema(URI(schema.uri))


async def get_metadata_schema(record_in: RecordModelIn) -> JSONSchema:
async def get_package_schema(package_in: PackageModelIn) -> JSONSchema:
if not (schema := Session.get(Schema, (package_in.schema_id, SchemaType.metadata))):
raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, 'Invalid schema id')

return schema_catalog.get_schema(URI(schema.uri))


async def get_record_schema(record_in: RecordModelIn) -> JSONSchema:
if not (schema := Session.get(Schema, (record_in.schema_id, SchemaType.metadata))):
raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, 'Invalid schema id')

return schema_catalog.get_schema(URI(schema.uri))


async def get_metadata_validity(metadata: dict[str, Any], schema: JSONSchema) -> Any:
if (result := schema.evaluate(JSON(metadata))).valid:
return result.output('flag')

return result.output('detailed')
58 changes: 46 additions & 12 deletions odp/api/routers/package.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,107 @@
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException
from jschon import JSONSchema
from sqlalchemy import select
from starlette.status import HTTP_404_NOT_FOUND

from odp.api.lib.auth import Authorize
from odp.api.lib.auth import Authorize, Authorized
from odp.api.lib.paging import Page, Paginator
from odp.api.lib.schema import get_metadata_validity, get_package_schema
from odp.api.models import PackageModel, PackageModelIn
from odp.api.routers.resource import output_resource_model
from odp.const import ODPScope
from odp.const.db import SchemaType
from odp.db import Session
from odp.db.models import Package
from odp.db.models import Package, Resource

router = APIRouter()


def output_package_model(package: Package) -> PackageModel:
record = next((r for r in package.records), None)
return PackageModel(
id=package.id,
provider_id=package.provider_id,
provider_key=package.provider.key,
record_id=package.record_id,
schema_id=package.schema_id,
metadata=package.metadata_,
validity=package.validity,
notes=package.notes,
timestamp=package.timestamp.isoformat(),
resources=[
output_resource_model(resource)
for resource in package.resources
]
resource_count=len(package.resources),
record_id=record.id if record else None,
record_doi=record.doi if record else None,
record_sid=record.sid if record else None,
)


@router.get(
'/',
response_model=Page[PackageModel],
dependencies=[Depends(Authorize(ODPScope.PACKAGE_READ))],
)
async def list_packages(
auth: Authorized = Depends(Authorize(ODPScope.PACKAGE_READ)),
provider_id: str = None,
paginator: Paginator = Depends(),
):
stmt = select(Package)

if auth.object_ids != '*':
stmt = stmt.where(Package.provider_id.in_(auth.object_ids))

if provider_id:
stmt = stmt.where(Package.provider_id == provider_id)

return paginator.paginate(
select(Package),
stmt,
lambda row: output_package_model(row.Package),
)


@router.get(
'/{package_id}',
response_model=PackageModel,
dependencies=[Depends(Authorize(ODPScope.PACKAGE_READ))],
)
async def get_package(
package_id: str,
auth: Authorized = Depends(Authorize(ODPScope.PACKAGE_READ)),
):
if not (package := Session.get(Package, package_id)):
raise HTTPException(HTTP_404_NOT_FOUND)

auth.enforce_constraint([package.provider_id])

return output_package_model(package)


@router.post(
'/',
response_model=PackageModel,
dependencies=[Depends(Authorize(ODPScope.PACKAGE_WRITE))],
)
async def create_package(
package_in: PackageModelIn,
metadata_schema: JSONSchema = Depends(get_package_schema),
auth: Authorized = Depends(Authorize(ODPScope.PACKAGE_WRITE)),
):
resources = [
Session.get(Resource, resource_id)
for resource_id in package_in.resource_ids
]
auth.enforce_constraint(
[package_in.provider_id] +
[resource.provider_id for resource in resources]
)

package = Package(
provider_id=package_in.provider_id,
schema_id=package_in.schema_id,
schema_type=SchemaType.metadata,
metadata_=package_in.metadata,
validity=await get_metadata_validity(package_in.metadata, metadata_schema),
notes=package_in.notes,
resources=resources,
timestamp=datetime.now(timezone.utc),
)
package.save()

return output_package_model(package)
33 changes: 13 additions & 20 deletions odp/api/routers/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from odp.api.lib.auth import Authorize, Authorized, TagAuthorize, UntagAuthorize
from odp.api.lib.paging import Page, Paginator
from odp.api.lib.schema import get_metadata_schema, get_tag_schema
from odp.api.lib.schema import get_metadata_validity, get_record_schema, get_tag_schema
from odp.api.lib.utils import output_published_record_model, output_tag_instance_model
from odp.api.models import (
AuditModel,
Expand Down Expand Up @@ -180,13 +180,6 @@ def touch_parent(record: Record, timestamp: datetime) -> None:
touch_parent(parent, timestamp)


def get_validity(metadata: dict[str, Any], schema: JSONSchema) -> Any:
if (result := schema.evaluate(JSON(metadata))).valid:
return result.output('flag')

return result.output('detailed')


def create_audit_record(
auth: Authorized,
record: Record,
Expand Down Expand Up @@ -330,10 +323,10 @@ async def get_record_by_doi(
)
async def create_record(
record_in: RecordModelIn,
metadata_schema: JSONSchema = Depends(get_metadata_schema),
metadata_schema: JSONSchema = Depends(get_record_schema),
auth: Authorized = Depends(Authorize(ODPScope.RECORD_WRITE)),
):
return _create_record(record_in, metadata_schema, auth)
return await _create_record(record_in, metadata_schema, auth)


@router.post(
Expand All @@ -342,13 +335,13 @@ async def create_record(
)
async def admin_create_record(
record_in: RecordModelIn,
metadata_schema: JSONSchema = Depends(get_metadata_schema),
metadata_schema: JSONSchema = Depends(get_record_schema),
auth: Authorized = Depends(Authorize(ODPScope.RECORD_ADMIN)),
):
return _create_record(record_in, metadata_schema, auth, True)
return await _create_record(record_in, metadata_schema, auth, True)


def _create_record(
async def _create_record(
record_in: RecordModelIn,
metadata_schema: JSONSchema,
auth: Authorized,
Expand Down Expand Up @@ -383,7 +376,7 @@ def _create_record(
schema_id=record_in.schema_id,
schema_type=SchemaType.metadata,
metadata_=record_in.metadata,
validity=get_validity(record_in.metadata, metadata_schema),
validity=await get_metadata_validity(record_in.metadata, metadata_schema),
timestamp=(timestamp := datetime.now(timezone.utc)),
)
record.save()
Expand All @@ -402,15 +395,15 @@ def _create_record(
async def update_record(
record_id: str,
record_in: RecordModelIn,
metadata_schema: JSONSchema = Depends(get_metadata_schema),
metadata_schema: JSONSchema = Depends(get_record_schema),
auth: Authorized = Depends(Authorize(ODPScope.RECORD_WRITE)),
):
auth.enforce_constraint([record_in.collection_id])

if not (record := Session.get(Record, record_id)):
raise HTTPException(HTTP_404_NOT_FOUND)

return _set_record(False, record, record_in, metadata_schema, auth)
return await _set_record(False, record, record_in, metadata_schema, auth)


@router.put(
Expand All @@ -422,7 +415,7 @@ async def admin_set_record(
# generated id, so we must validate that it is a uuid
record_id: UUID,
record_in: RecordModelIn,
metadata_schema: JSONSchema = Depends(get_metadata_schema),
metadata_schema: JSONSchema = Depends(get_record_schema),
auth: Authorized = Depends(Authorize(ODPScope.RECORD_ADMIN)),
):
auth.enforce_constraint([record_in.collection_id])
Expand All @@ -433,10 +426,10 @@ async def admin_set_record(
create = True
record = Record(id=str(record_id))

return _set_record(create, record, record_in, metadata_schema, auth, True)
return await _set_record(create, record, record_in, metadata_schema, auth, True)


def _set_record(
async def _set_record(
create: bool,
record: Record,
record_in: RecordModelIn,
Expand Down Expand Up @@ -491,7 +484,7 @@ def _set_record(
record.schema_id = record_in.schema_id
record.schema_type = SchemaType.metadata
record.metadata_ = record_in.metadata
record.validity = get_validity(record_in.metadata, metadata_schema)
record.validity = await get_metadata_validity(record_in.metadata, metadata_schema)
record.timestamp = (timestamp := datetime.now(timezone.utc))

parent_id = get_parent_id(record_in.metadata, record_in.schema_id)
Expand Down
35 changes: 32 additions & 3 deletions odp/api/routers/resource.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select
from starlette.status import HTTP_404_NOT_FOUND, HTTP_409_CONFLICT

Expand All @@ -10,7 +10,7 @@
from odp.const import ODPScope
from odp.const.db import AuditCommand
from odp.db import Session
from odp.db.models import ArchiveResource, Resource
from odp.db.models import ArchiveResource, PackageResource, Resource

router = APIRouter()

Expand Down Expand Up @@ -50,13 +50,40 @@ def create_audit_record(
)
async def list_resources(
paginator: Paginator = Depends(),
archive_id: str = None,
package_id: str = Query(None, title='Filter by package id'),
provider_id: list[str] = Query(None, title='Filter by provider id(s)'),
archive_id: str = Query(None, title='Only return resources stored in this archive'),
exclude_archive_id: str = Query(None, title='Exclude resources stored in this archive'),
exclude_packaged: bool = Query(False, title='Exclude resources associated with any package'),
):
stmt = select(Resource)

if package_id:
stmt = stmt.join(PackageResource)
stmt = stmt.where(PackageResource.package_id == package_id)

if provider_id:
stmt = stmt.where(Resource.provider_id.in_(provider_id))

if archive_id:
stmt = stmt.join(ArchiveResource)
stmt = stmt.where(ArchiveResource.archive_id == archive_id)

if exclude_archive_id:
archived_subq = (
select(ArchiveResource).
where(ArchiveResource.resource_id == Resource.id).
where(ArchiveResource.archive_id == exclude_archive_id)
).exists()
stmt = stmt.where(~archived_subq)

if exclude_packaged:
packaged_subq = (
select(PackageResource).
where(PackageResource.resource_id == Resource.id)
).exists()
stmt = stmt.where(~packaged_subq)

return paginator.paginate(
stmt,
lambda row: output_resource_model(row.Resource),
Expand All @@ -82,6 +109,8 @@ async def get_resource(
@router.post(
'/',
response_model=ResourceModel,
description='Register a new resource. It is up to the caller to '
'ensure the resource is stored in the specified archive.',
)
async def create_resource(
resource_in: ResourceModelIn,
Expand Down
6 changes: 6 additions & 0 deletions odp/db/models/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ class Package(Base):
package_resources = relationship('PackageResource', cascade='all, delete-orphan', passive_deletes=True)
resources = association_proxy('package_resources', 'resource', creator=lambda r: PackageResource(resource=r))

# view of associated record via one-to-many record_package relation
# the plural 'records' is used because these attributes are collections,
# although there can be only zero or one related record
package_records = relationship('RecordPackage', viewonly=True)
records = association_proxy('package_records', 'record')

_repr_ = 'id', 'provider_id', 'schema_id'


Expand Down

0 comments on commit 92e6817

Please sign in to comment.