new api service and logic implemented

This commit is contained in:
2025-01-23 22:27:25 +03:00
parent d91ecda9df
commit 32022ca521
245 changed files with 28004 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
"""
Authentication package initialization.
"""
from .auth import AUTH_CONFIG
__all__ = [
"AUTH_CONFIG",
]

View File

@@ -0,0 +1,154 @@
from uuid import UUID
from Events.Engine.abstract_class import Event
from .models import (
LoginSuperUserRequestModel,
LoginSuperUserResponseModel,
SelectCompanyOrOccupantTypeSuperUserRequestModel,
SelectCompanyOrOccupantTypeSuperUserResponseModel,
EmployeeSelectionSuperUserRequestModel,
EmployeeSelectionSuperUserResponseModel,
OccupantSelectionSuperUserRequestModel,
OccupantSelectionSuperUserResponseModel,
)
from .function_handlers import (
authentication_login_with_domain_and_creds,
authentication_select_company_or_occupant_type,
handle_employee_selection,
handle_occupant_selection,
authentication_check_token_is_valid,
authentication_refresh_user_info,
authentication_change_password,
authentication_create_password,
authentication_disconnect_user,
authentication_logout_user,
authentication_refresher_token,
authentication_forgot_password,
authentication_reset_password,
authentication_download_avatar,
)
# Auth Login
authentication_login_super_user_event = Event(
key=UUID("a5d2d0d1-3e9b-4b0f-8c7d-6d4a4b4c4d4e"),
request_validator=LoginSuperUserRequestModel,
response_validator=LoginSuperUserResponseModel,
description="Login super user",
)
authentication_login_super_user_event.endpoint_callable = authentication_login_with_domain_and_creds
# Auth Select Company or Occupant Type
authentication_select_company_or_occupant_type_super_user_event = Event(
key=UUID("a5d2d0d1-3e9b-4b0f-8c7d-6d4a4b4c4d4e"),
request_validator=SelectCompanyOrOccupantTypeSuperUserRequestModel,
response_validator=SelectCompanyOrOccupantTypeSuperUserResponseModel,
description="Select company or occupant type super user",
)
authentication_select_company_or_occupant_type_super_user_event.endpoint_callable = authentication_select_company_or_occupant_type
authentication_employee_selection_super_user_event = Event(
key=UUID("a5d2d0d1-3e9b-4b0f-8c7d-6d4a4b4c4d4e"),
request_validator=EmployeeSelectionSuperUserRequestModel,
response_validator=EmployeeSelectionSuperUserResponseModel,
description="Employee selection super user",
)
authentication_employee_selection_super_user_event.endpoint_callable = handle_employee_selection
authentication_occupant_selection_super_user_event = Event(
key=UUID("a5d2d0d1-3e9b-4b0f-8c7d-6d4a4b4c4d4e"),
request_validator=OccupantSelectionSuperUserRequestModel,
response_validator=OccupantSelectionSuperUserResponseModel,
description="Occupant selection super user",
)
authentication_occupant_selection_super_user_event.endpoint_callable = handle_occupant_selection
# Check Token Validity
authentication_check_token_event = Event(
key=UUID("b6e3d1e2-4f9c-5c1g-9d8e-7e5f6f5e5d5f"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Check if token is valid",
)
authentication_check_token_event.endpoint_callable = authentication_check_token_is_valid
# Refresh User Info
authentication_refresh_user_info_event = Event(
key=UUID("c7f4e2f3-5g0d-6d2h-0e9f-8f6g7g6f6e6g"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Refresh user information",
)
authentication_refresh_user_info_event.endpoint_callable = authentication_refresh_user_info
# Change Password
authentication_change_password_event = Event(
key=UUID("d8g5f3g4-6h1e-7e3i-1f0g-9g7h8h7g7f7h"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Change user password",
)
authentication_change_password_event.endpoint_callable = authentication_change_password
# Create Password
authentication_create_password_event = Event(
key=UUID("e9h6g4h5-7i2f-8f4j-2g1h-0h8i9i8h8g8i"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Create new password",
)
authentication_create_password_event.endpoint_callable = authentication_create_password
# Disconnect User
authentication_disconnect_user_event = Event(
key=UUID("f0i7h5i6-8j3g-9g5k-3h2i-1i9j0j9i9h9j"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Disconnect all user sessions",
)
authentication_disconnect_user_event.endpoint_callable = authentication_disconnect_user
# Logout User
authentication_logout_user_event = Event(
key=UUID("g1j8i6j7-9k4h-0h6l-4i3j-2j0k1k0j0i0k"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Logout user session",
)
authentication_logout_user_event.endpoint_callable = authentication_logout_user
# Refresh Token
authentication_refresher_token_event = Event(
key=UUID("h2k9j7k8-0l5i-1i7m-5j4k-3k1l2l1k1j1l"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Refresh authentication token",
)
authentication_refresher_token_event.endpoint_callable = authentication_refresher_token
# Forgot Password
authentication_forgot_password_event = Event(
key=UUID("i3l0k8l9-1m6j-2j8n-6k5l-4l2m3m2l2k2m"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Request password reset",
)
authentication_forgot_password_event.endpoint_callable = authentication_forgot_password
# Reset Password
authentication_reset_password_event = Event(
key=UUID("j4m1l9m0-2n7k-3k9o-7l6m-5m3n4n3m3l3n"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Reset user password",
)
authentication_reset_password_event.endpoint_callable = authentication_reset_password
# Download Avatar
authentication_download_avatar_event = Event(
key=UUID("k5n2m0n1-3o8l-4l0p-8m7n-6n4o5o4n4m4o"),
request_validator=None, # TODO: Add request validator
response_validator=None, # TODO: Add response validator
description="Download user avatar and profile info",
)
authentication_download_avatar_event.endpoint_callable = authentication_download_avatar

View File

@@ -0,0 +1,168 @@
"""
Authentication related API endpoints.
"""
from typing import Union
from Events.Engine.abstract_class import MethodToEvent
from Events.base_request_model import SuccessResponse
from ApiLayers.ApiLibrary.common.line_number import get_line_number_for_error
from ApiLayers.ApiServices.Token.token_handler import OccupantTokenObject, EmployeeTokenObject
from .api_events import (
authentication_login_super_user_event,
authentication_select_company_or_occupant_type_super_user_event,
authentication_employee_selection_super_user_event,
authentication_occupant_selection_super_user_event,
authentication_check_token_event,
authentication_refresh_user_info_event,
authentication_change_password_event,
authentication_create_password_event,
authentication_disconnect_user_event,
authentication_logout_user_event,
authentication_refresher_token_event,
authentication_forgot_password_event,
authentication_reset_password_event,
authentication_download_avatar_event,
)
# Type aliases for common types
TokenDictType = Union["EmployeeTokenObject", "OccupantTokenObject"]
AuthenticationLoginEventMethods = MethodToEvent(
events=[authentication_login_super_user_event],
headers=[],
errors=[],
url="/authentication/login",
method="POST",
summary="Login via domain and access key : [email] | [phone]",
description="Login to the system via domain, access key : [email] | [phone]",
)
AuthenticationSelectEventMethods = MethodToEvent(
events=[
authentication_select_company_or_occupant_type_super_user_event,
authentication_employee_selection_super_user_event,
authentication_occupant_selection_super_user_event
],
headers=[],
errors=[],
url="/authentication/select",
method="POST",
summary="Select company or occupant type",
description="Select company or occupant type",
)
AuthenticationCheckTokenEventMethods = MethodToEvent(
events=[authentication_check_token_event],
headers=[],
errors=[],
url="/authentication/check-token",
method="POST",
summary="Check if token is valid",
description="Check if access token is valid for user",
)
AuthenticationRefreshEventMethods = MethodToEvent(
events=[authentication_refresh_user_info_event],
headers=[],
errors=[],
url="/authentication/refresh",
method="POST",
summary="Refresh user info",
description="Refresh user info using access token",
)
AuthenticationChangePasswordEventMethods = MethodToEvent(
events=[authentication_change_password_event],
headers=[],
errors=[],
url="/authentication/change-password",
method="POST",
summary="Change password",
description="Change password with access token",
)
AuthenticationCreatePasswordEventMethods = MethodToEvent(
events=[authentication_create_password_event],
headers=[],
errors=[],
url="/authentication/create-password",
method="POST",
summary="Create password",
description="Create password with password reset token requested via email",
)
AuthenticationDisconnectUserEventMethods = MethodToEvent(
events=[authentication_disconnect_user_event],
headers=[],
errors=[],
url="/authentication/disconnect",
method="POST",
summary="Disconnect all sessions",
description="Disconnect all sessions of user in access token",
)
AuthenticationLogoutEventMethods = MethodToEvent(
events=[authentication_logout_user_event],
headers=[],
errors=[],
url="/authentication/logout",
method="POST",
summary="Logout user",
description="Logout only single session of user which domain is provided",
)
AuthenticationRefreshTokenEventMethods = MethodToEvent(
events=[authentication_refresher_token_event],
headers=[],
errors=[],
url="/authentication/refresh-token",
method="POST",
summary="Refresh token",
description="Refresh access token with refresher token",
)
AuthenticationForgotPasswordEventMethods = MethodToEvent(
events=[authentication_forgot_password_event],
headers=[],
errors=[],
url="/authentication/forgot-password",
method="POST",
summary="Request password reset",
description="Send an email to user for a valid password reset token",
)
AuthenticationResetPasswordEventMethods = MethodToEvent(
events=[authentication_reset_password_event],
headers=[],
errors=[],
url="/authentication/reset-password",
method="POST",
summary="Reset password",
description="Reset user password",
)
AuthenticationDownloadAvatarEventMethods = MethodToEvent(
events=[authentication_download_avatar_event],
headers=[],
errors=[],
url="/authentication/download-avatar",
method="POST",
summary="Download avatar",
description="Download avatar icon and profile info of user",
)

View File

@@ -0,0 +1,41 @@
from Events.Engine.abstract_class import CategoryCluster
from .info import authentication_page_info
from .auth import (
AuthenticationLoginEventMethods,
AuthenticationLogoutEventMethods,
AuthenticationRefreshTokenEventMethods,
AuthenticationForgotPasswordEventMethods,
AuthenticationChangePasswordEventMethods,
AuthenticationCheckTokenEventMethods,
AuthenticationCreatePasswordEventMethods,
AuthenticationDisconnectUserEventMethods,
AuthenticationDownloadAvatarEventMethods,
AuthenticationResetPasswordEventMethods,
AuthenticationRefreshEventMethods,
AuthenticationSelectEventMethods,
)
AuthCluster = CategoryCluster(
tags=["authentication"],
prefix="/authentication",
description="Authentication cluster",
pageinfo=authentication_page_info,
endpoints=[
AuthenticationLoginEventMethods,
AuthenticationLogoutEventMethods,
AuthenticationRefreshTokenEventMethods,
AuthenticationForgotPasswordEventMethods,
AuthenticationChangePasswordEventMethods,
AuthenticationCheckTokenEventMethods,
AuthenticationCreatePasswordEventMethods,
AuthenticationDisconnectUserEventMethods,
AuthenticationDownloadAvatarEventMethods,
AuthenticationResetPasswordEventMethods,
AuthenticationRefreshEventMethods,
AuthenticationSelectEventMethods,
],
include_in_schema=True,
sub_category=[],
)

View File

@@ -0,0 +1,481 @@
from typing import Any, TYPE_CHECKING, Union
from Events.base_request_model import TokenDictType
from Events.Engine.abstract_class import MethodToEvent
from Events.base_request_model import SuccessResponse
from ApiLayers.ApiLibrary.common.line_number import get_line_number_for_error
from ApiLayers.ApiLibrary.date_time_actions.date_functions import DateTimeLocal
from ApiLayers.ApiServices.middleware.auth_middleware import AuthMiddlewareModule
from ApiLayers.ApiServices.Login.user_login_handler import UserLoginModule
from ApiLayers.ApiServices.Token.token_handler import TokenService
from ApiLayers.ApiValidations.Custom.token_objects import CompanyToken, OccupantToken
from ApiLayers.ErrorHandlers import HTTPExceptionApi
from ApiLayers.Schemas import (
BuildLivingSpace,
BuildParts,
RelationshipEmployee2Build,
Companies,
Departments,
Duties,
Duty,
Staff,
Employees,
Event2Employee,
Event2Occupant,
OccupantTypes,
Users
)
from ApiLayers.ApiServices.Token.token_handler import OccupantTokenObject, EmployeeTokenObject
if TYPE_CHECKING:
from fastapi import Request
# Type aliases for common types
TokenDictType = Union["EmployeeTokenObject", "OccupantTokenObject"]
def authentication_login_with_domain_and_creds(request: Request, data: Any):
"""
Authenticate user with domain and credentials.
Args:
request: FastAPI request object
data: Request body containing login credentials
{
"domain": "evyos.com.tr",
"access_key": "karatay.berkay.sup@evyos.com.tr",
"password": "string",
"remember_me": false
}
Returns:
SuccessResponse containing authentication token and user info
"""
# Get token from login module
user_login_module = UserLoginModule(request=request)
token = user_login_module.login_user_via_credentials(access_data=data)
# Return response with token and headers
return {
"completed": True,
"message": "User is logged in successfully",
"access_token": token.get("access_token"),
"refresh_token": token.get("refresher_token"),
"access_object": {
"user_type": token.get("user_type"),
"companies_list": token.get("companies_list"),
},
"user": token.get("user"),
}
@AuthMiddlewareModule.auth_required
def handle_employee_selection(request: Request, data: Any, token_dict: TokenDictType):
Users.set_user_define_properties(token=token_dict)
db_session = Users.new_session()
if data.company_uu_id not in token_dict.companies_uu_id_list:
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang=token_dict.lang,
loc=get_line_number_for_error(),
sys_msg="Company not found in token",
)
selected_company = Companies.filter_one(
Companies.uu_id == data.company_uu_id,
db=db_session,
).first
if not selected_company:
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang=token_dict.lang,
loc=get_line_number_for_error(),
sys_msg="Company not found in token",
)
# Get department IDs for the company
department_ids = [
dept.id
for dept in Departments.filter_all(
Departments.company_id == selected_company.id,
db=db_session,
).data
]
# Get duties IDs for the company
duties_ids = [
duty.id
for duty in Duties.filter_all(
Duties.company_id == selected_company.id, db=db_session
).data
]
# Get staff IDs
staff_ids = [
staff.id
for staff in Staff.filter_all(
Staff.duties_id.in_(duties_ids), db=db_session
).data
]
# Get employee
employee = Employees.filter_one(
Employees.people_id == token_dict.person_id,
Employees.staff_id.in_(staff_ids),
db=db_session,
).first
if not employee:
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang=token_dict.lang,
loc=get_line_number_for_error(),
sys_msg="Employee not found in token",
)
# Get reachable events
reachable_event_codes = Event2Employee.get_event_codes(employee_id=employee.id)
reachable_event_endpoints = Event2Employee.get_event_endpoints(
employee_id=employee.id
)
# Get staff and duties
staff = Staff.filter_one(Staff.id == employee.staff_id, db=db_session).data
duties = Duties.filter_one(Duties.id == staff.duties_id, db=db_session).data
department = Departments.filter_one(
Departments.id == duties.department_id, db=db_session
).data
# Get bulk duty
bulk_id = Duty.filter_by_one(system=True, duty_code="BULK", db=db_session).data
bulk_duty_id = Duties.filter_by_one(
company_id=selected_company.id,
duties_id=bulk_id.id,
**Duties.valid_record_dict,
db=db_session,
).data
# Create company token
company_token = CompanyToken(
company_uu_id=selected_company.uu_id.__str__(),
company_id=selected_company.id,
department_id=department.id,
department_uu_id=department.uu_id.__str__(),
duty_id=duties.id,
duty_uu_id=duties.uu_id.__str__(),
bulk_duties_id=bulk_duty_id.id,
staff_id=staff.id,
staff_uu_id=staff.uu_id.__str__(),
employee_id=employee.id,
employee_uu_id=employee.uu_id.__str__(),
reachable_event_codes=reachable_event_codes,
reachable_event_endpoints=reachable_event_endpoints,
)
try: # Update Redis
update_token = TokenService.update_token_at_redis(
request=request, add_payload=company_token
)
return update_token
except Exception as e:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg=f"{e}",
)
@AuthMiddlewareModule.auth_required
def handle_occupant_selection(request: Request, data: Any, token_dict: TokenDictType):
"""Handle occupant type selection"""
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
)
reachable_event_endpoints = Event2Occupant.get_event_endpoints(
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,
reachable_event_endpoints=reachable_event_endpoints,
)
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}",
)
@AuthMiddlewareModule.auth_required
def authentication_select_company_or_occupant_type(request: Request, data: Any, token_dict: TokenDictType):
"""Handle selection of company or occupant type"""
if token_dict.is_employee:
return cls._handle_employee_selection(data, token_dict, request)
elif token_dict.is_occupant:
return cls._handle_occupant_selection(data, token_dict, request)
@AuthMiddlewareModule.auth_required
def authentication_check_token_is_valid(request: "Request", data: Any):
"""Check if token is valid for user"""
# try:
# if RedisActions.get_object_via_access_key(request=request):
# return ResponseHandler.success("Access Token is valid")
# except HTTPException:
# return ResponseHandler.unauthorized("Access Token is NOT valid")
return
@AuthMiddlewareModule.auth_required
def authentication_refresh_user_info(request: "Request", token_dict: TokenDictType, data: Any):
"""Refresh user info using access token"""
# try:
# access_token = request.headers.get(Auth.ACCESS_TOKEN_TAG)
# if not access_token:
# return ResponseHandler.unauthorized()
# found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data
# if not found_user:
# return ResponseHandler.not_found("User not found")
# user_token = UsersTokens.filter_one(
# UsersTokens.domain == found_user.domain_name,
# UsersTokens.user_id == found_user.id,
# UsersTokens.token_type == "RememberMe",
# ).data
# response_data = {
# "access_token": access_token,
# "refresh_token": getattr(user_token, "token", None),
# "user": found_user.get_dict(),
# }
# return ResponseHandler.success(
# "User info refreshed successfully",
# data=response_data,
# )
# except Exception as e:
# return ResponseHandler.error(str(e))
return
@AuthMiddlewareModule.auth_required
def authentication_change_password(request: "Request", token_dict: TokenDictType, data: Any):
"""Change password with access token"""
# try:
# if not isinstance(token_dict, EmployeeTokenObject):
# return ResponseHandler.unauthorized("Only employees can change password")
# found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data
# if not found_user:
# return ResponseHandler.not_found("User not found")
# if not found_user.check_password(data.old_password):
# return ResponseHandler.unauthorized("Old password is incorrect")
# found_user.set_password(data.new_password)
# return ResponseHandler.success("Password changed successfully")
# except Exception as e:
# return ResponseHandler.error(str(e))
return
def authentication_create_password(request: "Request", data: Any):
"""Create password with password reset token requested via email"""
# if not data.re_password == data.password:
# raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail="Password must match")
# if found_user := Users.filter_one(Users.password_token == data.password_token).data:
# found_user.create_password(found_user=found_user, password=data.password)
# found_user.password_token = ""
# found_user.save()
# return ResponseHandler.success("Password is created successfully", data=found_user.get_dict())
# return ResponseHandler.not_found("Record not found")
return
@AuthMiddlewareModule.auth_required
def authentication_disconnect_user(request: "Request", token_dict: TokenDictType, data: Any):
"""Disconnect all sessions of user in access token"""
# found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data
# if not found_user:
# return ResponseHandler.not_found("User not found")
# if already_tokens := RedisActions.get_object_via_user_uu_id(user_id=str(found_user.uu_id)):
# for key, token_user in already_tokens.items():
# RedisActions.delete(key)
# selected_user = Users.filter_one(Users.uu_id == token_user.get("uu_id")).data
# selected_user.remove_refresher_token(domain=data.domain, disconnect=True)
# return ResponseHandler.success("All sessions are disconnected", data=selected_user.get_dict())
# return ResponseHandler.not_found("Invalid data")
return
def authentication_logout_user(request: "Request", data: Any, token_dict: TokenDictType):
"""Logout only single session of user which domain is provided"""
# token_user = None
# if already_tokens := RedisActions.get_object_via_access_key(request=request):
# for key in already_tokens:
# token_user = RedisActions.get_json(key)
# if token_user.get("domain") == data.domain:
# RedisActions.delete(key)
# selected_user = Users.filter_one(Users.uu_id == token_user.get("uu_id")).data
# selected_user.remove_refresher_token(domain=data.domain)
# return ResponseHandler.success("Session is logged out", data=token_user)
# return ResponseHandler.not_found("Logout is not successfully completed")
return
@AuthMiddlewareModule.auth_required
def authentication_refresher_token(request: "Request", token_dict: TokenDictType, data: Any):
"""Refresh access token with refresher token"""
# token_refresher = UsersTokens.filter_by_one(
# token=data.refresh_token,
# domain=data.domain,
# **UsersTokens.valid_record_dict,
# ).data
# if not token_refresher:
# return ResponseHandler.not_found("Invalid data")
# if found_user := Users.filter_one(Users.id == token_refresher.user_id).data:
# access_key = AuthActions.save_access_token_to_redis(
# request=request, found_user=found_user, domain=data.domain
# )
# found_user.last_agent = request.headers.get("User-Agent", None)
# found_user.last_platform = request.headers.get("Origin", None)
# found_user.last_remote_addr = getattr(request, "remote_addr", None) or request.headers.get("X-Forwarded-For", None)
# found_user.last_seen = str(system_arrow.now())
# response_data = {
# "access_token": access_key,
# "refresh_token": data.refresh_token,
# }
# return ResponseHandler.success("User is logged in successfully via refresher token", data=response_data)
# return ResponseHandler.not_found("Invalid data")
return
def authentication_forgot_password(request: "Request", data: Any):
"""Send an email to user for a valid password reset token"""
# found_user: Users = Users.check_user_exits(access_key=data.access_key, domain=data.domain)
# forgot_key = AuthActions.save_access_token_to_redis(request=request, found_user=found_user, domain=data.domain)
# forgot_link = ApiStatic.forgot_link(forgot_key=forgot_key)
# send_email_completed = send_email(
# subject=f"Dear {found_user.user_tag}, your forgot password link has been sent.",
# receivers=[str(found_user.email)],
# html=change_your_password_template(user_name=found_user.user_tag, forgot_link=forgot_link),
# )
# if not send_email_completed:
# raise HTTPException(status_code=400, detail="Email can not be sent. Try again later")
# found_user.password_token = forgot_key
# found_user.password_token_is_valid = str(system_arrow.shift(days=1))
# found_user.save()
# return ResponseHandler.success("Password is change link is sent to your email or phone", data={})
return
@AuthMiddlewareModule.auth_required
def authentication_reset_password(request: "Request", data: Any):
"""Reset password with forgot password token"""
# from sqlalchemy import or_
# found_user = Users.query.filter(
# or_(
# Users.email == str(data.access_key).lower(),
# Users.phone_number == str(data.access_key).replace(" ", ""),
# ),
# ).first()
# if not found_user:
# raise HTTPException(
# status_code=status.HTTP_400_BAD_REQUEST,
# detail="Given access key or domain is not matching with the any user record.",
# )
# reset_password_token = found_user.reset_password_token(found_user=found_user)
# send_email_completed = send_email(
# subject=f"Dear {found_user.user_tag}, a password reset request has been received.",
# receivers=[str(found_user.email)],
# html=change_your_password_template(
# user_name=found_user.user_tag,
# forgot_link=ApiStatic.forgot_link(forgot_key=reset_password_token),
# ),
# )
# if not send_email_completed:
# raise found_user.raise_http_exception(status_code=400, message="Email can not be sent. Try again later")
# return ResponseHandler.success("Password change link is sent to your email or phone", data=found_user.get_dict())
return
@AuthMiddlewareModule.auth_required
def authentication_download_avatar(request: "Request", data: Any, token_dict: TokenDictType):
"""Download avatar icon and profile info of user"""
# if found_user := Users.filter_one(Users.id == token_dict.user_id).data:
# expired_starts = str(system_arrow.now() - system_arrow.get(str(found_user.expiry_ends)))
# expired_int = (system_arrow.now() - system_arrow.get(str(found_user.expiry_ends))).days
# user_info = {
# "lang": token_dict.lang,
# "full_name": found_user.person.full_name,
# "avatar": found_user.avatar,
# "remember_me": found_user.remember_me,
# "expiry_ends": str(found_user.expiry_ends),
# "expired_str": expired_starts,
# "expired_int": int(expired_int),
# }
# return ResponseHandler.success("Avatar and profile is shared via user credentials", data=user_info)
# return ResponseHandler.not_found("Invalid data")
return

View File

@@ -0,0 +1,13 @@
from Events.Engine.abstract_class import PageInfo
authentication_page_info = PageInfo(
name="Authentication",
title={"en": "Authentication"},
description={"en": "Authentication"},
icon="",
parent="",
url="",
)

View File

@@ -0,0 +1,34 @@
from pydantic import BaseModel
class LoginSuperUserRequestModel(BaseModel):
pass
class LoginSuperUserResponseModel(BaseModel):
pass
class SelectCompanyOrOccupantTypeSuperUserRequestModel(BaseModel):
pass
class SelectCompanyOrOccupantTypeSuperUserResponseModel(BaseModel):
pass
class EmployeeSelectionSuperUserRequestModel(BaseModel):
pass
class EmployeeSelectionSuperUserResponseModel(BaseModel):
pass
class OccupantSelectionSuperUserRequestModel(BaseModel):
pass
class OccupantSelectionSuperUserResponseModel(BaseModel):
pass

View File

View File

@@ -0,0 +1,9 @@
"""
Account records package initialization.
"""
from .endpoints import ACCOUNT_RECORDS_CONFIG
__all__ = [
"ACCOUNT_RECORDS_CONFIG",
]

View File

@@ -0,0 +1,351 @@
"""
Account records service implementation.
"""
from typing import Union
from pydantic import Field
from ApiEvents.abstract_class import MethodToEvent, endpoint_wrapper
from ApiEvents.base_request_model import DictRequestModel
from ApiValidations.Custom.token_objects import (
OccupantTokenObject,
EmployeeTokenObject,
)
from ApiLibrary import system_arrow
from ApiValidations.Request.account_records import (
InsertAccountRecord,
UpdateAccountRecord,
)
from ApiValidations.Request.base_validations import ListOptions
from Schemas import (
BuildLivingSpace,
AccountRecords,
BuildIbans,
BuildDecisionBookPayments,
ApiEnumDropdown,
)
from Services.PostgresDb.Models.alchemy_response import (
AlchemyJsonResponse,
)
from ApiValidations.Response import AccountRecordResponse
from .models import (
InsertAccountRecordRequestModel,
UpdateAccountRecordRequestModel,
ListOptionsRequestModel,
)
class AccountListEventMethod(MethodToEvent):
event_type = "SELECT"
event_description = ""
event_category = ""
__event_keys__ = {
"7192c2aa-5352-4e36-98b3-dafb7d036a3d": "account_records_list",
"208e6273-17ef-44f0-814a-8098f816b63a": "account_records_list_flt_res",
}
__event_validation__ = {
"7192c2aa-5352-4e36-98b3-dafb7d036a3d": (
AccountRecordResponse,
[AccountRecords.__language_model__],
),
"208e6273-17ef-44f0-814a-8098f816b63a": (
AccountRecordResponse,
[AccountRecords.__language_model__],
),
}
@classmethod
def account_records_list(
cls,
list_options: ListOptionsRequestModel,
token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
):
db_session = AccountRecords.new_session()
if isinstance(token_dict, OccupantTokenObject):
AccountRecords.pre_query = AccountRecords.filter_all(
AccountRecords.company_id
== token_dict.selected_occupant.responsible_company_id,
db=db_session,
).query
elif isinstance(token_dict, EmployeeTokenObject):
AccountRecords.pre_query = AccountRecords.filter_all(
AccountRecords.company_id == token_dict.selected_company.company_id,
db=db_session,
).query
AccountRecords.filter_attr = list_options
records = AccountRecords.filter_all(db=db_session)
return AlchemyJsonResponse(
completed=True,
message="Account records listed successfully",
result=records,
cls_object=AccountRecords,
filter_attributes=list_options,
response_model=AccountRecordResponse,
)
@classmethod
def account_records_list_flt_res(
cls,
list_options: ListOptionsRequestModel,
token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
):
db_session = AccountRecords.new_session()
if not isinstance(token_dict, OccupantTokenObject):
raise AccountRecords.raise_http_exception(
status_code="HTTP_404_NOT_FOUND",
error_case="UNAUTHORIZED",
message="Only Occupant can see this data",
data={},
)
return_list = []
living_space: BuildLivingSpace = BuildLivingSpace.filter_by_one(
id=token_dict.selected_occupant.living_space_id
).data
if not living_space:
raise AccountRecords.raise_http_exception(
status_code="HTTP_404_NOT_FOUND",
error_case="UNAUTHORIZED",
message="Living space not found",
data={},
)
if not list_options:
list_options = ListOptions()
main_filters = [
AccountRecords.living_space_id
== token_dict.selected_occupant.living_space_id,
BuildDecisionBookPayments.process_date
>= str(system_arrow.now().shift(months=-3).date()),
BuildDecisionBookPayments.process_date
< str(system_arrow.find_last_day_of_month(living_space.expiry_ends)),
BuildDecisionBookPayments.process_date
>= str(system_arrow.get(living_space.expiry_starts)),
BuildDecisionBookPayments.is_confirmed == True,
AccountRecords.active == True,
]
order_type = "desc"
if list_options.order_type:
order_type = "asc" if list_options.order_type[0] == "a" else "desc"
order_by_list = BuildDecisionBookPayments.process_date.desc()
if list_options.order_field:
if list_options.order_field == "process_date":
order_by_list = (
BuildDecisionBookPayments.process_date.asc()
if order_type == "asc"
else BuildDecisionBookPayments.process_date.desc()
)
if list_options.order_field == "bank_date":
order_by_list = (
AccountRecords.bank_date.desc()
if order_type == "asc"
else AccountRecords.bank_date.asc()
)
if list_options.order_field == "currency_value":
order_by_list = (
AccountRecords.currency_value.desc()
if order_type == "asc"
else AccountRecords.currency_value.asc()
)
if list_options.order_field == "process_comment":
order_by_list = (
AccountRecords.process_comment.desc()
if order_type == "asc"
else AccountRecords.process_comment.asc()
)
if list_options.order_field == "payment_amount":
order_by_list = (
BuildDecisionBookPayments.payment_amount.desc()
if order_type == "asc"
else BuildDecisionBookPayments.payment_amount.asc()
)
if list_options.query:
for key, value in list_options.query.items():
if key == "process_date":
main_filters.append(BuildDecisionBookPayments.process_date == value)
if key == "bank_date":
main_filters.append(AccountRecords.bank_date == value)
if key == "currency":
main_filters.append(BuildDecisionBookPayments.currency == value)
if key == "currency_value":
main_filters.append(AccountRecords.currency_value == value)
if key == "process_comment":
main_filters.append(AccountRecords.process_comment == value)
if key == "payment_amount":
main_filters.append(
BuildDecisionBookPayments.payment_amount == value
)
query = (
AccountRecords.session.query(
BuildDecisionBookPayments.process_date,
BuildDecisionBookPayments.payment_amount,
BuildDecisionBookPayments.currency,
AccountRecords.bank_date,
AccountRecords.currency_value,
AccountRecords.process_comment,
BuildDecisionBookPayments.uu_id,
)
.join(
AccountRecords,
AccountRecords.id == BuildDecisionBookPayments.account_records_id,
)
.filter(*main_filters)
).order_by(order_by_list)
query.limit(list_options.size or 5).offset(
(list_options.page or 1 - 1) * list_options.size or 5
)
for list_of_values in query.all() or []:
return_list.append(
{
"process_date": list_of_values[0],
"payment_amount": list_of_values[1],
"currency": list_of_values[2],
"bank_date": list_of_values[3],
"currency_value": list_of_values[4],
"process_comment": list_of_values[5],
}
)
return AlchemyJsonResponse(
completed=True,
message="Account records listed successfully",
result=return_list,
cls_object=AccountRecords,
filter_attributes=list_options,
response_model=AccountRecordResponse,
)
class AccountCreateEventMethod(MethodToEvent):
event_type = "CREATE"
event_description = ""
event_category = ""
__event_keys__ = {
"31f4f32f-0cd4-4995-8a6a-f9f56335848a": "account_records_create",
}
__event_validation__ = {
"31f4f32f-0cd4-4995-8a6a-f9f56335848a": (
InsertAccountRecord,
[AccountRecords.__language_model__],
),
}
@classmethod
def account_records_create(
cls,
data: InsertAccountRecordRequestModel,
token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
):
data_dict = data.excluded_dump()
if isinstance(token_dict, OccupantTokenObject):
db_session = AccountRecords.new_session()
build_iban = BuildIbans.filter_one(
BuildIbans.iban == data.iban,
BuildIbans.build_id == token_dict.selected_occupant.build_id,
db=db_session,
).data
if not build_iban:
raise BuildIbans.raise_http_exception(
status_code="HTTP_404_NOT_FOUND",
error_case="UNAUTHORIZED",
message=f"{data.iban} is not found in company related to your organization",
data={
"iban": data.iban,
},
)
account_record = AccountRecords.find_or_create(**data.excluded_dump())
return AlchemyJsonResponse(
completed=True,
message="Account record created successfully",
result=account_record,
)
elif isinstance(token_dict, EmployeeTokenObject):
# Build.pre_query = Build.select_action(
# employee_id=token_dict.selected_employee.employee_id,
# )
# build_ids_list = Build.filter_all(
# )
# build_iban = BuildIbans.filter_one(
# BuildIbans.iban == data.iban,
# BuildIbans.build_id.in_([build.id for build in build_ids_list.data]),
# ).data
# if not build_iban:
# BuildIbans.raise_http_exception(
# status_code="HTTP_404_NOT_FOUND",
# error_case="UNAUTHORIZED",
# message=f"{data.iban} is not found in company related to your organization",
# data={
# "iban": data.iban,
# },
# )
bank_date = system_arrow.get(data.bank_date)
data_dict["bank_date_w"] = bank_date.weekday()
data_dict["bank_date_m"] = bank_date.month
data_dict["bank_date_d"] = bank_date.day
data_dict["bank_date_y"] = bank_date.year
if int(data.currency_value) < 0:
debit_type = ApiEnumDropdown.filter_by_one(
system=True, enum_class="DebitTypes", key="DT-D"
).data
data_dict["receive_debit"] = debit_type.id
data_dict["receive_debit_uu_id"] = str(debit_type.uu_id)
else:
debit_type = ApiEnumDropdown.filter_by_one(
system=True, enum_class="DebitTypes", key="DT-R"
).data
data_dict["receive_debit"] = debit_type.id
data_dict["receive_debit_uu_id"] = str(debit_type.uu_id)
account_record = AccountRecords.insert_one(data_dict).data
return AlchemyJsonResponse(
completed=True,
message="Account record created successfully",
result=account_record,
)
class AccountUpdateEventMethod(MethodToEvent):
event_type = "UPDATE"
event_description = ""
event_category = ""
__event_keys__ = {
"ec98ef2c-bcd0-432d-a8f4-1822a56c33b2": "account_records_update",
}
__event_validation__ = {
"ec98ef2c-bcd0-432d-a8f4-1822a56c33b2": (
UpdateAccountRecord,
[AccountRecords.__language_model__],
),
}
@classmethod
def account_records_update(
cls,
build_uu_id: str,
data: UpdateAccountRecordRequestModel,
token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
):
if isinstance(token_dict, OccupantTokenObject):
pass
elif isinstance(token_dict, EmployeeTokenObject):
pass
AccountRecords.build_parts_id = token_dict.selected_occupant.build_part_id
account_record = AccountRecords.update_one(build_uu_id, data).data
return AlchemyJsonResponse(
completed=True,
message="Account record updated successfully",
result=account_record,
cls_object=AccountRecords,
response_model=UpdateAccountRecord,
)

View File

@@ -0,0 +1,131 @@
"""
Account records endpoint configurations.
"""
from ApiEvents.abstract_class import (
RouteFactoryConfig,
EndpointFactoryConfig,
endpoint_wrapper,
)
from ApiEvents.base_request_model import EndpointBaseRequestModel
from Services.PostgresDb.Models.alchemy_response import DictJsonResponse
from fastapi import Request, Path, Body
@endpoint_wrapper("/account/records/list")
async def address_list(request: "Request", data: EndpointBaseRequestModel):
"""Handle address list endpoint."""
auth_dict = address_list.auth
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/create")
async def address_create(request: "Request", data: EndpointBaseRequestModel):
"""Handle address creation endpoint."""
return {
"data": data,
"request": str(request.headers),
"request_url": str(request.url),
"request_base_url": str(request.base_url),
}
@endpoint_wrapper("/account/records/search")
async def address_search(request: "Request", data: EndpointBaseRequestModel):
"""Handle address search endpoint."""
auth_dict = address_search.auth
code_dict = getattr(address_search, "func_code", {"function_code": None})
return {"auth_dict": auth_dict, "code_dict": code_dict, "data": data}
@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"),
request_data: EndpointBaseRequestModel = Body(..., description="Request body"),
):
"""
Handle address update endpoint.
Args:
request: FastAPI request object
address_uu_id: UUID of the address to update
request_data: Request body containing updated address data
Returns:
DictJsonResponse: Response containing updated address info
"""
auth_dict = address_update.auth
return DictJsonResponse(
data={
"address_uu_id": address_uu_id,
"data": request_data.root,
"request": str(request.headers),
"request_url": str(request.url),
"request_base_url": str(request.base_url),
}
)
prefix = "/account/records"
# Account Records Router Configuration
ACCOUNT_RECORDS_CONFIG = RouteFactoryConfig(
name="account_records",
prefix=prefix,
tags=["Account Records"],
include_in_schema=True,
endpoints=[
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/list",
url_of_endpoint=f"{prefix}/list",
endpoint="/list",
method="POST",
summary="List Active/Delete/Confirm Address",
description="List Active/Delete/Confirm Address",
is_auth_required=True,
is_event_required=True,
endpoint_function=address_list,
),
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/create",
url_of_endpoint=f"{prefix}/create",
endpoint="/create",
method="POST",
summary="Create Address with given auth levels",
description="Create Address with given auth levels",
is_auth_required=False,
is_event_required=False,
endpoint_function=address_create,
),
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/search",
url_of_endpoint=f"{prefix}/search",
endpoint="/search",
method="POST",
summary="Search Address with given auth levels",
description="Search Address with given auth levels",
is_auth_required=True,
is_event_required=True,
endpoint_function=address_search,
),
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/{address_uu_id}",
url_of_endpoint="{prefix}/" + "{address_uu_id}",
endpoint="/{address_uu_id}",
method="PUT",
summary="Update Address with given auth levels",
description="Update Address with given auth levels",
is_auth_required=True,
is_event_required=True,
endpoint_function=address_update,
),
],
).as_dict()

