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.
- maleo_foundation/__init__.py +0 -0
- maleo_foundation/clients/__init__.py +7 -0
- maleo_foundation/clients/general/__init__.py +4 -0
- maleo_foundation/clients/general/http.py +41 -0
- maleo_foundation/clients/google/__init__.py +4 -0
- maleo_foundation/clients/google/cloud/__init__.py +8 -0
- maleo_foundation/clients/google/cloud/logging.py +45 -0
- maleo_foundation/clients/google/cloud/secret.py +92 -0
- maleo_foundation/clients/google/cloud/storage.py +77 -0
- maleo_foundation/constants.py +8 -0
- maleo_foundation/controller.py +50 -0
- maleo_foundation/db/__init__.py +3 -0
- maleo_foundation/db/database.py +41 -0
- maleo_foundation/db/engine.py +40 -0
- maleo_foundation/db/session.py +64 -0
- maleo_foundation/models/__init__.py +13 -0
- maleo_foundation/models/enums.py +66 -0
- maleo_foundation/models/responses.py +99 -0
- maleo_foundation/models/schemas/__init__.py +9 -0
- maleo_foundation/models/schemas/general.py +105 -0
- maleo_foundation/models/schemas/parameter.py +16 -0
- maleo_foundation/models/schemas/result.py +89 -0
- maleo_foundation/models/transfers/__init__.py +7 -0
- maleo_foundation/models/transfers/parameters/__init__.py +9 -0
- maleo_foundation/models/transfers/parameters/client.py +65 -0
- maleo_foundation/models/transfers/parameters/general.py +13 -0
- maleo_foundation/models/transfers/parameters/service.py +121 -0
- maleo_foundation/models/transfers/results/__init__.py +7 -0
- maleo_foundation/models/transfers/results/client/__init__.py +7 -0
- maleo_foundation/models/transfers/results/client/controllers/__init__.py +5 -0
- maleo_foundation/models/transfers/results/client/controllers/http.py +35 -0
- maleo_foundation/models/transfers/results/client/service.py +27 -0
- maleo_foundation/models/transfers/results/service/__init__.py +9 -0
- maleo_foundation/models/transfers/results/service/controllers/__init__.py +5 -0
- maleo_foundation/models/transfers/results/service/controllers/rest.py +22 -0
- maleo_foundation/models/transfers/results/service/general.py +38 -0
- maleo_foundation/models/transfers/results/service/query.py +42 -0
- maleo_foundation/models/types/__init__.py +9 -0
- maleo_foundation/models/types/client.py +40 -0
- maleo_foundation/models/types/query.py +40 -0
- maleo_foundation/models/types/service.py +40 -0
- maleo_foundation/utils/__init__.py +9 -0
- maleo_foundation/utils/exceptions.py +74 -0
- maleo_foundation/utils/formatter/__init__.py +4 -0
- maleo_foundation/utils/formatter/case.py +37 -0
- maleo_foundation/utils/logger.py +68 -0
- {maleo_foundation-0.0.1.dist-info → maleo_foundation-0.0.2.dist-info}/METADATA +1 -1
- maleo_foundation-0.0.2.dist-info/RECORD +50 -0
- maleo_foundation-0.0.2.dist-info/top_level.txt +1 -0
- maleo_foundation-0.0.1.dist-info/RECORD +0 -4
- maleo_foundation-0.0.1.dist-info/top_level.txt +0 -1
- {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,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 BaseClientServiceResultsTransfers
|
3
|
+
from .controllers import BaseClientControllersResultsTransfers
|
4
|
+
|
5
|
+
class BaseClientResultsTransfers:
|
6
|
+
Service = BaseClientServiceResultsTransfers
|
7
|
+
Controller = BaseClientControllersResultsTransfers
|
@@ -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
|