maleo-foundation 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. maleo_foundation/__init__.py +0 -0
  2. maleo_foundation/clients/__init__.py +7 -0
  3. maleo_foundation/clients/general/__init__.py +4 -0
  4. maleo_foundation/clients/general/http.py +41 -0
  5. maleo_foundation/clients/google/__init__.py +4 -0
  6. maleo_foundation/clients/google/cloud/__init__.py +8 -0
  7. maleo_foundation/clients/google/cloud/logging.py +45 -0
  8. maleo_foundation/clients/google/cloud/secret.py +92 -0
  9. maleo_foundation/clients/google/cloud/storage.py +77 -0
  10. maleo_foundation/constants.py +8 -0
  11. maleo_foundation/controller.py +50 -0
  12. maleo_foundation/db/__init__.py +3 -0
  13. maleo_foundation/db/database.py +41 -0
  14. maleo_foundation/db/engine.py +40 -0
  15. maleo_foundation/db/session.py +64 -0
  16. maleo_foundation/models/__init__.py +13 -0
  17. maleo_foundation/models/enums.py +66 -0
  18. maleo_foundation/models/responses.py +99 -0
  19. maleo_foundation/models/schemas/__init__.py +9 -0
  20. maleo_foundation/models/schemas/general.py +105 -0
  21. maleo_foundation/models/schemas/parameter.py +16 -0
  22. maleo_foundation/models/schemas/result.py +89 -0
  23. maleo_foundation/models/transfers/__init__.py +7 -0
  24. maleo_foundation/models/transfers/parameters/__init__.py +9 -0
  25. maleo_foundation/models/transfers/parameters/client.py +65 -0
  26. maleo_foundation/models/transfers/parameters/general.py +13 -0
  27. maleo_foundation/models/transfers/parameters/service.py +121 -0
  28. maleo_foundation/models/transfers/results/__init__.py +7 -0
  29. maleo_foundation/models/transfers/results/client/__init__.py +7 -0
  30. maleo_foundation/models/transfers/results/client/controllers/__init__.py +5 -0
  31. maleo_foundation/models/transfers/results/client/controllers/http.py +35 -0
  32. maleo_foundation/models/transfers/results/client/service.py +27 -0
  33. maleo_foundation/models/transfers/results/service/__init__.py +9 -0
  34. maleo_foundation/models/transfers/results/service/controllers/__init__.py +5 -0
  35. maleo_foundation/models/transfers/results/service/controllers/rest.py +22 -0
  36. maleo_foundation/models/transfers/results/service/general.py +38 -0
  37. maleo_foundation/models/transfers/results/service/query.py +42 -0
  38. maleo_foundation/models/types/__init__.py +9 -0
  39. maleo_foundation/models/types/client.py +40 -0
  40. maleo_foundation/models/types/query.py +40 -0
  41. maleo_foundation/models/types/service.py +40 -0
  42. maleo_foundation/utils/__init__.py +9 -0
  43. maleo_foundation/utils/exceptions.py +74 -0
  44. maleo_foundation/utils/formatter/__init__.py +4 -0
  45. maleo_foundation/utils/formatter/case.py +37 -0
  46. maleo_foundation/utils/logger.py +68 -0
  47. {maleo_foundation-0.0.1.dist-info → maleo_foundation-0.0.2.dist-info}/METADATA +1 -1
  48. maleo_foundation-0.0.2.dist-info/RECORD +50 -0
  49. maleo_foundation-0.0.2.dist-info/top_level.txt +1 -0
  50. maleo_foundation-0.0.1.dist-info/RECORD +0 -4
  51. maleo_foundation-0.0.1.dist-info/top_level.txt +0 -1
  52. {maleo_foundation-0.0.1.dist-info → maleo_foundation-0.0.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+ from fastapi import status
3
+ from pydantic import Field, model_validator
4
+ from typing import Optional, Any
5
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
6
+ from maleo_foundation.models.schemas.result import BaseResultSchemas
7
+
8
+ class BaseResponses:
9
+ class Fail(BaseResultSchemas.Fail):
10
+ other:Optional[Any] = Field("Please try again later or contact administrator.", description="Response's other information")
11
+
12
+ class Unauthorized(Fail):
13
+ code:str = "MAL-ATH-001"
14
+ message:str = "Unauthorized Request"
15
+ description:str = "You are unauthorized to request this resource"
16
+
17
+ class Forbidden(Fail):
18
+ code:str = "MAL-ATH-002"
19
+ message:str = "Forbidden Request"
20
+ description:str = "You are forbidden from requesting this resource"
21
+
22
+ class NotFound(Fail):
23
+ code:str = "MAL-NTF-001"
24
+ message:str = "Not Found Error"
25
+ description:str = "The resource you requested can not be found. Ensure your request is correct."
26
+
27
+ class ValidationError(Fail):
28
+ code:str = "MAL-VLD-001"
29
+ message:str = "Validation Error"
30
+ description:str = "Request validation failed due to missing or invalid fields. Check other for more info."
31
+
32
+ class RateLimitExceeded(Fail):
33
+ code:str = "MAL-RTL-001"
34
+ message:str = "Rate Limit Exceeded"
35
+ description:str = "This resource is requested too many times. Please try again later."
36
+
37
+ class ServerError(Fail):
38
+ code:str = "MAL-EXC-001"
39
+ message:str = "Unexpected Server Error"
40
+ description:str = "An unexpected error occurred while processing your request."
41
+
42
+ class NoData(BaseResultSchemas.NoData): pass
43
+
44
+ class SingleData(BaseResultSchemas.SingleData): pass
45
+
46
+ class UnpaginatedMultipleData(BaseResultSchemas.UnpaginatedMultipleData): pass
47
+
48
+ class PaginatedMultipleData(BaseResultSchemas.PaginatedMultipleData):
49
+ @model_validator(mode="before")
50
+ @classmethod
51
+ def calculate_pagination(cls, values: dict) -> dict:
52
+ """Calculates pagination metadata before validation."""
53
+ total_data = values.get("total_data", 0)
54
+ data = values.get("data", [])
55
+
56
+ #* Get pagination values from inherited SimplePagination
57
+ page = values.get("page", 1)
58
+ limit = values.get("limit", 10)
59
+
60
+ #* Calculate total pages
61
+ total_pages = (total_data // limit) + (1 if total_data % limit > 0 else 0)
62
+
63
+ #* Assign computed pagination object before validation
64
+ values["pagination"] = BaseGeneralSchemas.ExtendedPagination(
65
+ page=page,
66
+ limit=limit,
67
+ data_count=len(data),
68
+ total_data=total_data,
69
+ total_pages=total_pages
70
+ )
71
+ return values
72
+
73
+ #* ----- ----- Responses Class ----- ----- *#
74
+ other_responses={
75
+ status.HTTP_401_UNAUTHORIZED: {
76
+ "description": "Unauthorized Response",
77
+ "model": Unauthorized
78
+ },
79
+ status.HTTP_403_FORBIDDEN: {
80
+ "description": "Forbidden Response",
81
+ "model": Forbidden
82
+ },
83
+ status.HTTP_404_NOT_FOUND: {
84
+ "description": "Not Found Response",
85
+ "model": NotFound
86
+ },
87
+ status.HTTP_422_UNPROCESSABLE_ENTITY: {
88
+ "description": "Validation Error Response",
89
+ "model": ValidationError
90
+ },
91
+ status.HTTP_429_TOO_MANY_REQUESTS: {
92
+ "description": "Rate Limit Exceeded Response",
93
+ "model": RateLimitExceeded
94
+ },
95
+ status.HTTP_500_INTERNAL_SERVER_ERROR: {
96
+ "description": "Internal Server Error Response",
97
+ "model": ServerError
98
+ }
99
+ }
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+ from .general import BaseGeneralSchemas
3
+ from .parameter import BaseParameterSchemas
4
+ from .result import BaseResultSchemas
5
+
6
+ class BaseSchemas:
7
+ General = BaseGeneralSchemas
8
+ Parameter = BaseParameterSchemas
9
+ Result = BaseResultSchemas
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+ from datetime import date, datetime, timedelta, timezone
3
+ from pydantic import BaseModel, Field, model_validator, field_serializer, FieldSerializationInfo
4
+ from typing import Optional, Any
5
+ from uuid import UUID
6
+ from maleo_foundation.models.enums import BaseEnums
7
+ from maleo_foundation.constants import REFRESH_TOKEN_DURATION_DAYS, ACCESS_TOKEN_DURATION_MINUTES
8
+
9
+ class BaseGeneralSchemas:
10
+ class IDs(BaseModel):
11
+ ids:Optional[list[int]] = Field(None, description="Specific IDs")
12
+
13
+ class Search(BaseModel):
14
+ search:Optional[str] = Field(None, description="Search parameter string.")
15
+
16
+ class DateFilter(BaseModel):
17
+ name:str = Field(..., description="Column name.")
18
+ from_date:Optional[datetime] = Field(None, description="From date.")
19
+ to_date:Optional[datetime] = Field(None, description="To date.")
20
+
21
+ class Statuses(BaseModel):
22
+ statuses:Optional[list[BaseEnums.StatusType]] = Field(None, description="Data's status")
23
+
24
+ class SortColumn(BaseModel):
25
+ name:str = Field(..., description="Column name.")
26
+ order:BaseEnums.SortOrder = Field(..., description="Sort order.")
27
+
28
+ class SimplePagination(BaseModel):
29
+ page:int = Field(1, ge=1, description="Page number, must be >= 1.")
30
+ limit:int = Field(10, ge=1, le=100, description="Page size, must be 1 <= limit <= 100.")
31
+
32
+ class ExtendedPagination(SimplePagination):
33
+ data_count:int = Field(..., description="Fetched data count")
34
+ total_data:int = Field(..., description="Total data count")
35
+ total_pages:int = Field(..., description="Total pages count")
36
+
37
+ class StatusUpdate(BaseModel):
38
+ action:BaseEnums.StatusUpdateAction = Field(..., description="Status update's action to be executed")
39
+
40
+ class Expand(BaseModel):
41
+ expand:Optional[list[str]] = Field(None, description="Expanded field(s)")
42
+
43
+ class PrivateKey(BaseModel):
44
+ private_key:str = Field(..., description="Private key in str format.")
45
+
46
+ class PublicKey(BaseModel):
47
+ public_key:str = Field(..., description="Public key in str format.")
48
+
49
+ class KeyPair(PublicKey, PrivateKey): pass
50
+
51
+ class TokenPayload(BaseModel):
52
+ t:BaseEnums.TokenType = Field(..., description="Token Type")
53
+ sr:UUID = Field(..., description="System role")
54
+ u:UUID = Field(..., description="user")
55
+ o:Optional[UUID] = Field(..., description="Organization")
56
+ uor:Optional[list[UUID]] = Field(..., description="User Organization Role")
57
+ iat_dt:datetime = Field(datetime.now(timezone.utc), description="Issued at (datetime)")
58
+ iat:int = Field(None, description="Issued at (integer)")
59
+ exp_dt:datetime = Field(None, description="Expired at (datetime)")
60
+ exp:int = Field(None, description="Expired at (integet)")
61
+
62
+ @model_validator(mode="before")
63
+ @classmethod
64
+ def set_iat_and_exp(cls, values:dict):
65
+ iat_dt = values.get("iat_dt", None)
66
+ if not iat_dt:
67
+ iat_dt = datetime.now(timezone.utc)
68
+ else:
69
+ if not isinstance(iat_dt, datetime):
70
+ iat_dt = datetime.fromisoformat(iat_dt)
71
+ values["iat_dt"] = iat_dt
72
+ #* Convert `iat` to timestamp (int)
73
+ values["iat"] = int(iat_dt.timestamp())
74
+ exp_dt = values.get("exp_dt", None)
75
+ if not exp_dt:
76
+ if values["t"] == BaseEnums.TokenType.REFRESH:
77
+ exp_dt = iat_dt + timedelta(days=REFRESH_TOKEN_DURATION_DAYS)
78
+ elif values["t"] == BaseEnums.TokenType.ACCESS:
79
+ exp_dt = iat_dt + timedelta(minutes=ACCESS_TOKEN_DURATION_MINUTES)
80
+ else:
81
+ if not isinstance(exp_dt, datetime):
82
+ exp_dt = datetime.fromisoformat(exp_dt)
83
+ values["exp_dt"] = exp_dt
84
+ #* Convert `exp_dt` to timestamp (int)
85
+ values["exp"] = int(exp_dt.timestamp())
86
+ return values
87
+
88
+ @field_serializer('*')
89
+ def serialize_fields(self, value, info: FieldSerializationInfo) -> Any:
90
+ """Recursively serialize UUIDs, datetimes, and dates in complex structures."""
91
+
92
+ def serialize(v: Any) -> Any:
93
+ if isinstance(v, UUID):
94
+ return str(v)
95
+ if isinstance(v, (datetime, date)):
96
+ return v.isoformat()
97
+ if isinstance(v, list):
98
+ return [serialize(item) for item in v]
99
+ if isinstance(v, tuple):
100
+ return tuple(serialize(item) for item in v)
101
+ if isinstance(v, dict):
102
+ return {serialize(k): serialize(val) for k, val in v.items()}
103
+ return v
104
+
105
+ return serialize(value)
@@ -0,0 +1,16 @@
1
+ from pydantic import BaseModel, Field
2
+ from maleo_foundation.models.enums import BaseEnums
3
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
4
+
5
+ class BaseParameterSchemas:
6
+ class Filters(BaseModel):
7
+ filters:list[str] = Field([], description="Filters for date range, e.g. 'created_at|from::<ISO_DATETIME>|to::<ISO_DATETIME>'.")
8
+
9
+ class DateFilters(BaseModel):
10
+ date_filters:list[BaseGeneralSchemas.DateFilter] = Field([], description="Date filters to be applied")
11
+
12
+ class Sorts(BaseModel):
13
+ sorts:list[str] = Field(["id.asc"], description="Sorting columns in 'column_name.asc' or 'column_name.desc' format.")
14
+
15
+ class SortColumns(BaseModel):
16
+ sort_columns:list[BaseGeneralSchemas.SortColumn] = Field([BaseGeneralSchemas.SortColumn(name="id", order=BaseEnums.SortOrder.ASC)], description="List of columns to be sorted")
@@ -0,0 +1,89 @@
1
+ from datetime import datetime, date
2
+ from pydantic import BaseModel, FieldSerializationInfo, Field, field_serializer, field_validator
3
+ from typing import Literal, Optional, Union, Any
4
+ from uuid import UUID
5
+ from maleo_foundation.models.enums import BaseEnums
6
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
7
+
8
+ class BaseResultSchemas:
9
+ class Identifiers(BaseModel):
10
+ id:int = Field(..., ge=1, description="Data's ID, must be >= 1.")
11
+ uuid:UUID = Field(..., description="Data's UUID.")
12
+
13
+ @field_serializer('uuid')
14
+ def serialize_uuid(self, value:UUID, info:FieldSerializationInfo) -> str:
15
+ """Serializes UUID to a hex string."""
16
+ return str(value)
17
+
18
+ class Timestamps(BaseModel):
19
+ created_at:datetime = Field(..., description="Data's created_at timestamp")
20
+ updated_at:datetime = Field(..., description="Data's updated_at timestamp")
21
+ deleted_at:Optional[datetime] = Field(..., description="Data's deleted_at timestamp")
22
+ restored_at:Optional[datetime] = Field(..., description="Data's restored_at timestamp")
23
+ deactivated_at:Optional[datetime] = Field(..., description="Data's deactivated_at timestamp")
24
+ activated_at:datetime = Field(..., description="Data's activated_at timestamp")
25
+
26
+ @field_serializer('created_at', 'updated_at', 'deleted_at', 'restored_at', 'deactivated_at', 'activated_at')
27
+ def serialize_timestamps(self, value:Union[datetime, date], info:FieldSerializationInfo) -> str:
28
+ """Serializes datetime/date fields to ISO format."""
29
+ return value.isoformat()
30
+
31
+ class Status(BaseModel):
32
+ status:BaseEnums.StatusType = Field(..., description="Data's status")
33
+
34
+ class Row(Status, Timestamps, Identifiers):
35
+ @field_validator('*', mode="before")
36
+ def set_none(cls, values):
37
+ if isinstance(values, str) and (values == "" or len(values) == 0):
38
+ return None
39
+ return values
40
+
41
+ @field_serializer('*')
42
+ def serialize_fields(self, value, info:FieldSerializationInfo) -> Any:
43
+ """Serializes all unique-typed fields."""
44
+ if isinstance(value, UUID):
45
+ return str(value)
46
+ if isinstance(value, datetime) or isinstance(value, date):
47
+ return value.isoformat()
48
+ return value
49
+
50
+ class Config:
51
+ from_attributes=True
52
+
53
+ #* ----- ----- ----- Base ----- ----- ----- *#
54
+ class Base(BaseModel):
55
+ success:bool = Field(..., description="Success status")
56
+ code:Optional[str] = Field(None, description="Optional result code")
57
+ message:Optional[str] = Field(None, description="Optional message")
58
+ description:Optional[str] = Field(None, description="Optional description")
59
+ data:Any = Field(..., description="Data")
60
+ other:Optional[Any] = Field(None, description="Optional other information")
61
+
62
+ #* ----- ----- ----- Intermediary ----- ----- ----- *#
63
+ class Fail(Base):
64
+ success:Literal[False] = Field(False, description="Success status")
65
+ data:None = Field(None, description="No data")
66
+
67
+ class Success(Base):
68
+ success:Literal[True] = Field(True, description="Success status")
69
+ data:Any = Field(..., description="Data")
70
+
71
+ #* ----- ----- ----- Derived ----- ----- ----- *#
72
+ class NoData(Success):
73
+ success:Literal[True] = Field(True, description="Success status")
74
+ data:None = Field(None, description="No data")
75
+
76
+ class SingleData(Success):
77
+ success:Literal[True] = Field(True, description="Success status")
78
+ data:Any = Field(..., description="Fetched single data")
79
+
80
+ class UnpaginatedMultipleData(Success):
81
+ success:Literal[True] = Field(True, description="Success status")
82
+ data:list[Any] = Field(..., description="Unpaginated multiple data")
83
+
84
+ class PaginatedMultipleData(
85
+ UnpaginatedMultipleData,
86
+ BaseGeneralSchemas.SimplePagination
87
+ ):
88
+ total_data:int = Field(..., description="Total data count")
89
+ pagination:BaseGeneralSchemas.ExtendedPagination = Field(..., description="Pagination metadata")
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+ from .parameters import BaseParametersTransfers
3
+ from .results import BaseResultsTransfers
4
+
5
+ class BaseTransfers:
6
+ Parameters = BaseParametersTransfers
7
+ Results = BaseResultsTransfers
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+ from .general import BaseGeneralParametersTransfers
3
+ from .service import BaseServiceParametersTransfers
4
+ from .client import BaseClientParametersTransfers
5
+
6
+ class BaseParametersTransfers:
7
+ General = BaseGeneralParametersTransfers
8
+ Service = BaseServiceParametersTransfers
9
+ Client = BaseClientParametersTransfers
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+ from pydantic import model_validator
3
+ from typing import Any
4
+ from typing import Self
5
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
6
+ from maleo_foundation.models.schemas.parameter import BaseParameterSchemas
7
+
8
+ class BaseClientParametersTransfers:
9
+ class GetMultiple(
10
+ BaseParameterSchemas.DateFilters,
11
+ BaseParameterSchemas.SortColumns,
12
+ BaseGeneralSchemas.Search,
13
+ BaseGeneralSchemas.SimplePagination,
14
+ BaseGeneralSchemas.Statuses
15
+ ):
16
+ pass
17
+
18
+ class GetMultipleQuery(
19
+ BaseParameterSchemas.Filters,
20
+ BaseParameterSchemas.Sorts,
21
+ GetMultiple
22
+ ):
23
+ @model_validator(mode="after")
24
+ def set_sort(self) -> Self:
25
+ #* Process sort_columns parameters
26
+ sort = []
27
+ for item in self.sort_columns:
28
+ sort.append(f"{item.name}.{item.order.value}")
29
+
30
+ #* Only update if we have valid sort, otherwise keep the default
31
+ if sort:
32
+ self.sort = sort
33
+
34
+ return self
35
+
36
+ @model_validator(mode="after")
37
+ def set_filter(self) -> Self:
38
+ #* Process filter parameters
39
+ filter = []
40
+ for item in self.date_filters:
41
+ if item.from_date or item.to_date:
42
+ filter_string = item.name
43
+ if item.from_date:
44
+ filter_string += f"|from::{item.from_date.isoformat()}"
45
+ if item.to_date:
46
+ filter_string += f"|to::{item.to_date.isoformat()}"
47
+ filter.append(filter_string)
48
+
49
+ #* Only update if we have valid filter, otherwise keep the default
50
+ if filter:
51
+ self.filter = filter
52
+
53
+ return self
54
+
55
+ def to_query_params(self) -> dict[str, Any]:
56
+ params = {
57
+ "page": self.page,
58
+ "limit": self.limit,
59
+ "search": self.search,
60
+ "sort": self.sort,
61
+ "filter": self.filter,
62
+ }
63
+ if hasattr(self, "statuses") and self.statuses is not None:
64
+ params["statuses"] = self.statuses
65
+ return params
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+ from pydantic import Field
3
+ from typing import Union
4
+ from uuid import UUID
5
+ from maleo_foundation.models.enums import BaseEnums
6
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
7
+
8
+ class BaseGeneralParametersTransfers:
9
+ class GetSingleQuery(BaseGeneralSchemas.Statuses): pass
10
+
11
+ class GetSingle(BaseGeneralSchemas.Statuses):
12
+ identifier:BaseEnums.UniqueIdentifiers = Field(..., description="Identifier")
13
+ value:Union[int, UUID] = Field(..., description="Value")
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+ import re
3
+ import urllib.parse
4
+ from datetime import datetime
5
+ from pydantic import BaseModel, Field, field_validator, model_validator
6
+ from typing import Optional, Self, Any
7
+ from maleo_foundation.models.enums import BaseEnums
8
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
9
+ from maleo_foundation.models.schemas.parameter import BaseParameterSchemas
10
+ from maleo_foundation.constants import SORT_COLUMN_PATTERN, DATE_FILTER_PATTERN
11
+
12
+ class BaseServiceParametersTransfers:
13
+ class GetMultipleQuery(
14
+ BaseParameterSchemas.Filters,
15
+ BaseParameterSchemas.Sorts,
16
+ BaseGeneralSchemas.Search,
17
+ BaseGeneralSchemas.SimplePagination,
18
+ BaseGeneralSchemas.Statuses
19
+ ):
20
+ @field_validator("sort")
21
+ @classmethod
22
+ def validate_sort_columns(cls, values):
23
+ if not isinstance(values, list):
24
+ return ["id.asc"]
25
+ return [value for value in values if SORT_COLUMN_PATTERN.match(value)]
26
+
27
+ @field_validator("filter")
28
+ @classmethod
29
+ def validate_date_filters(cls, values):
30
+ if isinstance(values, list):
31
+ decoded_values = [urllib.parse.unquote(value) for value in values]
32
+ #* Replace space followed by 2 digits, colon, 2 digits with + and those digits
33
+ fixed_values = []
34
+ for value in decoded_values:
35
+ #* Look for the pattern: space followed by 2 digits, colon, 2 digits
36
+ fixed_value = re.sub(r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+) (\d{2}:\d{2})', r'\1+\2', value)
37
+ fixed_values.append(fixed_value)
38
+ final_values = [value for value in fixed_values if DATE_FILTER_PATTERN.match(value)]
39
+ return final_values
40
+
41
+ class GetMultiple(
42
+ BaseParameterSchemas.DateFilters,
43
+ BaseParameterSchemas.SortColumns,
44
+ GetMultipleQuery
45
+ ):
46
+ @model_validator(mode="after")
47
+ def set_sort_columns(self) -> Self:
48
+ #* Process sort parameters
49
+ sort_columns = []
50
+ for item in self.sorts:
51
+ parts = item.split('.')
52
+ if len(parts) == 2 and parts[1].lower() in ["asc", "desc"]:
53
+ try:
54
+ sort_columns.append(BaseGeneralSchemas.SortColumn(name=parts[0], order=BaseEnums.SortOrder(parts[1].lower())))
55
+ except ValueError:
56
+ continue
57
+
58
+ #* Only update if we have valid sort columns, otherwise keep the default
59
+ if sort_columns:
60
+ self.sort_columns = sort_columns
61
+ return self
62
+
63
+ @model_validator(mode="after")
64
+ def set_date_filters(self) -> Self:
65
+ #* Process filter parameters
66
+ date_filters = []
67
+ for filter_item in self.filters:
68
+ parts = filter_item.split('|')
69
+ if len(parts) >= 2 and parts[0]:
70
+ name = parts[0]
71
+ from_date = None
72
+ to_date = None
73
+
74
+ #* Process each part to extract from and to dates
75
+ for part in parts[1:]:
76
+ if part.startswith('from::'):
77
+ try:
78
+ from_date_str = part.replace('from::', '')
79
+ from_date = datetime.fromisoformat(from_date_str)
80
+ except ValueError:
81
+ continue
82
+ elif part.startswith('to::'):
83
+ try:
84
+ to_date_str = part.replace('to::', '')
85
+ to_date = datetime.fromisoformat(to_date_str)
86
+ except ValueError:
87
+ continue
88
+
89
+ #* Only add filter if at least one date is specified
90
+ if from_date or to_date:
91
+ date_filters.append(BaseGeneralSchemas.DateFilter(name=name, from_date=from_date, to_date=to_date))
92
+
93
+ #* Update date_filters
94
+ self.date_filters = date_filters
95
+ return self
96
+
97
+ class UniqueFieldCheck(BaseModel):
98
+ operation:BaseEnums.OperationType = Field(..., description="Operation to be conducted")
99
+ field:BaseEnums.UniqueIdentifiers = Field(..., description="Field to be checked")
100
+ new_value:Optional[Any] = Field(..., description="New field's value")
101
+ old_value:Optional[Any] = Field(None, description="Old field's value")
102
+ nullable:bool = Field(False, description="Whether to allow null field's value")
103
+ suggestion:Optional[str] = Field(None, description="Suggestion on discrepancy")
104
+
105
+ UniqueFieldChecks = list[UniqueFieldCheck]
106
+
107
+ status_update_criterias:dict[
108
+ BaseEnums.StatusUpdateAction,
109
+ Optional[list[BaseEnums.StatusType]]
110
+ ] = {
111
+ BaseEnums.StatusUpdateAction.DELETE: None,
112
+ BaseEnums.StatusUpdateAction.RESTORE: None,
113
+ BaseEnums.StatusUpdateAction.DEACTIVATE: [
114
+ BaseEnums.StatusType.INACTIVE,
115
+ BaseEnums.StatusType.ACTIVE,
116
+ ],
117
+ BaseEnums.StatusUpdateAction.ACTIVATE: [
118
+ BaseEnums.StatusType.INACTIVE,
119
+ BaseEnums.StatusType.ACTIVE,
120
+ ]
121
+ }
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+ from .service import BaseServiceResultsTransfers
3
+ from .client import BaseClientResultsTransfers
4
+
5
+ class BaseResultsTransfers:
6
+ Service = BaseServiceResultsTransfers
7
+ Client = BaseClientResultsTransfers
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+ from .service import BaseClientServiceResultsTransfers
3
+ from .controllers import BaseClientControllersResultsTransfers
4
+
5
+ class BaseClientResultsTransfers:
6
+ Service = BaseClientServiceResultsTransfers
7
+ Controller = BaseClientControllersResultsTransfers
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+ from .http import BaseClientHTTPControllerResults
3
+
4
+ class BaseClientControllersResultsTransfers:
5
+ HTTP = BaseClientHTTPControllerResults
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+ from httpx import Response
3
+ from pydantic import BaseModel, Field, model_validator
4
+ from typing import Any
5
+
6
+ class BaseClientHTTPControllerResults(BaseModel):
7
+ response:Response = Field(..., description="Client's HTTP Controller response")
8
+ status_code:int = Field(..., description="Client's HTTP Controller response status code")
9
+ content:Any = Field(..., description="Client's HTTP Controller response content")
10
+ success:bool = Field(..., description="Client's HTTP Controller success status")
11
+
12
+ class Config:
13
+ arbitrary_types_allowed=True
14
+
15
+ @model_validator(mode="before")
16
+ @classmethod
17
+ def process_response(cls, values:dict) -> dict:
18
+ """Process the response to set status_code, content, and success."""
19
+ response:Response = values.get("response")
20
+
21
+ if response:
22
+ values["status_code"] = response.status_code
23
+ values["success"] = response.is_success
24
+
25
+ #* Determine content type and parse accordingly
26
+ content_type:str = response.headers.get("content-type", "")
27
+ content_type = content_type.lower()
28
+ if "application/json" in content_type:
29
+ values["content"] = response.json()
30
+ elif "text/" in content_type or "application/xml" in content_type:
31
+ values["content"] = response.text
32
+ else:
33
+ values["content"] = response.content #* Raw bytes for unknown types
34
+
35
+ return values
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+ from pydantic import model_validator
3
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
4
+ from maleo_foundation.models.schemas.result import BaseResultSchemas
5
+
6
+ class BaseClientServiceResultsTransfers:
7
+ class Fail(BaseResultSchemas.Fail): pass
8
+
9
+ class NoData(BaseResultSchemas.NoData): pass
10
+
11
+ class SingleData(BaseResultSchemas.SingleData): pass
12
+
13
+ class UnpaginatedMultipleData(BaseResultSchemas.UnpaginatedMultipleData): pass
14
+
15
+ class PaginatedMultipleData(BaseResultSchemas.PaginatedMultipleData):
16
+ @model_validator(mode="before")
17
+ @classmethod
18
+ def calculate_pagination_component(cls, values: dict) -> dict:
19
+ """Extracts pagination components (page, limit, total_data) before validation."""
20
+ pagination = values.get("pagination")
21
+
22
+ if isinstance(pagination, BaseGeneralSchemas.ExtendedPagination):
23
+ values["page"] = pagination.page
24
+ values["limit"] = pagination.limit
25
+ values["total_data"] = pagination.total_data
26
+
27
+ return values
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+ from .general import BaseServiceGeneralResultsTransfers
3
+ from .query import BaseServiceQueryResultsTransfers
4
+ from .controllers import BaseServiceControllerResultsTransfers
5
+
6
+ class BaseServiceResultsTransfers:
7
+ General = BaseServiceGeneralResultsTransfers
8
+ Query = BaseServiceQueryResultsTransfers
9
+ Controller = BaseServiceControllerResultsTransfers
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+ from .rest import BaseServiceRESTControllerResults
3
+
4
+ class BaseServiceControllerResultsTransfers:
5
+ REST = BaseServiceRESTControllerResults