auth service up running

This commit is contained in:
berkay 2025-01-10 14:17:22 +03:00
parent 03accfed1b
commit 79aa3a1bc5
41 changed files with 480 additions and 340 deletions

View File

@ -1,3 +1,3 @@
from .core_response import AlchemyJsonResponse
__all__ = ["AlchemyJsonResponse"]
__all__ = ["AlchemyJsonResponse"]

View File

@ -22,7 +22,7 @@ def save_access_token_to_redis(
Employees,
Staff,
Addresses,
OccupantTypes
OccupantTypes,
)
if not found_user:
@ -32,7 +32,9 @@ def save_access_token_to_redis(
)
# Check user is already logged in or has a previous session
already_tokens = AccessObjectActions.get_object_via_user_uu_id(user_id=found_user.uu_id)
already_tokens = AccessObjectActions.get_object_via_user_uu_id(
user_id=found_user.uu_id
)
for key, token_user in already_tokens.items():
if token_user.get("domain", "") == domain:
redis_cli.delete(key)

View File

@ -9,10 +9,10 @@ class AccessObjectActions:
@classmethod
def save_object_to_redis(
cls,
access_token,
model_object: typing.Union[OccupantTokenObject, EmployeeTokenObject],
expiry_minutes: int = Auth.TOKEN_EXPIRE_MINUTES_30.total_seconds() // 60
cls,
access_token,
model_object: typing.Union[OccupantTokenObject, EmployeeTokenObject],
expiry_minutes: int = Auth.TOKEN_EXPIRE_MINUTES_30.total_seconds() // 60,
) -> bool:
"""Save access token object to Redis with expiry
Args:
@ -28,17 +28,14 @@ class AccessObjectActions:
RedisActions.save_object_to_redis(
access_token=access_token,
model_object=model_object,
expiry_minutes=expiry_minutes
expiry_minutes=expiry_minutes,
)
return True
except Exception as e:
print("Save Object to Redis Error: ", e)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=dict(
message="Failed to save token to Redis",
error=str(e)
),
detail=dict(message="Failed to save token to Redis", error=str(e)),
)
@classmethod
@ -64,23 +61,21 @@ class AccessObjectActions:
if not hasattr(request, "headers"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=dict(
message="Headers not found in request"
)
detail=dict(message="Headers not found in request"),
)
access_token = request.headers.get(Auth.ACCESS_TOKEN_TAG)
if not access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=dict(
message="Unauthorized user, please login"
),
detail=dict(message="Unauthorized user, please login"),
)
return access_token
@classmethod
def get_token_object(cls, request) -> typing.Union[OccupantTokenObject, EmployeeTokenObject]:
def get_token_object(
cls, request
) -> typing.Union[OccupantTokenObject, EmployeeTokenObject]:
"""Get and validate token object from request
Args:
request: The request object
@ -93,18 +88,16 @@ class AccessObjectActions:
return RedisActions.get_object_via_access_key(request)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=dict(
message=str(e)
)
status_code=status.HTTP_401_UNAUTHORIZED, detail=dict(message=str(e))
)
@classmethod
def get_object_via_access_key(
cls, request,
cls,
request,
) -> typing.Union[EmployeeTokenObject, OccupantTokenObject, None]:
from api_configs import Auth
access_object = RedisActions.get_with_regex(
value_regex=str(request.headers.get(Auth.ACCESS_TOKEN_TAG) + ":*")
).data
@ -120,7 +113,7 @@ class AccessObjectActions:
status_code=401,
detail=dict(
message="User type is not found in the token object. Please reach to your administrator."
)
),
)

View File

@ -36,7 +36,7 @@ class Config:
# Timezone Configuration
DEFAULT_TIMEZONE = "GMT+3" # Default timezone for the application
SYSTEM_TIMEZONE = "GMT+0" # System timezone (used for internal operations)
SYSTEM_TIMEZONE = "GMT+0" # System timezone (used for internal operations)
SUPPORTED_TIMEZONES = ["GMT+0", "GMT+3"] # List of supported timezones

View File

@ -6,16 +6,18 @@ from typing import TypeVar, Union, Dict, Any, Optional, Type
from api_objects.auth.token_objects import EmployeeTokenObject, OccupantTokenObject
TokenType = TypeVar('TokenType', bound=Union[EmployeeTokenObject, OccupantTokenObject])
TokenType = TypeVar("TokenType", bound=Union[EmployeeTokenObject, OccupantTokenObject])
class ActionsSchema(ABC):
"""Base class for defining API action schemas.
This class handles endpoint registration and validation in the database.
"""
def __init__(self, endpoint: str):
"""Initialize with an API endpoint path.
Args:
endpoint: The API endpoint path (e.g. "/users/create")
"""
@ -23,10 +25,10 @@ class ActionsSchema(ABC):
def retrieve_action_from_endpoint(self) -> Dict[str, Any]:
"""Retrieve the endpoint registration from the database.
Returns:
Dict containing the endpoint registration data
Raises:
HTTPException: If endpoint is not found in database
"""
@ -38,21 +40,22 @@ class ActionsSchema(ABC):
"endpoint_desc": "Temporary endpoint",
"endpoint_code": "dummy_code",
"id": 1,
"uu_id": "dummy_uuid"
"uu_id": "dummy_uuid",
}
class ActionsSchemaFactory:
"""Factory class for creating action schemas.
This class validates and initializes action schemas for API endpoints.
"""
def __init__(self, action: ActionsSchema):
"""Initialize with an action schema.
Args:
action: The action schema to initialize
Raises:
HTTPException: If action initialization fails
"""
@ -67,17 +70,18 @@ class ActionsSchemaFactory:
print(f"ActionsSchemaFactory Error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to initialize action schema"
detail="Failed to initialize action schema",
) from e
class MethodToEvent(ABC, ActionsSchemaFactory):
"""Base class for mapping methods to API events.
This class handles method registration and validation for API events.
"""
action_key: Optional[str] = None
event_type: Optional[str] = None
event_type: Optional[str] = None
event_description: str = ""
event_category: str = ""
@ -87,42 +91,40 @@ class MethodToEvent(ABC, ActionsSchemaFactory):
@classmethod
def call_event_method(cls, method_uu_id: str, *args: Any, **kwargs: Any) -> Any:
"""Call an event method by its UUID.
Args:
method_uu_id: UUID of the method to call
*args: Positional arguments to pass to method
**kwargs: Keyword arguments to pass to method
Returns:
The result of the called method
Raises:
AttributeError: If method UUID is not found
"""
function_name = cls.__event_keys__.get(method_uu_id)
if not function_name:
raise AttributeError(f"No method found for UUID: {method_uu_id}")
return getattr(cls, function_name)(*args, **kwargs)
@classmethod
def ban_token_objects(
cls,
token: TokenType,
ban_list: Type[TokenType]
) -> None:
def ban_token_objects(cls, token: TokenType, ban_list: Type[TokenType]) -> None:
"""Check if a token type is banned from accessing an event.
Args:
token: The token to check
ban_list: The token type that is banned
Raises:
HTTPException: If token type matches banned type
"""
if isinstance(token, ban_list):
user_type = "employee" if isinstance(token, EmployeeTokenObject) else "occupant"
user_type = (
"employee" if isinstance(token, EmployeeTokenObject) else "occupant"
)
raise HTTPException(
status_code=status.HTTP_406_NOT_ACCEPTABLE,
detail=f"No {user_type} can reach this event. A notification has been sent to admin."
detail=f"No {user_type} can reach this event. A notification has been sent to admin.",
)

View File

@ -54,7 +54,7 @@ class AccountRecordsListEventMethods(MethodToEvent):
result=records,
cls_object=AccountRecords,
filter_attributes=list_options,
response_model=AccountRecordResponse
response_model=AccountRecordResponse,
)
@classmethod
@ -224,7 +224,9 @@ class AccountRecordsCreateEventMethods(MethodToEvent):
)
account_record = AccountRecords.find_or_create(**data.excluded_dump())
return AlchemyJsonResponse(
completed=True, message="Account record created successfully", result=account_record
completed=True,
message="Account record created successfully",
result=account_record,
)
elif isinstance(token_dict, EmployeeTokenObject):
# Build.pre_query = Build.select_action(
@ -266,7 +268,9 @@ class AccountRecordsCreateEventMethods(MethodToEvent):
account_record = AccountRecords.insert_one(data_dict).data
return AlchemyJsonResponse(
completed=True, message="Account record created successfully", result=account_record
completed=True,
message="Account record created successfully",
result=account_record,
)
@ -297,7 +301,9 @@ class AccountRecordsUpdateEventMethods(MethodToEvent):
account_record = AccountRecords.update_one(build_uu_id, data).data
return AlchemyJsonResponse(
completed=True, message="Account record updated successfully", result=account_record
completed=True,
message="Account record updated successfully",
result=account_record,
)
@ -323,7 +329,9 @@ class AccountRecordsPatchEventMethods(MethodToEvent):
):
account_record = AccountRecords.patch_one(build_uu_id, data).data
return AlchemyJsonResponse(
completed=True, message="Account record patched successfully", result=account_record
completed=True,
message="Account record patched successfully",
result=account_record,
)

