From b1c8203a33e382587f6cc927323a1af7597717da Mon Sep 17 00:00:00 2001 From: berkay Date: Fri, 4 Apr 2025 12:03:00 +0300 Subject: [PATCH] updated events initializer --- ApiServices/AuthService/create_route.py | 3 + ApiServices/AuthService/endpoints/__init__.py | 5 + .../AuthService/endpoints/auth/route.py | 1 + ApiServices/AuthService/events/auth/auth.py | 564 +++++++++++++++++- .../AuthService/validations/custom/token.py | 2 + ApiServices/InitialService/Dockerfile | 28 + ApiServices/InitialService/app.py | 18 + ApiServices/README.md | 2 + ApiServices/TemplateService/__init__.py | 0 ApiServices/TemplateService/create_app.py | 10 +- ApiServices/TemplateService/create_route.py | 14 - .../endpoints/test_template/route.py | 16 +- .../TemplateService/events/template/event.py | 57 ++ .../TemplateService/initializer/__init__.py | 0 .../initializer/create_route.py | 41 ++ .../initializer/event_clusters.py | 84 +++ .../TemplateService/services/__init__.py | 0 ApiServices/__init__.py | 0 Controllers/Postgres/crud.py | 57 +- Controllers/Postgres/mixin.py | 74 ++- Modules/Token/password_module.py | 1 + Schemas/__init__.py | 2 + Schemas/event/event.py | 6 +- Schemas/rules/rules.py | 3 + 24 files changed, 874 insertions(+), 114 deletions(-) create mode 100644 ApiServices/InitialService/Dockerfile create mode 100644 ApiServices/InitialService/app.py create mode 100644 ApiServices/TemplateService/__init__.py delete mode 100644 ApiServices/TemplateService/create_route.py create mode 100644 ApiServices/TemplateService/events/template/event.py create mode 100644 ApiServices/TemplateService/initializer/__init__.py create mode 100644 ApiServices/TemplateService/initializer/create_route.py create mode 100644 ApiServices/TemplateService/initializer/event_clusters.py create mode 100644 ApiServices/TemplateService/services/__init__.py create mode 100644 ApiServices/__init__.py diff --git a/ApiServices/AuthService/create_route.py b/ApiServices/AuthService/create_route.py index bc8545c..e2796c7 100644 --- a/ApiServices/AuthService/create_route.py +++ b/ApiServices/AuthService/create_route.py @@ -8,7 +8,10 @@ class RouteRegisterController: self.router_list = router_list self.app = app + + def register_routes(self): for router in self.router_list: self.app.include_router(router) + self.add_router_to_database(router) return self.app diff --git a/ApiServices/AuthService/endpoints/__init__.py b/ApiServices/AuthService/endpoints/__init__.py index e69de29..55137e0 100644 --- a/ApiServices/AuthService/endpoints/__init__.py +++ b/ApiServices/AuthService/endpoints/__init__.py @@ -0,0 +1,5 @@ +from .auth.route import auth_route + +__all__ = [ + "auth_route", +] \ No newline at end of file diff --git a/ApiServices/AuthService/endpoints/auth/route.py b/ApiServices/AuthService/endpoints/auth/route.py index 3776228..573304c 100644 --- a/ApiServices/AuthService/endpoints/auth/route.py +++ b/ApiServices/AuthService/endpoints/auth/route.py @@ -41,6 +41,7 @@ def authentication_login_post( "language": language or "", "domain": domain or "", "eys-ext": f"{str(uuid.uuid4())}", + "timezone": tz or "GMT+3", } if not domain or not language: return JSONResponse( diff --git a/ApiServices/AuthService/events/auth/auth.py b/ApiServices/AuthService/events/auth/auth.py index 596f912..17b2135 100644 --- a/ApiServices/AuthService/events/auth/auth.py +++ b/ApiServices/AuthService/events/auth/auth.py @@ -1,2 +1,564 @@ +from typing import Any, List, Dict, Optional, Union +from ApiServices.AuthService.validations.custom.token import ( + EmployeeTokenObject, + OccupantTokenObject, + CompanyToken, + OccupantToken, + UserType, +) +from ApiServices.TemplateService.config import api_config +from Schemas import ( + Users, + People, + UsersTokens, + Credentials, + BuildLivingSpace, + BuildParts, + OccupantTypes, + Employees, + Addresses, + Companies, + Staff, + Duty, + Duties, + Departments, + Event2Employee, +) +from Modules.Token.password_module import PasswordModule +from Controllers.Redis.database import RedisActions +from Schemas.building.build import RelationshipEmployee2Build +from Schemas.event.event import Event2Occupant + +TokenDictType = Union[EmployeeTokenObject, OccupantTokenObject] + +class RedisHandlers: + AUTH_TOKEN: str = "AUTH_TOKEN" + + @classmethod + def process_redis_object(cls, redis_object: Dict[str, Any]) -> TokenDictType: + """Process Redis object and return appropriate token object.""" + if not redis_object.get("selected_company"): + redis_object["selected_company"] = None + if not redis_object.get("selected_occupant"): + redis_object["selected_occupant"] = None + if redis_object.get("user_type") == UserType.employee.value: + return EmployeeTokenObject(**redis_object) + elif redis_object.get("user_type") == UserType.occupant.value: + return OccupantTokenObject(**redis_object) + raise ValueError("Invalid user type") + + @classmethod + def get_object_from_redis(cls, access_token: str) -> TokenDictType: + redis_response = RedisActions.get_json( + list_keys=[RedisHandlers.AUTH_TOKEN, access_token, "*"] + ) + if not redis_response.status: + raise ValueError("EYS_0001") + if redis_object := redis_response.first: + return cls.process_redis_object(redis_object) + raise ValueError("EYS_0002") + + @classmethod + def set_object_to_redis(cls, user: Users, token, header_info): + result_delete = RedisActions.delete( + list_keys=[RedisHandlers.AUTH_TOKEN, "*", str(user.uu_id)] + ) + print('result_delete', result_delete) + generated_access_token = PasswordModule.generate_access_token() + keys = [RedisHandlers.AUTH_TOKEN, generated_access_token, str(user.uu_id)] + RedisActions.set_json( + list_keys=keys, + value={**token, **header_info}, + expires={"hours": 1, "minutes": 30}, + ) + return generated_access_token + + @classmethod + def update_token_at_redis(cls, token: str, add_payload: Union[CompanyToken, OccupantToken]): + if already_token_data := RedisActions.get_json( + list_keys=[RedisHandlers.AUTH_TOKEN, token, "*"] + ).first: + already_token = cls.process_redis_object(**already_token_data) + if already_token.is_employee: + already_token.selected_company = add_payload + elif already_token.is_occupant: + already_token.selected_occupant = add_payload + result = RedisActions.set_json( + list_keys=[RedisHandlers.AUTH_TOKEN, token, str(already_token.user_uu_id)], + value=already_token.model_dump(), + expires={"hours": 1, "minutes": 30}, + ) + return result.first + raise ValueError("Something went wrong") + + +class UserHandlers: + + @staticmethod + def check_user_exists(access_key: str, db_session) -> Users: + """ + Check if the user exists in the database. + """ + if "@" in access_key: + found_user: Users = Users.filter_one( + Users.email == access_key.lower(), db=db_session + ).data + else: + found_user: Users = Users.filter_one( + Users.phone_number == access_key.replace(" ", ""), db=db_session + ).data + if not found_user: + ValueError("EYS_0003") + return found_user + + @staticmethod + def check_password_valid( + domain: str, id_: str, password: str, password_hashed: str + ) -> bool: + """ + Check if the password is valid. + """ + if PasswordModule.check_password( + domain=domain, id_=id_, password=password, password_hashed=password_hashed + ): + return True + raise ValueError("EYS_0004") + + +class LoginHandler: + + @staticmethod + def is_occupant(email: str): + return not str(email).split("@")[1] == api_config.ACCESS_EMAIL_EXT + + @staticmethod + def is_employee(email: str): + return str(email).split("@")[1] == api_config.ACCESS_EMAIL_EXT + + + @classmethod + def do_employee_login( + cls, request: Any, data: Any, extra_dict: Optional[Dict[str, Any]] = None + ): + """ + Handle employee login. + """ + language = extra_dict.get("language", "tr") + domain = extra_dict.get("domain", None) + timezone = extra_dict.get("tz", None) or "GMT+3" + + user_handler = UserHandlers() + with Users.new_session() as db_session: + found_user = user_handler.check_user_exists( + access_key=data.access_key, db_session=db_session + ) + + if not user_handler.check_password_valid( + domain=data.domain, + id_=str(found_user.uu_id), + password=data.password, + password_hashed=found_user.hash_password + ): + raise ValueError("EYS_0005") + + list_employee = Employees.filter_all( + Employees.people_id == found_user.person_id, db=db_session + ).data + + companies_uu_id_list: list = [] + companies_id_list: list = [] + companies_list: list = [] + duty_uu_id_list: list = [] + duty_id_list: list = [] + + for employee in list_employee: + staff = Staff.filter_one( + Staff.id == employee.staff_id, db=db_session + ).data + if duties := Duties.filter_one( + Duties.id == staff.duties_id, db=db_session + ).data: + if duty_found := Duty.filter_by_one( + id=duties.duties_id, db=db_session + ).data: + duty_uu_id_list.append(str(duty_found.uu_id)) + duty_id_list.append(duty_found.id) + + department = Departments.filter_one( + Departments.id == duties.department_id, db=db_session + ).data + + if company := Companies.filter_one( + Companies.id == department.company_id, db=db_session + ).data: + companies_uu_id_list.append(str(company.uu_id)) + companies_id_list.append(company.id) + company_address = Addresses.filter_by_one( + id=company.official_address_id, db=db_session + ).data + companies_list.append( + { + "uu_id": str(company.uu_id), + "public_name": company.public_name, + "company_type": company.company_type, + "company_address": company_address, + } + ) + person = People.filter_one( + People.id == found_user.person_id, db=db_session + ).data + model_value = EmployeeTokenObject( + user_type=UserType.employee.value, + user_uu_id=str(found_user.uu_id), + user_id=found_user.id, + person_id=found_user.person_id, + person_uu_id=str(person.uu_id), + request=dict(request.headers), + companies_uu_id_list=companies_uu_id_list, + companies_id_list=companies_id_list, + duty_uu_id_list=duty_uu_id_list, + duty_id_list=duty_id_list, + ).model_dump() + + set_to_redis_dict = dict( + user=found_user, + token=model_value, + header_info=dict(language=language, domain=domain, timezone=timezone), + ) + redis_handler = RedisHandlers() + if access_token := redis_handler.set_object_to_redis(**set_to_redis_dict): + return { + "access_token": access_token, + "user_type": UserType.employee.name, + "selection_list": companies_list, + } + raise ValueError("Something went wrong") + + @classmethod + def do_employee_occupant( + cls, request: Any, data: Any, extra_dict: Optional[Dict[str, Any]] = None + ): + """ + Handle occupant login. + """ + language = extra_dict.get("language", "tr") + domain = extra_dict.get("domain", None) + timezone = extra_dict.get("tz", None) or "GMT+3" + + user_handler = UserHandlers() + with Users.new_session() as db_session: + found_user = user_handler.check_user_exists( + access_key=data.access_key, db_session=db_session + ) + if not user_handler.check_password_valid( + domain=data.domain, + id_=str(found_user.uu_id), + password=data.password, + password_hashed=found_user.hash_password + ): + raise ValueError("EYS_0005") + + occupants_selection_dict: Dict[str, Any] = {} + living_spaces: list[BuildLivingSpace] = BuildLivingSpace.filter_all( + BuildLivingSpace.person_id == found_user.person_id, db=db_session + ).data + + if not living_spaces: + raise ValueError("EYS_0006") + for living_space in living_spaces: + build_parts_selection = BuildParts.filter_all( + BuildParts.id == living_space.build_parts_id, + db=db_session, + ).data + if not build_parts_selection: + raise ValueError("EYS_0007") + + build_part = build_parts_selection[0] + + build = build_part.buildings + occupant_type = OccupantTypes.filter_by_one( + id=living_space.occupant_type, + db=db_session, + system=True, + ).data + + occupant_data = { + "part_uu_id": str(build_part.uu_id), + "part_name": build_part.part_name, + "part_level": build_part.part_level, + "uu_id": str(occupant_type.uu_id), + "description": occupant_type.occupant_description, + "code": occupant_type.occupant_code, + } + + build_key = str(build.uu_id) + if build_key not in occupants_selection_dict: + occupants_selection_dict[build_key] = { + "build_uu_id": build_key, + "build_name": build.build_name, + "build_no": build.build_no, + "occupants": [occupant_data], + } + else: + occupants_selection_dict[build_key]["occupants"].append(occupant_data) + + person = found_user.person + model_value = OccupantTokenObject( + user_type=UserType.occupant.value, + user_uu_id=str(found_user.uu_id), + user_id=found_user.id, + person_id=person.id, + person_uu_id=str(person.uu_id), + request=dict(request.headers), + available_occupants=occupants_selection_dict, + ).model_dump() + redis_handler = RedisHandlers() + if access_token := redis_handler.set_object_to_redis( + user=found_user, + token=model_value, + header_info=dict(language=language, domain=domain, timezone=timezone), + ): + return { + "access_token": access_token, + "user_type": UserType.occupant.name, + "selection_list": occupants_selection_dict, + } + raise ValueError("Something went wrong") + + @classmethod + def authentication_login_with_domain_and_creds(cls, request: Any, data: Any): + """ + Authenticate user with domain and credentials. + + Args: + request: FastAPI request object + data: Request body containing login credentials + { + "data": { + "domain": "evyos.com.tr", + "access_key": "karatay.berkay.sup@evyos.com.tr", + "password": "string", + "remember_me": false + } + } + Returns: + SuccessResponse containing authentication token and user info + """ + language = request.headers("language", "tr") + domain = request.headers("domain", None) + timezone = request.headers("tz", None) or "GMT+3" + + if cls.is_employee(data.access_key): + return cls.do_employee_login( + request=request, + data=data, + extra_dict=dict( + language=language, + domain=domain, + timezone=timezone, + ), + ) + elif cls.is_occupant(data.access_key): + return cls.do_employee_login( + request=request, + data=data, + extra_dict=dict( + language=language, + domain=domain, + timezone=timezone, + ), + ) + else: + raise ValueError("Invalid email format") + + @classmethod + def raise_error_if_request_has_no_token(cls, request: Any) -> None: + """Validate request has required token headers.""" + if not hasattr(request, "headers"): + raise ValueError("Request has no headers") + if not request.headers.get(api_config.ACCESS_TOKEN_TAG): + raise ValueError("Request has no access token") + + @classmethod + def get_access_token_from_request(cls, request: Any) -> str: + """Extract access token from request headers.""" + cls.raise_error_if_request_has_no_token(request=request) + return request.headers.get(api_config.ACCESS_TOKEN_TAG) + + @classmethod + def handle_employee_selection(cls, access_token: str, data: Any, token_dict: TokenDictType): + with Users.new_session() as db: + if data.company_uu_id not in token_dict.companies_uu_id_list: + ValueError("EYS_0011") + selected_company: Companies = Companies.filter_one( + Companies.uu_id == data.company_uu_id, db=db + ).data + if not selected_company: + ValueError("EYS_0009") + + # Get duties IDs for the company + duties_ids = [ + duty.id + for duty in Duties.filter_all( + Duties.company_id == selected_company.id, db=db + ).data + ] + + # Get staff IDs + staff_ids = [ + staff.id + for staff in Staff.filter_all(Staff.duties_id.in_(duties_ids), db=db).data + ] + + # Get employee + employee: Employees = Employees.filter_one( + Employees.people_id == token_dict.person_id, + Employees.staff_id.in_(staff_ids), + db=db, + ).data + + if not employee: + ValueError("EYS_0010") + + # Get reachable events + reachable_event_codes = Event2Employee.get_event_codes( + employee_id=employee.id, db=db + ) + + # Get staff and duties + staff = Staff.filter_one(Staff.id == employee.staff_id, db=db).data + duties = Duties.filter_one(Duties.id == staff.duties_id, db=db).data + department = Departments.filter_one( + Departments.id == duties.department_id, db=db + ).data + + # Get bulk duty + bulk_id = Duty.filter_by_one_system(duty_code="BULK", db=db).data + bulk_duty_id = Duties.filter_by_one( + company_id=selected_company.id, + duties_id=bulk_id.id, + db=db, + ).data + + # Create company token + company_token = CompanyToken( + company_uu_id=selected_company.uu_id.__str__(), + company_id=selected_company.id, + department_id=department.id, + department_uu_id=department.uu_id.__str__(), + duty_id=duties.id, + duty_uu_id=duties.uu_id.__str__(), + bulk_duties_id=bulk_duty_id.id, + staff_id=staff.id, + staff_uu_id=staff.uu_id.__str__(), + employee_id=employee.id, + employee_uu_id=employee.uu_id.__str__(), + reachable_event_codes=reachable_event_codes, + ) + redis_handler = RedisHandlers() + try: # Update Redis + return redis_handler.update_token_at_redis( + token=access_token, add_payload=company_token + ) + except Exception as e: + err = e + ValueError("EYS_0008") + + @classmethod + def handle_occupant_selection( + cls, access_token: str, data: Any, token_dict: TokenDictType + ): + """Handle occupant type selection""" + with BuildLivingSpace.new_session() as db: + # Get selected occupant type + selected_build_living_space: BuildLivingSpace = BuildLivingSpace.filter_one( + BuildLivingSpace.uu_id == data.build_living_space_uu_id, + db=db, + ).data + if not selected_build_living_space: + raise ValueError("EYS_0012") + + # Get reachable events + reachable_event_codes = Event2Occupant.get_event_codes( + build_living_space_id=selected_build_living_space.id, db=db + ) + occupant_type = OccupantTypes.filter_one_system( + OccupantTypes.id == selected_build_living_space.occupant_type_id, + db=db, + ).data + build_part = BuildParts.filter_one( + BuildParts.id == selected_build_living_space.build_parts_id, + db=db, + ).data + build = BuildParts.filter_one( + BuildParts.id == build_part.build_id, + db=db, + ).data + responsible_employee = Employees.filter_one( + Employees.id == build_part.responsible_employee_id, + db=db, + ).data + related_company = RelationshipEmployee2Build.filter_one( + RelationshipEmployee2Build.member_id == build.id, + db=db, + ).data + # Get company + company_related = Companies.filter_one( + Companies.id == related_company.company_id, + db=db, + ).data + + # Create occupant token + occupant_token = OccupantToken( + living_space_id=selected_build_living_space.id, + living_space_uu_id=selected_build_living_space.uu_id.__str__(), + occupant_type_id=occupant_type.id, + occupant_type_uu_id=occupant_type.uu_id.__str__(), + occupant_type=occupant_type.occupant_type, + build_id=build.id, + build_uuid=build.uu_id.__str__(), + build_part_id=build_part.id, + build_part_uuid=build_part.uu_id.__str__(), + responsible_employee_id=responsible_employee.id, + responsible_employee_uuid=responsible_employee.uu_id.__str__(), + responsible_company_id=company_related.id, + responsible_company_uuid=company_related.uu_id.__str__(), + reachable_event_codes=reachable_event_codes, + ) + redis_handler = RedisHandlers() + try: # Update Redis + return redis_handler.update_token_at_redis( + token=access_token, add_payload=occupant_token + ) + except Exception as e: + raise ValueError("EYS_0008") + + @classmethod # Requires auth context + def authentication_select_company_or_occupant_type(cls, request: Any, data: Any): + """ + Handle selection of company or occupant type + {"data": {"build_living_space_uu_id": ""}} | {"data": {"company_uu_id": ""}} + { + "data": {"company_uu_id": "e9869a25-ba4d-49dc-bb0d-8286343b184b"} + } + { + "data": {"build_living_space_uu_id": "e9869a25-ba4d-49dc-bb0d-8286343b184b"} + } + """ + access_token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None) + if not access_token: + raise ValueError("EYS_0001") + + token_object = RedisHandlers().get_object_from_redis(access_token=access_token) + if token_object.is_employee and isinstance(data, CompanyToken): + return cls.handle_employee_selection( + access_token=access_token, data=data, token_dict=token_object, + ) + elif token_object.is_occupant and isinstance(data, OccupantToken): + return cls.handle_occupant_selection( + access_token=access_token, data=data, token_dict=token_object, + ) + + class AuthHandlers: - pass + LoginHandler: LoginHandler = LoginHandler() diff --git a/ApiServices/AuthService/validations/custom/token.py b/ApiServices/AuthService/validations/custom/token.py index 3152533..278e792 100644 --- a/ApiServices/AuthService/validations/custom/token.py +++ b/ApiServices/AuthService/validations/custom/token.py @@ -60,6 +60,7 @@ class OccupantToken(BaseModel): responsible_employee_uuid: Optional[str] = None reachable_event_codes: Optional[list[str]] = None # ID list of reachable modules + reachable_app_codes: Optional[list[str]] = None # ID list of reachable modules class CompanyToken(BaseModel): @@ -83,6 +84,7 @@ class CompanyToken(BaseModel): bulk_duties_id: int reachable_event_codes: Optional[list[str]] = None # ID list of reachable modules + reachable_app_codes: Optional[list[str]] = None # ID list of reachable modules class OccupantTokenObject(ApplicationToken): diff --git a/ApiServices/InitialService/Dockerfile b/ApiServices/InitialService/Dockerfile new file mode 100644 index 0000000..40dfc17 --- /dev/null +++ b/ApiServices/InitialService/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.12-slim + +WORKDIR / + +# Install system dependencies and Poetry +RUN apt-get update && apt-get install -y --no-install-recommends gcc \ + && rm -rf /var/lib/apt/lists/* && pip install --no-cache-dir poetry + +# Copy Poetry configuration +COPY /pyproject.toml ./pyproject.toml + +# Configure Poetry and install dependencies with optimizations +RUN poetry config virtualenvs.create false \ + && poetry install --no-interaction --no-ansi --no-root --only main \ + && pip cache purge && rm -rf ~/.cache/pypoetry + +# Copy application code +COPY /ApiServices/InitialService /ApiServices/InitialService +COPY /Controllers /Controllers +COPY /Schemas/building /Schemas/building +COPY /Schemas/company /Schemas/company +COPY /Schemas/identity /Schemas/identity + +# Set Python path to include app directory +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +# Run the application using the configured uvicorn server +CMD ["poetry", "run", "python", "ApiServices/InitialService/app.py"] diff --git a/ApiServices/InitialService/app.py b/ApiServices/InitialService/app.py new file mode 100644 index 0000000..7dc0395 --- /dev/null +++ b/ApiServices/InitialService/app.py @@ -0,0 +1,18 @@ +from Schemas import ( + BuildLivingSpace, + BuildParts, + Companies, + Departments, + Duties, + Duty, + Staff, + Employees, + Event2Employee, + Event2Occupant, + OccupantTypes, + Users, + UsersTokens, +) + +if __name__ == "__main__": + pass \ No newline at end of file diff --git a/ApiServices/README.md b/ApiServices/README.md index f8747e5..bc5c6d6 100644 --- a/ApiServices/README.md +++ b/ApiServices/README.md @@ -17,3 +17,5 @@ AccountCreate: ApplicationService -> Serves only app pages that are reachable for Client Side + +IdentityService -> Super User Attach related events to user diff --git a/ApiServices/TemplateService/__init__.py b/ApiServices/TemplateService/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/TemplateService/create_app.py b/ApiServices/TemplateService/create_app.py index 4a1b3e6..334a6a3 100644 --- a/ApiServices/TemplateService/create_app.py +++ b/ApiServices/TemplateService/create_app.py @@ -1,18 +1,18 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse -from fastapi.staticfiles import StaticFiles -from ApiServices.TemplateService.create_route import RouteRegisterController from ApiServices.TemplateService.endpoints.routes import get_routes from ApiServices.TemplateService.open_api_creator import create_openapi_schema from ApiServices.TemplateService.middlewares.token_middleware import token_middleware -from ApiServices.TemplateService.config import template_api_config +from ApiServices.TemplateService.initializer.create_route import RouteRegisterController + +from .config import api_config def create_app(): - application = FastAPI(**template_api_config.api_info) + application = FastAPI(**api_config.api_info) # application.mount( # "/application/static", # StaticFiles(directory="application/static"), @@ -20,7 +20,7 @@ def create_app(): # ) application.add_middleware( CORSMiddleware, - allow_origins=template_api_config.ALLOW_ORIGINS, + allow_origins=api_config.ALLOW_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/ApiServices/TemplateService/create_route.py b/ApiServices/TemplateService/create_route.py deleted file mode 100644 index bc8545c..0000000 --- a/ApiServices/TemplateService/create_route.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import List -from fastapi import APIRouter, FastAPI - - -class RouteRegisterController: - - def __init__(self, app: FastAPI, router_list: List[APIRouter]): - self.router_list = router_list - self.app = app - - def register_routes(self): - for router in self.router_list: - self.app.include_router(router) - return self.app diff --git a/ApiServices/TemplateService/endpoints/test_template/route.py b/ApiServices/TemplateService/endpoints/test_template/route.py index 1977b47..d79b7d9 100644 --- a/ApiServices/TemplateService/endpoints/test_template/route.py +++ b/ApiServices/TemplateService/endpoints/test_template/route.py @@ -1,14 +1,28 @@ from fastapi import APIRouter, Request, Response +from ApiServices.TemplateService.events.events_setter import event_cluster + test_template_route = APIRouter(prefix="/test", tags=["Test"]) -@test_template_route.get(path="/template", description="Test Template Route") +@test_template_route.get( + path="/template", + description="Test Template Route", + operation_id="bb20c8c6-a289-4cab-9da7-34ca8a36c8e5" +) def test_template(request: Request, response: Response): """ Test Template Route """ headers = dict(request.headers) + event_cluster_matched = event_cluster.match_event( + event_keys=[ + "3f510dcf-9f84-4eb9-b919-f582f30adab1", + "9f403034-deba-4e1f-b43e-b25d3c808d39", + "b8ec6e64-286a-4f60-8554-7a3865454944" + ] + ) + event_cluster_matched.example_callable() response.headers["X-Header"] = "Test Header GET" return { "completed": True, diff --git a/ApiServices/TemplateService/events/template/event.py b/ApiServices/TemplateService/events/template/event.py new file mode 100644 index 0000000..1a6cc88 --- /dev/null +++ b/ApiServices/TemplateService/events/template/event.py @@ -0,0 +1,57 @@ +from ApiServices.TemplateService.initializer.event_clusters import EventCluster, Event + + +single_event = Event( + name="example_event", + key="176b829c-7622-4cf2-b474-411e5acb637c", + request_validator=None, # TODO: Add request validator + response_validator=None, # TODO: Add response validator + description="Example event description", +) + +def example_callable(): + """ + Example callable method + """ + return { + "completed": True, + "message": "Example callable method 2", + "info": { + "host": "example_host", + "user_agent": "example_user_agent", + }, + } +single_event.event_callable = example_callable + +other_event = Event( + name="example_event-2", + key="176b829c-7622-4cf2-b474-421e5acb637c", + request_validator=None, # TODO: Add request validator + response_validator=None, # TODO: Add response validator + description="Example event 2 description", +) +def example_callable_other(): + """ + Example callable method + """ + return { + "completed": True, + "message": "Example callable method 1", + "info": { + "host": "example_host", + "user_agent": "example_user_agent", + }, + } +other_event.event_callable = example_callable_other + +tokens_in_redis = [ + "3f510dcf-9f84-4eb9-b919-f582f30adab1", + "9f403034-deba-4e1f-b43e-b25d3c808d39", + "b8ec6e64-286a-4f60-8554-7a3865454944", + "176b829c-7622-4cf2-b474-421e5acb637c", + ] +template_event_cluster = EventCluster(endpoint_uu_id="bb20c8c6-a289-4cab-9da7-34ca8a36c8e5") +template_event_cluster.add_event([single_event, other_event]) +matched_event = template_event_cluster.match_event(event_keys=tokens_in_redis) + +print('event_callable', matched_event.event_callable()) diff --git a/ApiServices/TemplateService/initializer/__init__.py b/ApiServices/TemplateService/initializer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/TemplateService/initializer/create_route.py b/ApiServices/TemplateService/initializer/create_route.py new file mode 100644 index 0000000..3cc8017 --- /dev/null +++ b/ApiServices/TemplateService/initializer/create_route.py @@ -0,0 +1,41 @@ +from typing import List +from fastapi import APIRouter, FastAPI + + +class RouteRegisterController: + + def __init__(self, app: FastAPI, router_list: List[APIRouter]): + self.router_list = router_list + self.app = app + + @staticmethod + def add_router_with_event_to_database(route: APIRouter): + from Schemas import EndpointRestriction + + with EndpointRestriction.new_session() as db_session: + route_path = str(getattr(route, "path")) + route_summary = str(getattr(route, "name")) or "" + operation_id = str(getattr(route, "operation_id")) or "" + if not operation_id: + return + + for route_method in [method.lower() for method in getattr(route, "methods")]: + restriction = EndpointRestriction.find_or_create( + **dict( + endpoint_method=route_method, + endpoint_name=route_path, + endpoint_desc=route_summary.replace("_", " "), + endpoint_function=route_summary, + operation_uu_id=operation_id, # UUID of the endpoint + is_confirmed=True, + ) + ) + if not restriction.meta_data.created: + restriction.endpoint_code = f"AR{str(restriction.id).zfill(3)}" + restriction.save(db=db_session) + + def register_routes(self): + for router in self.router_list: + self.app.include_router(router) + self.add_router_with_event_to_database(router) + return self.app diff --git a/ApiServices/TemplateService/initializer/event_clusters.py b/ApiServices/TemplateService/initializer/event_clusters.py new file mode 100644 index 0000000..b987e25 --- /dev/null +++ b/ApiServices/TemplateService/initializer/event_clusters.py @@ -0,0 +1,84 @@ + +class EventCluster: + + def __init__(self, endpoint_uu_id: str): + self.endpoint_uu_id = endpoint_uu_id + self.events = [] + + def add_event(self, list_of_events: list["Event"]): + """ + Add an event to the cluster + """ + for event in list_of_events: + self.events.append(event) + self.events = list(set(self.events)) + + def get_event(self, event_key: str): + """ + Get an event by its key + """ + + for event in self.events: + if event.key == event_key: + return event + return None + + def set_events_to_database(self): + from Schemas import Events, EndpointRestriction + with Events.new_session() as db_session: + + if to_save_endpoint := EndpointRestriction.filter_one( + EndpointRestriction.uu_id == self.endpoint_uu_id, + db=db_session, + ).data: + for event in self.events: + event_obj = Events.find_or_create( + function_code=event.key, + function_class=event.name, + description=event.description, + endpoint_id=to_save_endpoint.id, + endpoint_uu_id=str(to_save_endpoint.uu_id), + is_confirmed=True, + active=True, + db=db_session, + ) + event_obj.save() + print(f'UUID: {event_obj.uu_id} event is saved to {to_save_endpoint.uu_id}') + + def match_event(self, event_keys: list[str]) -> "Event": + """ + Match an event by its key + """ + print('set(event_keys)', set(event_keys)) + print('event.keys', set([event.key for event in self.events])) + intersection_of_key: set[str] = set(event_keys) & set([event.key for event in self.events]) + if not len(intersection_of_key) == 1: + raise ValueError( + f"Event key not found or multiple matches found: {intersection_of_key}" + ) + return self.get_event(event_key=list(intersection_of_key)[0]) + + +class Event: + + def __init__( + self, + name: str, + key: str, + request_validator: str = None, + response_validator: str = None, + description: str = "", + ): + self.name = name + self.key = key + self.request_validator = request_validator + self.response_validator = response_validator + self.description = description + + + def event_callable(self): + """ + Example callable method + """ + print(self.name) + return {} diff --git a/ApiServices/TemplateService/services/__init__.py b/ApiServices/TemplateService/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/__init__.py b/ApiServices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Controllers/Postgres/crud.py b/Controllers/Postgres/crud.py index ee44fd1..5652f92 100644 --- a/Controllers/Postgres/crud.py +++ b/Controllers/Postgres/crud.py @@ -1,23 +1,13 @@ import arrow import datetime -from typing import Optional, Any, Dict, List -from sqlalchemy.orm import Session, Mapped -from pydantic import BaseModel -from fastapi.exceptions import HTTPException +from typing import Optional, Any, Dict from decimal import Decimal +from fastapi.exceptions import HTTPException + from sqlalchemy import TIMESTAMP, NUMERIC from sqlalchemy.orm.attributes import InstrumentedAttribute - - -class Credentials(BaseModel): - """ - Class to store user credentials. - """ - - person_id: int - person_name: str - full_name: Optional[str] = None +from sqlalchemy.orm import Session, Mapped class MetaData: @@ -27,6 +17,7 @@ class MetaData: created: bool = False updated: bool = False + deleted: bool = False class CRUDModel: @@ -43,7 +34,7 @@ class CRUDModel: __abstract__ = True - creds: Credentials = None + # creds: Credentials = None meta_data: MetaData = MetaData() # Define required columns for CRUD operations @@ -57,23 +48,6 @@ class CRUDModel: "deleted": bool, } - @classmethod - def create_credentials(cls, record_created) -> None: - """ - Save user credentials for tracking. - - Args: - record_created: Record that created or updated - """ - if not cls.creds: - return - - if getattr(cls.creds, "person_id", None) and getattr( - cls.creds, "person_name", None - ): - record_created.created_by_id = cls.creds.person_id - record_created.created_by = cls.creds.person_name - @classmethod def raise_exception( cls, message: str = "Exception raised.", status_code: int = 400 @@ -126,7 +100,6 @@ class CRUDModel: for key, value in kwargs.items(): setattr(created_record, key, value) - cls.create_credentials(created_record) db.add(created_record) db.flush() return created_record @@ -194,6 +167,7 @@ class CRUDModel: return False, None except Exception as e: + err = e return False, None def get_dict( @@ -234,6 +208,7 @@ class CRUDModel: return return_dict except Exception as e: + err = e return {} @classmethod @@ -278,7 +253,6 @@ class CRUDModel: for key, value in kwargs.items(): setattr(created_record, key, value) - cls.create_credentials(created_record) db.add(created_record) db.flush() cls.meta_data.created = True @@ -308,7 +282,6 @@ class CRUDModel: for key, value in kwargs.items(): setattr(self, key, value) - self.update_credentials() db.flush() self.meta_data.updated = True return self @@ -317,17 +290,3 @@ class CRUDModel: self.meta_data.updated = False db.rollback() self.raise_exception(f"Failed to update record: {str(e)}", status_code=500) - - def update_credentials(self) -> None: - """ - Save user credentials for tracking. - """ - if not self.creds: - return - - person_id = getattr(self.creds, "person_id", None) - person_name = getattr(self.creds, "person_name", None) - - if person_id and person_name: - self.updated_by_id = self.creds.person_id - self.updated_by = self.creds.person_name diff --git a/Controllers/Postgres/mixin.py b/Controllers/Postgres/mixin.py index 014ed32..b407448 100644 --- a/Controllers/Postgres/mixin.py +++ b/Controllers/Postgres/mixin.py @@ -72,27 +72,6 @@ class CrudMixin(BasicMixin): comment="Record validity end timestamp", ) - -class CrudCollection(CrudMixin): - """ - Full-featured model class with all common fields. - - Includes: - - UUID and reference ID - - Timestamps - - User tracking - - Confirmation status - - Soft delete - - Notification flags - """ - - __abstract__ = True - __repr__ = ReprMixin.__repr__ - - ref_id: Mapped[str] = mapped_column( - String(100), nullable=True, index=True, comment="External reference ID" - ) - # Timestamps created_at: Mapped[TIMESTAMP] = mapped_column( TIMESTAMP(timezone=True), @@ -110,36 +89,51 @@ class CrudCollection(CrudMixin): comment="Last update timestamp", ) + +class CrudCollection(CrudMixin): + """ + Full-featured model class with all common fields. + + Includes: + - UUID and reference ID + - Timestamps + - User tracking + - Confirmation status + - Soft delete + - Notification flags + """ + + __abstract__ = True + __repr__ = ReprMixin.__repr__ + + # Outer reference fields + ref_id: Mapped[str] = mapped_column( + String(100), nullable=True, index=True, comment="External reference ID" + ) + replication_id: Mapped[int] = mapped_column( + SmallInteger, server_default="0", comment="Replication identifier" + ) + # Cryptographic and user tracking cryp_uu_id: Mapped[str] = mapped_column( String, nullable=True, index=True, comment="Cryptographic UUID" ) - # created_by: Mapped[str] = mapped_column( - # String, nullable=True, comment="Creator name" - # ) - # created_by_id: Mapped[int] = mapped_column( - # Integer, nullable=True, comment="Creator ID" - # ) - # updated_by: Mapped[str] = mapped_column( - # String, nullable=True, comment="Last modifier name" - # ) - # updated_by_id: Mapped[int] = mapped_column( - # Integer, nullable=True, comment="Last modifier ID" - # ) - confirmed_by: Mapped[str] = mapped_column( - String, nullable=True, comment="Confirmer name" + + # Token fields of modification + created_credentials_token: Mapped[str] = mapped_column( + String, nullable=True, comment="Created Credentials token" ) - confirmed_by_id: Mapped[int] = mapped_column( - Integer, nullable=True, comment="Confirmer ID" + updated_credentials_token: Mapped[str] = mapped_column( + String, nullable=True, comment="Updated Credentials token" + ) + confirmed_credentials_token: Mapped[str] = mapped_column( + String, nullable=True, comment="Confirmed Credentials token" ) # Status flags is_confirmed: Mapped[bool] = mapped_column( Boolean, server_default="0", comment="Record confirmation status" ) - replication_id: Mapped[int] = mapped_column( - SmallInteger, server_default="0", comment="Replication identifier" - ) deleted: Mapped[bool] = mapped_column( Boolean, server_default="0", comment="Soft delete flag" ) diff --git a/Modules/Token/password_module.py b/Modules/Token/password_module.py index f21ad95..4c458e1 100644 --- a/Modules/Token/password_module.py +++ b/Modules/Token/password_module.py @@ -25,6 +25,7 @@ class PasswordModule: i, random.choice(merged_letters), 1 ) return token_generated + raise ValueError("EYS_0004") @classmethod def generate_access_token(cls) -> str: diff --git a/Schemas/__init__.py b/Schemas/__init__.py index 19995e2..7a80c61 100644 --- a/Schemas/__init__.py +++ b/Schemas/__init__.py @@ -86,6 +86,7 @@ from Schemas.identity.identity import ( OccupantTypes, People, Users, + Credentials, RelationshipDutyPeople, Contracts, ) @@ -188,6 +189,7 @@ __all__ = [ "OccupantTypes", "People", "Users", + "Credentials", "RelationshipDutyPeople", "RelationshipEmployee2PostCode", "Contracts", diff --git a/Schemas/event/event.py b/Schemas/event/event.py index dec2e1a..b82f541 100644 --- a/Schemas/event/event.py +++ b/Schemas/event/event.py @@ -230,8 +230,7 @@ class Event2Employee(CrudCollection): ) @classmethod - def get_event_codes(cls, employee_id: int) -> list: - db = cls.new_session() + def get_event_codes(cls, employee_id: int, db) -> list: employee_events = cls.filter_all( cls.employee_id == employee_id, db=db, @@ -328,8 +327,7 @@ class Event2Occupant(CrudCollection): ) @classmethod - def get_event_codes(cls, build_living_space_id) -> list: - db = cls.new_session() + def get_event_codes(cls, build_living_space_id, db) -> list: occupant_events = cls.filter_all( cls.build_living_space_id == build_living_space_id, db=db, diff --git a/Schemas/rules/rules.py b/Schemas/rules/rules.py index 3ff41a2..b6c6f9e 100644 --- a/Schemas/rules/rules.py +++ b/Schemas/rules/rules.py @@ -18,6 +18,9 @@ class EndpointRestriction(CrudCollection): __tablename__ = "endpoint_restriction" __exclude__fields__ = [] + operation_uu_id: Mapped[UUID] = mapped_column( + String, comment="UUID of the operation", + ) endpoint_function: Mapped[str] = mapped_column( String, server_default="", comment="Function name of the API endpoint" )