diff --git a/ApiEvents/AuthServiceApi/auth/__init__.py b/ApiEvents/AuthServiceApi/events/auth/__init__.py similarity index 100% rename from ApiEvents/AuthServiceApi/auth/__init__.py rename to ApiEvents/AuthServiceApi/events/auth/__init__.py diff --git a/ApiEvents/AuthServiceApi/auth/auth.py b/ApiEvents/AuthServiceApi/events/auth/auth.py similarity index 87% rename from ApiEvents/AuthServiceApi/auth/auth.py rename to ApiEvents/AuthServiceApi/events/auth/auth.py index 36fd568..3ba9eb5 100644 --- a/ApiEvents/AuthServiceApi/auth/auth.py +++ b/ApiEvents/AuthServiceApi/events/auth/auth.py @@ -11,7 +11,7 @@ from ApiLibrary.common.line_number import get_line_number_for_error from ApiLibrary.date_time_actions.date_functions import DateTimeLocal from ApiServices.Login.user_login_handler import UserLoginModule from ApiServices.Token.token_handler import TokenService -from ApiValidations.Custom.token_objects import CompanyToken +from ApiValidations.Custom.token_objects import CompanyToken, OccupantToken from ApiValidations.Request.authentication import ( Login, EmployeeSelectionValidation, @@ -20,11 +20,16 @@ from ApiValidations.Request.authentication import ( EmployeeSelection, ) from ErrorHandlers import HTTPExceptionApi +from Schemas.building.build import ( + BuildLivingSpace, + BuildParts, + RelationshipEmployee2Build, +) from Schemas.company.company import Companies from Schemas.company.department import Departments, Duties, Duty from Schemas.company.employee import Staff, Employees -from Schemas.event.event import Event2Employee -from Schemas.identity.identity import Users +from Schemas.event.event import Event2Employee, Event2Occupant +from Schemas.identity.identity import OccupantTypes, Users from Services.Redis.Actions.actions import RedisActions from .models import ( LoginData, @@ -170,9 +175,7 @@ class AuthenticationSelectEventMethods(MethodToEvent): ) # Get reachable events - reachable_event_list_id = Event2Employee.get_event_id_by_employee_id( - employee_id=employee.id - ) + reachable_event_codes = Event2Employee.get_event_codes(employee_id=employee.id) # Get staff and duties staff = Staff.filter_one(Staff.id == employee.staff_id, db=db_session).data @@ -189,7 +192,6 @@ class AuthenticationSelectEventMethods(MethodToEvent): **Duties.valid_record_dict, db=db_session, ).data - # Create company token company_token = CompanyToken( company_uu_id=selected_company.uu_id.__str__(), @@ -203,7 +205,7 @@ class AuthenticationSelectEventMethods(MethodToEvent): staff_uu_id=staff.uu_id.__str__(), employee_id=employee.id, employee_uu_id=employee.uu_id.__str__(), - reachable_event_list_id=reachable_event_list_id, + reachable_event_codes=reachable_event_codes, ) try: # Update Redis update_token = TokenService.update_token_at_redis( @@ -226,12 +228,81 @@ class AuthenticationSelectEventMethods(MethodToEvent): request: "Request", ): """Handle occupant type selection""" - raise HTTPExceptionApi( - error_code="HTTP_400_BAD_REQUEST", - lang=token_dict.lang, - loc=get_line_number_for_error(), - sys_msg="Occupant selection not implemented", + db = BuildLivingSpace.new_session() + # Get selected occupant type + selected_build_living_space = BuildLivingSpace.filter_one( + BuildLivingSpace.uu_id == data.build_living_space_uu_id, + db=db, + ).data + if not selected_build_living_space: + raise HTTPExceptionApi( + error_code="HTTP_400_BAD_REQUEST", + lang=token_dict.lang, + loc=get_line_number_for_error(), + sys_msg="Selected occupant type not found", + ) + + # Get reachable events + reachable_event_codes = Event2Occupant.get_event_codes( + build_living_space_id=selected_build_living_space.id ) + occupant_type = OccupantTypes.filter_one( + OccupantTypes.id == selected_build_living_space.occupant_type_id, + db=db, + system=True, + ).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, + ) + + try: # Update Redis + update_token = TokenService.update_token_at_redis( + request=request, add_payload=occupant_token + ) + return update_token + except Exception as e: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg=f"{e}", + ) @classmethod async def authentication_select_company_or_occupant_type( @@ -246,14 +317,6 @@ class AuthenticationSelectEventMethods(MethodToEvent): elif token_dict.is_occupant: return cls._handle_occupant_selection(data, token_dict, request) - # except Exception as e: - # raise HTTPExceptionApi( - # error_code="HTTP_500_INTERNAL_SERVER_ERROR", - # lang="en", - # loc=get_line_number_for_error(), - # sys_msg=str(e), - # ) - class AuthenticationCheckTokenEventMethods(MethodToEvent): event_type = "LOGIN" diff --git a/ApiEvents/AuthServiceApi/auth/endpoints.py b/ApiEvents/AuthServiceApi/events/auth/endpoints.py similarity index 94% rename from ApiEvents/AuthServiceApi/auth/endpoints.py rename to ApiEvents/AuthServiceApi/events/auth/endpoints.py index 93a3d74..887170c 100644 --- a/ApiEvents/AuthServiceApi/auth/endpoints.py +++ b/ApiEvents/AuthServiceApi/events/auth/endpoints.py @@ -3,8 +3,8 @@ Authentication endpoint configurations. """ from typing import TYPE_CHECKING, Dict, Any, Union, Annotated -from fastapi import HTTPException, status, Body +from ApiServices.Token.token_handler import TokenService from ApiValidations.Request import ( Logout, Login, @@ -49,7 +49,7 @@ from ApiEvents.abstract_class import ( ) if TYPE_CHECKING: - from fastapi import Request + from fastapi import Request, HTTPException, status, Body from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject @@ -59,22 +59,18 @@ from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTok @endpoint_wrapper("/authentication/select") async def authentication_select_company_or_occupant_type( request: "Request", - data: EndpointBaseRequestModel, + data: Union[EmployeeSelection, OccupantSelection], ) -> Dict[str, Any]: """ Select company or occupant type. """ auth_dict = authentication_select_company_or_occupant_type.auth - if data.data.get("company_uu_id"): - data = EmployeeSelection(**data.data) - elif data.data.get("build_living_space_uu_id"): - data = OccupantSelection(**data.data) if await AuthenticationSelectEventMethods.authentication_select_company_or_occupant_type( request=request, data=data, token_dict=auth_dict ): - if isinstance(data, EmployeeSelection): + if data.is_employee: return {"selected_company": data.company_uu_id} - elif isinstance(data, OccupantSelection): + elif data.is_occupant: return {"selected_occupant": data.build_living_space_uu_id} @@ -99,10 +95,18 @@ async def authentication_check_token_is_valid( """ Check if a token is valid. """ - return { - "headers": dict(request.headers), - "data": data.model_dump(), - } + try: + access_token = TokenService.get_access_token_from_request(request=request) + if TokenService.get_object_via_access_key(access_token=access_token): + return { + "status": True, + "message": "Access Token is valid", + } + except HTTPException: + return { + "status": False, + "message": "Access Token is NOT valid", + } @endpoint_wrapper("/authentication/refresh") diff --git a/ApiEvents/AuthServiceApi/auth/models.py b/ApiEvents/AuthServiceApi/events/auth/models.py similarity index 100% rename from ApiEvents/AuthServiceApi/auth/models.py rename to ApiEvents/AuthServiceApi/events/auth/models.py diff --git a/ApiEvents/AuthServiceApi/route_configs.py b/ApiEvents/AuthServiceApi/route_configs.py index 1b096bb..28d4e33 100644 --- a/ApiEvents/AuthServiceApi/route_configs.py +++ b/ApiEvents/AuthServiceApi/route_configs.py @@ -6,15 +6,13 @@ to be used by the dynamic route creation system. """ from typing import Dict, List, Any -from .auth.endpoints import AUTH_CONFIG +from .events.auth.endpoints import AUTH_CONFIG # Registry of all route configurations -ROUTE_CONFIGS = [ - AUTH_CONFIG, -] +ROUTE_CONFIGS = [AUTH_CONFIG] def get_route_configs() -> List[Dict[str, Any]]: """Get all registered route configurations.""" - return [AUTH_CONFIG] + return ROUTE_CONFIGS diff --git a/ApiEvents/EventServiceApi/account/__init__.py b/ApiEvents/EventServiceApi/events/account/__init__.py similarity index 100% rename from ApiEvents/EventServiceApi/account/__init__.py rename to ApiEvents/EventServiceApi/events/account/__init__.py diff --git a/ApiEvents/EventServiceApi/account/account_records.py b/ApiEvents/EventServiceApi/events/account/account_records.py similarity index 95% rename from ApiEvents/EventServiceApi/account/account_records.py rename to ApiEvents/EventServiceApi/events/account/account_records.py index cf9e097..92afe15 100644 --- a/ApiEvents/EventServiceApi/account/account_records.py +++ b/ApiEvents/EventServiceApi/events/account/account_records.py @@ -2,7 +2,7 @@ Account records service implementation. """ -from typing import Dict, Any, Union +from typing import Union from pydantic import Field from ApiEvents.abstract_class import MethodToEvent, endpoint_wrapper @@ -26,7 +26,6 @@ from Schemas import ( ) from Services.PostgresDb.Models.alchemy_response import ( AlchemyJsonResponse, - DictJsonResponse, ) from ApiValidations.Response import AccountRecordResponse from .models import ( @@ -36,7 +35,7 @@ from .models import ( ) -class AccountRecordsListEventMethods(MethodToEvent): +class AccountListEventMethod(MethodToEvent): event_type = "SELECT" event_description = "" @@ -47,8 +46,8 @@ class AccountRecordsListEventMethods(MethodToEvent): "208e6273-17ef-44f0-814a-8098f816b63a": "account_records_list_flt_res", } __event_validation__ = { - "7192c2aa-5352-4e36-98b3-dafb7d036a3d": AccountRecordResponse, - "208e6273-17ef-44f0-814a-8098f816b63a": AccountRecordResponse, + "7192c2aa-5352-4e36-98b3-dafb7d036a3d": (AccountRecordResponse, [AccountRecords.__language_model__]), + "208e6273-17ef-44f0-814a-8098f816b63a": (AccountRecordResponse, [AccountRecords.__language_model__]), } @classmethod @@ -217,7 +216,7 @@ class AccountRecordsListEventMethods(MethodToEvent): ) -class AccountRecordsCreateEventMethods(MethodToEvent): +class AccountCreateEventMethod(MethodToEvent): event_type = "CREATE" event_description = "" @@ -227,7 +226,7 @@ class AccountRecordsCreateEventMethods(MethodToEvent): "31f4f32f-0cd4-4995-8a6a-f9f56335848a": "account_records_create", } __event_validation__ = { - "31f4f32f-0cd4-4995-8a6a-f9f56335848a": InsertAccountRecord, + "31f4f32f-0cd4-4995-8a6a-f9f56335848a": (InsertAccountRecord, [AccountRecords.__language_model__]), } @classmethod @@ -305,7 +304,7 @@ class AccountRecordsCreateEventMethods(MethodToEvent): ) -class AccountRecordsUpdateEventMethods(MethodToEvent): +class AccountUpdateEventMethod(MethodToEvent): event_type = "UPDATE" event_description = "" @@ -315,11 +314,11 @@ class AccountRecordsUpdateEventMethods(MethodToEvent): "ec98ef2c-bcd0-432d-a8f4-1822a56c33b2": "account_records_update", } __event_validation__ = { - "ec98ef2c-bcd0-432d-a8f4-1822a56c33b2": UpdateAccountRecord, + "ec98ef2c-bcd0-432d-a8f4-1822a56c33b2": (UpdateAccountRecord, [AccountRecords.__language_model__]), } @classmethod - def build_area_update( + def account_records_update( cls, build_uu_id: str, data: UpdateAccountRecordRequestModel, diff --git a/ApiEvents/EventServiceApi/account/endpoints.py b/ApiEvents/EventServiceApi/events/account/endpoints.py similarity index 91% rename from ApiEvents/EventServiceApi/account/endpoints.py rename to ApiEvents/EventServiceApi/events/account/endpoints.py index 28594a8..53f5672 100644 --- a/ApiEvents/EventServiceApi/account/endpoints.py +++ b/ApiEvents/EventServiceApi/events/account/endpoints.py @@ -14,19 +14,15 @@ from Services.PostgresDb.Models.alchemy_response import DictJsonResponse from fastapi import Request, Path, Body -@endpoint_wrapper("/account/records/address/list") +@endpoint_wrapper("/account/records/list") async def address_list(request: "Request", data: EndpointBaseRequestModel): """Handle address list endpoint.""" auth_dict = address_list.auth - return { - "data": data, - "request": str(request.headers), - "request_url": str(request.url), - "request_base_url": str(request.base_url), - } + code_dict = getattr(address_list, "func_code", {"function_code": None}) + return {"auth_dict": auth_dict, "code_dict": code_dict, "data": data} -@endpoint_wrapper("/account/records/address/create") +@endpoint_wrapper("/account/records/create") async def address_create(request: "Request", data: EndpointBaseRequestModel): """Handle address creation endpoint.""" return { @@ -37,7 +33,7 @@ async def address_create(request: "Request", data: EndpointBaseRequestModel): } -@endpoint_wrapper("/account/records/address/search") +@endpoint_wrapper("/account/records/search") async def address_search(request: "Request", data: EndpointBaseRequestModel): """Handle address search endpoint.""" auth_dict = address_search.auth @@ -45,7 +41,7 @@ async def address_search(request: "Request", data: EndpointBaseRequestModel): return {"auth_dict": auth_dict, "code_dict": code_dict, "data": data} -@endpoint_wrapper("/account/records/address/{address_uu_id}") +@endpoint_wrapper("/account/records/{address_uu_id}") async def address_update( request: Request, address_uu_id: str = Path(..., description="UUID of the address to update"), diff --git a/ApiEvents/EventServiceApi/account/models.py b/ApiEvents/EventServiceApi/events/account/models.py similarity index 100% rename from ApiEvents/EventServiceApi/account/models.py rename to ApiEvents/EventServiceApi/events/account/models.py diff --git a/ApiEvents/EventServiceApi/events/address/address.py b/ApiEvents/EventServiceApi/events/address/address.py new file mode 100644 index 0000000..516434a --- /dev/null +++ b/ApiEvents/EventServiceApi/events/address/address.py @@ -0,0 +1,336 @@ +""" + request models. +""" + +from typing import TYPE_CHECKING, Dict, Any, List, Optional, TypedDict, Union +from pydantic import BaseModel, Field, model_validator, RootModel, ConfigDict +from ApiEvents.abstract_class import MethodToEvent +from ApiEvents.base_request_model import BaseRequestModel, DictRequestModel +from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject +from ApiValidations.Request.address import SearchAddress, UpdateAddress +from ApiValidations.Request.base_validations import ListOptions +from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi +from Schemas.identity.identity import ( + AddressPostcode, + AddressStreet, + Addresses, + RelationshipEmployee2PostCode, +) +from ApiValidations.Request import ( + InsertAddress, +) +from ApiValidations.Response import ( + ListAddressResponse, +) + + +if TYPE_CHECKING: + from fastapi import Request + + +class AddressListEventMethod(MethodToEvent): + + event_type = "SELECT" + event_description = "List Address records" + event_category = "Address" + + __event_keys__ = { + "9c251d7d-da70-4d63-a72c-e69c26270442": "address_list_super_user", + "52afe375-dd95-4f4b-aaa2-4ec61bc6de52": "address_list_employee", + } + __event_validation__ = { + "9c251d7d-da70-4d63-a72c-e69c26270442": (ListAddressResponse, [Addresses.__language_model__]), + "52afe375-dd95-4f4b-aaa2-4ec61bc6de52": (ListAddressResponse, [Addresses.__language_model__]), + } + + @classmethod + def address_list_super_user( + cls, + list_options: ListOptions, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ): + db = RelationshipEmployee2PostCode.new_session() + post_code_list = RelationshipEmployee2PostCode.filter_all( + RelationshipEmployee2PostCode.company_id + == token_dict.selected_company.company_id, + db=db, + ).data + post_code_id_list = [post_code.member_id for post_code in post_code_list] + if not post_code_id_list: + raise HTTPExceptionApi( + status_code=404, + detail="User has no post code registered. User can not list addresses.", + ) + get_street_ids = [ + street_id[0] + for street_id in AddressPostcode.select_only( + AddressPostcode.id.in_(post_code_id_list), + select_args=[AddressPostcode.street_id], + order_by=AddressPostcode.street_id.desc(), + db=db, + ).data + ] + if not get_street_ids: + raise HTTPExceptionApi( + status_code=404, + detail="User has no street registered. User can not list addresses.", + ) + Addresses.pre_query = Addresses.filter_all( + Addresses.street_id.in_(get_street_ids), + db=db, + ).query + Addresses.filter_attr = list_options + records = Addresses.filter_all(db=db).data + return {} + # return AlchemyJsonResponse( + # completed=True, message="List Address records", result=records + # ) + + @classmethod + def address_list_employee( + cls, + list_options: ListOptions, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ): + # Addresses.filter_attr = list_options + Addresses.pre_query = Addresses.filter_all( + Addresses.street_id.in_(get_street_ids), + ) + records = Addresses.filter_all().data + return + # return AlchemyJsonResponse( + # completed=True, message="List Address records", result=records + # ) + + +class AddressCreateEventMethod(MethodToEvent): + + event_type = "CREATE" + event_description = "" + event_category = "" + + __event_keys__ = { + "ffdc445f-da10-4ce4-9531-d2bdb9a198ae": "create_address", + } + __event_validation__ = { + "ffdc445f-da10-4ce4-9531-d2bdb9a198ae": (InsertAddress, [Addresses.__language_model__]), + } + + @classmethod + def create_address( + cls, + data: InsertAddress, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ): + post_code = AddressPostcode.filter_one( + AddressPostcode.uu_id == data.post_code_uu_id, + ).data + if not post_code: + raise HTTPExceptionApi( + status_code=404, + detail="Post code not found. User can not create address without post code.", + ) + + data_dict = data.excluded_dump() + data_dict["street_id"] = post_code.street_id + data_dict["street_uu_id"] = str(post_code.street_uu_id) + del data_dict["post_code_uu_id"] + address = Addresses.find_or_create(**data_dict) + address.save() + address.update(is_confirmed=True) + address.save() + return AlchemyJsonResponse( + completed=True, + message="Address created successfully", + result=address.get_dict(), + ) + + +class AddressSearchEventMethod(MethodToEvent): + """Event methods for searching addresses. + + This class handles address search functionality including text search + and filtering. + """ + + event_type = "SEARCH" + event_description = "Search for addresses using text and filters" + event_category = "Address" + + __event_keys__ = { + "e0ac1269-e9a7-4806-9962-219ac224b0d0": "search_address", + } + __event_validation__ = { + "e0ac1269-e9a7-4806-9962-219ac224b0d0": (SearchAddress, [Addresses.__language_model__]), + } + + @classmethod + def _build_order_clause( + cls, filter_list: Dict[str, Any], schemas: List[str], filter_table: Any + ) -> Any: + """Build the ORDER BY clause for the query. + + Args: + filter_list: Dictionary of filter options + schemas: List of available schema fields + filter_table: SQLAlchemy table to query + + Returns: + SQLAlchemy order_by clause + """ + # Default to ordering by UUID if field not in schema + if filter_list.get("order_field") not in schemas: + filter_list["order_field"] = "uu_id" + else: + # Extract table and field from order field + table_name, field_name = str(filter_list.get("order_field")).split(".") + filter_table = getattr(databases.sql_models, table_name) + filter_list["order_field"] = field_name + + # Build order clause + field = getattr(filter_table, filter_list.get("order_field")) + return ( + field.desc() + if str(filter_list.get("order_type"))[0] == "d" + else field.asc() + ) + + @classmethod + def _format_record(cls, record: Any, schemas: List[str]) -> Dict[str, str]: + """Format a database record into a dictionary. + + Args: + record: Database record to format + schemas: List of schema fields + + Returns: + Formatted record dictionary + """ + result = {} + for index, schema in enumerate(schemas): + value = str(record[index]) + # Special handling for UUID fields + if "uu_id" in value: + value = str(value) + result[schema] = value + return result + + @classmethod + def search_address( + cls, + data: SearchAddress, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ) -> Any: + """Search for addresses using text search and filters. + + Args: + data: Search parameters including text and filters + token_dict: Authentication token + + Returns: + JSON response with search results + + Raises: + HTTPExceptionApi: If search fails + """ + try: + # Start performance measurement + start_time = perf_counter() + + # Get initial query + search_result = AddressStreet.search_address_text(search_text=data.search) + if not search_result: + raise HTTPExceptionApi( + status_code=status.HTTP_404_NOT_FOUND, + detail="No addresses found matching search criteria", + ) + + query = search_result.get("query") + schemas = search_result.get("schema") + + # Apply filters + filter_list = data.list_options.dump() + filter_table = AddressStreet + + # Build and apply order clause + order = cls._build_order_clause(filter_list, schemas, filter_table) + + # Apply pagination + page_size = int(filter_list.get("size")) + offset = (int(filter_list.get("page")) - 1) * page_size + + # Execute query + query = ( + query.order_by(order) + .limit(page_size) + .offset(offset) + .populate_existing() + ) + records = list(query.all()) + + # Format results + results = [cls._format_record(record, schemas) for record in records] + + # Log performance + duration = perf_counter() - start_time + print(f"Address search completed in {duration:.3f}s") + + return AlchemyJsonResponse( + completed=True, message="Address search results", result=results + ) + + except HTTPExceptionApi as e: + # Re-raise HTTP exceptions + raise e + except Exception as e: + # Log and wrap other errors + print(f"Address search error: {str(e)}") + raise HTTPExceptionApi( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to search addresses", + ) from e + + +class AddressUpdateEventMethod(MethodToEvent): + + event_type = "UPDATE" + event_description = "" + event_category = "" + + __event_keys__ = { + "1f9c3a9c-e5bd-4dcd-9b9a-3742d7e03a27": "update_address", + } + __event_validation__ = { + "1f9c3a9c-e5bd-4dcd-9b9a-3742d7e03a27": (UpdateAddress, [Addresses.__language_model__]), + } + + @classmethod + def update_address( + cls, + address_uu_id: str, + data: UpdateAddress, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ): + if isinstance(token_dict, EmployeeTokenObject): + address = Addresses.filter_one( + Addresses.uu_id == address_uu_id, + ).data + if not address: + raise HTTPExceptionApi( + status_code=404, + detail=f"Address not found. User can not update with given address uuid : {address_uu_id}", + ) + + data_dict = data.excluded_dump() + updated_address = address.update(**data_dict) + updated_address.save() + return AlchemyJsonResponse( + completed=True, + message="Address updated successfully", + result=updated_address.get_dict(), + ) + elif isinstance(token_dict, OccupantTokenObject): + raise HTTPExceptionApi( + status_code=403, + detail="Occupant can not update address.", + ) diff --git a/ApiEvents/EventServiceApi/events/address/endpoint.py b/ApiEvents/EventServiceApi/events/address/endpoint.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiEvents/EventServiceApi/events/address/models.py b/ApiEvents/EventServiceApi/events/address/models.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiEvents/EventServiceApi/route_configs.py b/ApiEvents/EventServiceApi/route_configs.py index 721b8f7..9aa74ba 100644 --- a/ApiEvents/EventServiceApi/route_configs.py +++ b/ApiEvents/EventServiceApi/route_configs.py @@ -6,7 +6,7 @@ to be used by the dynamic route creation system. """ from typing import Dict, List, Any -from .account.endpoints import ACCOUNT_RECORDS_CONFIG +from .events.account.endpoints import ACCOUNT_RECORDS_CONFIG # Registry of all route configurations @@ -17,4 +17,4 @@ ROUTE_CONFIGS = [ def get_route_configs() -> List[Dict[str, Any]]: """Get all registered route configurations.""" - return [ACCOUNT_RECORDS_CONFIG] + return ROUTE_CONFIGS diff --git a/ApiEvents/TemplateServiceApi/__init__.py b/ApiEvents/TemplateServiceApi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiEvents/TemplateServiceApi/endpoint/endpoint.py b/ApiEvents/TemplateServiceApi/endpoint/endpoint.py new file mode 100644 index 0000000..b623bc8 --- /dev/null +++ b/ApiEvents/TemplateServiceApi/endpoint/endpoint.py @@ -0,0 +1,52 @@ +from typing import TYPE_CHECKING, Dict, Any, Union + +from ApiEvents.base_request_model import DictRequestModel, EndpointBaseRequestModel +from ApiEvents.abstract_class import ( + RouteFactoryConfig, + EndpointFactoryConfig, + endpoint_wrapper, +) + +if TYPE_CHECKING: + from fastapi import Request, HTTPException, status, Body + +from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject + + +# Type aliases for common types + +prefix = "" + + +@endpoint_wrapper(f"{prefix}") +async def authentication_select_company_or_occupant_type( + request: "Request", + data: EndpointBaseRequestModel, +) -> Dict[str, Any]: + """ + Select company or occupant type. + """ + auth_dict = authentication_select_company_or_occupant_type.auth + return {} + + +_CONFIG = RouteFactoryConfig( + name="", + prefix=prefix, + tags=[""], + include_in_schema=True, + endpoints=[ + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/", + url_of_endpoint="/", + endpoint="/", + method="POST", + summary="", + description="", + is_auth_required=True, # Needs token_dict + is_event_required=False, + endpoint_function=lambda: "", + ), + ], +).as_dict() diff --git a/ApiEvents/TemplateServiceApi/endpoint/eventFile.py b/ApiEvents/TemplateServiceApi/endpoint/eventFile.py new file mode 100644 index 0000000..afb03fe --- /dev/null +++ b/ApiEvents/TemplateServiceApi/endpoint/eventFile.py @@ -0,0 +1,19 @@ +""" + request models. +""" + +from typing import TYPE_CHECKING, Dict, Any, Literal, Optional, TypedDict, Union +from pydantic import BaseModel, Field, model_validator, RootModel, ConfigDict +from ApiEvents.base_request_model import BaseRequestModel, DictRequestModel +from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject +from ApiValidations.Request.base_validations import ListOptions +from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi +from Schemas.identity.identity import ( + AddressPostcode, + Addresses, + RelationshipEmployee2PostCode, +) + + +if TYPE_CHECKING: + from fastapi import Request diff --git a/ApiEvents/TemplateServiceApi/endpoint/models.py b/ApiEvents/TemplateServiceApi/endpoint/models.py new file mode 100644 index 0000000..ec9c70e --- /dev/null +++ b/ApiEvents/TemplateServiceApi/endpoint/models.py @@ -0,0 +1,325 @@ +""" + request models. +""" + +from typing import TYPE_CHECKING, Dict, Any, Literal, Optional, TypedDict, Union +from pydantic import BaseModel, Field, model_validator, RootModel, ConfigDict +from ApiEvents.base_request_model import BaseRequestModel, DictRequestModel +from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject +from ApiValidations.Request.base_validations import ListOptions +from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi +from Schemas.identity.identity import ( + AddressPostcode, + Addresses, + RelationshipEmployee2PostCode, +) + + +if TYPE_CHECKING: + from fastapi import Request + + +class AddressListEventMethods(MethodToEvent): + + event_type = "SELECT" + event_description = "List Address records" + event_category = "Address" + + __event_keys__ = { + "9c251d7d-da70-4d63-a72c-e69c26270442": "address_list_super_user", + "52afe375-dd95-4f4b-aaa2-4ec61bc6de52": "address_list_employee", + } + __event_validation__ = { + "9c251d7d-da70-4d63-a72c-e69c26270442": ListAddressResponse, + "52afe375-dd95-4f4b-aaa2-4ec61bc6de52": ListAddressResponse, + } + + @classmethod + def address_list_super_user( + cls, + list_options: ListOptions, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ): + db = RelationshipEmployee2PostCode.new_session() + post_code_list = RelationshipEmployee2PostCode.filter_all( + RelationshipEmployee2PostCode.company_id + == token_dict.selected_company.company_id, + db=db, + ).data + post_code_id_list = [post_code.member_id for post_code in post_code_list] + if not post_code_id_list: + raise HTTPExceptionApi( + status_code=404, + detail="User has no post code registered. User can not list addresses.", + ) + get_street_ids = [ + street_id[0] + for street_id in AddressPostcode.select_only( + AddressPostcode.id.in_(post_code_id_list), + select_args=[AddressPostcode.street_id], + order_by=AddressPostcode.street_id.desc(), + ).data + ] + if not get_street_ids: + raise HTTPExceptionApi( + status_code=404, + detail="User has no street registered. User can not list addresses.", + ) + Addresses.pre_query = Addresses.filter_all( + Addresses.street_id.in_(get_street_ids), + ).query + Addresses.filter_attr = list_options + records = Addresses.filter_all().data + return + # return AlchemyJsonResponse( + # completed=True, message="List Address records", result=records + # ) + + @classmethod + def address_list_employee( + cls, + list_options: ListOptions, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ): + Addresses.filter_attr = list_options + Addresses.pre_query = Addresses.filter_all( + Addresses.street_id.in_(get_street_ids), + ) + records = Addresses.filter_all().data + return + # return AlchemyJsonResponse( + # completed=True, message="List Address records", result=records + # ) + + +class AddressCreateEventMethods(MethodToEvent): + + event_type = "CREATE" + event_description = "" + event_category = "" + + __event_keys__ = { + "ffdc445f-da10-4ce4-9531-d2bdb9a198ae": "create_address", + } + __event_validation__ = { + "ffdc445f-da10-4ce4-9531-d2bdb9a198ae": InsertAddress, + } + + @classmethod + def create_address( + cls, + data: InsertAddress, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ): + post_code = AddressPostcode.filter_one( + AddressPostcode.uu_id == data.post_code_uu_id, + ).data + if not post_code: + raise HTTPExceptionApi( + status_code=404, + detail="Post code not found. User can not create address without post code.", + ) + + data_dict = data.excluded_dump() + data_dict["street_id"] = post_code.street_id + data_dict["street_uu_id"] = str(post_code.street_uu_id) + del data_dict["post_code_uu_id"] + address = Addresses.find_or_create(**data_dict) + address.save() + address.update(is_confirmed=True) + address.save() + return AlchemyJsonResponse( + completed=True, + message="Address created successfully", + result=address.get_dict(), + ) + + +class AddressSearchEventMethods(MethodToEvent): + """Event methods for searching addresses. + + This class handles address search functionality including text search + and filtering. + """ + + event_type = "SEARCH" + event_description = "Search for addresses using text and filters" + event_category = "Address" + + __event_keys__ = { + "e0ac1269-e9a7-4806-9962-219ac224b0d0": "search_address", + } + __event_validation__ = { + "e0ac1269-e9a7-4806-9962-219ac224b0d0": SearchAddress, + } + + @classmethod + def _build_order_clause( + cls, filter_list: Dict[str, Any], schemas: List[str], filter_table: Any + ) -> Any: + """Build the ORDER BY clause for the query. + + Args: + filter_list: Dictionary of filter options + schemas: List of available schema fields + filter_table: SQLAlchemy table to query + + Returns: + SQLAlchemy order_by clause + """ + # Default to ordering by UUID if field not in schema + if filter_list.get("order_field") not in schemas: + filter_list["order_field"] = "uu_id" + else: + # Extract table and field from order field + table_name, field_name = str(filter_list.get("order_field")).split(".") + filter_table = getattr(databases.sql_models, table_name) + filter_list["order_field"] = field_name + + # Build order clause + field = getattr(filter_table, filter_list.get("order_field")) + return ( + field.desc() + if str(filter_list.get("order_type"))[0] == "d" + else field.asc() + ) + + @classmethod + def _format_record(cls, record: Any, schemas: List[str]) -> Dict[str, str]: + """Format a database record into a dictionary. + + Args: + record: Database record to format + schemas: List of schema fields + + Returns: + Formatted record dictionary + """ + result = {} + for index, schema in enumerate(schemas): + value = str(record[index]) + # Special handling for UUID fields + if "uu_id" in value: + value = str(value) + result[schema] = value + return result + + @classmethod + def search_address( + cls, + data: SearchAddress, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ) -> JSONResponse: + """Search for addresses using text search and filters. + + Args: + data: Search parameters including text and filters + token_dict: Authentication token + + Returns: + JSON response with search results + + Raises: + HTTPExceptionApi: If search fails + """ + try: + # Start performance measurement + start_time = perf_counter() + + # Get initial query + search_result = AddressStreet.search_address_text(search_text=data.search) + if not search_result: + raise HTTPExceptionApi( + status_code=status.HTTP_404_NOT_FOUND, + detail="No addresses found matching search criteria", + ) + + query = search_result.get("query") + schemas = search_result.get("schema") + + # Apply filters + filter_list = data.list_options.dump() + filter_table = AddressStreet + + # Build and apply order clause + order = cls._build_order_clause(filter_list, schemas, filter_table) + + # Apply pagination + page_size = int(filter_list.get("size")) + offset = (int(filter_list.get("page")) - 1) * page_size + + # Execute query + query = ( + query.order_by(order) + .limit(page_size) + .offset(offset) + .populate_existing() + ) + records = list(query.all()) + + # Format results + results = [cls._format_record(record, schemas) for record in records] + + # Log performance + duration = perf_counter() - start_time + print(f"Address search completed in {duration:.3f}s") + + return AlchemyJsonResponse( + completed=True, message="Address search results", result=results + ) + + except HTTPExceptionApi as e: + # Re-raise HTTP exceptions + raise e + except Exception as e: + # Log and wrap other errors + print(f"Address search error: {str(e)}") + raise HTTPExceptionApi( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to search addresses", + ) from e + + +class AddressUpdateEventMethods(MethodToEvent): + + event_type = "UPDATE" + event_description = "" + event_category = "" + + __event_keys__ = { + "1f9c3a9c-e5bd-4dcd-9b9a-3742d7e03a27": "update_address", + } + __event_validation__ = { + "1f9c3a9c-e5bd-4dcd-9b9a-3742d7e03a27": UpdateAddress, + } + + @classmethod + def update_address( + cls, + address_uu_id: str, + data: UpdateAddress, + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], + ): + if isinstance(token_dict, EmployeeTokenObject): + address = Addresses.filter_one( + Addresses.uu_id == address_uu_id, + ).data + if not address: + raise HTTPExceptionApi( + status_code=404, + detail=f"Address not found. User can not update with given address uuid : {address_uu_id}", + ) + + data_dict = data.excluded_dump() + updated_address = address.update(**data_dict) + updated_address.save() + return AlchemyJsonResponse( + completed=True, + message="Address updated successfully", + result=updated_address.get_dict(), + ) + elif isinstance(token_dict, OccupantTokenObject): + raise HTTPExceptionApi( + status_code=403, + detail="Occupant can not update address.", + ) diff --git a/ApiEvents/TemplateServiceApi/route_configs.py b/ApiEvents/TemplateServiceApi/route_configs.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiEvents/ValidationServiceApi/__init__.py b/ApiEvents/ValidationServiceApi/__init__.py index a2080bb..7b0d908 100644 --- a/ApiEvents/ValidationServiceApi/__init__.py +++ b/ApiEvents/ValidationServiceApi/__init__.py @@ -1,5 +1,6 @@ -"""Event Service API initialization""" +"""Validation Service API initialization""" from .route_configs import get_route_configs __all__ = ["get_route_configs"] + diff --git a/ApiEvents/ValidationServiceApi/events/validation/__init__.py b/ApiEvents/ValidationServiceApi/events/validation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiEvents/ValidationServiceApi/events/validation/endpoints.py b/ApiEvents/ValidationServiceApi/events/validation/endpoints.py new file mode 100644 index 0000000..6c62f35 --- /dev/null +++ b/ApiEvents/ValidationServiceApi/events/validation/endpoints.py @@ -0,0 +1,116 @@ +from typing import Dict, Any + +from .models import ValidationsPydantic +from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi + +from .validation import ( + ValidationsBoth, + ValidationsHeaders, + ValidationsValidations, +) +from ApiEvents.abstract_class import RouteFactoryConfig, EndpointFactoryConfig +from ApiEvents.base_request_model import EndpointBaseRequestModel +from ApiLibrary.common.line_number import get_line_number_for_error + +from Services.PostgresDb.Models.alchemy_response import DictJsonResponse +from fastapi import Request, Path, Body + +from middleware.token_event_middleware import TokenEventMiddleware + + +prefix = "/validation" + + +@TokenEventMiddleware.validation_required +async def validations_validations_select(request: Request, data: EndpointBaseRequestModel) -> Dict[str, Any]: + """ + Select validations. + """ + wrapped_context = getattr(validations_validations_select, "__wrapped__", None) + auth_context = getattr(wrapped_context, "auth", None) + validation_code = getattr(validations_validations_select, "validation_code", {"validation_code": None}) + if not validation_code: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Validation code not found", + ) + validations_pydantic = ValidationsPydantic( + class_model=validation_code.get("class", None), + reachable_event_code=validation_code.get("reachable_event_code", None), + lang=getattr(auth_context, "lang", None), + ) + validations_both = ValidationsBoth.retrieve_both_validations_and_headers(validations_pydantic) + return {"status": "OK", "validation_code": validation_code, **validations_both } + + +@TokenEventMiddleware.validation_required +async def validations_headers_select(request: Request, data: EndpointBaseRequestModel) -> Dict[str, Any]: + """ + Select headers. + """ + ValidationsHeaders.retrieve_headers() + return { + "status": "OK", + } + + +@TokenEventMiddleware.validation_required +async def validations_validations_and_headers_select(request: Request, data: EndpointBaseRequestModel) -> Dict[str, Any]: + """ + Select validations and headers. + """ + ValidationsBoth.retrieve_both_validations_and_headers() + return { + "status": "OK", + } + + +VALIDATION_CONFIG_MAIN =RouteFactoryConfig( + name="validations", + prefix=prefix, + tags=["Validation"], + include_in_schema=True, + endpoints=[ + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/select", + url_of_endpoint=f"{prefix}/validations/select", + endpoint="/select", + method="POST", + summary="Select company or occupant type", + description="Select company or occupant type", + is_auth_required=False, # Needs token_dict + is_event_required=False, + endpoint_function=validations_validations_select, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/headers/select", + url_of_endpoint=f"{prefix}/headers/select", + endpoint="/headers/select", + method="POST", + summary="Select company or occupant type", + description="Select company or occupant type", + is_auth_required=False, # Needs token_dict + is_event_required=False, + endpoint_function=validations_headers_select, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/both/select", + url_of_endpoint=f"{prefix}/validationsAndHeaders/select", + endpoint="/both/select", + method="POST", + summary="Select company or occupant type", + description="Select company or occupant type", + is_auth_required=False, # Needs token_dict + is_event_required=False, + endpoint_function=validations_validations_and_headers_select, + ), + ], +) +VALIDATION_CONFIG = VALIDATION_CONFIG_MAIN.as_dict() + +VALIDATION_ENDPOINTS = [endpoint.url_of_endpoint for endpoint in VALIDATION_CONFIG_MAIN.endpoints] diff --git a/ApiEvents/ValidationServiceApi/events/validation/models.py b/ApiEvents/ValidationServiceApi/events/validation/models.py new file mode 100644 index 0000000..7f5c7d8 --- /dev/null +++ b/ApiEvents/ValidationServiceApi/events/validation/models.py @@ -0,0 +1,29 @@ +""" +Validation records request and response models. +""" + +from typing import TYPE_CHECKING, Dict, Any +from pydantic import BaseModel, Field, RootModel +from ApiEvents.base_request_model import BaseRequestModel + +if TYPE_CHECKING: + from ApiValidations.Request import ( + ListOptions, + ) + +class ValidationsPydantic(BaseModel): + class_model: str + reachable_event_code: str + lang: str + + +class InsertValidationRecordRequestModel(BaseRequestModel): + pass + + +class UpdateValidationRecordRequestModel(BaseRequestModel): + pass + + +class ListOptionsValidationRecordRequestModel(BaseRequestModel): + pass diff --git a/ApiEvents/ValidationServiceApi/events/validation/validation.py b/ApiEvents/ValidationServiceApi/events/validation/validation.py new file mode 100644 index 0000000..00e8511 --- /dev/null +++ b/ApiEvents/ValidationServiceApi/events/validation/validation.py @@ -0,0 +1,97 @@ +""" +Validation request models. +""" + +from typing import TYPE_CHECKING, Dict, Any, Literal, Optional, TypedDict, Union +from pydantic import BaseModel, Field, model_validator, RootModel, ConfigDict + +from ApiLibrary.common.line_number import get_line_number_for_error +from ApiValidations.Custom.validation_response import ValidationModel, ValidationParser + +from ApiEvents.abstract_class import MethodToEvent +from ApiEvents.base_request_model import BaseRequestModel, DictRequestModel + +from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject +from ApiValidations.Request.base_validations import ListOptions + +from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi +from .models import ValidationsPydantic + + +if TYPE_CHECKING: + from fastapi import Request + + +class AllModelsImport: + + @classmethod + def import_all_models(cls): + from ApiEvents.events.account.account_records import ( + AccountListEventMethod, + AccountUpdateEventMethod, + AccountCreateEventMethod, + ) + from ApiEvents.events.address.address import ( + AddressListEventMethod, + AddressUpdateEventMethod, + AddressCreateEventMethod, + AddressSearchEventMethod, + ) + return dict( + AccountListEventMethod=AccountListEventMethod, + AccountUpdateEventMethod=AccountUpdateEventMethod, + AccountCreateEventMethod=AccountCreateEventMethod, + AddressListEventMethod=AddressListEventMethod, + AddressUpdateEventMethod=AddressUpdateEventMethod, + AddressCreateEventMethod=AddressCreateEventMethod, + AddressSearchEventMethod=AddressSearchEventMethod, + ) + + + +class ValidationsBoth(MethodToEvent): + + @classmethod + def retrieve_both_validations_and_headers( + cls, event: ValidationsPydantic + ) -> Dict[str, Any]: + EVENT_MODELS = AllModelsImport.import_all_models() + return_single_model = EVENT_MODELS.get(event.class_model, None) + print("return_single_model", return_single_model, type(return_single_model)) + # event_class_validation = getattr(return_single_model, "__event_validation__", None) + if not return_single_model: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Validation code not found", + ) + response_model = return_single_model.retrieve_event_response_model(event.reachable_event_code) + language_model_all = return_single_model.retrieve_language_parameters(function_code=event.reachable_event_code, language=event.lang) + language_model = language_model_all.get("language_model", None) + language_models = language_model_all.get("language_models", None) + + validation = ValidationModel(response_model, language_model, language_models) + return { + "headers": validation.headers, + "validation": validation.validation, + "language_models": language_model_all, + } + + +class ValidationsValidations(MethodToEvent): + + @classmethod + def retrieve_validations( + cls, event: ValidationsPydantic + ) -> Dict[str, Any]: + return {} + + +class ValidationsHeaders(MethodToEvent): + + @classmethod + def retrieve_headers( + cls, event: ValidationsPydantic + ) -> Dict[str, Any]: + return {} diff --git a/ApiEvents/ValidationServiceApi/retrieveValidation.py b/ApiEvents/ValidationServiceApi/retrieveValidation.py new file mode 100644 index 0000000..8c5459b --- /dev/null +++ b/ApiEvents/ValidationServiceApi/retrieveValidation.py @@ -0,0 +1,33 @@ +from typing import Callable + + +class ValidationActions: + + def __init__(self, function_code: str, func: Callable = None): + self.function_code = function_code + self.func = func + + @classmethod + def retrieve_validation(cls): + """ + Retrieve validation [] by validation via [Response Model of Table] + """ + return + + @classmethod + def retrieve_headers(cls): + """ + Retrieve headers for validations [] by event function code [Response Model of Table] + """ + return + + @classmethod + def retrieve_validations_and_headers(cls): + """ + Retrieve validations and headers [] via event function code [Response Model of Table][] + """ + return + + +# Singleton class +validation_action = ValidationActions(function_code="") diff --git a/ApiEvents/ValidationServiceApi/route_configs.py b/ApiEvents/ValidationServiceApi/route_configs.py index 630ae67..c9c8f95 100644 --- a/ApiEvents/ValidationServiceApi/route_configs.py +++ b/ApiEvents/ValidationServiceApi/route_configs.py @@ -5,13 +5,13 @@ This module collects and registers all route configurations from different modul to be used by the dynamic route creation system. """ -from typing import Dict, List, Any - +from typing import Dict, List, Any, TypeVar +from .events.validation.endpoints import VALIDATION_CONFIG # Registry of all route configurations -ROUTE_CONFIGS = [] +ROUTE_CONFIGS = [VALIDATION_CONFIG] def get_route_configs() -> List[Dict[str, Any]]: """Get all registered route configurations.""" - return [] + return ROUTE_CONFIGS diff --git a/ApiEvents/abstract_class.py b/ApiEvents/abstract_class.py index ab95907..e1be259 100644 --- a/ApiEvents/abstract_class.py +++ b/ApiEvents/abstract_class.py @@ -24,6 +24,10 @@ from fastapi import Request, Depends, APIRouter from functools import wraps import inspect +from ApiLibrary.common.line_number import get_line_number_for_error +from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi +from Schemas.rules.rules import EndpointRestriction + ResponseModel = TypeVar("ResponseModel", bound=BaseModel) @@ -239,14 +243,91 @@ class MethodToEvent: event_description: ClassVar[str] = "" event_category: ClassVar[str] = "" __event_keys__: ClassVar[Dict[str, str]] = {} - __event_validation__: ClassVar[Dict[str, Tuple[Type[ResponseModel], List[Any]]]] = ( - {} - ) + __event_validation__: Dict[str, Tuple[Type, Union[List, tuple]]] = {} + + @classmethod + def retrieve_event_response_model(cls, function_code: str) -> Tuple: + """Retrieve event validation for a specific function. + + Args: + function_code: Function identifier + + Returns: + Tuple containing response model and language models + """ + event_validation_list = cls.__event_validation__.get(function_code, None) + if not event_validation_list: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Function not found", + ) + return event_validation_list[0] + + @classmethod + def retrieve_event_languages(cls, function_code: str) -> Union[List, tuple]: + """Retrieve event description for a specific function. + + Args: + function_code: Function identifier + + Returns: + Event description + """ + event_keys_list = cls.__event_validation__.get(function_code, None) + print('event_keys_list', event_keys_list) + if not event_keys_list: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Function not found", + ) + function_language_models: Union[List, tuple] = event_keys_list[1] + if not function_language_models: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Function not found", + ) + return function_language_models + + @staticmethod + def merge_models(language_model: List) -> Tuple: + merged_models = {"tr": {}, "en": {}} + for model in language_model: + for lang in dict(model).keys(): + if lang not in merged_models: + merged_models[lang] = model[lang] + else: + merged_models[lang].update(model[lang]) + return merged_models @classmethod - def retrieve_language_parameters( - cls, language: str, function_code: str - ) -> Dict[str, str]: + def retrieve_event_function(cls, function_code: str) -> Dict[str, str]: + """Retrieve event parameters for a specific function. + + Args: + function_code: Function identifier + + Returns: + Dictionary of event parameters + """ + function_event = cls.__event_keys__[function_code] + function_itself = getattr(cls, function_event, None) + if not function_itself: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Function not found", + ) + return function_itself + + @classmethod + def retrieve_language_parameters(cls, function_code: str, language: str = "tr") -> Dict[str, str]: """Retrieve language-specific parameters for an event. Args: @@ -256,20 +337,24 @@ class MethodToEvent: Returns: Dictionary of language-specific field mappings """ - validation_dict = dict(cls.__event_validation__) - if function_code not in validation_dict: - return {} - - event_response_model, event_language_models = validation_dict[function_code] - - # Collect language-specific field mappings - language_models = {} - for model in event_language_models: - language_models.update(model.get(language, model.get("tr", {}))) - + event_language_models = cls.retrieve_event_languages(function_code) + event_response_model = cls.retrieve_event_response_model(function_code) + event_response_model_merged = cls.merge_models(event_language_models) + event_response_model_merged_lang = event_response_model_merged[language] # Map response model fields to language-specific values - return { - field: language_models[field] + print('event_response_model', dict( + event_response_model=event_response_model, + event_response_model_merged_lang=event_response_model_merged_lang, + event_response_model_merged=event_response_model_merged, + language=language, + function_code=function_code, + )) + only_language_dict = { + field: event_response_model_merged_lang[field] for field in event_response_model.model_fields - if field in language_models + if field in event_response_model_merged_lang + } + return { + "language_model": only_language_dict, + "language_models": event_response_model_merged, } diff --git a/ApiServices/Login/user_login_handler.py b/ApiServices/Login/user_login_handler.py index 52c3539..ec416c7 100644 --- a/ApiServices/Login/user_login_handler.py +++ b/ApiServices/Login/user_login_handler.py @@ -37,7 +37,6 @@ class UserLoginModule: async def login_user_via_credentials(self, access_data: "Login") -> Dict[str, Any]: """Login user via credentials.""" # Get the actual data from the BaseRequestModel if needed - print("access_data", access_data) if hasattr(access_data, "data"): access_data = access_data.data diff --git a/ApiServices/Token/token_handler.py b/ApiServices/Token/token_handler.py index f60f7bd..871fa63 100644 --- a/ApiServices/Token/token_handler.py +++ b/ApiServices/Token/token_handler.py @@ -1,6 +1,5 @@ """Token service for handling authentication tokens and user sessions.""" -import json from typing import List, Union, TypeVar, Dict, Any, Optional, TYPE_CHECKING from AllConfigs.Token.config import Auth @@ -75,7 +74,6 @@ class TokenService: ) occupants_selection_dict: Dict[str, Any] = {} - for living_space in living_spaces: build_parts_selection = BuildParts.filter_all( BuildParts.id == living_space.build_parts_id, @@ -221,7 +219,6 @@ class TokenService: company_address = Addresses.filter_by_one( id=company.official_address_id, db=db_session ).data - companies_list.append( { "uu_id": str(company.uu_id), @@ -287,15 +284,14 @@ class TokenService: Users.client_arrow = DateTimeLocal(is_client=True, timezone=user.local_timezone) db_session = UsersTokens.new_session() # Handle login based on user type - login_dict = ( - cls.do_occupant_login(request=request, user=user, domain=domain) - if user.is_occupant - else ( - cls.do_employee_login(request=request, user=user, domain=domain) - if user.is_employee - else {} + if user.is_occupant: + login_dict = cls.do_occupant_login( + request=request, user=user, domain=domain + ) + elif user.is_employee: + login_dict = cls.do_employee_login( + request=request, user=user, domain=domain ) - ) # Handle remember me functionality if remember: diff --git a/ApiValidations/Custom/token_objects.py b/ApiValidations/Custom/token_objects.py index f08711e..4054d34 100644 --- a/ApiValidations/Custom/token_objects.py +++ b/ApiValidations/Custom/token_objects.py @@ -58,7 +58,7 @@ class OccupantToken(BaseModel): responsible_employee_id: Optional[int] = None responsible_employee_uuid: Optional[str] = None - reachable_event_list_id: Optional[list] = None # ID list of reachable modules + reachable_event_codes: Optional[list[str]] = None # ID list of reachable modules # reachable_event_list_uu_id: Optional[list] = None # UUID list of reachable modules @@ -81,7 +81,7 @@ class CompanyToken(BaseModel): # Required Company Object for an employee bulk_duties_id: int - reachable_event_list_id: Optional[list] = None # ID list of reachable modules + reachable_event_codes: Optional[list[str]] = None # ID list of reachable modules # reachable_event_list_uu_id: Optional[list] = None # UUID list of reachable modules diff --git a/ApiValidations/Custom/validation_response.py b/ApiValidations/Custom/validation_response.py new file mode 100644 index 0000000..061bff5 --- /dev/null +++ b/ApiValidations/Custom/validation_response.py @@ -0,0 +1,68 @@ +import json +from typing import Any, ClassVar, TypeVar, Dict, Tuple, List +from pydantic import BaseModel + +from ErrorHandlers import HTTPExceptionApi +from ApiLibrary.common.line_number import get_line_number_for_error +from ApiValidations.Request.base_validations import CrudRecords, PydanticBaseModel + + +class ValidationParser: + + def __init__(self, active_validation: BaseModel): + self.core_validation = active_validation + self.annotations = active_validation.model_json_schema() + self.annotations = json.loads(json.dumps(self.annotations)) + self.schema = {} + self.parse() + + def parse(self): + from ApiValidations.Request.base_validations import CrudRecords, PydanticBaseModel + + properties = dict(self.annotations.get("properties")).items() + total_class_annotations = { + **self.core_validation.__annotations__, + **PydanticBaseModel.__annotations__, + **CrudRecords.__annotations__, + } + for key, value in properties: + default, required, possible_types = dict(value).get("default", None), True, [] + if dict(value).get("anyOf", None): + for _ in dict(value).get("anyOf") or []: + type_opt = json.loads(json.dumps(_)) + if not type_opt.get("type") == "null": + possible_types.append(type_opt.get("type")) + field_type = possible_types[0] + required = False + else: + field_type = dict(value).get("type", "string") + attribute_of_class = total_class_annotations.get(key, None) + aoc = str(attribute_of_class) if attribute_of_class else None + if attribute_of_class: + if aoc in ("", "typing.Optional[str]"): + field_type, required = "string", aoc == "" + elif aoc in ("", "typing.Optional[int]"): + field_type, required = "integer", aoc == "" + elif aoc in ("", "typing.Optional[bool]"): + field_type, required = "boolean", aoc == "" + elif aoc in ("", "typing.Optional[float]"): + field_type, required = "float", aoc == "" + elif aoc in ("", "typing.Optional[datetime.datetime]"): + field_type, required = "datetime", aoc == "" + self.schema[key] = { + "type": field_type, "required": required, "default": default + } + + +class ValidationModel: + + def __init__(self, response_model: BaseModel, language_model, language_models): + self.response_model = response_model + self.validation = None + self.headers = language_model + self.language_models = language_models + self.get_validation() + + def get_validation(self) -> Tuple: + self.headers = self.language_models + self.validation = ValidationParser(self.response_model).schema diff --git a/ApiValidations/Request/authentication.py b/ApiValidations/Request/authentication.py index bbef867..8f3b7ad 100644 --- a/ApiValidations/Request/authentication.py +++ b/ApiValidations/Request/authentication.py @@ -61,12 +61,21 @@ class OccupantSelection(BaseModel, OccupantSelectionValidation): model_config = ConfigDict( json_schema_extra={ - "example": { - "build_living_space_uu_id": "987fcdeb-51a2-43e7-9876-543210987654" - } + "example": [ + {"company_uu_id": "abcdef12-3456-7890-abcd-ef1234567890"}, + {"build_living_space_uu_id": "987fcdeb-51a2-43e7-9876-543210987654"}, + ], } ) + @property + def is_employee(self): + return False + + @property + def is_occupant(self): + return True + class EmployeeSelectionValidation: tr = {"company_uu_id": "Şirket UU ID"} @@ -78,10 +87,21 @@ class EmployeeSelection(BaseModel, EmployeeSelectionValidation): model_config = ConfigDict( json_schema_extra={ - "example": {"company_uu_id": "abcdef12-3456-7890-abcd-ef1234567890"} + "example": [ + {"company_uu_id": "abcdef12-3456-7890-abcd-ef1234567890"}, + {"build_living_space_uu_id": "987fcdeb-51a2-43e7-9876-543210987654"}, + ], } ) + @property + def is_employee(self): + return True + + @property + def is_occupant(self): + return False + class LoginValidation: tr = { diff --git a/ApiValidations/Response/__init__.py b/ApiValidations/Response/__init__.py index 5af54b7..8faf38b 100644 --- a/ApiValidations/Response/__init__.py +++ b/ApiValidations/Response/__init__.py @@ -1,3 +1,16 @@ from .account_responses import AccountRecordResponse +from .address_responses import ListAddressResponse +from .auth_responses import ( + AuthenticationLoginResponse, + AuthenticationRefreshResponse, + AuthenticationUserInfoResponse +) -__all__ = ["AccountRecordResponse"] + +__all__ = [ + "AccountRecordResponse", + "ListAddressResponse", + "AuthenticationLoginResponse", + "AuthenticationRefreshResponse", + "AuthenticationUserInfoResponse", +] diff --git a/ApiValidations/Response/address_responses.py b/ApiValidations/Response/address_responses.py new file mode 100644 index 0000000..e96f371 --- /dev/null +++ b/ApiValidations/Response/address_responses.py @@ -0,0 +1,20 @@ +from typing import Optional +from pydantic import BaseModel + + +class ListAddressResponse(BaseModel): + build_number: Optional[str] = None + door_number: Optional[str] = None + floor_number: Optional[str] = None + comment_address: Optional[str] = None + letter_address: Optional[str] = None + short_letter_address: Optional[str] = None + latitude: Optional[float] = None + longitude: Optional[float] = None + street_uu_id: Optional[str] = None + + +class AddressPostCodeResponse: + street_id: Optional[int] = None + street_uu_id: Optional[str] = None + postcode: Optional[str] = None diff --git a/ApiValidations/Response/auth_responses.py b/ApiValidations/Response/auth_responses.py new file mode 100644 index 0000000..7e03182 --- /dev/null +++ b/ApiValidations/Response/auth_responses.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from typing import Optional, List, Dict, Any +from datetime import datetime +from uuid import UUID + + +class AuthenticationLoginResponse(BaseModel): + """Response model for authentication login endpoint""" + + token: str + refresh_token: str + token_type: str + expires_in: int + user_info: Dict[str, Any] + + +class AuthenticationRefreshResponse(BaseModel): + """Response model for authentication refresh endpoint""" + + token: str + refresh_token: str + token_type: str + expires_in: int + + +class AuthenticationUserInfoResponse(BaseModel): + """Response model for authentication user info endpoint""" + + user_id: int + username: str + email: str + first_name: str + last_name: str + is_active: bool + created_at: datetime + updated_at: Optional[datetime] diff --git a/ApiValidations/Response/base_responses.py b/ApiValidations/Response/base_responses.py new file mode 100644 index 0000000..06e6c02 --- /dev/null +++ b/ApiValidations/Response/base_responses.py @@ -0,0 +1,105 @@ +from pydantic import BaseModel +from typing import Optional, TypeVar, Generic, List +from datetime import datetime +from uuid import UUID + +T = TypeVar("T") + + +class BaseResponse(BaseModel): + """Base response model that all response models inherit from. + + This model provides common fields that are present in all database records, + including tracking information (created/updated timestamps), user actions + (created by, updated by, confirmed by), and record status (active, deleted). + + Attributes: + uu_id (str): Unique identifier for the record, typically a UUID + created_at (datetime): Timestamp when the record was created + updated_at (Optional[datetime]): Timestamp when the record was last updated + created_by (Optional[str]): Username or identifier of the user who created the record + updated_by (Optional[str]): Username or identifier of the user who last updated the record + confirmed_by (Optional[str]): Username or identifier of the user who confirmed the record + is_confirmed (Optional[bool]): Whether the record has been confirmed/approved + active (Optional[bool]): Whether the record is currently active + deleted (Optional[bool]): Whether the record has been marked as deleted + expiry_starts (Optional[datetime]): When the record becomes valid/active + expiry_ends (Optional[datetime]): When the record expires/becomes inactive + is_notification_send (Optional[bool]): Whether notifications have been sent for this record + is_email_send (Optional[bool]): Whether emails have been sent for this record + """ + + uu_id: str + created_at: datetime + updated_at: Optional[datetime] + created_by: Optional[str] + updated_by: Optional[str] + confirmed_by: Optional[str] + is_confirmed: Optional[bool] = None + active: Optional[bool] = True + deleted: Optional[bool] = False + expiry_starts: Optional[datetime] + expiry_ends: Optional[datetime] + is_notification_send: Optional[bool] = False + is_email_send: Optional[bool] = False + + class Config: + """Pydantic configuration for the base response model. + + Attributes: + from_attributes (bool): Enables ORM mode for SQLAlchemy integration + """ + + from_attributes = True + + +class CrudCollection(BaseModel, Generic[T]): + """Base collection model for paginated responses. + + This model is used to return collections of items with pagination information. + It is generic over the type of items in the collection, allowing it to be + used with any response model. + + Type Parameters: + T: The type of items in the collection + + Attributes: + page (int): Current page number, 1-based indexing + size (int): Number of items per page + total (int): Total number of items across all pages + order_field (str): Field used for sorting the collection + order_type (str): Sort direction ('asc' or 'desc') + items (List[T]): List of items in the current page + + Example: + ```python + class UserResponse(BaseResponse): + name: str + email: str + + users = CrudCollection[UserResponse]( + page=1, + size=10, + total=100, + order_field="name", + order_type="asc", + items=[...] + ) + ``` + """ + + page: int = 1 + size: int = 10 + total: int = 0 + order_field: str = "id" + order_type: str = "asc" + items: List[T] = [] + + class Config: + """Pydantic configuration for the collection model. + + Attributes: + from_attributes (bool): Enables ORM mode for SQLAlchemy integration + """ + + from_attributes = True diff --git a/ApiValidations/Response/budget_responses.py b/ApiValidations/Response/budget_responses.py new file mode 100644 index 0000000..9ecd619 --- /dev/null +++ b/ApiValidations/Response/budget_responses.py @@ -0,0 +1,90 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime +from uuid import UUID +from decimal import Decimal +from .base_responses import BaseResponse, CrudCollection + + +class DecisionBookBudgetBooksResponse(BaseResponse): + """Response model for decision book budget books""" + + country: str + branch_type: int = 0 + company_id: int + company_uu_id: str + branch_id: Optional[int] + branch_uu_id: Optional[str] + build_decision_book_id: int + build_decision_book_uu_id: Optional[str] + + +class DecisionBookBudgetBooksCollection( + CrudCollection[DecisionBookBudgetBooksResponse] +): + """Collection of decision book budget books""" + + pass + + +class DecisionBookBudgetCodesResponse(BaseResponse): + """Response model for decision book budget codes""" + + budget_code: str + comment_line: str + budget_type: str + budget_code_seperator: str = "." + system_id: int = 0 + locked: bool = False + company_id: Optional[int] + company_uu_id: str + customer_id: Optional[int] + customer_uu_id: str + + +class DecisionBookBudgetCodesCollection( + CrudCollection[DecisionBookBudgetCodesResponse] +): + """Collection of decision book budget codes""" + + pass + + +class DecisionBookBudgetMasterResponse(BaseResponse): + """Response model for decision book budget master""" + + budget_type: str + currency: str = "TRY" + total_budget: Decimal + tracking_period_id: Optional[int] + tracking_period_uu_id: Optional[str] + budget_books_id: int + budget_books_uu_id: Optional[str] + department_id: int + department_uu_id: Optional[str] + + +class DecisionBookBudgetMasterCollection( + CrudCollection[DecisionBookBudgetMasterResponse] +): + """Collection of decision book budget masters""" + + pass + + +class DecisionBookBudgetsResponse(BaseResponse): + """Response model for decision book budgets""" + + process_date: datetime + budget_codes_id: int + total_budget: Decimal + used_budget: Decimal = Decimal("0") + remaining_budget: Decimal = Decimal("0") + decision_book_budget_master_id: int + decision_book_budget_master_uu_id: Optional[str] + + +class DecisionBookBudgetsCollection(CrudCollection[DecisionBookBudgetsResponse]): + """Collection of decision book budgets""" + + pass diff --git a/ApiValidations/Response/building_responses.py b/ApiValidations/Response/building_responses.py new file mode 100644 index 0000000..b639f6e --- /dev/null +++ b/ApiValidations/Response/building_responses.py @@ -0,0 +1,309 @@ +from typing import Optional, List, Generic +from datetime import datetime +from uuid import UUID +from decimal import Decimal + +from api_validations.validations_response.base_responses import ( + BaseResponse, + CrudCollection, +) +from api_validations.validations_request import PydanticBaseModel + + +class ListBuildingResponse(PydanticBaseModel): + + gov_address_code: str + build_name: str + build_types_uu_id: Optional[str] = None + build_no: Optional[str] = None + max_floor: Optional[int] = None + underground_floor: Optional[int] = None + address_uu_id: Optional[str] = None + build_date: Optional[str] = None + decision_period_date: Optional[str] = None + tax_no: Optional[str] = None + lift_count: Optional[int] = None + heating_system: Optional[bool] = None + cooling_system: Optional[bool] = None + hot_water_system: Optional[bool] = None + block_service_man_count: Optional[int] = None + security_service_man_count: Optional[int] = None + garage_count: Optional[int] = None + site_uu_id: Optional[str] = None + + +class BuildAreaListResponse(BaseResponse): + """Response model for building area list endpoint""" + + uu_id: UUID + build_id: int + build_uu_id: str + area_name: str + area_value: float + created_at: datetime + updated_at: Optional[datetime] + deleted: bool = False + + +class BuildAreaListCollection(CrudCollection[BuildAreaListResponse]): + """Collection of building area list""" + + pass + + +class BuildSitesListResponse(BaseResponse): + """Response model for building sites list endpoint""" + + uu_id: UUID + address_id: int + site_name: str + site_value: float + created_at: datetime + updated_at: Optional[datetime] + deleted: bool = False + + +class BuildSitesListCollection(CrudCollection[BuildSitesListResponse]): + """Collection of building sites list""" + + pass + + +class BuildTypesListResponse(BaseResponse): + """Response model for building types list endpoint""" + + uu_id: UUID + type_name: str + type_value: str + created_at: datetime + updated_at: Optional[datetime] + deleted: bool = False + + +class BuildTypesListCollection(CrudCollection[BuildTypesListResponse]): + """Collection of building types list""" + + pass + + +class BuildTypesResponse(BaseResponse): + """Response model for building types""" + + function_code: str + type_code: str + lang: str = "TR" + + +class BuildTypesCollection(CrudCollection[BuildTypesResponse]): + """Collection of building types""" + + pass + + +class Part2EmployeeResponse(BaseResponse): + """Response model for part to employee mapping""" + + build_id: int + part_id: int + employee_id: int + + +class Part2EmployeeCollection(CrudCollection[Part2EmployeeResponse]): + """Collection of part to employee mappings""" + + pass + + +class RelationshipEmployee2BuildResponse(BaseResponse): + """Response model for employee to build relationship""" + + company_id: int + employee_id: int + member_id: int + relationship_type: Optional[str] = "Employee" + show_only: bool = False + + +class RelationshipEmployee2BuildCollection( + CrudCollection[RelationshipEmployee2BuildResponse] +): + """Collection of employee to build relationships""" + + pass + + +class BuildResponse(BaseResponse): + """Response model for buildings""" + + gov_address_code: str = "" + build_name: str + build_no: str + max_floor: int = 1 + underground_floor: int = 0 + build_date: datetime + decision_period_date: datetime + tax_no: str = "" + lift_count: int = 0 + heating_system: bool = True + cooling_system: bool = False + hot_water_system: bool = False + block_service_man_count: int = 0 + security_service_man_count: int = 0 + garage_count: int = 0 + management_room_id: Optional[int] + site_id: Optional[int] + site_uu_id: Optional[str] + address_id: int + address_uu_id: str + build_types_id: int + build_types_uu_id: Optional[str] + + +class BuildCollection(CrudCollection[BuildResponse]): + """Collection of buildings""" + + pass + + +class BuildPartsResponse(BaseResponse): + """Response model for building parts""" + + address_gov_code: str + part_no: int = 0 + part_level: int = 0 + part_code: str + part_gross_size: int = 0 + part_net_size: int = 0 + default_accessory: str = "0" + human_livable: bool = True + due_part_key: str + build_id: int + build_uu_id: str + part_direction_id: Optional[int] + part_direction_uu_id: Optional[str] + part_type_id: int + part_type_uu_id: str + + +class BuildPartsCollection(CrudCollection[BuildPartsResponse]): + """Collection of building parts""" + + pass + + +class BuildLivingSpaceResponse(BaseResponse): + """Response model for building living space""" + + fix_value: Decimal = Decimal("0") + fix_percent: Decimal = Decimal("0") + agreement_no: str = "" + marketing_process: bool = False + marketing_layer: int = 0 + build_parts_id: int + build_parts_uu_id: str + person_id: int + person_uu_id: str + occupant_type: int + occupant_type_uu_id: str + + +class BuildLivingSpaceCollection(CrudCollection[BuildLivingSpaceResponse]): + """Collection of building living spaces""" + + pass + + +class BuildManagementResponse(BaseResponse): + """Response model for building management""" + + discounted_percentage: Decimal = Decimal("0.00") + discounted_price: Decimal = Decimal("0.00") + calculated_price: Decimal = Decimal("0.00") + occupant_type: int + occupant_type_uu_id: str + build_id: int + build_uu_id: str + build_parts_id: int + build_parts_uu_id: str + + +class BuildManagementCollection(CrudCollection[BuildManagementResponse]): + """Collection of building management records""" + + pass + + +class BuildAreaResponse(BaseResponse): + """Response model for building area""" + + area_name: str = "" + area_code: str = "" + area_type: str = "GREEN" + area_direction: str = "NN" + area_gross_size: Decimal = Decimal("0") + area_net_size: Decimal = Decimal("0") + width: int = 0 + size: int = 0 + build_id: int + build_uu_id: str + part_type_id: Optional[int] + part_type_uu_id: Optional[str] + + +class BuildAreaCollection(CrudCollection[BuildAreaResponse]): + """Collection of building areas""" + + pass + + +class BuildSitesResponse(BaseResponse): + """Response model for building sites""" + + site_name: str + site_no: str + address_id: int + address_uu_id: Optional[str] + + +class BuildSitesCollection(CrudCollection[BuildSitesResponse]): + """Collection of building sites""" + + pass + + +class BuildCompaniesProvidingResponse(BaseResponse): + """Response model for building companies providing services""" + + build_id: int + build_uu_id: Optional[str] + company_id: int + company_uu_id: Optional[str] + provide_id: Optional[int] + provide_uu_id: Optional[str] + contract_id: Optional[int] + + +class BuildCompaniesProvidingCollection( + CrudCollection[BuildCompaniesProvidingResponse] +): + """Collection of building companies providing services""" + + pass + + +class BuildPersonProvidingResponse(BaseResponse): + """Response model for building person providing services""" + + build_id: int + build_uu_id: Optional[str] + people_id: int + people_uu_id: Optional[str] + provide_id: Optional[int] + provide_uu_id: Optional[str] + contract_id: Optional[int] + + +class BuildPersonProvidingCollection(CrudCollection[BuildPersonProvidingResponse]): + """Collection of building person providing services""" + + pass diff --git a/ApiValidations/Response/company_responses.py b/ApiValidations/Response/company_responses.py new file mode 100644 index 0000000..1c261c4 --- /dev/null +++ b/ApiValidations/Response/company_responses.py @@ -0,0 +1,59 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime +from uuid import UUID + + +class CompanyListResponse(BaseModel): + """Response model for company list endpoint""" + + uu_id: UUID + company_name: str + company_code: str + company_email: str + company_phone: str + company_address: str + created_at: datetime + updated_at: Optional[datetime] + deleted: bool = False + + +class CompanyDepartmentListResponse(BaseModel): + """Response model for company department list endpoint""" + + uu_id: UUID + department_name: str + department_code: str + company_id: int + company_uu_id: str + created_at: datetime + updated_at: Optional[datetime] + deleted: bool = False + + +class CompanyDutyListResponse(BaseModel): + """Response model for company duty list endpoint""" + + uu_id: UUID + duty_name: str + duty_code: str + department_id: int + department_uu_id: str + created_at: datetime + updated_at: Optional[datetime] + deleted: bool = False + + +class CompanyEmployeeListResponse(BaseModel): + """Response model for company employee list endpoint""" + + uu_id: UUID + employee_id: int + employee_uu_id: str + company_id: int + company_uu_id: str + duty_id: int + duty_uu_id: str + created_at: datetime + updated_at: Optional[datetime] + deleted: bool = False diff --git a/ApiValidations/Response/decision_book_responses.py b/ApiValidations/Response/decision_book_responses.py new file mode 100644 index 0000000..53d504b --- /dev/null +++ b/ApiValidations/Response/decision_book_responses.py @@ -0,0 +1,204 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime +from uuid import UUID +from decimal import Decimal +from .base_responses import BaseResponse, CrudCollection + + +class BuildDecisionBookResponse(BaseResponse): + """Response model for building decision book""" + + decision_book_pdf_path: Optional[str] = "" + resp_company_fix_wage: float = 0 + contact_agreement_path: Optional[str] = "" + contact_agreement_date: Optional[datetime] + meeting_date: Optional[str] + decision_type: Optional[str] + + +class BuildDecisionBookCollection(CrudCollection[BuildDecisionBookResponse]): + """Collection of building decision books""" + + pass + + +class BuildDecisionBookInvitationsResponse(BaseResponse): + """Response model for building decision book invitations""" + + build_id: int + build_uu_id: Optional[str] + decision_book_id: int + decision_book_uu_id: Optional[str] + invitation_type: str + invitation_attempt: int = 1 + living_part_count: int = 1 + living_part_percentage: Decimal = Decimal("0.51") + message: Optional[str] + planned_date: datetime + planned_date_expires: datetime + + +class BuildDecisionBookInvitationsCollection( + CrudCollection[BuildDecisionBookInvitationsResponse] +): + """Collection of building decision book invitations""" + + pass + + +class BuildDecisionBookPersonResponse(BaseResponse): + """Response model for building decision book person""" + + dues_percent_discount: int = 0 + dues_fix_discount: Decimal = Decimal("0") + dues_discount_approval_date: datetime + send_date: datetime + is_attending: bool = False + confirmed_date: Optional[datetime] + token: str = "" + vicarious_person_id: Optional[int] + vicarious_person_uu_id: Optional[str] + invite_id: int + invite_uu_id: str + build_decision_book_id: int + build_decision_book_uu_id: str + build_living_space_id: int + build_living_space_uu_id: Optional[str] + person_id: int + + +class BuildDecisionBookPersonCollection( + CrudCollection[BuildDecisionBookPersonResponse] +): + """Collection of building decision book persons""" + + pass + + +class BuildDecisionBookPersonOccupantsResponse(BaseResponse): + """Response model for building decision book person occupants""" + + build_decision_book_person_id: int + build_decision_book_person_uu_id: Optional[str] + invite_id: Optional[int] + invite_uu_id: Optional[str] + occupant_type_id: int + occupant_type_uu_id: Optional[str] + + +class BuildDecisionBookPersonOccupantsCollection( + CrudCollection[BuildDecisionBookPersonOccupantsResponse] +): + """Collection of building decision book person occupants""" + + pass + + +class BuildDecisionBookItemsResponse(BaseResponse): + """Response model for building decision book items""" + + item_order: int + item_comment: str + item_objection: Optional[str] + info_is_completed: bool = False + is_payment_created: bool = False + info_type_id: Optional[int] + info_type_uu_id: Optional[str] + build_decision_book_id: int + build_decision_book_uu_id: Optional[str] + item_short_comment: Optional[str] + + +class BuildDecisionBookItemsCollection(CrudCollection[BuildDecisionBookItemsResponse]): + """Collection of building decision book items""" + + pass + + +class BuildDecisionBookItemsUnapprovedResponse(BaseResponse): + """Response model for building decision book items unapproved""" + + item_objection: str + item_order: int + decision_book_item_id: int + decision_book_item_uu_id: Optional[str] + person_id: int + person_uu_id: Optional[str] + build_decision_book_item: int + build_decision_book_item_uu_id: Optional[str] + + +class BuildDecisionBookItemsUnapprovedCollection( + CrudCollection[BuildDecisionBookItemsUnapprovedResponse] +): + """Collection of building decision book items unapproved""" + + pass + + +class BuildDecisionBookPaymentsResponse(BaseResponse): + """Response model for building decision book payments""" + + payment_plan_time_periods: str + process_date: datetime + payment_amount: Decimal + currency: str = "TRY" + payment_types_id: Optional[int] + payment_types_uu_id: Optional[str] + period_time: str + process_date_y: int + process_date_m: int + build_decision_book_item_id: int + build_decision_book_item_uu_id: str + build_parts_id: int + build_parts_uu_id: str + decision_book_project_id: Optional[int] + decision_book_project_uu_id: Optional[str] + account_records_id: Optional[int] + account_records_uu_id: Optional[str] + + +class BuildDecisionBookPaymentsCollection( + CrudCollection[BuildDecisionBookPaymentsResponse] +): + """Collection of building decision book payments""" + + pass + + +class BuildDecisionBookLegalResponse(BaseResponse): + """Response model for building decision book legal""" + + period_start_date: datetime + lawsuits_decision_number: str + lawsuits_decision_date: datetime + period_stop_date: datetime + decision_book_pdf_path: Optional[str] = "" + resp_company_total_wage: Optional[Decimal] = Decimal("0") + contact_agreement_path: Optional[str] = "" + contact_agreement_date: Optional[datetime] + meeting_date: str + lawsuits_type: str = "C" + lawsuits_name: str + lawsuits_note: str + lawyer_cost: Decimal + mediator_lawyer_cost: Decimal + other_cost: Decimal + legal_cost: Decimal + approved_cost: Decimal + total_price: Decimal + build_db_item_id: int + build_db_item_uu_id: Optional[str] + resp_attorney_id: int + resp_attorney_uu_id: Optional[str] + resp_attorney_company_id: int + resp_attorney_company_uu_id: Optional[str] + mediator_lawyer_person_id: int + mediator_lawyer_person_uu_id: Optional[str] + + +class BuildDecisionBookLegalCollection(CrudCollection[BuildDecisionBookLegalResponse]): + """Collection of building decision book legal records""" + + pass diff --git a/ApiValidations/Response/living_space_responses.py b/ApiValidations/Response/living_space_responses.py new file mode 100644 index 0000000..4b4c902 --- /dev/null +++ b/ApiValidations/Response/living_space_responses.py @@ -0,0 +1,52 @@ +from typing import Optional +from api_validations.core_validations import BaseModelRegular +from api_validations.validations_request import ( + CrudRecordValidation, + CrudRecords, +) + + +class LivingSpaceListValidation: + tr = { + **CrudRecordValidation.tr, + "fix_value": "Sabit Değer", + "fix_percent": "Sabit Yüzde", + "agreement_no": "Anlaşma No", + "marketing_process": "Pazarlama Süreci", + "marketing_layer": "Pazarlama Katmanı", + "build_parts_id": "Bölüm ID", + "build_parts_uu_id": "Bölüm UUID", + "person_id": "Sorumlu Kişi ID", + "person_uu_id": "Sorumlu Kişi UUID", + "occupant_type": "Kiracı Tipi", + "occupant_type_uu_id": "Kiracı Tipi UUID", + } + en = { + **CrudRecordValidation.en, + "fix_value": "Fixed Value", + "fix_percent": "Fixed Percent", + "agreement_no": "Agreement No", + "marketing_process": "Marketing Process", + "marketing_layer": "Marketing Layer", + "build_parts_id": "Part ID", + "build_parts_uu_id": "Part UUID", + "person_id": "Responsible Person ID", + "person_uu_id": "Responsible Person UUID", + "occupant_type": "Occupant Type", + "occupant_type_uu_id": "Occupant Type UUID", + } + + +class LivingSpaceListResponse(BaseModelRegular, CrudRecords, LivingSpaceListValidation): + + fix_value: Optional[float] = None + fix_percent: Optional[float] = None + agreement_no: Optional[str] = None + marketing_process: Optional[str] = None + marketing_layer: Optional[str] = None + build_parts_id: Optional[int] = None + build_parts_uu_id: Optional[str] = None + person_id: Optional[int] = None + person_uu_id: Optional[str] = None + occupant_type: Optional[str] = None + occupant_type_uu_id: Optional[str] = None diff --git a/ApiValidations/Response/parts_responses.py b/ApiValidations/Response/parts_responses.py new file mode 100644 index 0000000..bf518ea --- /dev/null +++ b/ApiValidations/Response/parts_responses.py @@ -0,0 +1,54 @@ +from typing import Optional +from api_validations.core_validations import BaseModelRegular +from api_validations.validations_request import ( + CrudRecordValidation, + CrudRecords, +) + + +class BuildPartsListValidation: + tr = { + **CrudRecordValidation.tr, + "address_gov_code": "Adres Kapı Kodu", + "part_no": "Bölüm No", + "part_level": "Bölüm Seviyesi", + "part_code": "Bölüm Kodu", + "part_gross": "Bölüm Brüt", + "part_net": "Bölüm Net", + "default_accessory": "Varsayılan Aksesuar", + "human_livable": "İnsan Yaşamı", + "due_part_key": "Sabit Ödeme Grubu", + "build_uu_id": "Bina UUID", + "part_direction_uu_id": "Bölüm Yönü UUID", + "part_type_uu_id": "Bölüm Tipi UUID", + } + en = { + **CrudRecordValidation.en, + "address_gov_code": "Address Government Code", + "part_no": "Part Number", + "part_level": "Part Level", + "part_code": "Part Code", + "part_gross": "Part Gross", + "part_net": "Part Net", + "default_accessory": "Default Accessory", + "human_livable": "Human Livable", + "due_part_key": "Constant Payment Group", + "build_uu_id": "Building UUID", + "part_direction_uu_id": "Part Direction UUID", + "part_type_uu_id": "Part Type UUID", + } + + +class BuildPartsListResponse(BaseModelRegular, CrudRecords, BuildPartsListValidation): + address_gov_code: Optional[str] = None + part_no: Optional[int] = None + part_level: Optional[int] = None + part_code: Optional[str] = None + part_gross: Optional[int] = None + part_net: Optional[int] = None + default_accessory: Optional[str] = None + human_livable: Optional[bool] = None + due_part_key: Optional[str] = None + build_uu_id: Optional[str] = None + part_direction_uu_id: Optional[str] = None + part_type_uu_id: Optional[str] = None diff --git a/ApiValidations/Response/people_responses.py b/ApiValidations/Response/people_responses.py new file mode 100644 index 0000000..fcf6da4 --- /dev/null +++ b/ApiValidations/Response/people_responses.py @@ -0,0 +1,57 @@ +from typing import Optional +from api_validations.core_validations import BaseModelRegular +from api_validations.validations_request import ( + CrudRecordValidation, + CrudRecords, +) + + +class PeopleListValidation: + tr = { + **CrudRecordValidation.tr, + "firstname": "Ad", + "surname": "Soyad", + "middle_name": "Orta İsim", + "sex_code": "Cinsiyet Kodu", + "person_ref": "Kişi Referansı", + "person_tag": "Kişi Etiketi", + "father_name": "Baba Adı", + "mother_name": "Anne Adı", + "country_code": "Ülke Kodu", + "national_identity_id": "Kimlik Numarası", + "birth_place": "Doğum Yeri", + "birth_date": "Doğum Tarihi", + "tax_no": "Vergi Numarası", + } + en = { + **CrudRecordValidation.en, + "firstname": "First Name", + "surname": "Surname", + "middle_name": "Middle Name", + "sex_code": "Sex Code", + "person_ref": "Person Reference", + "person_tag": "Person Tag", + "father_name": "Father's Name", + "mother_name": "Mother's Name", + "country_code": "Country Code", + "national_identity_id": "National Identity ID", + "birth_place": "Birth Place", + "birth_date": "Birth Date", + "tax_no": "Tax Number", + } + + +class PeopleListResponse(BaseModelRegular, CrudRecords, PeopleListValidation): + firstname: Optional[str] = None + surname: Optional[str] = None + middle_name: Optional[str] = None + sex_code: Optional[str] = None + person_ref: Optional[str] = None + person_tag: Optional[str] = None + father_name: Optional[str] = None + mother_name: Optional[str] = None + country_code: Optional[str] = None + national_identity_id: Optional[str] = None + birth_place: Optional[str] = None + birth_date: Optional[str] = None + tax_no: Optional[str] = None diff --git a/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py b/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py index 579e4a3..c23d3d3 100644 --- a/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py +++ b/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py @@ -14,6 +14,7 @@ from starlette.middleware.base import BaseHTTPMiddleware from ApiLibrary.common.line_number import get_line_number_for_error from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi +from AllConfigs.Token.config import Auth import inspect @@ -86,9 +87,8 @@ class MiddlewareModule: async def wrapper(request: Request, *args, **kwargs): # Get and validate token context from request # Create auth context and Attach auth context to both wrapper and original function - func.auth = cls.get_user_from_request( - request - ) # This ensures the context is available in both places + func.auth = cls.get_user_from_request(request) + wrapper.auth = func.auth # Call the original endpoint function if inspect.iscoroutinefunction(func): return await func(request, *args, **kwargs) @@ -141,10 +141,22 @@ class LoggerTimingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next: Callable) -> Response: # Log the request - print(f"Handling request: {request.method} {request.url}") + import arrow + + headers = dict(request.headers) response = await call_next(request) # Log the response print( - f"Completed request: {request.method} {request.url} with status {response.status_code}" + "Loggers :", + { + "url": request.url, + "method": request.method, + "access_token": headers.get(Auth.ACCESS_TOKEN_TAG, ""), + "referer": headers.get("referer", ""), + "origin": headers.get("origin", ""), + "user-agent": headers.get("user-agent", ""), + "datetime": arrow.now().format("YYYY-MM-DD HH:mm:ss ZZ"), + "status_code": response.status_code, + }, ) return response diff --git a/DockerApiServices/AllApiNeeds/middleware/token_event_middleware.py b/DockerApiServices/AllApiNeeds/middleware/token_event_middleware.py index 8380323..e60e523 100644 --- a/DockerApiServices/AllApiNeeds/middleware/token_event_middleware.py +++ b/DockerApiServices/AllApiNeeds/middleware/token_event_middleware.py @@ -2,11 +2,196 @@ Token event middleware for handling authentication and event tracking. """ -from functools import wraps -from typing import Callable, Dict, Any -from .auth_middleware import MiddlewareModule import inspect +from functools import wraps +from typing import Callable, Dict, Any, Optional, Union +from fastapi import Request +from pydantic import BaseModel + +from ApiLibrary.common.line_number import get_line_number_for_error +from ApiServices.Token.token_handler import TokenService +from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi +from Schemas.rules.rules import EndpointRestriction + +from .auth_middleware import MiddlewareModule +from Schemas import Events + + + +class EventFunctions: + + def __init__(self, endpoint: str, request: Request): + self.endpoint = endpoint + self.request = request + + + def match_endpoint_with_accesiable_event(self) -> Optional[Dict[str, Any]]: + """ + Match an endpoint with accessible events. + + Args: + endpoint: The endpoint to match + + Returns: + Dict containing the endpoint registration data + None if endpoint is not found in database + """ + access_token = TokenService.get_access_token_from_request(self.request) + token_context = TokenService.get_object_via_access_key( + access_token=access_token + ) + if token_context.is_employee: + reachable_event_codes: list[str] = ( + token_context.selected_company.reachable_event_codes + ) + elif token_context.is_occupant: + reachable_event_codes: list[str] = ( + token_context.selected_occupant.reachable_event_codes + ) + else: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Token not found", + ) + + if not access_token: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Token not found", + ) + + db = EndpointRestriction.new_session() + restriction = EndpointRestriction.filter_one( + EndpointRestriction.endpoint_name == self.endpoint, + db=db, + ).data + if not restriction: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Function code not found", + ) + + event_related = Events.filter_all( + Events.endpoint_id == restriction.id, + db=db, + ).data + if not event_related: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="No event is registered for this user.", + ) + an_event = event_related[0] + event_related_codes: list[str] = [ + event.function_code for event in event_related + ] + intersected_code: set = set(reachable_event_codes).intersection( + set(event_related_codes) + ) + if not len(list(intersected_code)) == 1: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="No event is registered for this user.", + ) + return { + "endpoint_url": self.endpoint, + "reachable_event_code": list(intersected_code)[0], + "class": an_event.function_class, + } + + def retrieve_function_dict(self) -> Optional[Dict[str, Any]]: + """ + Retrieve function dictionary for a given endpoint. + + Args: + endpoint: The endpoint to retrieve the function dictionary for + + Returns: + Dictionary containing the function dictionary + None if endpoint is not found + """ + access_token = TokenService.get_access_token_from_request(self.request) + token_context = TokenService.get_object_via_access_key( + access_token=access_token + ) + if token_context.is_employee: + reachable_event_codes: list[str] = ( + token_context.selected_company.reachable_event_codes + ) + elif token_context.is_occupant: + reachable_event_codes: list[str] = ( + token_context.selected_occupant.reachable_event_codes + ) + else: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Token not found", + ) + + if not access_token: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Token not found", + ) + + db = EndpointRestriction.new_session() + restriction = EndpointRestriction.filter_one( + EndpointRestriction.endpoint_name == self.endpoint, + db=db, + ).data + if not restriction: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Function code not found", + ) + + event_related = Events.filter_all( + Events.endpoint_id == restriction.id, + db=db, + ).data + if not event_related: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="No event is registered for this user.", + ) + an_event = event_related[0] + event_related_codes: list[str] = [ + event.function_code for event in event_related + ] + intersected_code: set = set(reachable_event_codes).intersection( + set(event_related_codes) + ) + if not len(list(intersected_code)) == 1: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="No event is registered for this user.", + ) + return { + "endpoint_url": self.endpoint, + "reachable_event_code": list(intersected_code)[0], + "class": an_event.function_class, + } + class TokenEventMiddleware: """ @@ -37,17 +222,71 @@ class TokenEventMiddleware: authenticated_func = MiddlewareModule.auth_required(func) @wraps(authenticated_func) - async def wrapper(*args, **kwargs) -> Dict[str, Any]: - # Create handler with context - function_code = ( - "7192c2aa-5352-4e36-98b3-dafb7d036a3d" # Keep function_code as URL - ) + async def wrapper(request: Request, *args, **kwargs) -> Dict[str, Any]: + # Get function code from the function's metadata + endpoint_url = getattr(authenticated_func, "url_of_endpoint", {}) + if not endpoint_url: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Function code not found", + ) + # Make handler available to all functions in the chain - func.func_code = {"function_code": function_code} + func.func_code = EventFunctions(endpoint_url, request).match_endpoint_with_accesiable_event() # Call the authenticated function if inspect.iscoroutinefunction(authenticated_func): - return await authenticated_func(*args, **kwargs) - return authenticated_func(*args, **kwargs) + return await authenticated_func(request, *args, **kwargs) + return authenticated_func(request, *args, **kwargs) return wrapper + + + @staticmethod + def validation_required( + func: Callable[..., Dict[str, Any]] + ) -> Callable[..., Dict[str, Any]]: + """ + Decorator for endpoints with token and event requirements. + This decorator: + 1. First validates authentication using MiddlewareModule.auth_required + 2. Then adds event tracking context + + Args: + func: The function to be decorated + + Returns: + Callable: The wrapped function with both auth and event handling + """ + # First apply authentication + authenticated_func = MiddlewareModule.auth_required(func) + + @wraps(authenticated_func) + async def wrapper( + request: Request, *args: Any, **kwargs: Any + ) -> Union[Dict[str, Any], BaseModel]: + # Handle both async and sync functions + endpoint_asked = getattr(kwargs.get("data", None), "data", None).get("endpoint", None) + if not endpoint_asked: + raise HTTPExceptionApi( + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Endpoint not found", + ) + wrapper.validation_code = EventFunctions(endpoint_asked, request).retrieve_function_dict() + if inspect.iscoroutinefunction(authenticated_func): + result = await authenticated_func(request, *args, **kwargs) + else: + result = authenticated_func(request, *args, **kwargs) + function_auth = getattr(authenticated_func, "auth", None) + wrapper.auth = function_auth + func.auth = function_auth + authenticated_func.auth = function_auth + # If result is a coroutine, await it + if inspect.iscoroutine(result): + result = await result + return result + return wrapper diff --git a/DockerApiServices/AllApiNeeds/open_api_creator.py b/DockerApiServices/AllApiNeeds/open_api_creator.py index fb22237..6163e2d 100644 --- a/DockerApiServices/AllApiNeeds/open_api_creator.py +++ b/DockerApiServices/AllApiNeeds/open_api_creator.py @@ -33,7 +33,7 @@ class OpenAPISchemaCreator: """ self.app = app _, self.protected_routes = get_all_routers() - self.tags_metadata = self._create_tags_metadata() + # self.tags_metadata = self._create_tags_metadata() @staticmethod def _create_tags_metadata() -> List[Dict[str, str]]: @@ -232,7 +232,6 @@ class OpenAPISchemaCreator: description=Config.DESCRIPTION, version="1.1.1", routes=self.app.routes, - tags=self.tags_metadata, ) # Add security schemes diff --git a/DockerApiServices/AuthServiceApi/Dockerfile b/DockerApiServices/AuthServiceApi/Dockerfile index 4f458bf..3ffe83a 100644 --- a/DockerApiServices/AuthServiceApi/Dockerfile +++ b/DockerApiServices/AuthServiceApi/Dockerfile @@ -35,9 +35,6 @@ COPY ApiEvents/AuthServiceApi /app/ApiEvents COPY ApiEvents/abstract_class.py /app/ApiEvents/abstract_class.py COPY ApiEvents/base_request_model.py /app/ApiEvents/base_request_model.py -# Create empty __init__.py files to make directories into Python packages -RUN touch /app/ApiEvents/__init__.py - # Set Python path to include app directory ENV PYTHONPATH=/app \ PYTHONUNBUFFERED=1 \ diff --git a/DockerApiServices/EventServiceApi/Dockerfile b/DockerApiServices/EventServiceApi/Dockerfile index 6960bd4..aa09676 100644 --- a/DockerApiServices/EventServiceApi/Dockerfile +++ b/DockerApiServices/EventServiceApi/Dockerfile @@ -35,8 +35,6 @@ COPY ApiEvents/EventServiceApi /app/ApiEvents COPY ApiEvents/abstract_class.py /app/ApiEvents/abstract_class.py COPY ApiEvents/base_request_model.py /app/ApiEvents/base_request_model.py -# Create empty __init__.py files to make directories into Python packages -RUN touch /app/ApiEvents/__init__.py # Set Python path to include app directory ENV PYTHONPATH=/app \ diff --git a/DockerApiServices/ValidationServiceApi/Dockerfile b/DockerApiServices/ValidationServiceApi/Dockerfile index 44d25a7..a6809dc 100644 --- a/DockerApiServices/ValidationServiceApi/Dockerfile +++ b/DockerApiServices/ValidationServiceApi/Dockerfile @@ -31,7 +31,9 @@ COPY Services /app/Services COPY ApiServices /app/ApiServices # Copy Events structure with consistent naming -COPY ApiEvents/EventServiceApi /app/ApiEvents +COPY ApiEvents/ValidationServiceApi /app/ApiEvents +ADD ApiEvents/AuthServiceApi/events /app/ApiEvents/events +ADD ApiEvents/EventServiceApi/events /app/ApiEvents/events COPY ApiEvents/abstract_class.py /app/ApiEvents/abstract_class.py COPY ApiEvents/base_request_model.py /app/ApiEvents/base_request_model.py diff --git a/Schemas/event/event.py b/Schemas/event/event.py index f031fd7..1479466 100644 --- a/Schemas/event/event.py +++ b/Schemas/event/event.py @@ -250,26 +250,33 @@ class Event2Employee(CrudCollection): ) @classmethod - def get_event_id_by_employee_id(cls, employee_id) -> list: + def get_event_codes(cls, employee_id: int) -> list: db = cls.new_session() - occupant_events = cls.filter_all( + employee_events = cls.filter_all( cls.employee_id == employee_id, db=db, ).data - active_events = Service2Events.filter_all( + active_event_ids = Service2Events.filter_all( Service2Events.service_id.in_( - [event.event_service_id for event in occupant_events] + [event.event_service_id for event in employee_events] ), db=db, system=True, ).data - active_events_id = [event.event_id for event in active_events] + active_events = Events.filter_all( + Events.id.in_([event.event_id for event in active_event_ids]), + db=db, + ).data if extra_events := Event2EmployeeExtra.filter_all( Event2EmployeeExtra.employee_id == employee_id, db=db, ).data: - active_events_id.extend([event.event_id for event in extra_events]) - return active_events_id + events_extra = Events.filter_all( + Events.id.in_([event.event_id for event in extra_events]), + db=db, + ).data + active_events.extend(events_extra) + return [event.function_code for event in active_events] class Event2Occupant(CrudCollection): @@ -307,22 +314,33 @@ class Event2Occupant(CrudCollection): ) @classmethod - def get_event_id_by_build_living_space_id(cls, build_living_space_id) -> list: + def get_event_codes(cls, build_living_space_id) -> list: + db = cls.new_session() occupant_events = cls.filter_all( cls.build_living_space_id == build_living_space_id, + db=db, ).data - active_events = Service2Events.filter_all( + active_event_ids = Service2Events.filter_all( Service2Events.service_id.in_( [event.event_service_id for event in occupant_events] ), + db=db, system=True, ).data - active_events_id = [event.event_id for event in active_events] + active_events = Events.filter_all( + Events.id.in_([event.event_id for event in active_event_ids]), + db=db, + ).data if extra_events := Event2OccupantExtra.filter_all( - Event2OccupantExtra.build_living_space_id == build_living_space_id + Event2OccupantExtra.build_living_space_id == build_living_space_id, + db=db, ).data: - active_events_id.extend([event.event_id for event in extra_events]) - return active_events_id + events_extra = Events.filter_all( + Events.id.in_([event.event_id for event in extra_events]), + db=db, + ).data + active_events.extend(events_extra) + return [event.function_code for event in active_events] class ModulePrice(CrudCollection):