import json import re from apollo_utils.service.legacy.schemas.mixins.market import MarketGlobalMixin from apollo_utils.service.legacy.schemas.mixins.vendor import VendorRequiredMixin from apollo_utils.service.schemas.fields.list import SplitUniqueListField from apollo_utils.service.schemas.mixins.isrc import IsrcListRequiredMixin, IsrcRequiredMixin from apollo_utils.service.schemas.mixins.market import MarketNoneMixin from apollo_utils.service.schemas.swagger.types import ApispecTypes from json import JSONDecodeError from marshmallow import EXCLUDE, INCLUDE, Schema, ValidationError, fields, post_dump, post_load, pre_load, validate, \ validates_schema from typing import Dict from apollo_main_db.apollo.models import MarketRegionTypeEnum from src.constants import core as constants from src.constants.core import ELASTICSEARCH_ESCAPING_CHARACTERS from src.constants.include import HHLatestDateInclude, HHTrackInclude from src.constants.order import HHTrackOrder from src.legacy.apollo_api.constants import LinkSourcePage, NPSResultStatus, SearchHistoryData from src.legacy.apollo_api.exceptions import BrokenIsrcError, BrokenUriError, DataAbsenceError, UriAbsenceError from src.legacy.apple_music.constants import APPLE_MAX_IMAGE_SIZE from src.legacy.core.serializers import BasePaginationOutputSchema, CacheModeMixin, JsonBodySchema, PaginationMixin, \ QueryWebArgs, UniqueArgsListField, get_pagination_mixin from src.legacy.spotify.constants import SPOTIFY_URI_PREFIX from src.schemas.core import QueryParams class VendorIdsMixin: spotify_id = fields.String(validate=validate.Length(min=1, max=100)) apple_id = fields.Integer(validate=validate.Range(min=1)) class IsrcStarredTrackMixin: isrc = fields.String(allow_none=True) # use custom validation only for returning custom exceptions @validates_schema(skip_on_field_errors=True) def validate_isrc(self, data, **kwargs): isrc = data.get("isrc") if isrc is None: raise DataAbsenceError if not re.compile(r"^[A-Za-z]{2}\w{3}\d{7}$").match(isrc): raise BrokenIsrcError @post_load def upper_isrc(self, data: dict, **kwargs): data["isrc"] = data["isrc"].upper() return data class OneIdMixin: _id_fields = {"spotify_id", "apple_id"} @pre_load def validate_single_argument(self, data, **kwargs): """Ensure that exactly one ID is provided.""" keys = self._id_fields & set(data.keys()) if len(keys) != 1: raise ValidationError("Provide one and only one ID of {}.".format(", ".join(self._id_fields))) return data class ClientTypeMixin: client_type = fields.String(validate=validate.OneOf(constants.CLIENTS_TYPES), missing=constants.CLIENT_PORTAL) class TrackIdInput(Schema, VendorIdsMixin, OneIdMixin): """Schema for validating JSON input with track ID.""" class ComparisonTracksQuerySchema(QueryWebArgs, MarketGlobalMixin): """ Schema used with flask-apispec @use_kwargs decorator to load data from request query for comparison tracks get/post. """ class ComparisonTracksPostOutputSchema(Schema): """ Output schema used with flask-apispec @marshal_with decorator for comparison tracks POST and partially GET (main schema with nested data). """ apple_id = fields.Integer() is_sony = fields.Bool() isrc = fields.String() spotify_id = fields.String() class ComparisonTracksSpotifyUrlOutputSchema(Schema): """Spotify URL output nested schema for comparison tracks GET.""" spotify = fields.Url() class ComparisonTracksIsrcOutputSchema(Schema): """ISRC output nested schema for comparison tracks GET.""" isrc = fields.String() class ImageOutputSchema(Schema): """Image output nested schema for comparison tracks GET.""" height = fields.Integer() url = fields.Url() width = fields.Integer() class ComparisonTracksArtistsOutputSchema(Schema): """Artists output nested schema for comparison tracks GET.""" external_urls = fields.Nested(ComparisonTracksSpotifyUrlOutputSchema) href = fields.Url() id = fields.String() name = fields.String() type = fields.String() uri = fields.String() class ComparisonTracksAlbumOutputSchema(Schema): """Album output nested schema for comparison tracks GET.""" album_type = fields.String() artists = fields.List(fields.Nested(ComparisonTracksArtistsOutputSchema)) available_markets = fields.List(fields.String(required=False)) external_urls = fields.Nested(ComparisonTracksSpotifyUrlOutputSchema) href = fields.String() id = fields.String() images = fields.List(fields.Nested(ImageOutputSchema)) name = fields.String() release_date = fields.Date() release_date_precision = fields.String() total_tracks = fields.Integer() type = fields.String() uri = fields.String() class ComparisonTracksDataOutputSchema(Schema): """Data output nested schema for comparison tracks GET.""" album = fields.Nested(ComparisonTracksAlbumOutputSchema) artists = fields.List(fields.Nested(ComparisonTracksArtistsOutputSchema)) available_markets = fields.List(fields.String(required=False)) disc_number = fields.Integer() duration_ms = fields.Number() explicit = fields.Boolean() external_ids = fields.Nested(ComparisonTracksIsrcOutputSchema) external_urls = fields.Nested(ComparisonTracksSpotifyUrlOutputSchema) href = fields.Url() id = fields.String() is_local = fields.Boolean() name = fields.String() popularity = fields.Integer() preview_url = fields.Url() track_number = fields.Integer() type = fields.String() uri = fields.String() class ComparisonTracksGetOutputSchema(ComparisonTracksPostOutputSchema): """ Full Output schema used with flask-apispec @marshal_with decorator only for Swagger documentation and without response validation for comparison tracks GET """ data = fields.Nested(ComparisonTracksDataOutputSchema) class ComparisonTracksPostInputSchema(QueryWebArgs, OneIdMixin): """ Schema for validating JSON input for adding comparison tracks used with flask-apispec @marshal_with decorator. """ spotify_id = fields.String(validate=validate.Length(min=1, max=100), missing=None, default=None) apple_id = fields.Integer(validate=validate.Range(min=1), missing=None, default=None) isrc = fields.String(validate=validate.Length(min=1, max=45), missing=None, default=None) class ComparisonTracksDeleteInputSchema(QueryWebArgs, OneIdMixin): """Schema for validating JSON input for deleting comparison tracks.""" spotify_id = fields.String(validate=validate.Length(min=1, max=100), missing=None, default=None) apple_id = fields.Integer(validate=validate.Range(min=1), missing=None, default=None) class StarredTrackPaginatedSchema(QueryWebArgs, MarketGlobalMixin, PaginationMixin): """Schema for validating JSON input for removing starred tracks.""" related = fields.Boolean(default=False, missing=False) vendors = SplitUniqueListField( fields.String(validate=validate.OneOf(constants.ALL_VENDORS)), validate=validate.Length(min=1, max=len(constants.ALL_VENDORS)), default=list(constants.CA_VENDORS), missing=list(constants.CA_VENDORS), ) class UriStarredTrackMixin: """URI input validation mixin for starred tracks POST.""" uri = fields.String(allow_none=True) # use custom validation only for returning custom exceptions @validates_schema(skip_on_field_errors=True) def validate_uri(self, data, **kwargs): uri = data.get("uri") if uri is None: raise UriAbsenceError if not (re.compile(r"^spotify:track:\d{1}\w{21}$").match(uri) or re.compile(r"^\d{9,10}$").match(uri)): raise BrokenUriError class StarredTrackPostSchema(Schema, IsrcStarredTrackMixin, UriStarredTrackMixin): """Schema for validating JSON input for adding starred tracks.""" class Meta: unknown = INCLUDE vendors = SplitUniqueListField( fields.String(validate=validate.OneOf(constants.ALL_VENDORS)), validate=validate.Length(min=1, max=len(constants.ALL_VENDORS)), default=list(constants.CA_VENDORS), missing=list(constants.CA_VENDORS), ) class StarredTrackDeleteSchema(QueryParams, IsrcStarredTrackMixin): """Schema for validating JSON input for removing starred tracks.""" pass class StarredTrackOutputSchema(Schema): """Output schema for starred track view.""" uri = fields.String() isrc = fields.String() class StarredTracksCheckSchema(QueryParams): """Query params validation schema for StarredTracks checking view.""" isrc = SplitUniqueListField( fields.String(validate=validate.Length(min=1, max=45)), validate=validate.Length(min=1, max=300), required=True ) class StarredTrackOutputDetailedSchema(Schema): """Output schema for starred track detailed view.""" id = fields.Method("get_id_value") uri = fields.Method("get_uri") vendor = fields.Method("get_vendor") isrc = fields.String() streams = fields.Integer(default=0, missing=0) is_sony = fields.Method("check_is_sony") extra_data = fields.Method("get_extra_data") related = fields.Method("get_related") available = fields.Method("get_available") def check_is_sony(self, track) -> bool: sony_ids = self.context.get("sony_ids", []) return track.track_id in sony_ids def get_extra_data(self, track) -> Dict: return self.context.get("extra_data", {}).get(track.isrc, {}) def get_uri(self, track) -> str: track_data = self.get_extra_data(track) vendor = track_data.get("vendor") _id = track_data.get("id") return f"{SPOTIFY_URI_PREFIX}{_id}" if vendor == constants.VENDOR_SPOTIFY and _id else _id def get_related(self, track) -> Dict: return self.context.get("related", {}).get(track.isrc, {}) def get_id_value(self, track) -> str or None: track_data = self.get_extra_data(track) return track_data.get("id") or track.track_id def get_vendor(self, track) -> str: track_data = self.get_extra_data(track) return track_data.get("vendor") or track.vendor def get_available(self, track) -> bool: track_data = self.get_extra_data(track) try: available = track_data.pop("available") except KeyError: available = False return available class StarredTrackOutputDetailedPaginatedSchema(BasePaginationOutputSchema): items = fields.List(fields.Nested(StarredTrackOutputDetailedSchema)) class OnBoardingInputSchema(QueryWebArgs, ClientTypeMixin): """Schema used with flask-apispec @use_kwargs decorator to load data from request query/body for receiving client type.""" class Meta: unknown = EXCLUDE class OnBoardingOutputSchema(Schema): """Onboarding fact response schema.""" user_onboarding = fields.Bool() created_at = fields.DateTime() class SetJobCategory(Schema): """JSON body data validate schema for saving job category data.""" category = fields.Integer( validate=validate.Range(min=min(constants.JOB_CATEGORIES), max=(max(constants.JOB_CATEGORIES))), required=True ) class MarketInputSchema(QueryParams, Schema): """Market model deserializer schema.""" extended = fields.Boolean(missing=False) region_type = fields.String(missing=None, validate=validate.OneOf([i.name.lower() for i in MarketRegionTypeEnum])) mapping_only = fields.Boolean(missing=True) additional_sorting = fields.Boolean(missing=False) class MarketSchema(Schema): """Market model serializer schema.""" class Region(Schema): code = fields.String(default=None) name = fields.String(default=None) rank = fields.Integer(default=None) id = fields.Integer() code = fields.String() name = fields.String() full_name = fields.Str() region = fields.Nested(Region) class TracksIsSonySchema(QueryWebArgs): """Tracks is Sony check schema.""" spotify_ids = fields.List(fields.String()) apple_ids = fields.List(fields.String()) isrc = fields.List(fields.String()) market = fields.String(validate=validate.Length(max=10), required=True) @post_load def change_market(self, data, **kwargs): """Change market value from '_gl' to 'global'.""" if data["market"] == constants.SPOTIFY_MARKET_GLOBAL: data["market"] = constants.MARKET_GLOBAL return data class AlbumsIsSonySchema(QueryParams, MarketGlobalMixin): """Albums Is_Sony check schema.""" upc_list = SplitUniqueListField( fields.String(validate=validate.Length(min=1, max=50)), data_key="upc", required=True, validate=validate.Length(min=1, max=1000), ) class VisitedTracksSchema(QueryParams, MarketGlobalMixin): """Visited tracks schema.""" pass class MobileVersionsParams(QueryParams): """URL query validate schema for receiving latest os version.""" os = fields.String(validate=validate.OneOf(constants.ALL_OS), required=True) class MobileVersionsSchema(Schema): """Apollo Go versions JSON data schema.""" os = fields.String() version = fields.String() created_at = fields.DateTime() class IsrcRelationBaseSchema(JsonBodySchema): """Isrc relation schema for post and response data.""" main_isrc = fields.String(validate=validate.Length(min=1, max=45), required=True) related = SplitUniqueListField( fields.String(validate=validate.Length(min=1, max=45)), required=True, validate=validate.Length(min=1), force_list=True, ) @validates_schema def check_isrc(self, data, **kwargs): main = data.get("main_isrc") related = set(data.get("related", [])) if main and related and main in related: raise ValidationError(f"Isrc {main} can not be related to itself.") class IsrcRelationGetSchema(MarketNoneMixin, QueryWebArgs): """Isrc relation schema for getting data.""" pass class IsrcRelationMainOutputSchema(Schema): """Isrc relation response schema main isrc representation.""" isrc = fields.String(required=True) track_id = fields.String(allow_none=True) class IsrcRelationGetOutputSchema(Schema): """Isrc relation response schema.""" main = fields.Nested(IsrcRelationMainOutputSchema) related = fields.List(fields.String()) class IsrcToIdRelation: """Nested Schema(s) to be used with flask-apispec @marshal_with / @use_kwargs decorator(s). For isrc-to-id-relation""" class Request(QueryWebArgs): """Query params validation schema""" isrc_list = fields.List(fields.String(required=True), data_key="isrc_list") class CheckMarket: """Nested Schema(s) to be used with flask-apispec @marshal_with / @use_kwargs decorator(s). For check-market is supported""" class Request(QueryWebArgs, MarketNoneMixin): """Query params validation schema""" vendor = fields.String(validate=validate.OneOf(constants.VENDORS), required=True) by_isrc = fields.Bool(missing=False) class ApplicationMarketSchema(QueryWebArgs): """Application market extended schema.""" market_id = fields.Integer(dump_to="id", data_key="id") market_fullname = fields.String(dump_to="name", data_key="name", required=True) language_code = fields.String(dump_to="cultureInfo", data_key="cultureInfo", required=True) market_name = fields.String(dump_to="spotifyRegionCode", data_key="spotifyRegionCode", required=True) active = fields.Bool(missing=True) ga_country_name = fields.String(dump_to="gaCountryName", data_key="gaCountryName", required=True) default_service = fields.String(dump_to="defaultService", data_key="defaultService", allow_none=True) services = fields.Function( deserialize=lambda x: (x if isinstance(x, str) else ",".join(x)) if x else "", serialize=lambda x: x.services and x.services.split(",") or [], ) workout = fields.Bool(dump_to="workoutMarket", data_key="workoutMarket", missing=False) include_other_playlists = fields.Bool( dump_to="includeOtherPlaylists", data_key="includeOtherPlaylists", missing=False ) class TwilioMessagingStatusCallbackSchema(Schema): """Twilio status schema.""" class Meta: unknown = EXCLUDE MessageStatus = fields.String(required=True) AccountSid = fields.String(required=True) class GTPDateMixin: gtp_date = fields.Date(required=True, data_key="date") class GTPTracksHistoryInputSchema(QueryWebArgs, GTPDateMixin): """GET GTP tracks history schema.""" type_id = fields.Integer(required=True) class GTPTracksDataInputSchema(QueryWebArgs, IsrcListRequiredMixin, GTPDateMixin): """GET GTP tracks data schema.""" pass class SpotifyTrack(Schema): """Spotify images item schema.""" id = fields.Integer() name = fields.Integer() artists = fields.List(fields.Nested(Schema)) album = fields.Nested(Schema) class GTPTracksHistoryItemSchema(Schema): """GET GTP tracks result single item schema.""" id = fields.String(required=True) isrc = fields.String() class GTPTracksHistoryOutputSchema(Schema): """GET GTP tracks result schema.""" tracks = fields.List(fields.Nested(GTPTracksHistoryItemSchema)) gtp_date = fields.Date(required=True) class HotHitsPlaylistItem(Schema): playlist_id = fields.String() market = fields.String() rank = fields.Integer() region = fields.String() playlist_name = fields.String() playlist_image = fields.String() class TracksHotHits: class Request(QueryWebArgs, IsrcListRequiredMixin): pass class Response(Schema): class FullPlaylistItem(HotHitsPlaylistItem): latest_date = fields.Date() class TrackItem(Schema): class TrackPlaylistItem(HotHitsPlaylistItem): entry_date = fields.Date() peak_position = fields.Integer() peak_date = fields.Date() class PreviousTrackPlaylistItem(TrackPlaylistItem): exit_date = fields.Date() class CurrentTrackPlaylistItem(TrackPlaylistItem): position = fields.Integer() trend = fields.Integer() is_entry = fields.Boolean() isrc = fields.String(required=True) non_featured = fields.Nested(HotHitsPlaylistItem, many=True) previous = fields.Nested(PreviousTrackPlaylistItem, many=True) current = fields.Nested(CurrentTrackPlaylistItem, many=True) avg_position = fields.Integer() current_count = fields.Integer() items = fields.Nested(TrackItem, many=True) hot_hits_date = fields.Date(required=True) regions = fields.List(fields.String()) outdated = fields.Nested(HotHitsPlaylistItem, many=True) playlists = fields.Nested(FullPlaylistItem, many=True) playlists_by_markets = fields.Dict(fields.String()) class GTPPositionOutputSchema(Schema): """GTP positions single position item output schema.""" date = fields.Date(allow_none=True) position = fields.Int() change = fields.Int(allow_none=True) class GTPChartsPositionsIsrcOutputSchema(Schema): """GTP charts positions single isrc output schema.""" isrc = fields.Str(required=True) start_date = fields.Date() end_date = fields.Date() market = fields.Str(required=True) trend = fields.Integer(required=False, allow_none=True) is_entered = fields.Bool() positions = fields.List(fields.Nested(GTPPositionOutputSchema)) class GTPChartsPositionsOutputSchema(Schema): chart_available = fields.Bool(default=True) data = fields.List(fields.Nested(GTPChartsPositionsIsrcOutputSchema)) class VersionCarouselPostSchema(JsonBodySchema): """Version carousel view POST schema.""" class Meta: unknown = EXCLUDE os = fields.String(validate=validate.OneOf(constants.ALL_OS), required=True) version = fields.String(required=True) class VersionCarouselGetSchema(QueryWebArgs, CacheModeMixin): """Version carousel view GET schema.""" os = fields.String(validate=validate.OneOf(constants.ALL_OS), required=True) version = fields.String(required=True) class VersionCarouselSlideSchema(Schema): """Version carousel slide schema.""" id = fields.Integer() slide_num = fields.Integer() title_1 = fields.String() img_url = fields.String(data_key="image_url") title_2 = fields.String() description = fields.String() created_at = fields.DateTime() class VersionCarouselSchema(Schema): """Version carousel schema.""" id = fields.Integer() os = fields.String() version = fields.String() created_at = fields.DateTime() slides = fields.Nested(VersionCarouselSlideSchema, many=True) class UserVersionCarouselSchema(Schema): """User version carousel schema.""" id = fields.Integer() user_id = fields.String() created_at = fields.DateTime() carousel_id = fields.Integer() class AppLinksInputSchema(QueryWebArgs): """Input validation schema for Link""" client = fields.String(validate=validate.OneOf(constants.ALL_OS), required=True) source_page = fields.String(validate=validate.OneOf(LinkSourcePage.ALL), required=True) class PlaylistsSearchInputSchema(QueryWebArgs, PaginationMixin): """Playlists Search Input Schema.""" vendors = UniqueArgsListField( fields.String(validate=validate.OneOf(constants.VENDORS)), data_key="vendor", default=constants.VENDORS, missing=constants.VENDORS, ) query = fields.String(validate=validate.Length(min=2, max=50), required=True) items_size = fields.Integer(validate=validate.Range(min=1, max=100), default=10, missing=10) image_size = fields.Int( missing=constants.DEFAULT_IMAGE_SIZE, validate=validate.Range(max=APPLE_MAX_IMAGE_SIZE, min=1) ) @post_load def check_and_lowercase_field(self, data, **kwargs): search_query = data.get("query") if search_query: escaped_query = "" for char in search_query.lower().strip(): escaped_query += f"\\{char}" if char in ELASTICSEARCH_ESCAPING_CHARACTERS else char data["query"] = escaped_query return data class SearchHistoryInputSchema(QueryWebArgs, PaginationMixin): """Search history view input schema.""" vendors = UniqueArgsListField( fields.String(validate=validate.OneOf(constants.VENDORS)), data_key="vendor", default=[], missing=[] ) type = fields.String(validate=validate.OneOf(SearchHistoryData.TYPES), required=True) order_by = UniqueArgsListField( fields.String(validate=validate.OneOf(SearchHistoryData.ORDER_VALUES)), default=[SearchHistoryData.TIMESTAMP_DESC], missing=[SearchHistoryData.TIMESTAMP_DESC], ) class NPSSurveyOutputSchema(Schema): """NPS Survey data retrieving output schema.""" class NPSSurveyQuestionSchema(Schema): type = fields.String() message = fields.String() question_num = fields.Integer(data_key="step") labels = fields.Method("get_labels", allow_none=True, example=ApispecTypes.DICT) def get_labels(self, obj, **kwargs): return obj.labels @post_dump def jsonify_labels_str(self, data, **kwargs): """Convert labels string to object.""" labels = data.get("labels") if not labels: data["labels"] = None return data try: data["labels"] = json.loads(labels) except (TypeError, JSONDecodeError): data["labels"] = None return data class NPSSurveySchema(Schema): id = fields.String() start_date = fields.Date() end_date = fields.Date() active = fields.Boolean(default=False, missing=False) class NPSSurveyResultSchema(Schema): status = fields.String(missing=None, default=None) skips_num = fields.Integer(default=0) updated_at = fields.Method("get_updated_at") def get_updated_at(self, obj): return obj.updated_at.timestamp() if obj.updated_at else None survey = fields.Nested(NPSSurveySchema) questions = fields.Nested(NPSSurveyQuestionSchema, many=True) user_response = fields.Nested(NPSSurveyResultSchema) class NPSSurveyInputSchema(JsonBodySchema): """NPS Survey schema validation for POST body.""" class NPSSurveyAnswerSchema(Schema): step = fields.Integer(required=True) value = fields.Raw(required=True) @validates_schema def validate_value(self, data, **kwargs): value, step = data["value"], data["step"] if type(value) is str and len(value) > 1000: raise ValidationError(f"Step #{step} wrong value: Longer than maximum length 1000.") survey_id = fields.Integer(required=True) status = fields.String(validate=validate.OneOf(NPSResultStatus.ALL), required=True) name = fields.String(required=True) email = fields.Email(required=True) answers = fields.Nested(NPSSurveyAnswerSchema, required=True, many=True, data_key="user_response") @post_load def jsonify_user_response(self, data: dict) -> dict: answers = data["answers"] if answers is not None: data["answers"] = json.dumps(answers, default=str) return data class KeyValueStorageInputSchema(QueryWebArgs): """Key value storage view input schema.""" key = fields.String(required=True) class Tracks: """ No Response schema because of dynamic keys in response Dict. Used DictTemplate with example in @marshal_with decorator to generate swagger. """ class Request(QueryWebArgs, VendorRequiredMixin): """ Schema used with flask-apispec @use_kwargs decorator to load data from request query for track-info GET. """ track_ids = fields.List(fields.String(data_key="track_ids"), required=True) class HHTracks: """ Schema(s) for validating request and response of Hot Hits tracks """ class Request(QueryWebArgs, IsrcRequiredMixin, get_pagination_mixin(page_limit=None)): order_by = UniqueArgsListField( fields.String(validate=validate.OneOf(HHTrackOrder.values())), default=[HHTrackOrder.POSITION.value, HHTrackOrder.CODE.value], missing=[HHTrackOrder.POSITION.value, HHTrackOrder.CODE.value], ) include = SplitUniqueListField( fields.String(validate=validate.OneOf(HHTrackInclude.values(with_all=True))), default=[], missing=[] ) region = fields.String(missing=None, default=None) fields_with_all = {"include": HHTrackInclude} class Response(Schema): class HHTracksPlaylistResponseItem(Schema): code = fields.String() name = fields.String() position = fields.Integer() playlist_id = fields.String() playlist_name = fields.String() image_url = fields.String() added_days = fields.Integer() is_new = fields.Boolean() position_change = fields.Integer() top_market = fields.Integer() class HHTracksRegionResponseItem(Schema): name = fields.String() code = fields.String() placements = fields.Integer() total = fields.Integer() items = fields.List(fields.Nested(HHTracksPlaylistResponseItem)) total = fields.Integer() positions_avg = fields.Integer() positions_top_total = fields.Integer() regions = fields.List(fields.Nested(HHTracksRegionResponseItem)) class GTPHotHitsLatestDate: """ Schema(s) for getting latest Hot Hits date. """ class Request(Schema): include = SplitUniqueListField( fields.String(validate=validate.OneOf(HHLatestDateInclude.values(with_all=True))), default=[], missing=[] ) fields_with_all = {"include": HHLatestDateInclude}