maleo-foundation 0.3.4__py3-none-any.whl → 0.3.6__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 (28) hide show
  1. maleo_foundation/constants.py +3 -3
  2. maleo_foundation/enums.py +18 -5
  3. maleo_foundation/managers/cache.py +9 -0
  4. maleo_foundation/managers/client/google/subscription.py +140 -0
  5. maleo_foundation/managers/configuration.py +102 -0
  6. maleo_foundation/managers/credential.py +82 -0
  7. maleo_foundation/managers/middleware.py +5 -37
  8. maleo_foundation/managers/service.py +62 -237
  9. maleo_foundation/middlewares/authentication.py +1 -7
  10. maleo_foundation/models/schemas/parameter.py +1 -1
  11. maleo_foundation/models/transfers/general/configurations/__init__.py +45 -0
  12. maleo_foundation/{managers → models/transfers/general/configurations}/cache/__init__.py +1 -8
  13. maleo_foundation/models/transfers/general/configurations/client/__init__.py +8 -0
  14. maleo_foundation/models/transfers/general/configurations/client/maleo.py +23 -0
  15. maleo_foundation/models/transfers/general/configurations/database.py +12 -0
  16. maleo_foundation/models/transfers/general/configurations/middleware.py +60 -0
  17. maleo_foundation/models/transfers/general/configurations/service.py +7 -0
  18. maleo_foundation/models/transfers/general/credentials.py +9 -0
  19. maleo_foundation/models/transfers/general/settings.py +19 -0
  20. maleo_foundation/utils/cache.py +27 -0
  21. maleo_foundation/utils/loaders/credential/google.py +145 -12
  22. maleo_foundation/utils/logging.py +1 -18
  23. {maleo_foundation-0.3.4.dist-info → maleo_foundation-0.3.6.dist-info}/METADATA +5 -2
  24. {maleo_foundation-0.3.4.dist-info → maleo_foundation-0.3.6.dist-info}/RECORD +27 -15
  25. maleo_foundation/managers/cache/base.py +0 -29
  26. /maleo_foundation/{managers → models/transfers/general/configurations}/cache/redis.py +0 -0
  27. {maleo_foundation-0.3.4.dist-info → maleo_foundation-0.3.6.dist-info}/WHEEL +0 -0
  28. {maleo_foundation-0.3.4.dist-info → maleo_foundation-0.3.6.dist-info}/top_level.txt +0 -0
@@ -26,11 +26,11 @@ STATUS_UPDATE_CRITERIAS: dict[
26
26
  ]
27
27
  }
28
28
  IDENTIFIER_TYPE_VALUE_TYPE_MAP: dict[
29
- BaseEnums.IdentifierTypes,
29
+ BaseEnums.IdentifierType,
30
30
  object
31
31
  ] = {
32
- BaseEnums.IdentifierTypes.ID: int,
33
- BaseEnums.IdentifierTypes.UUID: UUID
32
+ BaseEnums.IdentifierType.ID: int,
33
+ BaseEnums.IdentifierType.UUID: UUID
34
34
  }
35
35
  ALL_STATUSES: List[BaseEnums.StatusType] = [
36
36
  BaseEnums.StatusType.ACTIVE,
maleo_foundation/enums.py CHANGED
@@ -16,6 +16,22 @@ class BaseEnums:
16
16
  CORE = "core"
17
17
  AI = "ai"
18
18
 
19
+ class ShortService(StrEnum):
20
+ STUDIO = "studio"
21
+ NEXUS = "nexus"
22
+ TELEMETRY = "telemetry"
23
+ METADATA = "metadata"
24
+ IDENTITY = "identity"
25
+ ACCESS = "access"
26
+ WORKSHOP = "workshop"
27
+ SOAPIE = "soapie"
28
+ MEDIX = "medix"
29
+ DICOM = "dicom"
30
+ SCRIBE = "scribe"
31
+ CDS = "cds"
32
+ IMAGING = "imaging"
33
+ MCU = "mcu"
34
+
19
35
  class Service(StrEnum):
20
36
  MALEO_STUDIO = "maleo-studio"
21
37
  MALEO_NEXUS = "maleo-nexus"
@@ -66,12 +82,13 @@ class BaseEnums:
66
82
  RESTORE = "restore"
67
83
  DELETE = "delete"
68
84
 
69
- class IdentifierTypes(StrEnum):
85
+ class IdentifierType(StrEnum):
70
86
  ID = "id"
71
87
  UUID = "uuid"
72
88
 
73
89
  class ServiceControllerType(StrEnum):
74
90
  REST = "rest"
91
+ MESSAGE = "message"
75
92
 
76
93
  class ClientControllerType(StrEnum):
77
94
  HTTP = "http"
@@ -109,10 +126,6 @@ class BaseEnums:
109
126
  BaseEnums.RESTControllerResponseType.FILE: responses.FileResponse,
110
127
  }.get(self, responses.Response)