View File

@@ -0,0 +1,54 @@
"""
Account 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 (
InsertAccountRecord,
UpdateAccountRecord,
ListOptions,
)
class AddressUpdateRequest(RootModel[Dict[str, Any]]):
"""Request model for address update."""
model_config = {
"json_schema_extra": {
"example": {
"street": "123 Main St",
"city": "Example City",
"country": "Example Country",
}
}
}
class AddressUpdateResponse(BaseModel):
"""Response model for address update."""
address_uu_id: str = Field(..., description="UUID of the updated address")
data: Dict[str, Any] = Field(..., description="Updated address data")
function_code: str = Field(..., description="Function code for the endpoint")
class InsertAccountRecordRequestModel(BaseRequestModel["InsertAccountRecord"]):
"""Request model for inserting account records."""
pass
class UpdateAccountRecordRequestModel(BaseRequestModel["UpdateAccountRecord"]):
"""Request model for updating account records."""
pass
class ListOptionsRequestModel(BaseRequestModel["ListOptions"]):
"""Request model for list options."""
pass

View File

@@ -0,0 +1,351 @@
"""
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.",
)

View File

@@ -0,0 +1,112 @@
"""
Account records endpoint configurations.
"""
from ApiEvents.abstract_class import (
RouteFactoryConfig,
EndpointFactoryConfig,
endpoint_wrapper,
)
from ApiEvents.base_request_model import EndpointBaseRequestModel
from Services.PostgresDb.Models.alchemy_response import DictJsonResponse
from fastapi import Request, Path, Body
@endpoint_wrapper("/address/list")
async def address_list(request: "Request", data: EndpointBaseRequestModel):
"""Handle address list endpoint."""
auth_dict = address_list.auth
code_dict = getattr(address_list, "func_code", {"function_code": None})
return {"auth_dict": auth_dict, "code_dict": code_dict, "data": data}
@endpoint_wrapper("/address/create")
async def address_create(request: "Request", data: EndpointBaseRequestModel):
"""Handle address creation endpoint."""
return {
"data": data,
"request": str(request.headers),
"request_url": str(request.url),
"request_base_url": str(request.base_url),
}
@endpoint_wrapper("/address/update/{address_uu_id}")
async def address_update(
request: Request,
address_uu_id: str = Path(..., description="UUID of the address to update"),
request_data: EndpointBaseRequestModel = Body(..., description="Request body"),
):
"""
Handle address update endpoint.
Args:
request: FastAPI request object
address_uu_id: UUID of the address to update
request_data: Request body containing updated address data
Returns:
DictJsonResponse: Response containing updated address info
"""
auth_dict = address_update.auth
return DictJsonResponse(
data={
"address_uu_id": address_uu_id,
"data": request_data.root,
"request": str(request.headers),
"request_url": str(request.url),
"request_base_url": str(request.base_url),
}
)
prefix = "/address"
# Address Router Configuration
ADDRESS_CONFIG = RouteFactoryConfig(
name="address",
prefix=prefix,
tags=["Address"],
include_in_schema=True,
endpoints=[
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/list",
url_of_endpoint=f"{prefix}/list",
endpoint="/list",
method="POST",
summary="List Active/Delete/Confirm Address",
description="List Active/Delete/Confirm Address",
is_auth_required=True,
is_event_required=True,
endpoint_function=address_list,
),
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/create",
url_of_endpoint=f"{prefix}/create",
endpoint="/create",
method="POST",
summary="Create Address with given auth levels",
description="Create Address with given auth levels",
is_auth_required=False,
is_event_required=False,
endpoint_function=address_create,
),
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/{address_uu_id}",
url_of_endpoint="{prefix}/" + "{address_uu_id}",
endpoint="/{address_uu_id}",
method="PUT",
summary="Update Address with given auth levels",
description="Update Address with given auth levels",
is_auth_required=True,
is_event_required=True,
endpoint_function=address_update,
),
],
).as_dict()