View File

@ -138,16 +138,19 @@ class AddressCreateEventMethods(MethodToEvent):
address.update(is_confirmed=True)
address.save()
return AlchemyJsonResponse(
completed=True, message="Address created successfully", result=address.get_dict()
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"
@ -161,18 +164,15 @@ class AddressSearchEventMethods(MethodToEvent):
@classmethod
def _build_order_clause(
cls,
filter_list: Dict[str, Any],
schemas: List[str],
filter_table: Any
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
"""
@ -187,16 +187,20 @@ class AddressSearchEventMethods(MethodToEvent):
# 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()
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
"""
@ -216,14 +220,14 @@ class AddressSearchEventMethods(MethodToEvent):
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:
HTTPException: If search fails
"""
@ -236,23 +240,23 @@ class AddressSearchEventMethods(MethodToEvent):
if not search_result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No addresses found matching search criteria"
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)
@ -270,9 +274,7 @@ class AddressSearchEventMethods(MethodToEvent):
print(f"Address search completed in {duration:.3f}s")
return AlchemyJsonResponse(
completed=True,
message="Address search results",
result=results
completed=True, message="Address search results", result=results
)
except HTTPException as e:
@ -283,7 +285,7 @@ class AddressSearchEventMethods(MethodToEvent):
print(f"Address search error: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to search addresses"
detail="Failed to search addresses",
) from e
@ -321,7 +323,9 @@ class AddressUpdateEventMethods(MethodToEvent):
updated_address = address.update(**data_dict)
updated_address.save()
return AlchemyJsonResponse(
completed=True, message="Address updated successfully", result=updated_address.get_dict()
completed=True,
message="Address updated successfully",
result=updated_address.get_dict(),
)
elif isinstance(token_dict, OccupantTokenObject):
raise HTTPException(
@ -368,7 +372,9 @@ class AddressPatchEventMethods(MethodToEvent):
patched_address = address.patch(**data_dict)
return AlchemyJsonResponse(
completed=True, message="Address patched successfully", result=patched_address.get_dict()
completed=True,
message="Address patched successfully",
result=patched_address.get_dict(),
)
@ -416,7 +422,9 @@ class AddressPostCodeCreateEventMethods(MethodToEvent):
relation_table.update(is_confirmed=True)
relation_table.save()
return AlchemyJsonResponse(
completed=True, message="Post code created successfully", result=post_code.get_dict()
completed=True,
message="Post code created successfully",
result=post_code.get_dict(),
)
@ -457,7 +465,9 @@ class AddressPostCodeUpdateEventMethods(MethodToEvent):
updated_post_code = post_code.update(**data_dict)
updated_post_code.save()
return AlchemyJsonResponse(
completed=True, message="Post code updated successfully", result=updated_post_code.get_dict()
completed=True,
message="Post code updated successfully",
result=updated_post_code.get_dict(),
)
elif isinstance(token_dict, OccupantTokenObject):
raise HTTPException(

View File

@ -13,10 +13,21 @@ from api_configs import Auth, ApiStatic
from api_events.events.abstract_class import MethodToEvent, ActionsSchema
from databases import (
Companies, Staff, Duties, Departments, Employees,
BuildLivingSpace, BuildParts, Build, Duty, Event2Occupant,
Event2Employee, Users, UsersTokens, OccupantTypes,
RelationshipEmployee2Build
Companies,
Staff,
Duties,
Departments,
Employees,
BuildLivingSpace,
BuildParts,
Build,
Duty,
Event2Occupant,
Event2Employee,
Users,
UsersTokens,
OccupantTypes,
RelationshipEmployee2Build,
)
from api_services import (
@ -28,17 +39,23 @@ from api_services import (
)
from api_validations.validations_request import (
Login, Logout, ChangePassword, Remember,
Forgot, CreatePassword, OccupantSelection,
Login,
Logout,
ChangePassword,
Remember,
Forgot,
CreatePassword,
OccupantSelection,
EmployeeSelection,
)
from api_validations.validations_response import (
AuthenticationLoginResponse,
AuthenticationRefreshResponse,
AuthenticationUserInfoResponse
AuthenticationUserInfoResponse,
)
class AuthenticationLoginEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Login via domain and access key : [email] | [phone]"
@ -56,19 +73,22 @@ class AuthenticationLoginEventMethods(MethodToEvent):
try:
access_dict = Users.login_user_with_credentials(data=data, request=request)
found_user = access_dict.get("user")
if not found_user:
user_logger.log_login_attempt(
request, None, data.domain, data.access_key,
success=False, error="Invalid credentials"
request,
None,
data.domain,
data.access_key,
success=False,
error="Invalid credentials",
)
return ResponseHandler.unauthorized("Invalid credentials")
user_logger.log_login_attempt(
request, found_user.id, data.domain, data.access_key,
success=True
request, found_user.id, data.domain, data.access_key, success=True
)
response_data = {
"access_token": access_dict.get("access_token"),
"refresh_token": access_dict.get("refresher_token"),
@ -78,17 +98,13 @@ class AuthenticationLoginEventMethods(MethodToEvent):
return ResponseHandler.success(
message="User logged in successfully",
data=response_data,
response_model=AuthenticationLoginResponse
response_model=AuthenticationLoginResponse,
)
except Exception as e:
user_logger.log_login_attempt(
request, None, data.domain, data.access_key,
success=False, error=str(e)
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e)
request, None, data.domain, data.access_key, success=False, error=str(e)
)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
class AuthenticationSelectEventMethods(MethodToEvent):
@ -105,14 +121,13 @@ class AuthenticationSelectEventMethods(MethodToEvent):
@classmethod
def _handle_employee_selection(
cls,
data: EmployeeSelection,
token_dict: EmployeeTokenObject,
request: Request
cls, data: EmployeeSelection, token_dict: EmployeeTokenObject, request: Request
):
"""Handle employee company selection"""
if data.company_uu_id not in token_dict.companies_uu_id_list:
return ResponseHandler.unauthorized("Company not found in user's company list")
return ResponseHandler.unauthorized(
"Company not found in user's company list"
)
selected_company = Companies.filter_one(
Companies.uu_id == data.company_uu_id
@ -122,29 +137,27 @@ class AuthenticationSelectEventMethods(MethodToEvent):
# Get department IDs for the company
department_ids = [
dept.id for dept in Departments.filter_all(
dept.id
for dept in Departments.filter_all(
Departments.company_id == selected_company.id
).data
]
# Get duties IDs for the company
duties_ids = [
duty.id for duty in Duties.filter_all(
Duties.company_id == selected_company.id
).data
duty.id
for duty in Duties.filter_all(Duties.company_id == selected_company.id).data
]
# Get staff IDs
staff_ids = [
staff.id for staff in Staff.filter_all(
Staff.duties_id.in_(duties_ids)
).data
staff.id for staff in Staff.filter_all(Staff.duties_id.in_(duties_ids)).data
]
# Get employee
employee = Employees.filter_one(
Employees.people_id == token_dict.person_id,
Employees.staff_id.in_(staff_ids)
Employees.staff_id.in_(staff_ids),
).data
if not employee:
@ -158,16 +171,14 @@ class AuthenticationSelectEventMethods(MethodToEvent):
# Get staff and duties
staff = Staff.filter_one(Staff.id == employee.staff_id).data
duties = Duties.filter_one(Duties.id == staff.duties_id).data
department = Departments.filter_one(
Departments.id == duties.department_id
).data
department = Departments.filter_one(Departments.id == duties.department_id).data
# Get bulk duty
bulk_id = Duty.filter_by_one(system=True, duty_code="BULK").data
bulk_duty_id = Duties.filter_by_one(
company_id=selected_company.id,
duties_id=bulk_id.id,
**Duties.valid_record_dict
**Duties.valid_record_dict,
).data
# Create company token
@ -183,7 +194,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_list_id=reachable_event_list_id,
)
# Update Redis
@ -192,24 +203,19 @@ class AuthenticationSelectEventMethods(MethodToEvent):
@classmethod
def _handle_occupant_selection(
cls,
data: OccupantSelection,
token_dict: OccupantTokenObject,
request: Request
cls, data: OccupantSelection, token_dict: OccupantTokenObject, request: Request
):
"""Handle occupant type selection"""
# Get occupant type
occupant_type = OccupantTypes.filter_by_one(
system=True,
uu_id=data.occupant_uu_id
system=True, uu_id=data.occupant_uu_id
).data
if not occupant_type:
return ResponseHandler.not_found("Occupant Type not found")
# Get build part
build_part = BuildParts.filter_by_one(
system=True,
uu_id=data.build_part_uu_id
system=True, uu_id=data.build_part_uu_id
).data
if not build_part:
return ResponseHandler.not_found("Build Part not found")
@ -230,7 +236,7 @@ class AuthenticationSelectEventMethods(MethodToEvent):
selected_occupant_type = BuildLivingSpace.filter_one(
BuildLivingSpace.occupant_type == occupant_type.id,
BuildLivingSpace.person_id == token_dict.person_id,
BuildLivingSpace.build_parts_id == build_part.id
BuildLivingSpace.build_parts_id == build_part.id,
).data
if not selected_occupant_type:
return ResponseHandler.not_found("Selected occupant type not found")
@ -255,7 +261,7 @@ class AuthenticationSelectEventMethods(MethodToEvent):
responsible_employee_uuid=responsible_employee.uu_id.__str__(),
responsible_company_id=company_related.id,
responsible_company_uuid=company_related.uu_id.__str__(),
reachable_event_list_id=reachable_event_list_id
reachable_event_list_id=reachable_event_list_id,
)
# Update Redis
@ -276,13 +282,11 @@ class AuthenticationSelectEventMethods(MethodToEvent):
elif isinstance(token_dict, OccupantTokenObject):
return cls._handle_occupant_selection(data, token_dict, request)
return ResponseHandler.error(
"Invalid token type",
status_code=status.HTTP_400_BAD_REQUEST
"Invalid token type", status_code=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
return ResponseHandler.error(
str(e),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@ -331,9 +335,7 @@ class AuthenticationRefreshEventMethods(MethodToEvent):
return ResponseHandler.unauthorized()
# Get user and token info
found_user = Users.filter_one(
Users.uu_id == token_dict.user_uu_id
).data
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")
@ -349,12 +351,12 @@ class AuthenticationRefreshEventMethods(MethodToEvent):
response_data = {
"access_token": access_token,
"refresh_token": getattr(user_token, "token", None),
"user": found_user.get_dict()
"user": found_user.get_dict(),
}
return ResponseHandler.success(
"User info refreshed successfully",
data=response_data,
response_model=AuthenticationRefreshResponse
response_model=AuthenticationRefreshResponse,
)
except Exception as e:
return ResponseHandler.error(str(e))
@ -381,7 +383,9 @@ class AuthenticationChangePasswordEventMethods(MethodToEvent):
):
try:
if not isinstance(token_dict, EmployeeTokenObject):
return ResponseHandler.unauthorized("Only employees can change password")
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:
@ -389,22 +393,27 @@ class AuthenticationChangePasswordEventMethods(MethodToEvent):
if not found_user.check_password(data.old_password):
user_logger.log_password_change(
request, found_user.id, "change",
success=False, error="Invalid old password"
request,
found_user.id,
"change",
success=False,
error="Invalid old password",
)
return ResponseHandler.unauthorized("Old password is incorrect")
found_user.set_password(data.new_password)
user_logger.log_password_change(
request, found_user.id, "change",
success=True
request, found_user.id, "change", success=True
)
return ResponseHandler.success("Password changed successfully")
except Exception as e:
user_logger.log_password_change(
request, found_user.id if found_user else None,
"change", success=False, error=str(e)
request,
found_user.id if found_user else None,
"change",
success=False,
error=str(e),
)
return ResponseHandler.error(str(e))
@ -471,7 +480,9 @@ class AuthenticationDisconnectUserEventMethods(MethodToEvent):
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)):
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(key)
selected_user = Users.filter_one(
@ -566,7 +577,7 @@ class AuthenticationRefreshTokenEventMethods(MethodToEvent):
return ResponseHandler.success(
"User is logged in successfully via refresher token",
data=response_data,
response_model=AuthenticationRefreshResponse
response_model=AuthenticationRefreshResponse,
)
return ResponseHandler.not_found("Invalid data")
@ -697,7 +708,7 @@ class AuthenticationDownloadAvatarEventMethods(MethodToEvent):
return ResponseHandler.success(
"Avatar and profile is shared via user credentials",
data=user_info,
response_model=AuthenticationUserInfoResponse
response_model=AuthenticationUserInfoResponse,
)
return ResponseHandler.not_found("Invalid data")

View File

@ -59,7 +59,7 @@ class BuildListEventMethods(MethodToEvent):
result=records,
cls_object=Build,
filter_attributes=list_options,
response_model=BuildResponse
response_model=BuildResponse,
)
@ -213,7 +213,7 @@ class BuildUpdateEventMethods(MethodToEvent):
completed=False,
message="Building not found",
result={},
status_code="HTTP_404_NOT_FOUND"
status_code="HTTP_404_NOT_FOUND",
)
build.update(**data.excluded_dump())
@ -256,7 +256,7 @@ class BuildPatchEventMethods(MethodToEvent):
completed=False,
message="Building not found",
result={},
status_code="HTTP_404_NOT_FOUND"
status_code="HTTP_404_NOT_FOUND",
)
build.update(**data.excluded_dump())

View File

@ -51,12 +51,12 @@ class BuildAreaListEventMethods(MethodToEvent):
BuildArea.filter_attr = list_options
records = BuildArea.filter_all()
return AlchemyJsonResponse(
completed=True,
message="Building areas listed successfully",
completed=True,
message="Building areas listed successfully",
result=records,
cls_object=BuildArea,
filter_attributes=list_options,
response_model=BuildResponse
response_model=BuildResponse,
)
@ -112,9 +112,7 @@ class BuildAreaCreateEventMethods(MethodToEvent):
data_dict["build_uu_id"] = str(selected_build.uu_id)
area = BuildArea.insert_one(data_dict).data
return AlchemyJsonResponse(
completed=True,
message="Building area created successfully",
result=area
completed=True, message="Building area created successfully", result=area
)
@ -137,9 +135,7 @@ class BuildAreaUpdateEventMethods(MethodToEvent):
):
area = BuildArea.update_one(build_uu_id, data).data
return AlchemyJsonResponse(
completed=True,
message="Building area updated successfully",
result=area
completed=True, message="Building area updated successfully", result=area
)
@ -162,9 +158,7 @@ class BuildAreaPatchEventMethods(MethodToEvent):
):
area = BuildArea.patch_one(build_uu_id, data).data
return AlchemyJsonResponse(
completed=True,
message="Building area patched successfully",
result=area
completed=True, message="Building area patched successfully", result=area
)

View File

@ -14,6 +14,7 @@ from databases import (
BuildParts,
)
class BuildingBuildPartsListEventMethods(MethodToEvent):
event_type = "SELECT"

View File

@ -59,12 +59,12 @@ class BuildSitesListEventMethods(MethodToEvent):
BuildSites.filter_attr = list_options
records = BuildSites.filter_all()
return AlchemyJsonResponse(
completed=True,
message="Building sites listed successfully",
completed=True,
message="Building sites listed successfully",
result=records,
cls_object=BuildSites,
filter_attributes=list_options,
response_model=BuildSitesResponse
response_model=BuildSitesResponse,
)
@ -110,9 +110,7 @@ class BuildSitesCreateEventMethods(MethodToEvent):
data_dict = data.excluded_dump()
site = BuildSites.insert_one(data_dict).data
return AlchemyJsonResponse(
completed=True,
message="Building site created successfully",
result=site
completed=True, message="Building site created successfully", result=site
)
@ -135,9 +133,7 @@ class BuildSitesUpdateEventMethods(MethodToEvent):
):
site = BuildSites.update_one(build_uu_id, data).data
return AlchemyJsonResponse(
completed=True,
message="Building site updated successfully",
result=site
completed=True, message="Building site updated successfully", result=site
)
@ -160,9 +156,7 @@ class BuildSitesPatchEventMethods(MethodToEvent):
):
site = BuildSites.patch_one(build_uu_id, data).data
return AlchemyJsonResponse(
completed=True,
message="Building site patched successfully",
result=site
completed=True, message="Building site patched successfully", result=site
)

View File

@ -16,9 +16,7 @@ class BuildTypesListEventMethods(MethodToEvent):
__event_keys__ = {
"5344d03c-fc47-43ec-8c44-6c2acd7e5d9f": "build_types_list",
}
__event_validation__ = {
"5344d03c-fc47-43ec-8c44-6c2acd7e5d9f": BuildTypesResponse
}
__event_validation__ = {"5344d03c-fc47-43ec-8c44-6c2acd7e5d9f": BuildTypesResponse}
@classmethod
def build_types_list(
@ -37,7 +35,7 @@ class BuildTypesListEventMethods(MethodToEvent):
result=results,
cls_object=BuildTypes,
filter_attributes=list_options,
response_model=BuildTypesListResponse
response_model=BuildTypesListResponse,
)
elif isinstance(token_dict, OccupantTokenObject):
return AlchemyJsonResponse(
@ -46,7 +44,7 @@ class BuildTypesListEventMethods(MethodToEvent):
result=None,
cls_object=BuildTypes,
filter_attributes=list_options,
response_model=BuildTypesResponse
response_model=BuildTypesResponse,
)

View File

@ -73,7 +73,7 @@ class BuildingLivingSpacesListEventMethods(MethodToEvent):
return AlchemyJsonResponse(
completed=True,
message="Living spaces listed successfully",
result=records
result=records,
)
elif isinstance(token_dict, EmployeeTokenObject):
build_id_list_query = Build.select_action(
@ -217,7 +217,7 @@ class BuildingLivingSpacesCreateEventMethods(MethodToEvent):
return AlchemyJsonResponse(
completed=True,
message="Living space created successfully",
result=created_living_space
result=created_living_space,
)
@ -307,7 +307,7 @@ class BuildingLivingSpacesUpdateEventMethods(MethodToEvent):
return AlchemyJsonResponse(
completed=True,
message="Living space updated successfully",
result=living_space
result=living_space,
)

View File

@ -108,7 +108,7 @@ class CompanyUpdateEventMethods(MethodToEvent):
completed=False,
message="Company not found",
result={},
status_code="HTTP_404_NOT_FOUND"
status_code="HTTP_404_NOT_FOUND",
)
company.save()
return AlchemyJsonResponse(
@ -142,7 +142,7 @@ class CompanyPatchEventMethods(MethodToEvent):
completed=False,
message="Company not found",
result={},
status_code="HTTP_404_NOT_FOUND"
status_code="HTTP_404_NOT_FOUND",
)
company.save()
return AlchemyJsonResponse(

View File

@ -4,17 +4,16 @@ from fastapi import status
from fastapi.responses import JSONResponse
from api_validations.validations_response import PeopleListResponse
from api_validations.validations_request import InsertPerson, UpdateUsers
from api_events.events.abstract_class import MethodToEvent, ActionsSchema
from api_objects.auth.token_objects import EmployeeTokenObject, OccupantTokenObject
from ApiServices.api_handlers import AlchemyJsonResponse
from databases import (
People,
Users,
Companies,
)
from api_validations.validations_request import InsertPerson, UpdateUsers
from api_events.events.abstract_class import MethodToEvent, ActionsSchema
from api_objects.auth.token_objects import EmployeeTokenObject, OccupantTokenObject
from ApiServices.api_handlers import AlchemyJsonResponse
class PeopleListEventMethods(MethodToEvent):

View File

@ -8,8 +8,10 @@ class DateTimeLocal:
def __init__(self, timezone: str = None, is_client: bool = True):
if timezone and timezone not in Config.SUPPORTED_TIMEZONES:
raise ValueError(f"Unsupported timezone: {timezone}. Must be one of {Config.SUPPORTED_TIMEZONES}")
raise ValueError(
f"Unsupported timezone: {timezone}. Must be one of {Config.SUPPORTED_TIMEZONES}"
)
self.timezone = Config.SYSTEM_TIMEZONE
if is_client:
self.timezone = (timezone or Config.DEFAULT_TIMEZONE).replace("-", "+")
@ -84,11 +86,11 @@ class DateTimeLocal:
components = [str(base_key)]
components.extend(str(arg) for arg in args)
components.append(f"tz_{self.timezone}")
return ':'.join(components)
return ":".join(components)
def format_for_db(self, date):
"""Format date for database storage"""
return self.get(date).format('YYYY-MM-DD HH:mm:ss.SSSZZ')
return self.get(date).format("YYYY-MM-DD HH:mm:ss.SSSZZ")
def parse_from_db(self, date_str):
"""Parse date from database format"""
@ -99,16 +101,17 @@ class DateTimeLocal:
def get_day_boundaries(self, date=None):
"""Get start and end of day in current timezone"""
dt = self.get(date) if date else self.now()
start = dt.floor('day')
end = dt.ceil('day')
start = dt.floor("day")
end = dt.ceil("day")
return start, end
def get_month_boundaries(self, date=None):
"""Get start and end of month in current timezone"""
dt = self.get(date) if date else self.now()
start = dt.floor('month')
end = dt.ceil('month')
start = dt.floor("month")
end = dt.ceil("month")
return start, end
client_arrow = DateTimeLocal(is_client=True)
system_arrow = DateTimeLocal(is_client=False)

View File

@ -4,18 +4,19 @@ from typing import Optional, Dict, Any
from fastapi.requests import Request
from api_library.date_time_actions.date_functions import system_arrow
class UserActivityLogger:
def __init__(self):
self.logger = logging.getLogger("user_activity")
self.logger.setLevel(logging.INFO)
# Add handlers if not already added
if not self.logger.handlers:
log_path = "/service_app/logs/user_activity.log"
os.makedirs(os.path.dirname(log_path), exist_ok=True)
handler = logging.FileHandler(log_path)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
self.logger.addHandler(handler)
@ -24,9 +25,10 @@ class UserActivityLogger:
"""Extract common metadata from request"""
return {
"agent": request.headers.get("User-Agent"),
"ip": getattr(request, "remote_addr", None) or request.headers.get("X-Forwarded-For"),
"ip": getattr(request, "remote_addr", None)
or request.headers.get("X-Forwarded-For"),
"platform": request.headers.get("Origin"),
"timestamp": str(system_arrow.now())
"timestamp": str(system_arrow.now()),
}
def log_login_attempt(
@ -36,7 +38,7 @@ class UserActivityLogger:
domain: str,
access_key: str,
success: bool,
error: Optional[str] = None
error: Optional[str] = None,
):
"""Log login attempts"""
metadata = self._get_request_metadata(request)
@ -47,9 +49,9 @@ class UserActivityLogger:
"access_key": access_key,
"success": success,
"error": error,
**metadata
**metadata,
}
if success:
self.logger.info("Login successful", extra=log_data)
else:
@ -61,7 +63,7 @@ class UserActivityLogger:
user_id: int,
change_type: str,
success: bool,
error: Optional[str] = None
error: Optional[str] = None,
):
"""Log password changes"""
metadata = self._get_request_metadata(request)
@ -71,9 +73,9 @@ class UserActivityLogger:
"change_type": change_type,
"success": success,
"error": error,
**metadata
**metadata,
}
if success:
self.logger.info("Password change successful", extra=log_data)
else:
@ -86,7 +88,7 @@ class UserActivityLogger:
activity_type: str,
domain: Optional[str] = None,
success: bool = True,
error: Optional[str] = None
error: Optional[str] = None,
):
"""Log session activities (logout, disconnect, etc)"""
metadata = self._get_request_metadata(request)
@ -97,13 +99,14 @@ class UserActivityLogger:
"domain": domain,
"success": success,
"error": error,
**metadata
**metadata,
}
if success:
self.logger.info(f"{activity_type} successful", extra=log_data)
else:
self.logger.warning(f"{activity_type} failed", extra=log_data)
# Global logger instance
user_logger = UserActivityLogger()

View File

@ -2,9 +2,12 @@ from typing import Any, Optional
from fastapi import status
from fastapi.responses import JSONResponse
class ResponseHandler:
@staticmethod
def success(message: str, data: Optional[Any] = None, status_code: int = status.HTTP_200_OK) -> JSONResponse:
def success(
message: str, data: Optional[Any] = None, status_code: int = status.HTTP_200_OK
) -> JSONResponse:
"""Create a success response"""
return JSONResponse(
content={
@ -16,7 +19,11 @@ class ResponseHandler:
)
@staticmethod
def error(message: str, data: Optional[Any] = None, status_code: int = status.HTTP_400_BAD_REQUEST) -> JSONResponse:
def error(
message: str,
data: Optional[Any] = None,
status_code: int = status.HTTP_400_BAD_REQUEST,
) -> JSONResponse:
"""Create an error response"""
return JSONResponse(
content={

View File

@ -8,4 +8,4 @@ from .templates.password_templates import (
change_your_password_template,
)
update_selected_to_redis = RedisActions.set_json
update_selected_to_redis = RedisActions.set_json

View File

@ -70,7 +70,9 @@ class RedisActions:
already_tokens = redis_cli.scan_iter(match=str(value_regex))
already_tokens_list = {}
for already_token in already_tokens:
already_tokens_list[already_token.decode()] = json.loads(redis_cli.get(already_token))
already_tokens_list[already_token.decode()] = json.loads(
redis_cli.get(already_token)
)
return RedisResponse(
status=True,
message="Values are listed successfully.",
@ -187,21 +189,22 @@ class RedisActions:
try:
search_name = str(name) if isinstance(name, str) else name.decode()
expiry_time = system_arrow.get_expiry_time(**expiry_kwargs)
seconds_until_expiry = int(expiry_time.timestamp() - system_arrow.now().timestamp())
seconds_until_expiry = int(
expiry_time.timestamp() - system_arrow.now().timestamp()
)
redis_cli.setex(
name=search_name,
time=seconds_until_expiry,
value=json.dumps({
'value': value,
'expires_at': expiry_time.timestamp()
})
value=json.dumps(
{"value": value, "expires_at": expiry_time.timestamp()}
),
)
return RedisResponse(
status=True,
message="Value is set successfully with expiry.",
data={'value': value, 'expires_at': expiry_time.timestamp()},
data={"value": value, "expires_at": expiry_time.timestamp()},
)
except Exception as e:
return RedisResponse(
@ -216,25 +219,25 @@ class RedisActions:
try:
search_name = str(name) if isinstance(name, str) else name.decode()
result = redis_cli.get(name=search_name)
if not result:
return RedisResponse(
status=False,
message="Key not found.",
)
data = json.loads(result)
if system_arrow.is_expired(data.get('expires_at')):
if system_arrow.is_expired(data.get("expires_at")):
redis_cli.delete(search_name)
return RedisResponse(
status=False,
message="Cache expired.",
)
return RedisResponse(
status=True,
message="Value retrieved successfully.",
data=data['value'],
data=data["value"],
)
except Exception as e:
return RedisResponse(
@ -272,24 +275,22 @@ class RedisActions:
try:
key = f"{access_token}:{model_object.user_uu_id}"
expiry_time = system_arrow.get_expiry_time(minutes=expiry_minutes)
seconds_until_expiry = max(1, int(expiry_time.timestamp() - system_arrow.now().timestamp()))
seconds_until_expiry = max(
1, int(expiry_time.timestamp() - system_arrow.now().timestamp())
)
# Add expiry time to the model data
model_data = json.loads(model_object.model_dump_json())
model_data['expires_at'] = expiry_time.timestamp()
model_data["expires_at"] = expiry_time.timestamp()
if redis_cli.setex(
name=key,
time=seconds_until_expiry,
value=json.dumps(model_data)
name=key, time=seconds_until_expiry, value=json.dumps(model_data)
):
return access_token
except Exception as e:
raise Exception(
f"Failed to save object to Redis. Error: {str(e)}"
)
raise Exception(f"Failed to save object to Redis. Error: {str(e)}")
raise Exception("Failed to save token to Redis")
@classmethod
@ -304,21 +305,21 @@ class RedisActions:
"""
from api_configs.configs import Auth
from api_library.date_time_actions.date_functions import system_arrow
if not hasattr(request, "headers"):
raise Exception("Headers not found in request")
access_token = request.headers.get(Auth.ACCESS_TOKEN_TAG)
if not access_token:
raise Exception("Unauthorized user, please login")
# Scan for matching tokens
token_pattern = f"{access_token}:*"
matching_tokens = list(redis_cli.scan_iter(match=token_pattern))
if not matching_tokens:
raise Exception("Invalid credentials. Please login again")
try:
# Check if token has expired in Redis
token_key = matching_tokens[0]
@ -326,23 +327,23 @@ class RedisActions:
if ttl <= 0:
redis_cli.delete(token_key)
raise Exception("Token expired. Please login again")
# Get the token data
token_data = json.loads(redis_cli.get(token_key) or '{}')
token_data = json.loads(redis_cli.get(token_key) or "{}")
# Return appropriate token object based on user type
if token_data.get("user_type") == 1: # Employee
if not token_data.get("selected_company"):
token_data["selected_company"] = None
return EmployeeTokenObject(**token_data)
elif token_data.get("user_type") == 2: # Occupant
if not token_data.get("selected_occupant"):
token_data["selected_occupant"] = None
return OccupantTokenObject(**token_data)
raise Exception("Invalid user type in token")
except Exception as e:
raise Exception(f"Failed to retrieve token: {str(e)}")
@ -356,24 +357,28 @@ class RedisActions:
"""
token_pattern = f"*:{str(user_id)}"
matching_tokens = redis_cli.scan_iter(match=token_pattern)
tokens_dict = {}
for token_key in matching_tokens:
token_data = json.loads(redis_cli.get(token_key) or '{}')
token_data = json.loads(redis_cli.get(token_key) or "{}")
# Skip expired tokens and clean them up
if system_arrow.is_expired(token_data.get('expires_at')):
if system_arrow.is_expired(token_data.get("expires_at")):
redis_cli.delete(token_key)
continue
tokens_dict[token_key.decode()] = token_data
return tokens_dict
class RedisResponse:
def __init__(
self, status: bool, message: str, data: typing.Union[dict | list] = None, error: str = None
self,
status: bool,
message: str,
data: typing.Union[dict | list] = None,
error: str = None,
):
self.status = status
self.message = message

View File

@ -8,16 +8,18 @@ from api_configs import Auth
from databases import Users, UsersTokens
from api_library.date_time_actions.date_functions import system_arrow
class TokenService:
@staticmethod
def validate_token(request: Request) -> Union[OccupantTokenObject, EmployeeTokenObject]:
def validate_token(
request: Request,
) -> Union[OccupantTokenObject, EmployeeTokenObject]:
"""Validate and return token object from request"""
try:
return RedisActions.get_object_via_access_key(request)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"message": str(e)}
status_code=status.HTTP_401_UNAUTHORIZED, detail={"message": str(e)}
)
@staticmethod
@ -26,12 +28,12 @@ class TokenService:
return RedisActions.get_object_via_user_uu_id(user_id)
@staticmethod
def validate_refresh_token(domain: str, refresh_token: str) -> Optional[UsersTokens]:
def validate_refresh_token(
domain: str, refresh_token: str
) -> Optional[UsersTokens]:
"""Validate refresh token and return token object"""
return UsersTokens.filter_by_one(
token=refresh_token,
domain=domain,
**UsersTokens.valid_record_dict
token=refresh_token, domain=domain, **UsersTokens.valid_record_dict
).data
@staticmethod
@ -39,10 +41,9 @@ class TokenService:
"""Update user metadata from request"""
user.last_agent = request.headers.get("User-Agent")
user.last_platform = request.headers.get("Origin")
user.last_remote_addr = (
getattr(request, "remote_addr", None) or
request.headers.get("X-Forwarded-For")
)
user.last_remote_addr = getattr(
request, "remote_addr", None
) or request.headers.get("X-Forwarded-For")
user.last_seen = str(system_arrow.now())
user.save()
@ -64,12 +65,12 @@ class TokenService:
user = Users.filter_one(Users.password_token == token).data
if not user:
return None
# Check if token is expired
token_valid_until = system_arrow.get(str(user.password_token_is_valid))
if system_arrow.now() > token_valid_until:
user.password_token = ""
user.save()
return None
return user

View File

@ -19,19 +19,19 @@ from .building_responses import (
BuildCompaniesProvidingResponse,
BuildCompaniesProvidingCollection,
BuildManagementResponse,
BuildManagementCollection
BuildManagementCollection,
)
from .account_responses import (
AccountRecordResponse,
AccountRecordCollection,
AccountRecordExchangeResponse,
AccountRecordExchangeCollection
AccountRecordExchangeCollection,
)
from .address_responses import ListAddressResponse, AddressPostCodeResponse
from .auth_responses import (
AuthenticationLoginResponse,
AuthenticationRefreshResponse,
AuthenticationUserInfoResponse
AuthenticationUserInfoResponse,
)
from .base_responses import BaseResponse, CrudCollection
from .budget_responses import (
@ -42,13 +42,13 @@ from .budget_responses import (
DecisionBookBudgetMasterResponse,
DecisionBookBudgetMasterCollection,
DecisionBookBudgetsResponse,
DecisionBookBudgetsCollection
DecisionBookBudgetsCollection,
)
from .company_responses import (
CompanyListResponse,
CompanyDepartmentListResponse,
CompanyDutyListResponse,
CompanyEmployeeListResponse
CompanyEmployeeListResponse,
)
from .decision_book_responses import (
BuildDecisionBookResponse,
@ -66,7 +66,7 @@ from .decision_book_responses import (
BuildDecisionBookPaymentsResponse,
BuildDecisionBookPaymentsCollection,
BuildDecisionBookLegalResponse,
BuildDecisionBookLegalCollection
BuildDecisionBookLegalCollection,
)
from .living_space_responses import LivingSpaceListResponse
from .parts_responses import BuildPartsListResponse
@ -115,5 +115,5 @@ __all__ = [
"BuildDecisionBookLegalCollection",
"LivingSpaceListResponse",
"BuildPartsListResponse",
"PeopleListResponse"
"PeopleListResponse",
]

View File

@ -8,6 +8,7 @@ from .base_responses import BaseResponse, CrudCollection
class AccountBooksResponse(BaseResponse):
"""Response model for account books"""
country: str
branch_type: int
company_id: int
@ -18,11 +19,13 @@ class AccountBooksResponse(BaseResponse):
class AccountBooksCollection(CrudCollection[AccountBooksResponse]):
"""Collection of account books"""
pass
class AccountCodesResponse(BaseResponse):
"""Response model for account codes"""
account_code: str
comment_line: str
is_receive_or_debit: bool
@ -42,11 +45,13 @@ class AccountCodesResponse(BaseResponse):
class AccountCodesCollection(CrudCollection[AccountCodesResponse]):
"""Collection of account codes"""
pass
class AccountCodeParserResponse(BaseResponse):
"""Response model for account code parser"""
account_code_1: str
account_code_2: str
account_code_3: str
@ -59,11 +64,13 @@ class AccountCodeParserResponse(BaseResponse):
class AccountCodeParserCollection(CrudCollection[AccountCodeParserResponse]):
"""Collection of account code parsers"""
pass
class AccountMasterResponse(BaseResponse):
"""Response model for account master"""
doc_date: datetime
plug_type: str
plug_number: int
@ -106,11 +113,13 @@ class AccountMasterResponse(BaseResponse):
class AccountMasterCollection(CrudCollection[AccountMasterResponse]):
"""Collection of account masters"""
pass
class AccountDetailResponse(BaseResponse):
"""Response model for account detail"""
doc_date: datetime
line_no: int
receive_debit: str
@ -160,15 +169,16 @@ class AccountDetailResponse(BaseResponse):
class AccountDetailCollection(CrudCollection[AccountDetailResponse]):
"""Collection of account details"""
pass
class AccountRecordResponse(BaseResponse):
"""Response model for account financial records.
This model represents a financial transaction record in the system,
including bank transaction details, amounts, and related metadata.
Attributes:
iban (str): International Bank Account Number
bank_date (datetime): Date when the transaction occurred at the bank
@ -206,6 +216,7 @@ class AccountRecordResponse(BaseResponse):
build_parts_uu_id (Optional[str]): Related building part ID
build_decision_book_uu_id (Optional[str]): Related decision book ID
"""
iban: str
bank_date: datetime
currency_value: Decimal
@ -245,19 +256,20 @@ class AccountRecordResponse(BaseResponse):
class AccountRecordCollection(CrudCollection[AccountRecordResponse]):
"""Collection of account financial records.
This model represents a paginated list of account records with
sorting and pagination information.
"""
pass
class AccountRecordExchangeResponse(BaseResponse):
"""Response model for currency exchange records.
This model represents a currency exchange transaction, tracking
exchange rates and converted amounts for financial records.
Attributes:
account_record_id (int): ID of the related account record
account_record_uu_id (str): UUID of the related account record
@ -266,6 +278,7 @@ class AccountRecordExchangeResponse(BaseResponse):
exchange_value (Decimal): Converted amount
exchange_date (datetime): When the exchange was calculated
"""
account_record_id: int
account_record_uu_id: str
exchange_rate: Decimal
@ -276,15 +289,17 @@ class AccountRecordExchangeResponse(BaseResponse):
class AccountRecordExchangeCollection(CrudCollection[AccountRecordExchangeResponse]):
"""Collection of currency exchange records.
This model represents a paginated list of currency exchange records
with sorting and pagination information.
"""
pass
class AccountRecordsListResponse(BaseModel):
"""Response model for account records list endpoint"""
uu_id: UUID
account_name: str
account_code: str

View File

@ -6,6 +6,7 @@ from uuid import UUID
class AuthenticationLoginResponse(BaseModel):
"""Response model for authentication login endpoint"""
token: str
refresh_token: str
token_type: str
@ -15,6 +16,7 @@ class AuthenticationLoginResponse(BaseModel):
class AuthenticationRefreshResponse(BaseModel):
"""Response model for authentication refresh endpoint"""
token: str
refresh_token: str
token_type: str
@ -23,6 +25,7 @@ class AuthenticationRefreshResponse(BaseModel):
class AuthenticationUserInfoResponse(BaseModel):
"""Response model for authentication user info endpoint"""
user_id: int
username: str
email: str

View File

@ -3,15 +3,16 @@ from typing import Optional, TypeVar, Generic, List
from datetime import datetime
from uuid import UUID
T = TypeVar('T')
T = TypeVar("T")
class BaseResponse(BaseModel):
"""Base response model that all response models inherit from.
This model provides common fields that are present in all database records,
including tracking information (created/updated timestamps), user actions
(created by, updated by, confirmed by), and record status (active, deleted).
Attributes:
uu_id (str): Unique identifier for the record, typically a UUID
created_at (datetime): Timestamp when the record was created
@ -27,6 +28,7 @@ class BaseResponse(BaseModel):
is_notification_send (Optional[bool]): Whether notifications have been sent for this record
is_email_send (Optional[bool]): Whether emails have been sent for this record
"""
uu_id: str
created_at: datetime
updated_at: Optional[datetime]
@ -43,23 +45,24 @@ class BaseResponse(BaseModel):
class Config:
"""Pydantic configuration for the base response model.
Attributes:
from_attributes (bool): Enables ORM mode for SQLAlchemy integration
"""
from_attributes = True
class CrudCollection(BaseModel, Generic[T]):
"""Base collection model for paginated responses.
This model is used to return collections of items with pagination information.
It is generic over the type of items in the collection, allowing it to be
used with any response model.
Type Parameters:
T: The type of items in the collection
Attributes:
page (int): Current page number, 1-based indexing
size (int): Number of items per page
@ -67,13 +70,13 @@ class CrudCollection(BaseModel, Generic[T]):
order_field (str): Field used for sorting the collection
order_type (str): Sort direction ('asc' or 'desc')
items (List[T]): List of items in the current page
Example:
```python
class UserResponse(BaseResponse):
name: str
email: str
users = CrudCollection[UserResponse](
page=1,
size=10,
@ -84,6 +87,7 @@ class CrudCollection(BaseModel, Generic[T]):
)
```
"""
page: int = 1
size: int = 10
total: int = 0
@ -93,8 +97,9 @@ class CrudCollection(BaseModel, Generic[T]):
class Config:
"""Pydantic configuration for the collection model.
Attributes:
from_attributes (bool): Enables ORM mode for SQLAlchemy integration
"""
from_attributes = True

View File

@ -8,6 +8,7 @@ from .base_responses import BaseResponse, CrudCollection
class DecisionBookBudgetBooksResponse(BaseResponse):
"""Response model for decision book budget books"""
country: str
branch_type: int = 0
company_id: int
@ -18,13 +19,17 @@ class DecisionBookBudgetBooksResponse(BaseResponse):
build_decision_book_uu_id: Optional[str]
class DecisionBookBudgetBooksCollection(CrudCollection[DecisionBookBudgetBooksResponse]):
class DecisionBookBudgetBooksCollection(
CrudCollection[DecisionBookBudgetBooksResponse]
):
"""Collection of decision book budget books"""
pass
class DecisionBookBudgetCodesResponse(BaseResponse):
"""Response model for decision book budget codes"""
budget_code: str
comment_line: str
budget_type: str
@ -37,13 +42,17 @@ class DecisionBookBudgetCodesResponse(BaseResponse):
customer_uu_id: str
class DecisionBookBudgetCodesCollection(CrudCollection[DecisionBookBudgetCodesResponse]):
class DecisionBookBudgetCodesCollection(
CrudCollection[DecisionBookBudgetCodesResponse]
):
"""Collection of decision book budget codes"""
pass
class DecisionBookBudgetMasterResponse(BaseResponse):
"""Response model for decision book budget master"""
budget_type: str
currency: str = "TRY"
total_budget: Decimal
@ -55,13 +64,17 @@ class DecisionBookBudgetMasterResponse(BaseResponse):
department_uu_id: Optional[str]
class DecisionBookBudgetMasterCollection(CrudCollection[DecisionBookBudgetMasterResponse]):
class DecisionBookBudgetMasterCollection(
CrudCollection[DecisionBookBudgetMasterResponse]
):
"""Collection of decision book budget masters"""
pass
class DecisionBookBudgetsResponse(BaseResponse):
"""Response model for decision book budgets"""
process_date: datetime
budget_codes_id: int
total_budget: Decimal
@ -73,4 +86,5 @@ class DecisionBookBudgetsResponse(BaseResponse):
class DecisionBookBudgetsCollection(CrudCollection[DecisionBookBudgetsResponse]):
"""Collection of decision book budgets"""
pass

View File

@ -8,6 +8,7 @@ from .base_responses import BaseResponse, CrudCollection
class BuildAreaListResponse(BaseResponse):
"""Response model for building area list endpoint"""
uu_id: UUID
build_id: int
build_uu_id: str
@ -20,11 +21,13 @@ class BuildAreaListResponse(BaseResponse):
class BuildAreaListCollection(CrudCollection[BuildAreaListResponse]):
"""Collection of building area list"""
pass
class BuildSitesListResponse(BaseResponse):
"""Response model for building sites list endpoint"""
uu_id: UUID
address_id: int
site_name: str
@ -36,11 +39,13 @@ class BuildSitesListResponse(BaseResponse):
class BuildSitesListCollection(CrudCollection[BuildSitesListResponse]):
"""Collection of building sites list"""
pass
class BuildTypesListResponse(BaseResponse):
"""Response model for building types list endpoint"""
uu_id: UUID
type_name: str
type_value: str
@ -51,11 +56,13 @@ class BuildTypesListResponse(BaseResponse):
class BuildTypesListCollection(CrudCollection[BuildTypesListResponse]):
"""Collection of building types list"""
pass
class BuildTypesResponse(BaseResponse):
"""Response model for building types"""
function_code: str
type_code: str
lang: str = "TR"
@ -63,11 +70,13 @@ class BuildTypesResponse(BaseResponse):
class BuildTypesCollection(CrudCollection[BuildTypesResponse]):
"""Collection of building types"""
pass
class Part2EmployeeResponse(BaseResponse):
"""Response model for part to employee mapping"""
build_id: int
part_id: int
employee_id: int
@ -75,11 +84,13 @@ class Part2EmployeeResponse(BaseResponse):
class Part2EmployeeCollection(CrudCollection[Part2EmployeeResponse]):
"""Collection of part to employee mappings"""
pass
class RelationshipEmployee2BuildResponse(BaseResponse):
"""Response model for employee to build relationship"""
company_id: int
employee_id: int
member_id: int
@ -87,13 +98,17 @@ class RelationshipEmployee2BuildResponse(BaseResponse):
show_only: bool = False
class RelationshipEmployee2BuildCollection(CrudCollection[RelationshipEmployee2BuildResponse]):
class RelationshipEmployee2BuildCollection(
CrudCollection[RelationshipEmployee2BuildResponse]
):
"""Collection of employee to build relationships"""
pass
class BuildResponse(BaseResponse):
"""Response model for buildings"""
gov_address_code: str = ""
build_name: str
build_no: str
@ -120,11 +135,13 @@ class BuildResponse(BaseResponse):
class BuildCollection(CrudCollection[BuildResponse]):
"""Collection of buildings"""
pass
class BuildPartsResponse(BaseResponse):
"""Response model for building parts"""
address_gov_code: str
part_no: int = 0
part_level: int = 0
@ -144,11 +161,13 @@ class BuildPartsResponse(BaseResponse):
class BuildPartsCollection(CrudCollection[BuildPartsResponse]):
"""Collection of building parts"""
pass
class BuildLivingSpaceResponse(BaseResponse):
"""Response model for building living space"""
fix_value: Decimal = Decimal("0")
fix_percent: Decimal = Decimal("0")
agreement_no: str = ""
@ -164,11 +183,13 @@ class BuildLivingSpaceResponse(BaseResponse):
class BuildLivingSpaceCollection(CrudCollection[BuildLivingSpaceResponse]):
"""Collection of building living spaces"""
pass
class BuildManagementResponse(BaseResponse):
"""Response model for building management"""
discounted_percentage: Decimal = Decimal("0.00")
discounted_price: Decimal = Decimal("0.00")
calculated_price: Decimal = Decimal("0.00")
@ -182,11 +203,13 @@ class BuildManagementResponse(BaseResponse):
class BuildManagementCollection(CrudCollection[BuildManagementResponse]):
"""Collection of building management records"""
pass
class BuildAreaResponse(BaseResponse):
"""Response model for building area"""
area_name: str = ""
area_code: str = ""
area_type: str = "GREEN"
@ -203,11 +226,13 @@ class BuildAreaResponse(BaseResponse):
class BuildAreaCollection(CrudCollection[BuildAreaResponse]):
"""Collection of building areas"""
pass
class BuildSitesResponse(BaseResponse):
"""Response model for building sites"""
site_name: str
site_no: str
address_id: int
@ -216,11 +241,13 @@ class BuildSitesResponse(BaseResponse):
class BuildSitesCollection(CrudCollection[BuildSitesResponse]):
"""Collection of building sites"""
pass
class BuildCompaniesProvidingResponse(BaseResponse):
"""Response model for building companies providing services"""
build_id: int
build_uu_id: Optional[str]
company_id: int
@ -230,13 +257,17 @@ class BuildCompaniesProvidingResponse(BaseResponse):
contract_id: Optional[int]
class BuildCompaniesProvidingCollection(CrudCollection[BuildCompaniesProvidingResponse]):
class BuildCompaniesProvidingCollection(
CrudCollection[BuildCompaniesProvidingResponse]
):
"""Collection of building companies providing services"""
pass
class BuildPersonProvidingResponse(BaseResponse):
"""Response model for building person providing services"""
build_id: int
build_uu_id: Optional[str]
people_id: int
@ -248,4 +279,5 @@ class BuildPersonProvidingResponse(BaseResponse):
class BuildPersonProvidingCollection(CrudCollection[BuildPersonProvidingResponse]):
"""Collection of building person providing services"""
pass

View File

@ -6,6 +6,7 @@ from uuid import UUID
class CompanyListResponse(BaseModel):
"""Response model for company list endpoint"""
uu_id: UUID
company_name: str
company_code: str
@ -19,6 +20,7 @@ class CompanyListResponse(BaseModel):
class CompanyDepartmentListResponse(BaseModel):
"""Response model for company department list endpoint"""
uu_id: UUID
department_name: str
department_code: str
@ -31,6 +33,7 @@ class CompanyDepartmentListResponse(BaseModel):
class CompanyDutyListResponse(BaseModel):
"""Response model for company duty list endpoint"""
uu_id: UUID
duty_name: str
duty_code: str
@ -43,6 +46,7 @@ class CompanyDutyListResponse(BaseModel):
class CompanyEmployeeListResponse(BaseModel):
"""Response model for company employee list endpoint"""
uu_id: UUID
employee_id: int
employee_uu_id: str

View File

@ -8,6 +8,7 @@ from .base_responses import BaseResponse, CrudCollection
class BuildDecisionBookResponse(BaseResponse):
"""Response model for building decision book"""
decision_book_pdf_path: Optional[str] = ""
resp_company_fix_wage: float = 0
contact_agreement_path: Optional[str] = ""
@ -18,11 +19,13 @@ class BuildDecisionBookResponse(BaseResponse):
class BuildDecisionBookCollection(CrudCollection[BuildDecisionBookResponse]):
"""Collection of building decision books"""
pass
class BuildDecisionBookInvitationsResponse(BaseResponse):
"""Response model for building decision book invitations"""
build_id: int
build_uu_id: Optional[str]
decision_book_id: int
@ -36,13 +39,17 @@ class BuildDecisionBookInvitationsResponse(BaseResponse):
planned_date_expires: datetime
class BuildDecisionBookInvitationsCollection(CrudCollection[BuildDecisionBookInvitationsResponse]):
class BuildDecisionBookInvitationsCollection(
CrudCollection[BuildDecisionBookInvitationsResponse]
):
"""Collection of building decision book invitations"""
pass
class BuildDecisionBookPersonResponse(BaseResponse):
"""Response model for building decision book person"""
dues_percent_discount: int = 0
dues_fix_discount: Decimal = Decimal("0")
dues_discount_approval_date: datetime
@ -61,13 +68,17 @@ class BuildDecisionBookPersonResponse(BaseResponse):
person_id: int
class BuildDecisionBookPersonCollection(CrudCollection[BuildDecisionBookPersonResponse]):
class BuildDecisionBookPersonCollection(
CrudCollection[BuildDecisionBookPersonResponse]
):
"""Collection of building decision book persons"""
pass
class BuildDecisionBookPersonOccupantsResponse(BaseResponse):
"""Response model for building decision book person occupants"""
build_decision_book_person_id: int
build_decision_book_person_uu_id: Optional[str]
invite_id: Optional[int]
@ -76,13 +87,17 @@ class BuildDecisionBookPersonOccupantsResponse(BaseResponse):
occupant_type_uu_id: Optional[str]
class BuildDecisionBookPersonOccupantsCollection(CrudCollection[BuildDecisionBookPersonOccupantsResponse]):
class BuildDecisionBookPersonOccupantsCollection(
CrudCollection[BuildDecisionBookPersonOccupantsResponse]
):
"""Collection of building decision book person occupants"""
pass
class BuildDecisionBookItemsResponse(BaseResponse):
"""Response model for building decision book items"""
item_order: int
item_comment: str
item_objection: Optional[str]
@ -97,11 +112,13 @@ class BuildDecisionBookItemsResponse(BaseResponse):
class BuildDecisionBookItemsCollection(CrudCollection[BuildDecisionBookItemsResponse]):
"""Collection of building decision book items"""
pass
class BuildDecisionBookItemsUnapprovedResponse(BaseResponse):
"""Response model for building decision book items unapproved"""
item_objection: str
item_order: int
decision_book_item_id: int
@ -112,13 +129,17 @@ class BuildDecisionBookItemsUnapprovedResponse(BaseResponse):
build_decision_book_item_uu_id: Optional[str]
class BuildDecisionBookItemsUnapprovedCollection(CrudCollection[BuildDecisionBookItemsUnapprovedResponse]):
class BuildDecisionBookItemsUnapprovedCollection(
CrudCollection[BuildDecisionBookItemsUnapprovedResponse]
):
"""Collection of building decision book items unapproved"""
pass
class BuildDecisionBookPaymentsResponse(BaseResponse):
"""Response model for building decision book payments"""
payment_plan_time_periods: str
process_date: datetime
payment_amount: Decimal
@ -138,13 +159,17 @@ class BuildDecisionBookPaymentsResponse(BaseResponse):
account_records_uu_id: Optional[str]
class BuildDecisionBookPaymentsCollection(CrudCollection[BuildDecisionBookPaymentsResponse]):
class BuildDecisionBookPaymentsCollection(
CrudCollection[BuildDecisionBookPaymentsResponse]
):
"""Collection of building decision book payments"""
pass
class BuildDecisionBookLegalResponse(BaseResponse):
"""Response model for building decision book legal"""
period_start_date: datetime
lawsuits_decision_number: str
lawsuits_decision_date: datetime
@ -175,4 +200,5 @@ class BuildDecisionBookLegalResponse(BaseResponse):
class BuildDecisionBookLegalCollection(CrudCollection[BuildDecisionBookLegalResponse]):
"""Collection of building decision book legal records"""
pass

View File

@ -70,7 +70,7 @@ RelationshipDutyPeopleLanguageModel = dict(
"member_id": "Member ID",
"relationship_type": "Relationship Type",
"show_only": "Show Only",
}
},
)
PeopleLanguageModel = dict(
@ -105,7 +105,7 @@ PeopleLanguageModel = dict(
"birth_place": "Birth Place",
"birth_date": "Birth Date",
"tax_no": "Tax No",
}
},
)
RelationshipEmployee2PostCodeLanguageModel = dict(
@ -124,7 +124,7 @@ RelationshipEmployee2PostCodeLanguageModel = dict(
"member_id": "Member ID",
"relationship_type": "Relationship Type",
"show_only": "Show Only",
}
},
)
AddressPostcodeLanguageModel = dict(
@ -168,7 +168,7 @@ AddressesLanguageModel = dict(
"longitude": "Longitude",
"street_id": "Street ID",
"street_uu_id": "Street UUID",
}
},
)
AddressGeographicLocationsLanguageModel = dict(
@ -195,7 +195,7 @@ AddressGeographicLocationsLanguageModel = dict(
"geo_description": "Description",
"geo_area_size": "Area",
"geo_population": "Population",
}
},
)
AddressCountryLanguageModel = dict(

View File

@ -16,5 +16,5 @@ EndpointRestrictionLanguageModel = dict(
"endpoint_method": "API Method",
"endpoint_desc": "API Description",
"endpoint_code": "API Code",
}
},
)

View File

@ -13,8 +13,8 @@ def validate_timestamp(doc):
"""Validate and fix timestamp fields in MongoDB documents"""
if not doc:
return doc
timestamp_fields = ['modified_at', 'created_at', 'accessed_at', 'timestamp']
timestamp_fields = ["modified_at", "created_at", "accessed_at", "timestamp"]
for field in timestamp_fields:
if field in doc and not isinstance(doc[field], (int, float)):
# Convert to proper timestamp if it's not already

View File

@ -41,6 +41,7 @@ from databases.language_models.building.build import (
BuildPersonProvidingLanguageModel,
)
class BuildTypes(CrudCollection):
"""
BuildTypes class based on declarative_base and BaseMixin via session
@ -177,12 +178,8 @@ class Build(CrudCollection, SelectActionWithEmployee):
heating_system: Mapped[bool] = mapped_column(Boolean, server_default="True")
cooling_system: Mapped[bool] = mapped_column(Boolean, server_default="False")
hot_water_system: Mapped[bool] = mapped_column(Boolean, server_default="False")
block_service_man_count: Mapped[int] = mapped_column(
Integer, server_default="0"
)
security_service_man_count: Mapped[int] = mapped_column(
Integer, server_default="0"
)
block_service_man_count: Mapped[int] = mapped_column(Integer, server_default="0")
security_service_man_count: Mapped[int] = mapped_column(Integer, server_default="0")
garage_count: Mapped[int] = mapped_column(
Integer, server_default="0", comment="Garage Count"
)

View File

@ -2,7 +2,16 @@ from fastapi.exceptions import HTTPException
from databases.sql_models.core_mixin import CrudCollection
from sqlalchemy import String, Integer, Boolean, ForeignKey, Index, Identity, TIMESTAMP, func
from sqlalchemy import (
String,
Integer,
Boolean,
ForeignKey,
Index,
Identity,
TIMESTAMP,
func,
)
from sqlalchemy.orm import mapped_column, relationship, Mapped
from api_configs import RelationAccess

View File

@ -9,12 +9,14 @@ engine_config = {
"pool_size": 20,
"max_overflow": 10,
"echo": True,
"echo_pool":True,
"echo_pool": True,
"isolation_level": "READ COMMITTED",
"pool_pre_ping": True,
}
engine = create_engine(**engine_config, )
engine = create_engine(
**engine_config,
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
session = scoped_session(sessionmaker(bind=engine))

View File

@ -52,9 +52,9 @@ class FilterAttributes:
meta_data = getattr(cls, "meta_data", {})
meta_data_created = meta_data.get("created", False)
if meta_data_created:
print('meta_data_created commit', meta_data_created)
print("meta_data_created commit", meta_data_created)
cls.__session__.commit()
print('meta_data_created rollback', meta_data_created)
print("meta_data_created rollback", meta_data_created)
cls.__session__.rollback()
# cls.raise_http_exception(
# status_code="HTTP_304_NOT_MODIFIED",
@ -82,6 +82,7 @@ class FilterAttributes:
data={},
message=str(e.__context__).split("\n")[0],
)
@classmethod
def rollback(cls):
"""Rollback the current session."""

View File

@ -16,6 +16,7 @@ def do_alembic():
generate_alembic_with_session(text=text)
#
# def create_one_address():
# from databases import (

View File

@ -36,10 +36,9 @@ CrudCollection.http_exception = HTTPException
new_person_find_or_create = People.find_or_create(**person_dict)
new_person_find_or_create.save_via_metadata()
print('meta_data', new_person_find_or_create.meta_data)
print('new_person_find_or_create', new_person_find_or_create)
print("meta_data", new_person_find_or_create.meta_data)
print("new_person_find_or_create", new_person_find_or_create)
quit()
new_user_find_or_create = Users.find_or_create(**user_dict)
new_user_find_or_abort = Users.find_or_abort(**user_dict)

View File

@ -9,7 +9,7 @@ def create_test_occupant_token(
person_id: int = 1,
person_uu_id: str = "test-person",
credentials: Optional[Dict[str, Any]] = None,
available_occupants: Optional[Dict[str, Any]] = None
available_occupants: Optional[Dict[str, Any]] = None,
) -> OccupantTokenObject:
"""Create a test occupant token object"""
return OccupantTokenObject(
@ -32,7 +32,7 @@ def create_test_employee_token(
person_id: int = 1,
person_uu_id: str = "test-person",
credentials: Optional[Dict[str, Any]] = None,
selected_company: Optional[Dict[str, Any]] = None
selected_company: Optional[Dict[str, Any]] = None,
) -> EmployeeTokenObject:
"""Create a test employee token object"""
return EmployeeTokenObject(
@ -50,5 +50,6 @@ def create_test_employee_token(
class MockRequest:
"""Mock request object for testing"""
def __init__(self, headers: Optional[Dict[str, str]] = None):
self.headers = headers or {}

View File

@ -35,9 +35,7 @@ def test_save_object_to_redis_success():
# Test save
result = AccessObjectActions.save_object_to_redis(
access_token=access_token,
model_object=model_object,
expiry_minutes=1
access_token=access_token, model_object=model_object, expiry_minutes=1
)
assert result is True
@ -69,7 +67,7 @@ def test_save_object_to_redis_expiry():
AccessObjectActions.save_object_to_redis(
access_token=access_token,
model_object=model_object,
expiry_minutes=2/60 # 2 seconds
expiry_minutes=2 / 60, # 2 seconds
)
# Verify token exists
@ -86,13 +84,15 @@ def test_save_object_to_redis_expiry():
RedisActions.get_object_via_access_key(
MockRequest(headers={Auth.ACCESS_TOKEN_TAG: access_token})
)
assert any(msg in str(exc_info.value) for msg in ["Token expired", "Invalid credentials"])
assert any(
msg in str(exc_info.value) for msg in ["Token expired", "Invalid credentials"]
)
def test_get_object_via_user_uu_id():
"""Test retrieving all tokens for a user"""
user_id = "test-uuid-3"
# Create multiple tokens for same user
tokens = []
for i in range(3):
@ -114,7 +114,7 @@ def test_get_object_via_user_uu_id():
# Get all tokens
user_tokens = AccessObjectActions.get_object_via_user_uu_id(user_id)
assert len(user_tokens) == 3
# Verify each token
for token in tokens:
found = False
@ -167,7 +167,7 @@ def test_timezone_handling():
token_data = RedisActions.get_object_via_access_key(
MockRequest(headers={Auth.ACCESS_TOKEN_TAG: access_token})
)
# Verify expiry time is in future
assert system_arrow.from_timestamp(token_data.expires_at) > system_arrow.now()