111
128
 
112
- class MiddlewareLoggerType(StrEnum):
113
- BASE = "base"
114
- AUTHENTICATION = "authentication"
115
-
116
129
  class ServiceLoggerType(StrEnum):
117
130
  REPOSITORY = "repository"
118
131
  DATABASE = "database"
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel, Field
3
+ from redis.asyncio.client import Redis
4
+
5
+ class CacheManagers(BaseModel):
6
+ redis:Redis = Field(..., description="Redis client")
7
+
8
+ class Config:
9
+ arbitrary_types_allowed=True
@@ -0,0 +1,140 @@
1
+ import asyncio
2
+ import inspect
3
+ from google.cloud import pubsub_v1
4
+ from google.cloud.pubsub_v1.subscriber.futures import StreamingPullFuture
5
+ from google.cloud.pubsub_v1.subscriber.message import Message
6
+ from google.oauth2.service_account import Credentials
7
+ from pathlib import Path
8
+ from pydantic import BaseModel
9
+ from typing import Awaitable, Callable, Dict, List, Optional, Union
10
+ from maleo_foundation.managers.client.google.base import GoogleClientManager
11
+
12
+ SyncController = Callable[[str, Message], bool]
13
+ AsyncController = Callable[[str, Message], Awaitable[bool]]
14
+ Controller = Union[SyncController, AsyncController]
15
+ OptionalController = Optional[Controller]
16
+
17
+ class SubscriptionConfigurations(BaseModel):
18
+ subscription_name: str
19
+ max_messages: int = 10
20
+ ack_deadline: int = 30
21
+ controller: OptionalController = None
22
+
23
+ class SubscriptionManager(GoogleClientManager):
24
+ def __init__(
25
+ self,
26
+ subscriptions: List[SubscriptionConfigurations],
27
+ log_config,
28
+ service_key: Optional[str] = None,
29
+ credentials: Optional[Credentials] = None,
30
+ credentials_path: Optional[Union[Path, str]] = None,
31
+ ):
32
+ key = "google-subscription-manager"
33
+ name = "GoogleSubscriptionManager"
34
+ super().__init__(key, name, log_config, service_key, credentials, credentials_path)
35
+ self.subscriber = pubsub_v1.SubscriberClient(credentials=self._credentials)
36
+ self.subscriptions = subscriptions
37
+ self.active_listeners: Dict[str, StreamingPullFuture] = {}
38
+ self.loop: Optional[asyncio.AbstractEventLoop] = None
39
+
40
+ async def _handle_async_controller(
41
+ self,
42
+ controller: AsyncController,
43
+ subscription_name: str,
44
+ message: Message
45
+ ) -> None:
46
+ success = await controller(subscription_name, message)
47
+ message.ack() if success else message.nack()
48
+
49
+ def _handle_sync_controller(
50
+ self,
51
+ controller: SyncController,
52
+ subscription_name: str,
53
+ message: Message
54
+ ) -> None:
55
+ success = controller(subscription_name, message)
56
+ message.ack() if success else message.nack()
57
+
58
+ def _message_callback(
59
+ self,
60
+ controller: OptionalController,
61
+ subscription_name: str,
62
+ message: Message
63
+ ):
64
+ # If controller is not given, conduct default message processing
65
+ if controller is None:
66
+ self._default_message_processing(subscription_name, message)
67
+ return
68
+
69
+ # Check controller function type and handle accordingly
70
+ is_async_controller = inspect.iscoroutinefunction(controller)
71
+ if is_async_controller:
72
+ if not self.loop:
73
+ raise RuntimeError("Event loop not set in SubscriptionManager")
74
+ asyncio.run_coroutine_threadsafe(
75
+ self._handle_async_controller(
76
+ controller,
77
+ subscription_name,
78
+ message
79
+ ),
80
+ self.loop
81
+ )
82
+ else:
83
+ self._handle_sync_controller(
84
+ controller,
85
+ subscription_name,
86
+ message
87
+ )
88
+
89
+ def _default_message_processing(
90
+ self,
91
+ subscription_name: str,
92
+ message: Message
93
+ ) -> None:
94
+ try:
95
+ self._logger.info("Default message processing for subscription '%s': %s", subscription_name, message.data.decode("utf-8"))
96
+ message.ack()
97
+ except Exception as e:
98
+ self._logger.error("Error handling message through default processor: %s", e, exc_info=True)
99
+ message.nack()
100
+
101
+ async def _start_subscription_listener(
102
+ self,
103
+ config: SubscriptionConfigurations
104
+ ) -> None:
105
+ subscription_path = self.subscriber.subscription_path(self.credentials.project_id, config.subscription_name)
106
+ flow_control = pubsub_v1.types.FlowControl(max_messages=config.max_messages)
107
+ future = self.subscriber.subscribe(
108
+ subscription_path,
109
+ callback=lambda message: self._message_callback(
110
+ config.controller,
111
+ config.subscription_name,
112
+ message
113
+ ),
114
+ flow_control=flow_control,
115
+ await_callbacks_on_shutdown=True
116
+ )
117
+ self.active_listeners[subscription_path] = future
118
+ try:
119
+ await asyncio.get_event_loop().run_in_executor(None, future.result)
120
+ except Exception as e:
121
+ if not isinstance(e, asyncio.CancelledError):
122
+ self._logger.error("Listener error for subscription '%s': %s", config.subscription_name, e, exc_info=True)
123
+
124
+ async def start_listeners(
125
+ self,
126
+ loop: asyncio.AbstractEventLoop
127
+ ) -> None:
128
+ self.loop = loop
129
+ for config in self.subscriptions:
130
+ asyncio.create_task(self._start_subscription_listener(config))
131
+ await asyncio.sleep(0.1)
132
+
133
+ async def stop_listeners(self):
134
+ for future in self.active_listeners.values():
135
+ future.cancel()
136
+ try:
137
+ future.result()
138
+ except Exception:
139
+ pass
140
+ self.active_listeners.clear()
@@ -0,0 +1,102 @@
1
+ from pathlib import Path
2
+ from maleo_foundation.models.transfers.general.configurations.cache.redis import (
3
+ RedisCacheNamespaces,
4
+ RedisCacheConfigurations
5
+ )
6
+ from maleo_foundation.models.transfers.general.configurations.cache \
7
+ import CacheConfigurations
8
+ from maleo_foundation.models.transfers.general.configurations.database \
9
+ import DatabaseConfigurations
10
+ from maleo_foundation.models.transfers.general.configurations import (
11
+ RuntimeConfigurations,
12
+ StaticConfigurations,
13
+ Configurations
14
+ )
15
+ from maleo_foundation.models.transfers.general.settings import Settings
16
+ from maleo_foundation.utils.loaders.yaml import YAMLLoader
17
+ from maleo_foundation.utils.merger import deep_merge
18
+ from .credential import CredentialManager
19
+
20
+ class ConfigurationManager:
21
+ def __init__(
22
+ self,
23
+ settings: Settings,
24
+ credential_manager: CredentialManager
25
+ ):
26
+ self.settings = settings
27
+ self.credential_manager = credential_manager
28
+
29
+ self._load_configs()
30
+
31
+ def _load_static_configurations(self) -> StaticConfigurations:
32
+ config_path = Path(self.settings.STATIC_CONFIGURATIONS_PATH)
33
+
34
+ if config_path.exists() and config_path.is_file():
35
+ data = YAMLLoader.load_from_path(str(config_path))
36
+ else:
37
+ secret_data = self.credential_manager.secret_manager.get(
38
+ f"maleo-static-config-{self.settings.ENVIRONMENT}"
39
+ )
40
+ data = YAMLLoader.load_from_string(secret_data)
41
+
42
+ return StaticConfigurations.model_validate(data)
43
+
44
+ def _load_runtime_configurations(self) -> RuntimeConfigurations:
45
+ config_path = Path(self.settings.RUNTIME_CONFIGURATIONS_PATH)
46
+
47
+ if config_path.exists() and config_path.is_file():
48
+ data = YAMLLoader.load_from_path(str(config_path))
49
+ else:
50
+ secret_data = self.credential_manager.secret_manager.get(
51
+ f"{self.settings.SERVICE_KEY}-runtime-config-{self.settings.ENVIRONMENT}"
52
+ )
53
+ data = YAMLLoader.load_from_string(secret_data)
54
+
55
+ return RuntimeConfigurations.model_validate(data)
56
+
57
+ def _load_cache_configurations(self) -> CacheConfigurations:
58
+ namespaces = RedisCacheNamespaces(base=self.settings.SERVICE_KEY)
59
+ host = self.credential_manager.secret_manager.get(
60
+ f"maleo-redis-host-{self.settings.ENVIRONMENT}"
61
+ )
62
+ password = self.credential_manager.secret_manager.get(
63
+ f"maleo-redis-password-{self.settings.ENVIRONMENT}"
64
+ )
65
+ redis = RedisCacheConfigurations(
66
+ namespaces=namespaces,
67
+ host=host,
68
+ password=password
69
+ )
70
+ return CacheConfigurations(redis=redis)
71
+
72
+ def _load_database_configurations(self, database_name: str) -> DatabaseConfigurations:
73
+ password = self.credential_manager.secret_manager.get(
74
+ f"maleo-db-password-{self.settings.ENVIRONMENT}"
75
+ )
76
+ host = self.credential_manager.secret_manager.get(
77
+ f"maleo-db-host-{self.settings.ENVIRONMENT}"
78
+ )
79
+ return DatabaseConfigurations(
80
+ password=password,
81
+ host=host,
82
+ database=database_name
83
+ )
84
+
85
+ def _load_configs(self) -> None:
86
+ static_configs = self._load_static_configurations()
87
+ runtime_configs = self._load_runtime_configurations()
88
+ cache_configs = self._load_cache_configurations()
89
+ database_configs = self._load_database_configurations(runtime_configs.database)
90
+
91
+ merged_configs = deep_merge(
92
+ static_configs.model_dump(),
93
+ runtime_configs.model_dump(exclude={"database"}),
94
+ {"cache": cache_configs.model_dump()},
95
+ {"database": database_configs.model_dump()}
96
+ )
97
+
98
+ self._configs = Configurations.model_validate(merged_configs)
99
+
100
+ @property
101
+ def configs(self) -> Configurations:
102
+ return self._configs
@@ -0,0 +1,82 @@
1
+ from google.oauth2.service_account import Credentials
2
+ from uuid import UUID
3
+ from maleo_foundation.enums import BaseEnums
4
+ from maleo_foundation.managers.client.google.secret import GoogleSecretManager
5
+ from maleo_foundation.models.transfers.general.credentials import MaleoCredentials
6
+ from maleo_foundation.models.transfers.general.settings import Settings
7
+ from maleo_foundation.utils.loaders.credential.google import GoogleCredentialsLoader
8
+ from maleo_foundation.utils.logging import SimpleConfig
9
+
10
+ class CredentialManager:
11
+ def __init__(
12
+ self,
13
+ settings: Settings,
14
+ log_config: SimpleConfig
15
+ ):
16
+ self.settings = settings
17
+ self.log_config = log_config
18
+ self._initialize()
19
+
20
+ def _load_google_credentials(self) -> None:
21
+ """Load Google service account credentials with validation."""
22
+ try:
23
+ self._google_credentials = GoogleCredentialsLoader.load(
24
+ credentials_path=self.settings.GOOGLE_CREDENTIALS_PATH
25
+ )
26
+
27
+ # Validate the loaded credentials
28
+ GoogleCredentialsLoader.validate_credentials(self._google_credentials)
29
+
30
+ except Exception as e:
31
+ raise RuntimeError(f"Failed to load Google credentials: {str(e)}")
32
+
33
+ def _initialize_secret_manager(self) -> None:
34
+ self._secret_manager = GoogleSecretManager(
35
+ log_config=self.log_config,
36
+ service_key=self.settings.SERVICE_KEY,
37
+ credentials=self._google_credentials
38
+ )
39
+
40
+ def _get_environment_for_credentials(self) -> str:
41
+ return (
42
+ BaseEnums.EnvironmentType.STAGING
43
+ if self.settings.ENVIRONMENT == BaseEnums.EnvironmentType.LOCAL
44
+ else self.settings.ENVIRONMENT
45
+ )
46
+
47
+ def _load_maleo_credentials(self) -> None:
48
+ environment = self._get_environment_for_credentials()
49
+
50
+ try:
51
+ id = int(self._secret_manager.get(f"maleo-service-account-id-{environment}"))
52
+ uuid = UUID(self._secret_manager.get(f"maleo-service-account-uuid-{environment}"))
53
+ email = self._secret_manager.get("maleo-service-account-email")
54
+ username = self._secret_manager.get("maleo-service-account-username")
55
+ password = self._secret_manager.get("maleo-service-account-password")
56
+
57
+ self._maleo_credentials = MaleoCredentials(
58
+ id=id,
59
+ uuid=uuid,
60
+ username=username,
61
+ email=email,
62
+ password=password
63
+ )
64
+ except Exception as e:
65
+ raise RuntimeError(f"Failed to load Maleo credentials: {str(e)}")
66
+
67
+ def _initialize(self):
68
+ self._load_google_credentials()
69
+ self._initialize_secret_manager()
70
+ self._load_maleo_credentials()
71
+
72
+ @property
73
+ def google_credentials(self) -> Credentials:
74
+ return self._google_credentials
75
+
76
+ @property
77
+ def secret_manager(self) -> GoogleSecretManager:
78
+ return self._secret_manager
79
+
80
+ @property
81
+ def maleo_credentials(self) -> MaleoCredentials:
82
+ return self._maleo_credentials
@@ -1,57 +1,26 @@
1
1
  from fastapi import FastAPI