View File

View File

View File

View File

@@ -0,0 +1,116 @@
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,
)
from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from ApiLibrary.common.line_number import get_line_number_for_error
if TYPE_CHECKING:
from fastapi import Request, HTTPException, status, Body
# Type aliases for common types
prefix = "/available"
async def check_endpoints_available(request: "Request") -> Dict[str, Any]:
"""
Check if endpoints are available.
"""
auth_dict = check_endpoints_available.auth
selection_of_user = None
if auth_dict.is_occupant:
selection_of_user = auth_dict.selected_occupant
else:
selection_of_user = auth_dict.selected_company
if not selection_of_user:
raise HTTPExceptionApi(
error_code="",
lang=auth_dict.lang,
loc=get_line_number_for_error(),
sys_msg="User selection not found",
)
return {"reachable_event_endpoints": selection_of_user.reachable_event_endpoints}
async def check_endpoint_available(
request: "Request",
data: EndpointBaseRequestModel,
) -> Dict[str, Any]:
"""
Check if endpoints are available.
"""
auth_dict = check_endpoint_available.auth
print("data", data)
data_dict = data.data
endpoint_asked = data_dict.get("endpoint", None)
if not endpoint_asked:
raise HTTPExceptionApi(
error_code="",
lang=auth_dict.lang,
loc=get_line_number_for_error(),
sys_msg="Endpoint not found",
)
selection_of_user = None
if auth_dict.is_occupant:
selection_of_user = auth_dict.selected_occupant
else:
selection_of_user = auth_dict.selected_company
if not selection_of_user:
raise HTTPExceptionApi(
error_code="",
lang=auth_dict.lang,
loc=get_line_number_for_error(),
sys_msg="User selection not found",
)
if endpoint_asked not in selection_of_user.reachable_event_endpoints:
raise HTTPExceptionApi(
error_code="",
lang=auth_dict.lang,
loc=get_line_number_for_error(),
sys_msg="Endpoint not found",
)
return {"endpoint": endpoint_asked, "status": "OK"}
AVAILABLE_CONFIG = RouteFactoryConfig(
name="available_endpoints",
prefix=prefix,
tags=["Available Endpoints"],
include_in_schema=True,
endpoints=[
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/endpoints",
url_of_endpoint=f"{prefix}/endpoints",
endpoint="/endpoints",
method="POST",
summary="Retrieve all endpoints available for user",
description="",
is_auth_required=True, # Needs token_dict
is_event_required=False,
endpoint_function=check_endpoints_available,
),
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/endpoint",
url_of_endpoint=f"{prefix}/endpoint",
endpoint="/endpoint",
method="POST",
summary="Retrieve an endpoint available for user",
description="",
is_auth_required=True, # Needs token_dict
is_event_required=False,
endpoint_function=check_endpoint_available,
),
],
).as_dict()

