validations updated

This commit is contained in:
2025-01-19 21:06:00 +03:00
parent d6785ed36f
commit 8e34497c80
49 changed files with 2642 additions and 142 deletions

View File

@@ -11,7 +11,7 @@ from ApiLibrary.common.line_number import get_line_number_for_error
from ApiLibrary.date_time_actions.date_functions import DateTimeLocal
from ApiServices.Login.user_login_handler import UserLoginModule
from ApiServices.Token.token_handler import TokenService
from ApiValidations.Custom.token_objects import CompanyToken
from ApiValidations.Custom.token_objects import CompanyToken, OccupantToken
from ApiValidations.Request.authentication import (
Login,
EmployeeSelectionValidation,
@@ -20,11 +20,16 @@ from ApiValidations.Request.authentication import (
EmployeeSelection,
)
from ErrorHandlers import HTTPExceptionApi
from Schemas.building.build import (
BuildLivingSpace,
BuildParts,
RelationshipEmployee2Build,
)
from Schemas.company.company import Companies
from Schemas.company.department import Departments, Duties, Duty
from Schemas.company.employee import Staff, Employees
from Schemas.event.event import Event2Employee
from Schemas.identity.identity import Users
from Schemas.event.event import Event2Employee, Event2Occupant
from Schemas.identity.identity import OccupantTypes, Users
from Services.Redis.Actions.actions import RedisActions
from .models import (
LoginData,
@@ -170,9 +175,7 @@ class AuthenticationSelectEventMethods(MethodToEvent):
)
# Get reachable events
reachable_event_list_id = Event2Employee.get_event_id_by_employee_id(
employee_id=employee.id
)
reachable_event_codes = Event2Employee.get_event_codes(employee_id=employee.id)
# Get staff and duties
staff = Staff.filter_one(Staff.id == employee.staff_id, db=db_session).data
@@ -189,7 +192,6 @@ class AuthenticationSelectEventMethods(MethodToEvent):
**Duties.valid_record_dict,
db=db_session,
).data
# Create company token
company_token = CompanyToken(
company_uu_id=selected_company.uu_id.__str__(),
@@ -203,7 +205,7 @@ class AuthenticationSelectEventMethods(MethodToEvent):
staff_uu_id=staff.uu_id.__str__(),
employee_id=employee.id,
employee_uu_id=employee.uu_id.__str__(),
reachable_event_list_id=reachable_event_list_id,
reachable_event_codes=reachable_event_codes,
)
try: # Update Redis
update_token = TokenService.update_token_at_redis(
@@ -226,12 +228,81 @@ class AuthenticationSelectEventMethods(MethodToEvent):
request: "Request",
):
"""Handle occupant type selection"""
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang=token_dict.lang,
loc=get_line_number_for_error(),
sys_msg="Occupant selection not implemented",
db = BuildLivingSpace.new_session()
# Get selected occupant type
selected_build_living_space = BuildLivingSpace.filter_one(
BuildLivingSpace.uu_id == data.build_living_space_uu_id,
db=db,
).data
if not selected_build_living_space:
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang=token_dict.lang,
loc=get_line_number_for_error(),
sys_msg="Selected occupant type not found",
)
# Get reachable events
reachable_event_codes = Event2Occupant.get_event_codes(
build_living_space_id=selected_build_living_space.id
)
occupant_type = OccupantTypes.filter_one(
OccupantTypes.id == selected_build_living_space.occupant_type_id,
db=db,
system=True,
).data
build_part = BuildParts.filter_one(
BuildParts.id == selected_build_living_space.build_parts_id,
db=db,
).data
build = BuildParts.filter_one(
BuildParts.id == build_part.build_id,
db=db,
).data
responsible_employee = Employees.filter_one(
Employees.id == build_part.responsible_employee_id,
db=db,
).data
related_company = RelationshipEmployee2Build.filter_one(
RelationshipEmployee2Build.member_id == build.id,
db=db,
).data
# Get company
company_related = Companies.filter_one(
Companies.id == related_company.company_id,
db=db,
).data
# Create occupant token
occupant_token = OccupantToken(
living_space_id=selected_build_living_space.id,
living_space_uu_id=selected_build_living_space.uu_id.__str__(),
occupant_type_id=occupant_type.id,
occupant_type_uu_id=occupant_type.uu_id.__str__(),
occupant_type=occupant_type.occupant_type,
build_id=build.id,
build_uuid=build.uu_id.__str__(),
build_part_id=build_part.id,
build_part_uuid=build_part.uu_id.__str__(),
responsible_employee_id=responsible_employee.id,
responsible_employee_uuid=responsible_employee.uu_id.__str__(),
responsible_company_id=company_related.id,
responsible_company_uuid=company_related.uu_id.__str__(),
reachable_event_codes=reachable_event_codes,
)
try: # Update Redis
update_token = TokenService.update_token_at_redis(
request=request, add_payload=occupant_token
)
return update_token
except Exception as e:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg=f"{e}",
)
@classmethod
async def authentication_select_company_or_occupant_type(
@@ -246,14 +317,6 @@ class AuthenticationSelectEventMethods(MethodToEvent):
elif token_dict.is_occupant:
return cls._handle_occupant_selection(data, token_dict, request)
# except Exception as e:
# raise HTTPExceptionApi(
# error_code="HTTP_500_INTERNAL_SERVER_ERROR",
# lang="en",
# loc=get_line_number_for_error(),
# sys_msg=str(e),
# )
class AuthenticationCheckTokenEventMethods(MethodToEvent):
event_type = "LOGIN"

View File

@@ -3,8 +3,8 @@ Authentication endpoint configurations.
"""
from typing import TYPE_CHECKING, Dict, Any, Union, Annotated
from fastapi import HTTPException, status, Body
from ApiServices.Token.token_handler import TokenService
from ApiValidations.Request import (
Logout,
Login,
@@ -49,7 +49,7 @@ from ApiEvents.abstract_class import (
)
if TYPE_CHECKING:
from fastapi import Request
from fastapi import Request, HTTPException, status, Body
from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject
@@ -59,22 +59,18 @@ from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTok
@endpoint_wrapper("/authentication/select")
async def authentication_select_company_or_occupant_type(
request: "Request",
data: EndpointBaseRequestModel,
data: Union[EmployeeSelection, OccupantSelection],
) -> Dict[str, Any]:
"""
Select company or occupant type.
"""
auth_dict = authentication_select_company_or_occupant_type.auth
if data.data.get("company_uu_id"):
data = EmployeeSelection(**data.data)
elif data.data.get("build_living_space_uu_id"):
data = OccupantSelection(**data.data)
if await AuthenticationSelectEventMethods.authentication_select_company_or_occupant_type(
request=request, data=data, token_dict=auth_dict
):
if isinstance(data, EmployeeSelection):
if data.is_employee:
return {"selected_company": data.company_uu_id}
elif isinstance(data, OccupantSelection):
elif data.is_occupant:
return {"selected_occupant": data.build_living_space_uu_id}
@@ -99,10 +95,18 @@ async def authentication_check_token_is_valid(
"""
Check if a token is valid.
"""
return {
"headers": dict(request.headers),
"data": data.model_dump(),
}
try:
access_token = TokenService.get_access_token_from_request(request=request)
if TokenService.get_object_via_access_key(access_token=access_token):
return {
"status": True,
"message": "Access Token is valid",
}
except HTTPException:
return {
"status": False,
"message": "Access Token is NOT valid",
}
@endpoint_wrapper("/authentication/refresh")

View File

@@ -6,15 +6,13 @@ to be used by the dynamic route creation system.
"""
from typing import Dict, List, Any
from .auth.endpoints import AUTH_CONFIG
from .events.auth.endpoints import AUTH_CONFIG
# Registry of all route configurations
ROUTE_CONFIGS = [
AUTH_CONFIG,
]
ROUTE_CONFIGS = [AUTH_CONFIG]
def get_route_configs() -> List[Dict[str, Any]]:
"""Get all registered route configurations."""
return [AUTH_CONFIG]
return ROUTE_CONFIGS

View File

@@ -2,7 +2,7 @@
Account records service implementation.
"""
from typing import Dict, Any, Union
from typing import Union
from pydantic import Field
from ApiEvents.abstract_class import MethodToEvent, endpoint_wrapper
@@ -26,7 +26,6 @@ from Schemas import (
)
from Services.PostgresDb.Models.alchemy_response import (
AlchemyJsonResponse,
DictJsonResponse,
)
from ApiValidations.Response import AccountRecordResponse
from .models import (
@@ -36,7 +35,7 @@ from .models import (
)
class AccountRecordsListEventMethods(MethodToEvent):
class AccountListEventMethod(MethodToEvent):
event_type = "SELECT"
event_description = ""
@@ -47,8 +46,8 @@ class AccountRecordsListEventMethods(MethodToEvent):
"208e6273-17ef-44f0-814a-8098f816b63a": "account_records_list_flt_res",
}
__event_validation__ = {
"7192c2aa-5352-4e36-98b3-dafb7d036a3d": AccountRecordResponse,
"208e6273-17ef-44f0-814a-8098f816b63a": AccountRecordResponse,
"7192c2aa-5352-4e36-98b3-dafb7d036a3d": (AccountRecordResponse, [AccountRecords.__language_model__]),
"208e6273-17ef-44f0-814a-8098f816b63a": (AccountRecordResponse, [AccountRecords.__language_model__]),
}
@classmethod
@@ -217,7 +216,7 @@ class AccountRecordsListEventMethods(MethodToEvent):
)
class AccountRecordsCreateEventMethods(MethodToEvent):
class AccountCreateEventMethod(MethodToEvent):
event_type = "CREATE"
event_description = ""
@@ -227,7 +226,7 @@ class AccountRecordsCreateEventMethods(MethodToEvent):
"31f4f32f-0cd4-4995-8a6a-f9f56335848a": "account_records_create",
}
__event_validation__ = {
"31f4f32f-0cd4-4995-8a6a-f9f56335848a": InsertAccountRecord,
"31f4f32f-0cd4-4995-8a6a-f9f56335848a": (InsertAccountRecord, [AccountRecords.__language_model__]),
}
@classmethod
@@ -305,7 +304,7 @@ class AccountRecordsCreateEventMethods(MethodToEvent):
)
class AccountRecordsUpdateEventMethods(MethodToEvent):
class AccountUpdateEventMethod(MethodToEvent):
event_type = "UPDATE"
event_description = ""
@@ -315,11 +314,11 @@ class AccountRecordsUpdateEventMethods(MethodToEvent):
"ec98ef2c-bcd0-432d-a8f4-1822a56c33b2": "account_records_update",
}
__event_validation__ = {
"ec98ef2c-bcd0-432d-a8f4-1822a56c33b2": UpdateAccountRecord,
"ec98ef2c-bcd0-432d-a8f4-1822a56c33b2": (UpdateAccountRecord, [AccountRecords.__language_model__]),
}
@classmethod
def build_area_update(
def account_records_update(
cls,
build_uu_id: str,
data: UpdateAccountRecordRequestModel,

View File

@@ -14,19 +14,15 @@ from Services.PostgresDb.Models.alchemy_response import DictJsonResponse
from fastapi import Request, Path, Body
@endpoint_wrapper("/account/records/address/list")
@endpoint_wrapper("/account/records/list")
async def address_list(request: "Request", data: EndpointBaseRequestModel):
"""Handle address list endpoint."""
auth_dict = address_list.auth
return {
"data": data,
"request": str(request.headers),
"request_url": str(request.url),
"request_base_url": str(request.base_url),
}
code_dict = getattr(address_list, "func_code", {"function_code": None})
return {"auth_dict": auth_dict, "code_dict": code_dict, "data": data}
@endpoint_wrapper("/account/records/address/create")
@endpoint_wrapper("/account/records/create")
async def address_create(request: "Request", data: EndpointBaseRequestModel):
"""Handle address creation endpoint."""
return {
@@ -37,7 +33,7 @@ async def address_create(request: "Request", data: EndpointBaseRequestModel):
}
@endpoint_wrapper("/account/records/address/search")
@endpoint_wrapper("/account/records/search")
async def address_search(request: "Request", data: EndpointBaseRequestModel):
"""Handle address search endpoint."""
auth_dict = address_search.auth
@@ -45,7 +41,7 @@ async def address_search(request: "Request", data: EndpointBaseRequestModel):
return {"auth_dict": auth_dict, "code_dict": code_dict, "data": data}
@endpoint_wrapper("/account/records/address/{address_uu_id}")
@endpoint_wrapper("/account/records/{address_uu_id}")
async def address_update(
request: Request,
address_uu_id: str = Path(..., description="UUID of the address to update"),

View File

@@ -0,0 +1,336 @@
"""
request models.
"""
from typing import TYPE_CHECKING, Dict, Any, List, Optional, TypedDict, Union
from pydantic import BaseModel, Field, model_validator, RootModel, ConfigDict
from ApiEvents.abstract_class import MethodToEvent
from ApiEvents.base_request_model import BaseRequestModel, DictRequestModel
from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject
from ApiValidations.Request.address import SearchAddress, UpdateAddress
from ApiValidations.Request.base_validations import ListOptions
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from Schemas.identity.identity import (
AddressPostcode,
AddressStreet,
Addresses,
RelationshipEmployee2PostCode,
)
from ApiValidations.Request import (
InsertAddress,
)
from ApiValidations.Response import (
ListAddressResponse,
)
if TYPE_CHECKING:
from fastapi import Request
class AddressListEventMethod(MethodToEvent):
event_type = "SELECT"
event_description = "List Address records"
event_category = "Address"
__event_keys__ = {
"9c251d7d-da70-4d63-a72c-e69c26270442": "address_list_super_user",
"52afe375-dd95-4f4b-aaa2-4ec61bc6de52": "address_list_employee",
}
__event_validation__ = {
"9c251d7d-da70-4d63-a72c-e69c26270442": (ListAddressResponse, [Addresses.__language_model__]),
"52afe375-dd95-4f4b-aaa2-4ec61bc6de52": (ListAddressResponse, [Addresses.__language_model__]),
}
@classmethod
def address_list_super_user(
cls,
list_options: ListOptions,
token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
):
db = RelationshipEmployee2PostCode.new_session()
post_code_list = RelationshipEmployee2PostCode.filter_all(
RelationshipEmployee2PostCode.company_id
== token_dict.selected_company.company_id,
db=db,
).data
post_code_id_list = [post_code.member_id for post_code in post_code_list]
if not post_code_id_list:
raise HTTPExceptionApi(
status_code=404,
detail="User has no post code registered. User can not list addresses.",
)
get_street_ids = [
street_id[0]
for street_id in AddressPostcode.select_only(
AddressPostcode.id.in_(post_code_id_list),
select_args=[AddressPostcode.street_id],
order_by=AddressPostcode.street_id.desc(),
db=db,
).data
]
if not get_street_ids:
raise HTTPExceptionApi(
status_code=404,
detail="User has no street registered. User can not list addresses.",
)
Addresses.pre_query = Addresses.filter_all(
Addresses.street_id.in_(get_street_ids),
db=db,
).query
Addresses.filter_attr = list_options
records = Addresses.filter_all(db=db).data
return {}
# return AlchemyJsonResponse(
# completed=True, message="List Address records", result=records
# )
@classmethod
def address_list_employee(
cls,
list_options: ListOptions,
token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
):
# Addresses.filter_attr = list_options
Addresses.pre_query = Addresses.filter_all(
Addresses.street_id.in_(get_street_ids),
)
records = Addresses.filter_all().data
return
# return AlchemyJsonResponse(
# completed=True, message="List Address records", result=records
# )
class AddressCreateEventMethod(MethodToEvent):
event_type = "CREATE"
event_description = ""
event_category = ""
__event_keys__ = {
"ffdc445f-da10-4ce4-9531-d2bdb9a198ae": "create_address",
}
__event_validation__ = {
"ffdc445f-da10-4ce4-9531-d2bdb9a198ae": (InsertAddress, [Addresses.__language_model__]),
}
@classmethod
def create_address(
cls,
data: InsertAddress,
token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
):
post_code = AddressPostcode.filter_one(
AddressPostcode.uu_id == data.post_code_uu_id,
).data
if not post_code:
raise HTTPExceptionApi(
status_code=404,
detail="Post code not found. User can not create address without post code.",
)
data_dict = data.excluded_dump()
data_dict["street_id"] = post_code.street_id
data_dict["street_uu_id"] = str(post_code.street_uu_id)
del data_dict["post_code_uu_id"]
address = Addresses.find_or_create(**data_dict)
address.save()
address.update(is_confirmed=True)
address.save()
return AlchemyJsonResponse(
completed=True,
message="Address created successfully",
result=address.get_dict(),
)
class AddressSearchEventMethod(MethodToEvent):
"""Event methods for searching addresses.
This class handles address search functionality including text search
and filtering.
"""
event_type = "SEARCH"
event_description = "Search for addresses using text and filters"
event_category = "Address"
__event_keys__ = {
"e0ac1269-e9a7-4806-9962-219ac224b0d0": "search_address",
}
__event_validation__ = {
"e0ac1269-e9a7-4806-9962-219ac224b0d0": (SearchAddress, [Addresses.__language_model__]),
}
@classmethod
def _build_order_clause(
cls, filter_list: Dict[str, Any], schemas: List[str], filter_table: Any
) -> Any:
"""Build the ORDER BY clause for the query.
Args:
filter_list: Dictionary of filter options
schemas: List of available schema fields
filter_table: SQLAlchemy table to query
Returns:
SQLAlchemy order_by clause
"""
# Default to ordering by UUID if field not in schema
if filter_list.get("order_field") not in schemas:
filter_list["order_field"] = "uu_id"
else:
# Extract table and field from order field
table_name, field_name = str(filter_list.get("order_field")).split(".")
filter_table = getattr(databases.sql_models, table_name)
filter_list["order_field"] = field_name
# Build order clause
field = getattr(filter_table, filter_list.get("order_field"))
return (
field.desc()
if str(filter_list.get("order_type"))[0] == "d"
else field.asc()
)
@classmethod
def _format_record(cls, record: Any, schemas: List[str]) -> Dict[str, str]:
"""Format a database record into a dictionary.
Args:
record: Database record to format
schemas: List of schema fields
Returns:
Formatted record dictionary
"""
result = {}
for index, schema in enumerate(schemas):
value = str(record[index])
# Special handling for UUID fields
if "uu_id" in value:
value = str(value)
result[schema] = value
return result
@classmethod
def search_address(
cls,
data: SearchAddress,
token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
) -> Any:
"""Search for addresses using text search and filters.
Args:
data: Search parameters including text and filters
token_dict: Authentication token
Returns:
JSON response with search results
Raises:
HTTPExceptionApi: If search fails
"""
try:
# Start performance measurement
start_time = perf_counter()
# Get initial query
search_result = AddressStreet.search_address_text(search_text=data.search)
if not search_result:
raise HTTPExceptionApi(
status_code=status.HTTP_404_NOT_FOUND,
detail="No addresses found matching search criteria",
)
query = search_result.get("query")
schemas = search_result.get("schema")
# Apply filters
filter_list = data.list_options.dump()
filter_table = AddressStreet
# Build and apply order clause
order = cls._build_order_clause(filter_list, schemas, filter_table)
# Apply pagination
page_size = int(filter_list.get("size"))
offset = (int(filter_list.get("page")) - 1) * page_size
# Execute query
query = (
query.order_by(order)
.limit(page_size)
.offset(offset)
.populate_existing()
)
records = list(query.all())
# Format results
results = [cls._format_record(record, schemas) for record in records]
# Log performance
duration = perf_counter() - start_time
print(f"Address search completed in {duration:.3f}s")
return AlchemyJsonResponse(
completed=True, message="Address search results", result=results
)
except HTTPExceptionApi as e:
# Re-raise HTTP exceptions
raise e
except Exception as e:
# Log and wrap other errors
print(f"Address search error: {str(e)}")
raise HTTPExceptionApi(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to search addresses",
) from e
class AddressUpdateEventMethod(MethodToEvent):
event_type = "UPDATE"
event_description = ""
event_category = ""
__event_keys__ = {
"1f9c3a9c-e5bd-4dcd-9b9a-3742d7e03a27": "update_address",
}
__event_validation__ = {
"1f9c3a9c-e5bd-4dcd-9b9a-3742d7e03a27": (UpdateAddress, [Addresses.__language_model__]),
}
@classmethod
def update_address(
cls,
address_uu_id: str,
data: UpdateAddress,
token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
):
if isinstance(token_dict, EmployeeTokenObject):
address = Addresses.filter_one(
Addresses.uu_id == address_uu_id,
).data
if not address:
raise HTTPExceptionApi(
status_code=404,
detail=f"Address not found. User can not update with given address uuid : {address_uu_id}",
)
data_dict = data.excluded_dump()
updated_address = address.update(**data_dict)
updated_address.save()
return AlchemyJsonResponse(
completed=True,
message="Address updated successfully",
result=updated_address.get_dict(),
)
elif isinstance(token_dict, OccupantTokenObject):
raise HTTPExceptionApi(
status_code=403,
detail="Occupant can not update address.",
)

View File

@@ -6,7 +6,7 @@ to be used by the dynamic route creation system.
"""
from typing import Dict, List, Any
from .account.endpoints import ACCOUNT_RECORDS_CONFIG
from .events.account.endpoints import ACCOUNT_RECORDS_CONFIG
# Registry of all route configurations
@@ -17,4 +17,4 @@ ROUTE_CONFIGS = [
def get_route_configs() -> List[Dict[str, Any]]:
"""Get all registered route configurations."""
return [ACCOUNT_RECORDS_CONFIG]
return ROUTE_CONFIGS

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

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

@@ -1,5 +1,6 @@
"""Event Service API initialization"""
"""Validation Service API initialization"""
from .route_configs import get_route_configs
__all__ = ["get_route_configs"]

View File

@@ -0,0 +1,116 @@
from typing import Dict, Any
from .models import ValidationsPydantic
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from .validation import (
ValidationsBoth,
ValidationsHeaders,
ValidationsValidations,
)
from ApiEvents.abstract_class import RouteFactoryConfig, EndpointFactoryConfig
from ApiEvents.base_request_model import EndpointBaseRequestModel
from ApiLibrary.common.line_number import get_line_number_for_error
from Services.PostgresDb.Models.alchemy_response import DictJsonResponse
from fastapi import Request, Path, Body
from middleware.token_event_middleware import TokenEventMiddleware
prefix = "/validation"
@TokenEventMiddleware.validation_required
async def validations_validations_select(request: Request, data: EndpointBaseRequestModel) -> Dict[str, Any]:
"""
Select validations.
"""
wrapped_context = getattr(validations_validations_select, "__wrapped__", None)
auth_context = getattr(wrapped_context, "auth", None)
validation_code = getattr(validations_validations_select, "validation_code", {"validation_code": None})
if not validation_code:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Validation code not found",
)
validations_pydantic = ValidationsPydantic(
class_model=validation_code.get("class", None),
reachable_event_code=validation_code.get("reachable_event_code", None),
lang=getattr(auth_context, "lang", None),
)
validations_both = ValidationsBoth.retrieve_both_validations_and_headers(validations_pydantic)
return {"status": "OK", "validation_code": validation_code, **validations_both }
@TokenEventMiddleware.validation_required
async def validations_headers_select(request: Request, data: EndpointBaseRequestModel) -> Dict[str, Any]:
"""
Select headers.
"""
ValidationsHeaders.retrieve_headers()
return {
"status": "OK",
}
@TokenEventMiddleware.validation_required
async def validations_validations_and_headers_select(request: Request, data: EndpointBaseRequestModel) -> Dict[str, Any]:
"""
Select validations and headers.
"""
ValidationsBoth.retrieve_both_validations_and_headers()
return {
"status": "OK",
}
VALIDATION_CONFIG_MAIN =RouteFactoryConfig(
name="validations",
prefix=prefix,
tags=["Validation"],
include_in_schema=True,
endpoints=[
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/select",
url_of_endpoint=f"{prefix}/validations/select",
endpoint="/select",
method="POST",
summary="Select company or occupant type",
description="Select company or occupant type",
is_auth_required=False, # Needs token_dict
is_event_required=False,
endpoint_function=validations_validations_select,
),
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/headers/select",
url_of_endpoint=f"{prefix}/headers/select",
endpoint="/headers/select",
method="POST",
summary="Select company or occupant type",
description="Select company or occupant type",
is_auth_required=False, # Needs token_dict
is_event_required=False,
endpoint_function=validations_headers_select,
),
EndpointFactoryConfig(
url_prefix=prefix,
url_endpoint="/both/select",
url_of_endpoint=f"{prefix}/validationsAndHeaders/select",
endpoint="/both/select",
method="POST",
summary="Select company or occupant type",
description="Select company or occupant type",
is_auth_required=False, # Needs token_dict
is_event_required=False,
endpoint_function=validations_validations_and_headers_select,
),
],
)
VALIDATION_CONFIG = VALIDATION_CONFIG_MAIN.as_dict()
VALIDATION_ENDPOINTS = [endpoint.url_of_endpoint for endpoint in VALIDATION_CONFIG_MAIN.endpoints]