2
- from pydantic import BaseModel, Field
3
- from typing import List
4
2
  from maleo_foundation.client.manager import MaleoFoundationClientManager
5
3
  from maleo_foundation.models.schemas import BaseGeneralSchemas
4
+ from maleo_foundation.models.transfers.general.configurations.middleware \
5
+ import MiddlewareConfigurations
6
6
  from maleo_foundation.middlewares.authentication import add_authentication_middleware
7
7
  from maleo_foundation.middlewares.base import add_base_middleware
8
8
  from maleo_foundation.middlewares.cors import add_cors_middleware
9
9
  from maleo_foundation.utils.logging import MiddlewareLogger
10
10
 
11
- _ALLOW_METHODS: List[str] = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
12
- _ALLOW_HEADERS: List[str] = ["X-Organization", "X-User", "X-Signature"]
13
- _EXPOSE_HEADERS: List[str] = ["X-Request-Timestamp", "X-Response-Timestamp", "X-Process-Time", "X-Signature"]
14
-
15
- class GeneralMiddlewareConfigurations(BaseModel):
16
- allow_origins: List[str] = Field(default_factory=list, description="Allowed origins")
17
- allow_methods: List[str] = Field(_ALLOW_METHODS, description="Allowed methods")
18
- allow_headers: list[str] = Field(_ALLOW_HEADERS, description="Allowed headers")
19
- allow_credentials: bool = Field(True, description="Allowed credentials")
20
-
21
- class CORSMiddlewareConfigurations(BaseModel):
22
- expose_headers: List[str] = Field(_EXPOSE_HEADERS, description="Exposed headers")
23
-
24
- class BaseMiddlewareConfigurations(BaseModel):
25
- limit: int = Field(10, description="Request limit (per 'window' seconds)")
26
- window: int = Field(1, description="Request limit window (seconds)")
27
- cleanup_interval: int = Field(60, description="Interval for middleware cleanup (seconds)")
28
- ip_timeout: int = Field(300, description="Idle IP's timeout (seconds)")
29
-
30
- class MiddlewareConfigurations(BaseModel):
31
- general: GeneralMiddlewareConfigurations = Field(..., description="Middleware's general configurations")
32
- cors: CORSMiddlewareConfigurations = Field(..., description="CORS middleware's configurations")
33
- base: BaseMiddlewareConfigurations = Field(..., description="Base middleware's configurations")
34
-
35
- class MiddlewareLoggers(BaseModel):
36
- base: MiddlewareLogger = Field(..., description="Base middleware's logger")
37
- authentication: MiddlewareLogger = Field(..., description="Authentication middleware's logger")
38
-
39
- class Config:
40
- arbitrary_types_allowed=True
41
-
42
11
  class MiddlewareManager:
