Source code for wwt_api_client.constellations.data

# Copyright 2023 the .NET Foundation
# Distributed under the MIT license

"""
Data classes for WWT Constellations APIs.
"""

from typing import List, Optional

from dataclasses import dataclass, field
from dataclasses_json import config, dataclass_json

from html_sanitizer.sanitizer import Sanitizer, DEFAULT_SETTINGS
from license_expression import (
    build_spdx_licensing,
    get_license_index,
    vendored_scancode_licensedb_index_location,
    ExpressionError,
)

__all__ = """
HandleImageStats
HandleInfo
HandlePermissions
HandleSceneStats
HandleStats
HandleUpdate
ImageApiPermissions
ImageContentPermissions
ImageInfo
ImageDisplayInfo
ImageStorage
ImageSummary
ImageUpdate
ImageWwt
SceneContent
SceneContentHydrated
SceneContentUpdate
SceneHydrated
SceneImageLayer
SceneImageLayerHydrated
SceneInfo
ScenePermissions
ScenePlace
ScenePreviews
SceneUpdate
""".split()


def _make_constellations_licensing():
    index = get_license_index(vendored_scancode_licensedb_index_location)
    index.append({"spdx_license_key": "LicenseRef-None"})
    index.append({"spdx_license_key": "LicenseRef-WWT"})
    return build_spdx_licensing(index)


CX_LICENSING = _make_constellations_licensing()
CX_SANITIZER_SETTINGS = DEFAULT_SETTINGS.copy()
CX_SANITIZER_SETTINGS.update(
    tags={"b", "strong", "i", "em", "a", "br"}, empty=set(), separate=set()
)
CX_SANITIZER = Sanitizer(settings=CX_SANITIZER_SETTINGS)


def _strip_nulls_in_place(d: dict):
    """
    Remove None values a dictionary and its sub-dictionaries.

    For the backend APIs, our convention is to have values be missing entirely
    rather than nulls; that's more future-proof if/when we add new fields to
    things.

    Returns the input for convenience.
    """

    keys_to_remove = []

    for key, val in d.items():
        if isinstance(val, dict):
            _strip_nulls_in_place(val)
        elif val is None:
            keys_to_remove.append(key)

    for key in keys_to_remove:
        del d[key]

    return d


[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class HandleInfo: handle: str display_name: str
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class HandlePermissions: handle: str view_dashboard: bool
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class HandleUpdate: display_name: Optional[str] = None
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class HandleImageStats: count: int
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class HandleSceneStats: count: int impressions: int likes: int clicks: Optional[int] shares: Optional[int]
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class HandleStats: handle: str images: HandleImageStats scenes: HandleSceneStats
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ImageWwt: """A description of the WWT data parameters associated with a Constellations image.""" base_degrees_per_tile: float bottoms_up: bool center_x: float center_y: float file_type: str offset_x: float offset_y: float projection: str quad_tree_map: str rotation: float tile_levels: int width_factor: int thumbnail_url: str
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ImageStorage: """A description of data storage associated with a Constellations image.""" legacy_url_template: Optional[str] = None
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ImageSummary: """Summary information about a Constellations image.""" id: str = field(metadata=config(field_name="_id")) # 24 hex digits handle_id: str # 24 hex digits creation_date: str # format: 2023-03-28T16:53:18.364Z note: str storage: ImageStorage
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ImageContentPermissions: copyright: str license: str credits: Optional[str] = None def __post_init__(self): license_info = CX_LICENSING.validate(self.license, strict=True) if license_info.errors: msg = "\n".join(license_info.errors) raise ExpressionError(f"Invalid SPDX license:\n{msg}") if self.credits: self.credits = CX_SANITIZER.sanitize(self.credits)
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ImageDisplayInfo: id: str # 24 hex digits wwt: ImageWwt permissions: ImageContentPermissions storage: ImageStorage
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ImageInfo: """Note that this class is *not* what is returned by the ``/handle/:handle/imageinfo`` endpoint. That returns a :class:`ImageSummary`.""" id: str # 24 hex digits handle_id: str # 24 hex digits handle: HandleInfo creation_date: str # format: 2023-03-28T16:53:18.364Z wwt: ImageWwt permissions: ImageContentPermissions storage: ImageStorage note: str
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ImageUpdate: note: Optional[str] = None permissions: Optional[ImageContentPermissions] = None
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ImageApiPermissions: id: str edit: bool
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ScenePlace: ra_rad: float dec_rad: float roll_rad: float roi_height_deg: float roi_aspect_ratio: float
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class SceneImageLayer: image_id: str opacity: float
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class SceneContent: image_layers: Optional[List[SceneImageLayer]] = None
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class SceneImageLayerHydrated: image: ImageDisplayInfo opacity: float
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class SceneContentHydrated: background: Optional[ImageDisplayInfo] = None image_layers: Optional[List[SceneImageLayerHydrated]] = None
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ScenePreviews: video: Optional[str] = None thumbnail: Optional[str] = None
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class SceneHydrated: id: str handle_id: str handle: HandleInfo creation_date: str likes: int liked: bool impressions: int clicks: Optional[int] shares: Optional[int] place: ScenePlace content: SceneContentHydrated text: str previews: ScenePreviews published: bool
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class SceneInfo: _id: str creation_date: str impressions: int likes: int clicks: Optional[int] shares: Optional[int] text: str published: bool
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class ScenePermissions: id: str edit: bool
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class SceneContentUpdate: background_id: Optional[str] = None
[docs] @dataclass_json(undefined="EXCLUDE") @dataclass class SceneUpdate: outgoing_url: Optional[str] = None place: Optional[ScenePlace] = None text: Optional[str] = None content: Optional[SceneContentUpdate] = None published: Optional[bool] = None