import jsonschema from aiohttp import web from aiohttp_apispec import docs, headers_schema, json_schema, querystring_schema from apollo_utils.service.clients.aiohttp.utils.response import dump_response_schema from apollo_utils.service.exceptions import BadRequest, NotFound from apollo_utils.service.schemas.headers import AppHeader from sqlalchemy.engine import Row from typing import Tuple from server.constants.core import BASE_API_PREFIX, LEGACY_API_PREFIX from server.db.models.core import ApplicationSettingsHistory from server.db.models.users import Account, Settings, SettingsSchema from server.db.users import settings as db_settings from server.domains.users.settings import set_single_settings, settings_migration_handler from server.schemas.settings import SettingsBaseResponse, SettingsCreateUpdateRequest, SettingsGet, SettingsGetBulk, \ SettingsHeader, SettingsPostList from server.utils.pagination import paginate router = web.RouteTableDef() async def validate_data(app_slug: str, subtype: str, version: int, data: dict): """Validate data schema. Args: app_slug: Application slug. subtype: Application subtype. version: Settings version. data: Settings data. Raises: BadRequest: Bad data. """ schema = await SettingsSchema.get(app_slug=app_slug, type=subtype, version=version) if schema: try: jsonschema.validate(instance=data, schema=schema.data, format_checker=jsonschema.FormatChecker()) except jsonschema.ValidationError as ex: raise BadRequest(ex.message) async def check( app_slug: str, user_id: str, subtype: str or None, version: int, data: dict or None, raise_account: bool = False ) -> Tuple[Row, Row or None, str, bool]: """Validate application slug, user ID and subtype. Args: app_slug: Application slug. user_id: User ID. subtype: Application subtype if it exists. version: Settings version. data: Settings data. raise_account: Raise error if account is not found. Returns: Application and account DB records, subtype, account created flag. Raises: NotFound: Application or account are not found. BadRequest: Incorrect application subtype value or usage. """ application = await ApplicationSettingsHistory.get(slug=app_slug, version=version) if not application: raise NotFound("Application not found.") if application.types: if subtype not in application.types: if len(application.types) > 1 or subtype: raise BadRequest("Incorrect subtype.") subtype = application.types[0] elif subtype: raise BadRequest("Application does not have any subtypes.") created = False if raise_account: account = await Account.get(user_id=user_id, app_slug=app_slug) if not account: raise NotFound("Account not found.") else: account, created = await Account.get_or_create(user_id=user_id, app_slug=app_slug) if data: await validate_data(app_slug, subtype, version, data) return application, account, subtype, created async def get_or_create( app_slug: str, user_id: str, subtype: str or None, version: int, data: dict or None = None, is_inherited: bool = False, ) -> Tuple[Row, bool, dict]: """Get or create settings. Args: app_slug: Application slug. user_id: User ID. subtype: Application subtype if it exists. version: Version. data: Settings data to use, default from application if None. is_inherited: Define if we'd like to create new version of settings by inheriting old one's Returns: Settings DB records, a flag if settings were created or not and default value from application. """ application, account, subtype, is_created = await check(app_slug, user_id, subtype, version, data) setting = None if is_created else await Settings.get(account_id=account.id, type=subtype, version=version) default_settings = application.settings.get(subtype) if subtype else application.settings is_created = False if not setting and is_inherited: migrate_func = settings_migration_handler(version) setting = await migrate_func(account.id, subtype) is_created = True if not setting: is_created = True setting = await Settings.create( dict(account_id=account.id, type=subtype, version=version, data=data if data else default_settings) ) return setting, is_created, default_settings @router.get(LEGACY_API_PREFIX + "/settings/") @docs( tags=["settings", "legacy"], summary="Get or create settings (legacy version).", ) @headers_schema(SettingsHeader) @querystring_schema(SettingsGet.Request) @dump_response_schema(SettingsGet.Response, apply=False) async def get_or_create_settings(request: web.Request) -> web.Response: headers, params = request["headers"], request["querystring"] setting, _, default_setting = await get_or_create( headers["app"], headers["user_id"], headers.get("type"), headers.get("version"), is_inherited=params["is_inherited"], ) return web.json_response( {"id": setting.id, "account_id": setting.account_id, "data": setting.data, "default": default_setting} ) @router.post(LEGACY_API_PREFIX + "/settings/") @docs( tags=["settings", "legacy"], summary="Create settings (legacy version).", ) @headers_schema(SettingsHeader) @json_schema(SettingsCreateUpdateRequest) @dump_response_schema(SettingsBaseResponse, code=201, apply=True) async def create_settings(request: web.Request) -> web.Response: headers, data = request["headers"], request["json"]["data"] settings, is_created, _ = await get_or_create( headers["app"], headers["user_id"], headers.get("type"), headers.get("version"), data ) if not is_created: raise BadRequest(f"Settings already exist.") return settings @router.put(LEGACY_API_PREFIX + "/settings/") @docs( tags=["settings", "legacy"], summary="Update settings (legacy version).", ) @headers_schema(SettingsHeader) @json_schema(SettingsCreateUpdateRequest) @dump_response_schema(SettingsBaseResponse, apply=True) async def update_settings(request: web.Request) -> web.Response: headers, data = request["headers"], request["json"]["data"] version = headers["version"] _, account, subtype, _ = await check( headers["app"], headers["user_id"], headers["type"], version, data, raise_account=True ) account_id = account.id settings = await Settings.get(account_id=account_id, type=subtype, version=version) if not settings: raise NotFound("Settings not found.") settings = await Settings.update({"data": data}, id=settings.id, return_source=Settings) return settings @router.get(LEGACY_API_PREFIX + "/settings/bulk/") @docs( tags=["settings", "bulk", "legacy"], summary="List settings (legacy version).", ) @headers_schema(AppHeader) @querystring_schema(SettingsGetBulk.Request) @dump_response_schema(SettingsGetBulk.Response, apply=True) @paginate() async def get_settings_bulk(request: web.Request) -> web.Response: headers, params = request["headers"], request["querystring"] return await Settings.list( headers["app"], user_list=params.pop("user", None), type_list=params.pop("type", None), version_list=params.pop("version", None), **params, ) @router.post(BASE_API_PREFIX + "/service/users/settings/list/") @docs(tags=["settings", "list"], summary="List settings.") @headers_schema(AppHeader) @json_schema(SettingsPostList.Request) @dump_response_schema(SettingsPostList.Response, apply=False) async def post_settings_list(request: web.Request) -> web.Response: headers, params = request["headers"], request["json"] result = await db_settings.get_accounts_with_include(headers["app"], **params) set_single_settings(result) return SettingsPostList.Response(exclude=[] if params["include"] else ["data.devices"]).dump({"data": result})