43
12
  def __init__(
44
13
  self,
45
14
  app: FastAPI,
46
15
  configurations: MiddlewareConfigurations,
47
16
  keys: BaseGeneralSchemas.RSAKeys,
48
- loggers: MiddlewareLoggers,
17
+ logger: MiddlewareLogger,
49
18
  maleo_foundation: MaleoFoundationClientManager
50
19
  ):
51
20
  self._app = app
52
21
  self._configurations = configurations
53
22
  self._keys = keys
54
- self._loggers = loggers
23
+ self._logger = logger
55
24
  self._maleo_foundation = maleo_foundation
56
25
 
57
26
  def add_all(self):
@@ -73,7 +42,7 @@ class MiddlewareManager:
73
42
  add_base_middleware(
74
43
  app=self._app,
75
44
  keys=self._keys,
76
- logger=self._loggers.base,
45
+ logger=self._logger,
77
46
  maleo_foundation=self._maleo_foundation,
78
47
  allow_origins=self._configurations.general.allow_origins,
79
48
  allow_methods=self._configurations.general.allow_methods,
@@ -89,6 +58,5 @@ class MiddlewareManager:
89
58
  add_authentication_middleware(
90
59
  app=self._app,
91
60
  keys=self._keys,
92
- logger=self._loggers.authentication,
93
61
  maleo_foundation=self._maleo_foundation
94
62
  )