View File

@@ -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.",
)

View File

@@ -0,0 +1,128 @@
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=True, # 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=True, # 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=True, # 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
]

View File

@@ -0,0 +1,30 @@
"""
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

View File

@@ -0,0 +1,138 @@
"""
Validation request models.
"""
from typing import TYPE_CHECKING, Dict, Any
from ApiEvents.abstract_class import MethodToEvent
from ApiLibrary.common.line_number import get_line_number_for_error
from ApiValidations.Custom.validation_response import ValidationModel, ValidationParser
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from .models import ValidationsPydantic
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)
# 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)
"""
Headers: Headers which is merged with response model && language models of event
Validation: Validation of event which is merged with response model && language models of event
"""
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]:
EVENT_MODELS = AllModelsImport.import_all_models()
return_single_model = EVENT_MODELS.get(event.class_model, None)
# 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)
"""
Headers: Headers which is merged with response model && language models of event
Validation: Validation of event which is merged with response model && language models of event
"""
return {
"validation": validation.validation,
# "headers": validation.headers,
# "language_models": language_model_all,
}
class ValidationsHeaders(MethodToEvent):
@classmethod
def retrieve_headers(cls, event: ValidationsPydantic
) -> Dict[str, Any]:
EVENT_MODELS = AllModelsImport.import_all_models()
return_single_model = EVENT_MODELS.get(event.class_model, None)
# 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)
"""
Headers: Headers which is merged with response model && language models of event
Validation: Validation of event which is merged with response model && language models of event
"""
return {
"headers": validation.headers,
# "validation": validation.validation,
# "language_models": language_model_all,
}

21
Events/Engine/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
"""ApiEvents package initialization.
This module serves as the main entry point for the ApiEvents package,
making common utilities and base classes available for all API services.
"""
from .abstract_class import (
MethodToEvent,
PageInfo,
ClusterToMethod,
Event,
)
# from .base_request_model import BaseRequestModel, DictRequestModel
# Re-export commonly used classes
__all__ = [
"MethodToEvent",
"PageInfo",
"ClusterToMethod",
"Event",
]

View File

@@ -0,0 +1,176 @@
from typing import Any, ClassVar, Dict, List, Optional
from uuid import UUID
from .pageinfo import PageInfo
class Event:
KEY_: str # static string uuid.uuid4().__str__()
RESPONSE_VALIDATOR: ClassVar["PydanticModel"]
REQUEST_VALIDATOR: ClassVar["PydanticModel"]
DESCRIPTION: str
EXTRA_OPTIONS: Optional[Dict[str, Any]] = None
def __init__(
self, key: UUID, request_validator: "PydanticModel", response_validator: "PydanticModel",
description: str, extra_options: Optional[Dict[str, Any]] = None
) -> None:
self.KEY_ = key
self.REQUEST_VALIDATOR = request_validator
self.RESPONSE_VALIDATOR = response_validator
self.DESCRIPTION = description
self.EXTRA_OPTIONS = extra_options
@property
def name(self):
return f"This is an event of {self.__class__.__name__}. Description: {self.DESCRIPTION}"
@property
def key(self):
return str(self.KEY_)
@abstractmethod
def endpoint_callable(request: "Request", data: Any):
"""
return cls.retrieve_event(event_function_code).retrieve_callable(token_dict, data)
"""
pass
class MethodToEvent:
"""
for all endpoint callable
def endpoint_callable(request: Request, data: PydanticModel):
return cls.retrieve_event(event_function_code).retrieve_callable(token_dict, data)
"""
EVENTS: list[Event]
HEADER_LANGUAGE_MODELS: list[Dict] # [Table.__language_model__ | Dict[__language_model__]]
ERRORS_LANGUAGE_MODELS: Optional[list[Dict]] # [Dict[ErrorCode][lang]]
URL: str
METHOD: str
SUMMARY: str
DESCRIPTION: str
EXTRA_OPTIONS: Optional[Dict[str, Any]] = None
def __init__(
self,
events: list[Event],
headers: list[Dict],
url: str,
method: str,
summary: str,
description: str,
errors: Optional[list[Dict]] = None,
extra_options: Optional[Dict[str, Any]] = None,
):
self.EVENTS = events
self.URL = url
self.METHOD = method
self.SUMMARY = summary
self.DESCRIPTION = description
self.HEADER_LANGUAGE_MODELS = headers
self.ERRORS_LANGUAGE_MODELS = errors
self.EXTRA_OPTIONS = extra_options
def retrieve_all_event_keys():
"""
self.EVENTS.iter()
[FUNCTION_CODE]
"""
pass
def retrieve_event(event_function_code: str):
if list_found := [event for event in self.EVENTS if str(event.key) == event_function_code]:
return list_found[0]
raise ValueError(f"Event with function code {event_function_code} not found")
def retrieve_redis_value() -> Dict:
"""
Key(f"METHOD_FUNCTION_CODES:{ClusterToMethod}:MethodEvent:Endpoint") : Value([FUNCTION_CODE, ...])
"""
pass
class CategoryCluster:
TAGS: list
PREFIX: str
PAGEINFO: PageInfo
DESCRIPTION: str
ENDPOINTS: list = [MethodToEvent]
SUBCATEGORY: Optional[List["CategoryCluster"]] = []
INCLUDE_IN_SCHEMA: Optional[bool] = True
def __init__(
self,
tags: list,
prefix: str,
description: str,
pageinfo: PageInfo,
endpoints: list,
sub_category: list = [],
include_in_schema: Optional[bool] = True,
):
self.TAGS = tags
self.PREFIX = prefix
self.PAGEINFO = pageinfo
self.DESCRIPTION = description
self.ENDPOINTS = endpoints or []
self.SUBCATEGORY = sub_category or []
self.INCLUDE_IN_SCHEMA = include_in_schema
def retrieve_all_function_codes():
"""
[FUNCTION_CODE, ...]
self.ENDPOINTS -> iter()
"""
pass
def retrieve_page_info():
"""
PAGE_INFO:ClusterToMethod = {
"PageInfo": {...}
"subCategory": PAGE_INFO:ClusterToMethod
}
PAGE_INFO:ClusterToMethod = {
"PageInfo": {...}
"subCategory": PAGE_INFO:ClusterToMethod
}
"""
pass
def retrieve_redis_value() -> Dict:
"""
Key(CLUSTER_FUNCTION_CODES:ClusterToMethod) : Value(PAGE_INFO, [FUNCTION_CODE, ...])
"""
pass
class PageInfo:
NAME: str
BUTTON_NAME: str
PAGE_URL: str
PAGEINFO: "PageInfo"
def __init__(
self,
name: str,
title: Dict[str, Any],
description: Dict[str, Any],
icon: str,
parent: str,
url: str,
):
self.NAME = name
self.TITLE = title
self.DESCRIPTION = description
self.ICON = icon
self.PARENT = parent

View File

@@ -0,0 +1,2 @@

View File

@@ -0,0 +1,7 @@
import AllEvents.auth as auths_events
import AllEvents.events as events_events
import AllEvents.validations as validations_events
for event in [*auths_events.__all__, *events_events.__all__, *validations_events.__all__]:
print(event)

View File

View File

@@ -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()

View File

@@ -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

38
Events/abstract_class.py Normal file
View File

@@ -0,0 +1,38 @@
class ClusterToMethod:
TAGS: list = ["Tag or Router"]
PREFIX: str = "/..."
PAGEINFO: PageInfo
ENDPOINTS: list = [MethodEvent, ...]
SUBCATEGORY: List[ClassVar[Any]] = [ClusterToMethod, ...]
def retrieve_all_function_codes():
"""
[FUNCTION_CODE, ...]
self.ENDPOINTS -> iter()
"""
pass
def retrieve_page_info():
"""
PAGE_INFO:ClusterToMethod = {
"PageInfo": {...}
"subCategory": PAGE_INFO:ClusterToMethod
}
PAGE_INFO:ClusterToMethod = {
"PageInfo": {...}
"subCategory": PAGE_INFO:ClusterToMethod
}
"""
pass
def retrieve_redis_value() -> Dict:
"""
Key(CLUSTER_FUNCTION_CODES:ClusterToMethod) : Value(PAGE_INFO, [FUNCTION_CODE, ...])
"""
pass

View File

@@ -0,0 +1,919 @@
"""
Abstract base classes for API route and event handling.
This module provides core abstractions for route configuration and factory,
with support for authentication and event handling.
"""
import uuid
import inspect
from typing import (
Tuple,
TypeVar,
Optional,
Callable,
Dict,
Any,
List,
Type,
ClassVar,
Union,
Set,
)
from collections import defaultdict
from dataclasses import dataclass, field
from pydantic import BaseModel
from fastapi import Request, Depends, APIRouter
from functools import wraps
from ApiLayers.ApiLibrary.common.line_number import get_line_number_for_error
from ApiLayers.ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from ApiLayers.Schemas.rules.rules import EndpointRestriction
ResponseModel = TypeVar("ResponseModel", bound=BaseModel)
def endpoint_wrapper(url_of_endpoint: Optional[str] = None):
"""Create a wrapper for endpoints that stores url_of_endpoint in closure.
Args:
url_of_endpoint: Optional URL path for the endpoint
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
async def wrapper(
*args: Any, **kwargs: Any
) -> Union[Dict[str, Any], BaseModel]:
# Handle both async and sync functions
if inspect.iscoroutinefunction(func):
result = await func(*args, **kwargs)
else:
result = func(*args, **kwargs)
# If result is a coroutine, await it
if inspect.iscoroutine(result):
result = await result
# Add endpoint to the result
if isinstance(result, dict):
result["endpoint"] = url_of_endpoint
return result
elif isinstance(result, BaseModel):
# Convert Pydantic model to dict and add endpoint
result_dict = result.model_dump()
result_dict["endpoint"] = url_of_endpoint
return result_dict
return result
wrapper.url_of_endpoint = url_of_endpoint
return wrapper
return decorator
@dataclass
class EndpointFactoryConfig:
"""Configuration class for API endpoints.
Attributes:
url_of_endpoint: Full URL path for this endpoint
endpoint: URL path for this endpoint
method: HTTP method (GET, POST, etc.)
summary: Short description for API documentation
description: Detailed description for API documentation
endpoint_function: Function to handle the endpoint
is_auth_required: Whether authentication is required
response_model: Optional response model for OpenAPI schema
request_model: Optional request model for OpenAPI schema
is_event_required: Whether event handling is required
extra_options: Additional endpoint options
"""
url_prefix: str
url_endpoint: str
url_of_endpoint: str
endpoint: str
method: str
summary: str
description: str
endpoint_function: Callable[..., Any] # Now accepts any parameters and return type
response_model: Optional[type] = None
request_model: Optional[type] = None
is_auth_required: bool = True
is_event_required: bool = False
extra_options: Dict[str, Any] = field(default_factory=dict)
def __post_init__(self):
"""Post initialization hook.
Wraps endpoint function with appropriate middleware based on configuration:
- If auth and event required -> wrap with TokenEventMiddleware
- If only event required -> wrap with EventMiddleware
- If only auth required -> wrap with MiddlewareModule.auth_required
"""
# First apply auth/event middleware
if self.is_event_required:
from middleware import TokenEventMiddleware
self.endpoint_function = TokenEventMiddleware.event_required(
self.endpoint_function
)
elif self.is_auth_required:
from middleware import MiddlewareModule
self.endpoint_function = MiddlewareModule.auth_required(
self.endpoint_function
)
# Then wrap with endpoint_wrapper to store url_of_endpoint
self.endpoint_function = endpoint_wrapper(self.url_of_endpoint)(
self.endpoint_function
)
class RouteFactoryConfig:
"""Configuration class for API route factories.
Attributes:
name: Route name
tags: List of tags for API documentation
prefix: URL prefix for all endpoints in this route
include_in_schema: Whether to include in OpenAPI schema
endpoints: List of endpoint configurations
extra_options: Additional route options
"""
def __init__(
self,
name: str,
tags: List[str],
prefix: str,
include_in_schema: bool = True,
endpoints: List[EndpointFactoryConfig] = None,
extra_options: Dict[str, Any] = None,
):
self.name = name
self.tags = tags
self.prefix = prefix
self.include_in_schema = include_in_schema
self.endpoints = endpoints or []
self.extra_options = extra_options or {}
def __post_init__(self):
"""Validate and normalize configuration after initialization."""
if self.endpoints is None:
self.endpoints = []
if self.extra_options is None:
self.extra_options = {}
def as_dict(self) -> Dict[str, Any]:
"""Convert configuration to dictionary format."""
return {
"name": self.name,
"tags": self.tags,
"prefix": self.prefix,
"include_in_schema": self.include_in_schema,
"endpoints": [endpoint.__dict__ for endpoint in self.endpoints],
"extra_options": self.extra_options,
}
class MethodToEvent:
"""Base class for mapping methods to API events with type safety and endpoint configuration.
This class provides a framework for handling API events with proper
type checking for tokens and response models, as well as managing
endpoint configurations and frontend page structure.
Type Parameters:
TokenType: Type of authentication token
ResponseModel: Type of response model
Class Variables:
action_key: Unique identifier for the action
event_type: Type of event (e.g., 'query', 'command')
event_description: Human-readable description of the event
event_category: Category for grouping related events
__event_keys__: Mapping of UUIDs to event names
__event_validation__: Validation rules for events
__endpoint_config__: API endpoint configuration
__page_info__: Frontend page configuration
"""
action_key: ClassVar[Optional[str]] = None
event_type: ClassVar[Optional[str]] = None
event_description: ClassVar[str] = ""
event_category: ClassVar[str] = ""
__event_keys__: ClassVar[Dict[str, str]] = {}
__event_validation__: Dict[str, Tuple[Type, Union[List, tuple]]] = {}
__endpoint_config__: ClassVar[Dict[str, Dict[str, Any]]] = {
"endpoints": {}, # Mapping of event UUIDs to endpoint configs
"router_prefix": "", # Router prefix for all endpoints in this class
"tags": [], # OpenAPI tags
}
__page_info__: ClassVar[Dict[str, Any]] = {
"name": "", # Page name (e.g., "AccountPage")
"title": {"tr": "", "en": ""}, # Multi-language titles
"icon": "", # Icon name
"url": "", # Frontend route
"component": None, # Optional component name
"parent": None, # Parent page name if this is a subpage
}
@classmethod
def register_endpoint(
cls,
event_uuid: str,
path: str,
method: str = "POST",
response_model: Optional[Type] = None,
**kwargs
) -> None:
"""Register an API endpoint configuration for an event.
Args:
event_uuid: UUID of the event
path: Endpoint path (will be prefixed with router_prefix)
method: HTTP method (default: POST)
response_model: Pydantic model for response
**kwargs: Additional FastAPI endpoint parameters
"""
if event_uuid not in cls.__event_keys__:
raise ValueError(f"Event UUID {event_uuid} not found in {cls.__name__}")
cls.__endpoint_config__["endpoints"][event_uuid] = {
"path": path,
"method": method,
"response_model": response_model,
**kwargs
}
@classmethod
def configure_router(cls, prefix: str, tags: List[str]) -> None:
"""Configure the API router settings.
Args:
prefix: Router prefix for all endpoints
tags: OpenAPI tags for documentation
"""
cls.__endpoint_config__["router_prefix"] = prefix
cls.__endpoint_config__["tags"] = tags
@classmethod
def configure_page(
cls,
name: str,
title: Dict[str, str],
icon: str,
url: str,
component: Optional[str] = None,
parent: Optional[str] = None
) -> None:
"""Configure the frontend page information.
Args:
name: Page name
title: Multi-language titles (must include 'tr' and 'en')
icon: Icon name
url: Frontend route
component: Optional component name
parent: Parent page name for subpages
"""
required_langs = {"tr", "en"}
if not all(lang in title for lang in required_langs):
raise ValueError(f"Title must contain all required languages: {required_langs}")
cls.__page_info__.update({
"name": name,
"title": title,
"icon": icon,
"url": url,
"component": component,
"parent": parent
})
@classmethod
def get_endpoint_config(cls) -> Dict[str, Any]:
"""Get the complete endpoint configuration."""
return cls.__endpoint_config__
@classmethod
def get_page_info(cls) -> Dict[str, Any]:
"""Get the frontend page configuration."""
return cls.__page_info__
@classmethod
def has_available_events(cls, user_permission_uuids: Set[str]) -> bool:
"""Check if any events are available based on user permissions."""
return bool(set(cls.__event_keys__.keys()) & user_permission_uuids)
@classmethod
def get_page_info_with_permissions(
cls,
user_permission_uuids: Set[str],
include_endpoints: bool = False
) -> Optional[Dict[str, Any]]:
"""Get page info if user has required permissions.
Args:
user_permission_uuids: Set of UUIDs the user has permission for
include_endpoints: Whether to include available endpoint information
Returns:
Dict with page info if user has permissions, None otherwise
"""
# Check if user has any permissions for this page's events
if not cls.has_available_events(user_permission_uuids):
return None
# Start with basic page info
page_info = {
**cls.__page_info__,
"category": cls.event_category,
"type": cls.event_type,
"description": cls.event_description
}
# Optionally include available endpoints
if include_endpoints:
available_endpoints = {}
for uuid, endpoint in cls.__endpoint_config__["endpoints"].items():
if uuid in user_permission_uuids:
available_endpoints[uuid] = {
"path": f"{cls.__endpoint_config__['router_prefix']}{endpoint['path']}",
"method": endpoint["method"],
"event_name": cls.__event_keys__[uuid]
}
if available_endpoints:
page_info["available_endpoints"] = available_endpoints
return page_info
@classmethod
def get_events_config(cls) -> Dict[str, Any]:
"""Get the complete configuration including events, endpoints, and page info."""
return {
"events": cls.__event_keys__,
"endpoints": cls.__endpoint_config__,
"page_info": cls.__page_info__,
"category": cls.event_category,
"type": cls.event_type,
"description": cls.event_description
}
@classmethod
def retrieve_event_response_model(cls, function_code: str) -> Any:
"""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)
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) -> Dict:
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_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, Any]:
"""Retrieve language-specific parameters for an event.
Args:
language: Language code (e.g. 'tr', 'en')
function_code: Function identifier
Returns:
Dictionary of language-specific field mappings
"""
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
only_language_dict = {
field: event_response_model_merged_lang[field]
for field in event_response_model.model_fields
if field in event_response_model_merged_lang
}
"""
__event_validation__ : {"key": [A, B, C]}
Language Model : Language Model that is model pydatnic requires
Language Models : All language_models that is included in Langugage Models Section
Merged Language Models : Merged with all models in list event_validation
"""
return {
"language_model": only_language_dict,
"language_models": event_response_model_merged,
}
class EventMethodRegistry:
"""Registry for mapping event method UUIDs to categories and managing permissions."""
def __init__(self):
self._uuid_map: Dict[str, Tuple[Type[MethodToEvent], str]] = {} # uuid -> (method_class, event_name)
self._category_events: Dict[str, Set[str]] = defaultdict(set) # category -> set of uuids
def register_method(self, category_name: str, method_class: Type[MethodToEvent]) -> None:
"""Register a method class with its category."""
# Register all UUIDs from the method
for event_uuid, event_name in method_class.__event_keys__.items():
self._uuid_map[event_uuid] = (method_class, event_name)
self._category_events[category_name].add(event_uuid)
def get_method_by_uuid(self, event_uuid: str) -> Optional[Tuple[Type[MethodToEvent], str]]:
"""Get method class and event name by UUID."""
return self._uuid_map.get(event_uuid)
def get_events_for_category(self, category_name: str) -> Set[str]:
"""Get all event UUIDs for a category."""
return self._category_events.get(category_name, set())
class EventCategory:
"""Base class for defining event categories similar to frontend page structure."""
def __init__(
self,
name: str,
title: Dict[str, str],
icon: str,
url: str,
component: Optional[str] = None,
page_info: Any = None,
all_endpoints: Dict[str, Set[str]] = None, # category -> set of event UUIDs
sub_categories: List = None,
):
self.name = name
self.title = self._validate_title(title)
self.icon = icon
self.url = url
self.component = component
self.page_info = page_info
self.all_endpoints = all_endpoints or {}
self.sub_categories = self._process_subcategories(sub_categories or [])
def _validate_title(self, title: Dict[str, str]) -> Dict[str, str]:
"""Validate title has required languages."""
required_langs = {"tr", "en"}
if not all(lang in title for lang in required_langs):
raise ValueError(f"Title must contain all required languages: {required_langs}")
return title
def _process_subcategories(self, categories: List[Union[Dict, "EventCategory"]]) -> List["EventCategory"]:
"""Process subcategories ensuring they are all EventCategory instances."""
processed = []
for category in categories:
if isinstance(category, dict):
processed.append(EventCategory.from_dict(category))
elif isinstance(category, EventCategory):
processed.append(category)
else:
raise ValueError(f"Invalid subcategory type: {type(category)}")
return processed
def has_available_events(self, user_permission_uuids: Set[str]) -> bool:
"""Check if category has available events based on UUID intersection."""
# Check current category's events
return any(
bool(events & user_permission_uuids)
for events in self.all_endpoints.values()
)
def get_menu_item(self, user_permission_uuids: Set[str]) -> Optional[Dict[str, Any]]:
"""Get menu item if category has available events."""
# First check if this category has available events
if not self.has_available_events(user_permission_uuids):
return None
menu_item = {
"name": self.name,
"title": self.title,
"icon": self.icon,
"url": self.url
}
if self.component:
menu_item["component"] = self.component
# Only process subcategories if parent has permissions
sub_items = []
for subcategory in self.sub_categories:
if sub_menu := subcategory.get_menu_item(user_permission_uuids):
sub_items.append(sub_menu)
if sub_items:
menu_item["items"] = sub_items
return menu_item
def get_available_events(self, registry: EventMethodRegistry, user_permission_uuids: Set[str]) -> Dict[str, List[Dict[str, Any]]]:
"""Get available events based on user permission UUIDs."""
available_events = defaultdict(list)
# Process endpoints in current category
category_events = self.all_endpoints.get(self.name, set())
for event_uuid in category_events & user_permission_uuids:
method_info = registry.get_method_by_uuid(event_uuid)
if method_info:
method_class, event_name = method_info
available_events[method_class.event_type].append({
"uuid": event_uuid,
"name": event_name,
"description": method_class.event_description,
"category": method_class.event_category
})
# Process subcategories recursively
for subcategory in self.sub_categories:
sub_events = subcategory.get_available_events(registry, user_permission_uuids)
for event_type, events in sub_events.items():
available_events[event_type].extend(events)
return dict(available_events)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "EventCategory":
"""Create category from dictionary."""
return cls(
name=data["name"],
title=data["title"],
icon=data["icon"],
url=data["url"],
component=data.get("component"),
page_info=data.get("pageInfo"),
all_endpoints=data.get("allEndpoints", {}),
sub_categories=data.get("subCategories", [])
)
def to_dict(self, registry: EventMethodRegistry, user_permission_uuids: Optional[Set[str]] = None) -> Dict[str, Any]:
"""Convert category to dictionary with optional permission filtering."""
result = {
"name": self.name,
"title": self.title,
"icon": self.icon,
"url": self.url,
"pageInfo": self.page_info,
}
if user_permission_uuids is not None:
# Only include endpoints and their info if user has permissions
available_events = self.get_available_events(registry, user_permission_uuids)
if available_events:
result["availableEvents"] = available_events
result["allEndpoints"] = self.all_endpoints
else:
# Include all endpoints if no permissions specified
result["allEndpoints"] = self.all_endpoints
# Process subcategories
subcategories = [
sub.to_dict(registry, user_permission_uuids) for sub in self.sub_categories
]
# Only include subcategories that have available events
if user_permission_uuids is None or any(
"availableEvents" in sub for sub in subcategories
):
result["subCategories"] = subcategories
if self.component:
result["component"] = self.component
return result
class EventCategoryManager:
"""Manager class for handling event categories and their relationships."""
def __init__(self):
self.categories: List[EventCategory] = []
self.registry = EventMethodRegistry()
def get_menu_tree(self, user_permission_uuids: Set[str]) -> List[Dict[str, Any]]:
"""Get menu tree based on available events."""
return [
menu_item for category in self.categories
if (menu_item := category.get_menu_item(user_permission_uuids))
]
def register_category(self, category: EventCategory) -> None:
"""Register a category and its endpoints in the registry."""
self.categories.append(category)
def add_category(self, category: Union[EventCategory, Dict[str, Any]]) -> None:
"""Add a new category."""
if isinstance(category, dict):
category = EventCategory.from_dict(category)
self.register_category(category)
def add_categories(self, categories: List[Union[EventCategory, Dict[str, Any]]]) -> None:
"""Add multiple categories at once."""
for category in categories:
self.add_category(category)
def get_category(self, name: str) -> Optional[EventCategory]:
"""Get category by name."""
return next((cat for cat in self.categories if cat.name == name), None)
def get_all_categories(self, user_permission_uuids: Optional[Set[str]] = None) -> List[Dict[str, Any]]:
"""Get all categories as dictionary, filtered by user permissions."""
return [cat.to_dict(self.registry, user_permission_uuids) for cat in self.categories]
def get_category_endpoints(self, category_name: str) -> Set[str]:
"""Get all endpoint UUIDs for a category."""
category = self.get_category(category_name)
return category.all_endpoints.get(category_name, set()) if category else set()
def get_subcategories(self, category_name: str, user_permission_uuids: Optional[Set[str]] = None) -> List[Dict[str, Any]]:
"""Get subcategories for a category."""
category = self.get_category(category_name)
if not category:
return []
return [sub.to_dict(self.registry, user_permission_uuids) for sub in category.sub_categories]
def find_category_by_url(self, url: str) -> Optional[EventCategory]:
"""Find a category by its URL."""
for category in self.categories:
if category.url == url:
return category
for subcategory in category.sub_categories:
if subcategory.url == url:
return subcategory
return None
class EventMethodRegistry:
"""Registry for all MethodToEvent classes and menu building."""
_instance = None
_method_classes: Dict[str, Type[MethodToEvent]] = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def register_method_class(cls, method_class: Type[MethodToEvent]) -> None:
"""Register a MethodToEvent class."""
if not issubclass(method_class, MethodToEvent):
raise ValueError(f"{method_class.__name__} must be a subclass of MethodToEvent")
page_info = method_class.get_page_info()
cls._method_classes[page_info["name"]] = method_class
@classmethod
def get_all_menu_items(
cls,
user_permission_uuids: Set[str],
include_endpoints: bool = False
) -> List[Dict[str, Any]]:
"""Get all menu items based on user permissions.
Args:
user_permission_uuids: Set of UUIDs the user has permission for
include_endpoints: Whether to include available endpoint information
Returns:
List of menu items organized in a tree structure
"""
# First get all page infos
page_infos = {}
for method_class in cls._method_classes.values():
if page_info := method_class.get_page_info_with_permissions(user_permission_uuids, include_endpoints):
page_infos[page_info["name"]] = page_info
# Build tree structure
menu_tree = []
child_pages = set()
# First pass: identify all child pages
for page_info in page_infos.values():
if page_info.get("parent"):
child_pages.add(page_info["name"])
# Second pass: build tree structure
for name, page_info in page_infos.items():
# Skip if this is a child page
if name in child_pages:
continue
# Start with this page's info
menu_item = page_info.copy()
# Find and add children
children = []
for child_info in page_infos.values():
if child_info.get("parent") == name:
children.append(child_info)
if children:
menu_item["items"] = sorted(
children,
key=lambda x: x["name"]
)
menu_tree.append(menu_item)
return sorted(menu_tree, key=lambda x: x["name"])
@classmethod
def get_available_endpoints(
cls,
user_permission_uuids: Set[str]
) -> Dict[str, Dict[str, Any]]:
"""Get all available endpoints based on user permissions.
Args:
user_permission_uuids: Set of UUIDs the user has permission for
Returns:
Dict mapping event UUIDs to endpoint configurations
"""
available_endpoints = {}
for method_class in cls._method_classes.values():
if page_info := method_class.get_page_info_with_permissions(
user_permission_uuids,
include_endpoints=True
):
if endpoints := page_info.get("available_endpoints"):
available_endpoints.update(endpoints)
return available_endpoints
"""
Example usage
# Register your MethodToEvent classes
registry = EventMethodRegistry()
registry.register_method_class(AccountEventMethods)
registry.register_method_class(AccountDetailsEventMethods)
# Get complete menu structure
user_permissions = {
"uuid1",
"uuid2",
"uuid3"
}
menu_items = registry.get_all_menu_items(user_permissions, include_endpoints=True)
# Result:
[
{
"name": "AccountPage",
"title": {"tr": "Hesaplar", "en": "Accounts"},
"icon": "User",
"url": "/account",
"category": "account",
"type": "query",
"description": "Account management operations",
"available_endpoints": {
"uuid1": {"path": "/api/account/view", "method": "GET"},
"uuid2": {"path": "/api/account/edit", "method": "POST"}
},
"items": [
{
"name": "AccountDetailsPage",
"title": {"tr": "Hesap Detayları", "en": "Account Details"},
"icon": "FileText",
"url": "/account/details",
"parent": "AccountPage",
"category": "account_details",
"type": "query",
"available_endpoints": {
"uuid3": {"path": "/api/account/details/view", "method": "GET"}
}
}
]
}
]
# Get all available endpoints
endpoints = registry.get_available_endpoints(user_permissions)
# Result:
{
"uuid1": {
"path": "/api/account/view",
"method": "GET",
"event_name": "view_account"
},
"uuid2": {
"path": "/api/account/edit",
"method": "POST",
"event_name": "edit_account"
},
"uuid3": {
"path": "/api/account/details/view",
"method": "GET",
"event_name": "view_details"
}
}
# Get event UUIDs from MethodToEvent classes
account_events = {uuid for uuid in AccountEventMethods.__event_keys__}
# Define categories with event UUIDs
PAGES_INFO = [
{
"name": "AccountPage",
"title": {"tr": "Hesaplar", "en": "Accounts"},
"icon": "User",
"url": "/account",
"pageInfo": AccountPageInfo,
"allEndpoints": {"AccountPage": account_events},
"subCategories": [
{
"name": "AccountDetailsPage",
"title": {"tr": "Hesap Detayları", "en": "Account Details"},
"icon": "FileText",
"url": "/account/details",
"allEndpoints": {} # No direct endpoints, only shown if parent has permissions
}
]
}
]
# Initialize manager
manager = EventCategoryManager()
manager.add_categories(PAGES_INFO)
# Get menu tree based on available events
user_permission_uuids = {
"31f4f32f-0cd4-4995-8a6a-f9f56335848a",
"ec98ef2c-bcd0-432d-a8f4-1822a56c33b2"
}
menu_tree = manager.get_menu_tree(user_permission_uuids)
"""

View File

@@ -0,0 +1,51 @@
"""
Base request models for API endpoints.
This module provides base request models that can be used across different endpoints
to ensure consistent request handling and validation.
"""
from typing import Dict, Any, TypeVar
from pydantic import BaseModel, Field, ConfigDict
T = TypeVar("T")
class EndpointBaseRequestModel(BaseModel):
data: dict = Field(..., description="Data to be sent with the request")
class Config:
json_schema_extra = {
"data": {
"key": "value",
}
}
class SuccessResponse(BaseModel):
"""Standard success response model."""
data: Dict[str, Any] = Field(
...,
example={
"id": "123",
"username": "john.doe",
"email": "john@example.com",
"role": "user",
},
)
model_config = ConfigDict(
json_schema_extra={
"example": {
"data": {
"id": "123",
"username": "john.doe",
"email": "john@example.com",
"role": "user",
},
}
}
)