View File

@@ -0,0 +1,29 @@
"""
Validation records request and response models.
"""
from typing import TYPE_CHECKING, Dict, Any
from pydantic import BaseModel, Field, RootModel
from ApiEvents.base_request_model import BaseRequestModel
if TYPE_CHECKING:
from ApiValidations.Request import (
ListOptions,
)
class ValidationsPydantic(BaseModel):
class_model: str
reachable_event_code: str
lang: str
class InsertValidationRecordRequestModel(BaseRequestModel):
pass
class UpdateValidationRecordRequestModel(BaseRequestModel):
pass
class ListOptionsValidationRecordRequestModel(BaseRequestModel):
pass

View File

@@ -0,0 +1,97 @@
"""
Validation request models.
"""
from typing import TYPE_CHECKING, Dict, Any, Literal, Optional, TypedDict, Union
from pydantic import BaseModel, Field, model_validator, RootModel, ConfigDict
from ApiLibrary.common.line_number import get_line_number_for_error
from ApiValidations.Custom.validation_response import ValidationModel, ValidationParser
from ApiEvents.abstract_class import MethodToEvent
from ApiEvents.base_request_model import BaseRequestModel, DictRequestModel
from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject
from ApiValidations.Request.base_validations import ListOptions
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from .models import ValidationsPydantic
if TYPE_CHECKING:
from fastapi import Request
class AllModelsImport:
@classmethod
def import_all_models(cls):
from ApiEvents.events.account.account_records import (
AccountListEventMethod,
AccountUpdateEventMethod,
AccountCreateEventMethod,
)
from ApiEvents.events.address.address import (
AddressListEventMethod,
AddressUpdateEventMethod,
AddressCreateEventMethod,
AddressSearchEventMethod,
)
return dict(
AccountListEventMethod=AccountListEventMethod,
AccountUpdateEventMethod=AccountUpdateEventMethod,
AccountCreateEventMethod=AccountCreateEventMethod,
AddressListEventMethod=AddressListEventMethod,
AddressUpdateEventMethod=AddressUpdateEventMethod,
AddressCreateEventMethod=AddressCreateEventMethod,
AddressSearchEventMethod=AddressSearchEventMethod,
)
class ValidationsBoth(MethodToEvent):
@classmethod
def retrieve_both_validations_and_headers(
cls, event: ValidationsPydantic
) -> Dict[str, Any]:
EVENT_MODELS = AllModelsImport.import_all_models()
return_single_model = EVENT_MODELS.get(event.class_model, None)
print("return_single_model", return_single_model, type(return_single_model))
# event_class_validation = getattr(return_single_model, "__event_validation__", None)
if not return_single_model:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Validation code not found",
)
response_model = return_single_model.retrieve_event_response_model(event.reachable_event_code)
language_model_all = return_single_model.retrieve_language_parameters(function_code=event.reachable_event_code, language=event.lang)
language_model = language_model_all.get("language_model", None)
language_models = language_model_all.get("language_models", None)
validation = ValidationModel(response_model, language_model, language_models)
return {
"headers": validation.headers,
"validation": validation.validation,
"language_models": language_model_all,
}
class ValidationsValidations(MethodToEvent):
@classmethod
def retrieve_validations(
cls, event: ValidationsPydantic
) -> Dict[str, Any]:
return {}
class ValidationsHeaders(MethodToEvent):
@classmethod
def retrieve_headers(
cls, event: ValidationsPydantic
) -> Dict[str, Any]:
return {}

