diff --git a/neon_users_service/databases/__init__.py b/neon_users_service/databases/__init__.py index d30f48e..beaf7a7 100644 --- a/neon_users_service/databases/__init__.py +++ b/neon_users_service/databases/__init__.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from neon_users_service.exceptions import UserNotFoundError -from neon_users_service.models import User +from neon_data_models.models.user.database import User class UserDatabase(ABC): diff --git a/neon_users_service/databases/sqlite.py b/neon_users_service/databases/sqlite.py index 33074f3..8d49444 100644 --- a/neon_users_service/databases/sqlite.py +++ b/neon_users_service/databases/sqlite.py @@ -8,7 +8,7 @@ from neon_users_service.databases import UserDatabase from neon_users_service.exceptions import UserNotFoundError, UserExistsError, DatabaseError -from neon_users_service.models import User, AccessRoles +from neon_data_models.models.user.database import User class SQLiteUserDatabase(UserDatabase): diff --git a/neon_users_service/models.py b/neon_users_service/models.py deleted file mode 100644 index c234ac5..0000000 --- a/neon_users_service/models.py +++ /dev/null @@ -1,150 +0,0 @@ -from time import time -from typing import Dict, Any, List, Literal, Optional -from enum import IntEnum -from uuid import uuid4 -from pydantic import BaseModel, Field -from datetime import date - - -class AccessRoles(IntEnum): - """ - Defines access roles such that a larger value always corresponds to more - permissions. `0` equates to no permission, negative numbers correspond to - non-user roles. In this way, an activity can require, for example, - `permission > AccessRoles.GUEST` to grant access to all registered users, - admins, and owners. - """ - NONE = 0 - GUEST = 1 - USER = 2 - ADMIN = 3 - OWNER = 4 - - NODE = -1 - - -class _UserConfig(BaseModel): - first_name: str = "" - middle_name: str = "" - last_name: str = "" - preferred_name: str = "" - dob: Optional[date] = None - email: str = "" - avatar_url: str = "" - about: str = "" - phone: str = "" - phone_verified: bool = False - email_verified: bool = False - - -class _LanguageConfig(BaseModel): - input_languages: List[str] = ["en-us"] - output_languages: List[str] = ["en-us"] - - -class _UnitsConfig(BaseModel): - time: Literal[12, 24] = 12 - date: Literal["MDY", "YMD", "YDM", "DMY"] = "MDY" - measure: Literal["imperial", "metric"] = "imperial" - - -class _ResponseConfig(BaseModel): - hesitation: bool = False - limit_dialog: bool = False - tts_gender: Literal["male", "female"] = "female" - tts_speed_multiplier: float = 1.0 - - -class _LocationConfig(BaseModel): - latitude: Optional[float] = None - longitude: Optional[float] = None - name: Optional[str] = None - timezone: Optional[str] = None - - -class _PrivacyConfig(BaseModel): - save_text: bool = True - save_audio: bool = False - - -class NeonUserConfig(BaseModel): - """ - Defines user configuration used in Neon Core. - """ - skills: Dict[str, Dict[str, Any]] = {} - user: _UserConfig = _UserConfig() - # Former `speech` schema is replaced by `language` which is a more general - # format. - language: _LanguageConfig = _LanguageConfig() - units: _UnitsConfig = _UnitsConfig() - # Former `location` schema is replaced here with a minimal spec from which - # the remaining values may be calculated - location: _LocationConfig = _LocationConfig() - response_mode: _ResponseConfig = _ResponseConfig() - privacy: _PrivacyConfig = _PrivacyConfig() - - -class KlatConfig(BaseModel): - """ - Defines user configuration used in PyKlatChat. - """ - is_tmp: bool = True - preferences: Dict[str, Any] = {} - - -class BrainForgeConfig(BaseModel): - """ - Defines configuration used in BrainForge LLM applications. - """ - inference_access: Dict[str, Dict[str, List[str]]] = {} - - -class PermissionsConfig(BaseModel): - """ - Defines roles for supported projects/service families. - """ - klat: AccessRoles = AccessRoles.NONE - core: AccessRoles = AccessRoles.NONE - diana: AccessRoles = AccessRoles.NONE - node: AccessRoles = AccessRoles.NONE - hub: AccessRoles = AccessRoles.NONE - llm: AccessRoles = AccessRoles.NONE - - class Config: - use_enum_values = True - - -class TokenConfig(BaseModel): - username: str - client_id: str - permissions: dict - refresh_token: str - expiration: int - refresh_expiration: int - token_name: str - creation_timestamp: int - last_refresh_timestamp: int - access_token: Optional[str] = None - - -class User(BaseModel): - username: str - password_hash: Optional[str] = None - user_id: str = Field(default_factory=lambda: str(uuid4())) - created_timestamp: int = Field(default_factory=lambda: round(time())) - neon: NeonUserConfig = NeonUserConfig() - klat: KlatConfig = KlatConfig() - llm: BrainForgeConfig = BrainForgeConfig() - permissions: PermissionsConfig = PermissionsConfig() - tokens: Optional[List[TokenConfig]] = [] - - def __eq__(self, other): - return self.model_dump() == other.model_dump() - - -class MQRequest(BaseModel): - operation: Literal["create", "read", "update", "delete"] - username: str - password: Optional[str] = None - access_token: Optional[str] = None - user: Optional[User] = None diff --git a/neon_users_service/mq_connector.py b/neon_users_service/mq_connector.py index c85fa20..8983892 100644 --- a/neon_users_service/mq_connector.py +++ b/neon_users_service/mq_connector.py @@ -6,7 +6,8 @@ from neon_mq_connector.connector import MQConnector from neon_mq_connector.utils.network_utils import b64_to_dict, dict_to_b64 from neon_users_service.exceptions import UserNotFoundError, AuthenticationError, UserNotMatchedError, UserExistsError -from neon_users_service.models import MQRequest, User +from neon_data_models.models.user.database import User +from neon_data_models.models.api.mq import UserDbRequest from neon_users_service.service import NeonUsersService @@ -20,7 +21,7 @@ def __init__(self, config: Optional[dict], service_name: str = "neon_users_servi self.service = NeonUsersService(module_config) def parse_mq_request(self, mq_req: dict) -> dict: - mq_req = MQRequest(**mq_req) + mq_req = UserDbRequest(**mq_req) # Ensure supplied `user` object is consistent with request params if mq_req.user and mq_req.username != mq_req.user.username: diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0f381c0..0c5de65 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,3 +1,4 @@ pydantic~=2.0 ovos-config~=0.1 -ovos-utils~=0.0 \ No newline at end of file +ovos-utils~=0.0 +neon-data-models \ No newline at end of file diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index 6a492c0..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,94 +0,0 @@ -from unittest import TestCase - -from pydantic import ValidationError -from datetime import date -from neon_users_service.models import NeonUserConfig, TokenConfig, User, MQRequest - - -class TestModelValidation(TestCase): - def test_neon_user_config(self): - default = NeonUserConfig() - valid_with_extras = NeonUserConfig( - user={"first_name": "Daniel", - "middle_name": "James", - "last_name": "McKnight", - "preferred_name": "tester", - "dob": "2001-01-01", - "email": "developers@neon.ai", - "extra_key": "This should be removed by validation" - }, - language={"input": ["en-us", "uk-ua"], - "output": ["en-us", "es-es"]}, - units={"time": 24, "date": "YMD", "measure": "metric"}, - location={"latitude": 47.6765382, "longitude": -122.2070775, - "name": "Kirkland, WA", - "timezone": "America/Los_Angeles"}, - extra_section={"test": True} - ) - - # Ensure invalid keys are removed and defaults are added - self.assertEqual(default.model_dump().keys(), - valid_with_extras.model_dump().keys()) - for section in default.model_dump().keys(): - if section == "skills": - # `skills` is not a model, the contents are arbitrary - continue - default_keys = getattr(default, section).model_dump().keys() - extras_keys = getattr(valid_with_extras, section).model_dump().keys() - self.assertEqual(default_keys, extras_keys) - - # Validation errors - with self.assertRaises(ValidationError): - NeonUserConfig(units={"time": 13}) - with self.assertRaises(ValidationError): - NeonUserConfig(location={"latitude": "test"}) - with self.assertRaises(ValidationError): - NeonUserConfig(user={"dob": "01/01/2001"}) - - # Valid type casting - config = NeonUserConfig(location={"latitude": "47.6765382", - "longitude": "-122.2070775"}) - self.assertIsInstance(config.location.latitude, float) - self.assertIsInstance(config.location.longitude, float) - - config = NeonUserConfig(user={"dob": "2001-01-01"}) - self.assertIsInstance(config.user.dob, date) - - def test_user_model(self): - user_kwargs = dict(username="test", - password_hash="test", - tokens=[{"description": "test", - "client_id": "test_id", - "expiration_timestamp": 0, - "refresh_token": "", - "last_used_timestamp": 0}]) - default_user = User(**user_kwargs) - self.assertIsInstance(default_user.tokens[0], TokenConfig) - with self.assertRaises(ValidationError): - User() - - with self.assertRaises(ValidationError): - User(username="test", password_hash="test", - tokens=[{"description": "test"}]) - - duplicate_user = User(**user_kwargs) - self.assertNotEqual(default_user, duplicate_user) - self.assertEqual(default_user.tokens, duplicate_user.tokens) - - def test_mq_request_model(self): - valid_model = MQRequest(operation="create", username="test_user") - self.assertIsInstance(valid_model, MQRequest) - with self.assertRaises(ValidationError): - MQRequest(operation="get", username="test") - with self.assertRaises(ValidationError): - MQRequest(operation="delete", username="test_user", user="test_user") - - -class TestEnumClasses(TestCase): - def test_access_roles(self): - from neon_users_service.models import AccessRoles - self.assertGreater(AccessRoles.OWNER, AccessRoles.ADMIN) - self.assertGreater(AccessRoles.ADMIN, AccessRoles.USER) - self.assertGreater(AccessRoles.USER, AccessRoles.GUEST) - self.assertGreater(AccessRoles.GUEST, AccessRoles.NONE) - self.assertFalse(AccessRoles.NONE)