View File

@@ -0,0 +1,33 @@
from typing import Callable
class ValidationActions:
def __init__(self, function_code: str, func: Callable = None):
self.function_code = function_code
self.func = func
@classmethod
def retrieve_validation(cls):
"""
Retrieve validation [] by validation via [Response Model of Table]
"""
return
@classmethod
def retrieve_headers(cls):
"""
Retrieve headers for validations [] by event function code [Response Model of Table]
"""
return
@classmethod
def retrieve_validations_and_headers(cls):
"""
Retrieve validations and headers [] via event function code [Response Model of Table][]
"""
return
# Singleton class
validation_action = ValidationActions(function_code="")

View File

@@ -5,13 +5,13 @@ This module collects and registers all route configurations from different modul
to be used by the dynamic route creation system.
"""
from typing import Dict, List, Any
from typing import Dict, List, Any, TypeVar
from .events.validation.endpoints import VALIDATION_CONFIG
# Registry of all route configurations
ROUTE_CONFIGS = []
ROUTE_CONFIGS = [VALIDATION_CONFIG]
def get_route_configs() -> List[Dict[str, Any]]:
"""Get all registered route configurations."""
return []
return ROUTE_CONFIGS

View File

@@ -24,6 +24,10 @@ from fastapi import Request, Depends, APIRouter
from functools import wraps
import inspect
from ApiLibrary.common.line_number import get_line_number_for_error
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from Schemas.rules.rules import EndpointRestriction
ResponseModel = TypeVar("ResponseModel", bound=BaseModel)
@@ -239,14 +243,91 @@ class MethodToEvent:
event_description: ClassVar[str] = ""
event_category: ClassVar[str] = ""
__event_keys__: ClassVar[Dict[str, str]] = {}
__event_validation__: ClassVar[Dict[str, Tuple[Type[ResponseModel], List[Any]]]] = (
{}
)
__event_validation__: Dict[str, Tuple[Type, Union[List, tuple]]] = {}
@classmethod
def retrieve_event_response_model(cls, function_code: str) -> Tuple:
"""Retrieve event validation for a specific function.
Args:
function_code: Function identifier
Returns:
Tuple containing response model and language models
"""
event_validation_list = cls.__event_validation__.get(function_code, None)
if not event_validation_list:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Function not found",
)
return event_validation_list[0]
@classmethod
def retrieve_event_languages(cls, function_code: str) -> Union[List, tuple]:
"""Retrieve event description for a specific function.
Args:
function_code: Function identifier
Returns:
Event description
"""
event_keys_list = cls.__event_validation__.get(function_code, None)
print('event_keys_list', event_keys_list)
if not event_keys_list:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Function not found",
)
function_language_models: Union[List, tuple] = event_keys_list[1]
if not function_language_models:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Function not found",
)
return function_language_models
@staticmethod
def merge_models(language_model: List) -> Tuple:
merged_models = {"tr": {}, "en": {}}
for model in language_model:
for lang in dict(model).keys():
if lang not in merged_models:
merged_models[lang] = model[lang]
else:
merged_models[lang].update(model[lang])
return merged_models
@classmethod
def retrieve_language_parameters(
cls, language: str, function_code: str
) -> Dict[str, str]:
def retrieve_event_function(cls, function_code: str) -> Dict[str, str]:
"""Retrieve event parameters for a specific function.
Args:
function_code: Function identifier
Returns:
Dictionary of event parameters
"""
function_event = cls.__event_keys__[function_code]
function_itself = getattr(cls, function_event, None)
if not function_itself:
raise HTTPExceptionApi(
error_code="",
lang="en",
loc=get_line_number_for_error(),
sys_msg="Function not found",
)
return function_itself
@classmethod
def retrieve_language_parameters(cls, function_code: str, language: str = "tr") -> Dict[str, str]:
"""Retrieve language-specific parameters for an event.
Args:
@@ -256,20 +337,24 @@ class MethodToEvent:
Returns:
Dictionary of language-specific field mappings
"""
validation_dict = dict(cls.__event_validation__)
if function_code not in validation_dict:
return {}
event_response_model, event_language_models = validation_dict[function_code]
# Collect language-specific field mappings
language_models = {}
for model in event_language_models:
language_models.update(model.get(language, model.get("tr", {})))
event_language_models = cls.retrieve_event_languages(function_code)
event_response_model = cls.retrieve_event_response_model(function_code)
event_response_model_merged = cls.merge_models(event_language_models)
event_response_model_merged_lang = event_response_model_merged[language]
# Map response model fields to language-specific values
return {
field: language_models[field]
print('event_response_model', dict(
event_response_model=event_response_model,
event_response_model_merged_lang=event_response_model_merged_lang,
event_response_model_merged=event_response_model_merged,
language=language,
function_code=function_code,
))
only_language_dict = {
field: event_response_model_merged_lang[field]
for field in event_response_model.model_fields
if field in language_models
if field in event_response_model_merged_lang
}
return {
"language_model": only_language_dict,
"language_models": event_response_model_merged,
}