trovesuite 1.0.9__py3-none-any.whl → 1.0.11__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.
@@ -1,153 +1,57 @@
1
1
  import os
2
- import warnings
3
- from typing import Optional
4
-
5
2
  class Settings:
6
- """Settings configuration for TroveSuite Auth Service"""
7
3
 
8
- # =============================================================================
9
- # DATABASE CONFIGURATION
10
- # =============================================================================
11
- DATABASE_URL: str = os.getenv(
12
- "DATABASE_URL",
13
- "postgresql://username:password@localhost:5432/database_name"
14
- )
4
+ # Database URL
5
+ DATABASE_URL: str = os.getenv("DATABASE_URL")
15
6
 
16
- # Alternative database configuration
17
- DB_USER: Optional[str] = os.getenv("DB_USER")
18
- DB_HOST: Optional[str] = os.getenv("DB_HOST")
19
- DB_NAME: Optional[str] = os.getenv("DB_NAME")
20
- DB_PORT: int = int(os.getenv("DB_PORT", "5432"))
21
- DB_PASSWORD: Optional[str] = os.getenv("DB_PASSWORD")
22
- ENVIRONMENT: str = os.getenv("ENVIRONMENT", "development")
7
+ DB_USER: str = os.getenv("DB_USER")
8
+ DB_HOST: str = os.getenv("DB_HOST")
9
+ DB_NAME: str = os.getenv("DB_NAME")
10
+ DB_PORT: str = os.getenv("DB_PORT")
11
+ DB_PASSWORD: str = os.getenv("DB_PASSWORD")
23
12
 
24
- # =============================================================================
25
- # APPLICATION SETTINGS
26
- # =============================================================================
27
- APP_NAME: str = os.getenv("APP_NAME", "TroveSuite Auth Service")
28
- DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
29
-
30
- # =============================================================================
31
- # SECURITY SETTINGS
32
- # =============================================================================
33
- ALGORITHM: str = os.getenv("ALGORITHM", "HS256")
34
- SECRET_KEY: str = os.getenv("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
35
- ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60"))
13
+ # Application settings
14
+ APP_NAME: str = os.getenv("APP_NAME", "Python Template API")
15
+ DEBUG: bool = os.getenv("DEBUG", "True").lower() in ("true",1)
16
+ APP_VERSION: str = os.getenv("APP_VERSION", "1.0.0")
36
17
 
37
- # =============================================================================
38
- # LOGGING SETTINGS
39
- # =============================================================================
18
+ # Logging settings
40
19
  LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
41
20
  LOG_FORMAT: str = os.getenv("LOG_FORMAT", "detailed") # detailed, json, simple
42
- LOG_TO_FILE: bool = os.getenv("LOG_TO_FILE", "False").lower() == "false"
21
+ LOG_TO_FILE: bool = os.getenv("LOG_TO_FILE", "False").lower() in ("true", 1)
43
22
  LOG_MAX_SIZE: int = int(os.getenv("LOG_MAX_SIZE", "10485760")) # 10MB
44
23
  LOG_BACKUP_COUNT: int = int(os.getenv("LOG_BACKUP_COUNT", "5"))
45
24
  LOG_DIR: str = os.getenv("LOG_DIR", "logs")
46
-
25
+
26
+ # Security settings
27
+ ENVIRONMENT: str = os.getenv("ENVIRONMENT")
28
+ ALGORITHM: str = os.getenv("ALGORITHM")
29
+ SECRET_KEY: str = os.getenv("SECRET_KEY")
30
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "120"))
31
+
47
32
  # =============================================================================
48
- # DATABASE TABLE NAMES
33
+ # SHARED TABLES (main schema)
49
34
  # =============================================================================
50
- # Main schema tables
51
- MAIN_TENANTS_TABLE: str = os.getenv("MAIN_TENANTS_TABLE", "tenants")
52
- ROLE_PERMISSIONS_TABLE: str = os.getenv("ROLE_PERMISSIONS_TABLE", "role_permissions")
53
-
54
- # Tenant-specific tables (used in queries with tenant schema)
55
- TENANT_LOGIN_SETTINGS_TABLE: str = os.getenv("TENANT_LOGIN_SETTINGS_TABLE", "login_settings")
56
- USER_GROUPS_TABLE: str = os.getenv("USER_GROUPS_TABLE", "user_groups")
57
- ASSIGN_ROLES_TABLE: str = os.getenv("ASSIGN_ROLES_TABLE", "assign_roles")
35
+ MAIN_TENANTS_TABLE = os.getenv("MAIN_TENANTS_TABLE")
36
+ MAIN_ROLE_PERMISSIONS_TABLE = os.getenv("MAIN_ROLE_PERMISSIONS_TABLE")
37
+ MAIN_USER_SUBSCRIPTIONS_TABLE = os.getenv("MAIN_USER_SUBSCRIPTIONS_TABLE")
38
+ MAIN_USER_SUBSCRIPTION_HISTORY_TABLE = os.getenv("MAIN_USER_SUBSCRIPTION_HISTORY_TABLE")
39
+ MAIN_SUBSCRIPTIONS_TABLE = os.getenv("MAIN_SUBSCRIPTIONS_TABLE")
58
40
 
59
41
  # =============================================================================
60
- # AZURE CONFIGURATION (Optional - for queue functionality)
42
+ # TENANT-SPECIFIC TABLES (tenant schemas)
61
43
  # =============================================================================
62
- STORAGE_ACCOUNT_NAME: str = os.getenv("STORAGE_ACCOUNT_NAME", "")
63
- USER_ASSIGNED_MANAGED_IDENTITY: str = os.getenv("USER_ASSIGNED_MANAGED_IDENTITY", "")
64
-
44
+ TENANT_LOGIN_SETTINGS_TABLE = os.getenv("TENANT_LOGIN_SETTINGS_TABLE")
45
+ TENANT_ASSIGN_ROLES_TABLE = os.getenv("TENANT_ASSIGN_ROLES_TABLE")
46
+ TENANT_USER_GROUPS_TABLE = os.getenv("TENANT_USER_GROUPS_TABLE")
47
+
65
48
  @property
66
49
  def database_url(self) -> str:
67
- """Get the database URL, either from DATABASE_URL or constructed from individual components"""
68
- if self.DATABASE_URL != "postgresql://username:password@localhost:5432/database_name":
50
+ if self.DATABASE_URL:
69
51
  return self.DATABASE_URL
70
-
71
- # Validate individual components
72
- if not all([self.DB_USER, self.DB_HOST, self.DB_NAME, self.DB_PASSWORD]):
73
- missing = []
74
- if not self.DB_USER:
75
- missing.append("DB_USER")
76
- if not self.DB_HOST:
77
- missing.append("DB_HOST")
78
- if not self.DB_NAME:
79
- missing.append("DB_NAME")
80
- if not self.DB_PASSWORD:
81
- missing.append("DB_PASSWORD")
82
-
83
- raise ValueError(
84
- f"Database configuration incomplete. Missing environment variables: {', '.join(missing)}. "
85
- f"Please set these variables or provide a complete DATABASE_URL."
86
- )
87
-
88
- return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
89
-
90
- def validate_configuration(self) -> None:
91
- """Validate the current configuration and warn about potential issues"""
92
- warnings_list = []
93
-
94
- # Check for default secret key
95
- if self.SECRET_KEY == "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7":
96
- warnings_list.append(
97
- "SECRET_KEY is using the default value. This is insecure for production. "
98
- "Please set a strong, unique SECRET_KEY environment variable."
99
- )
100
-
101
- # Check for development environment in production-like settings
102
- if self.ENVIRONMENT == "development" and self.DEBUG is False:
103
- warnings_list.append(
104
- "ENVIRONMENT is set to 'development' but DEBUG is False. "
105
- "Consider setting ENVIRONMENT to 'production' for production deployments."
106
- )
107
-
108
- # Check database configuration
109
- try:
110
- self.database_url
111
- except ValueError as e:
112
- warnings_list.append(f"Database configuration issue: {str(e)}")
113
-
114
- # Check for missing Azure configuration if needed
115
- if self.ENVIRONMENT == "production" and not self.STORAGE_ACCOUNT_NAME:
116
- warnings_list.append(
117
- "STORAGE_ACCOUNT_NAME is not set. Azure queue functionality may not work properly."
118
- )
119
-
120
- # Emit warnings
121
- for warning in warnings_list:
122
- warnings.warn(warning, UserWarning)
123
-
124
- def get_configuration_summary(self) -> dict:
125
- """Get a summary of the current configuration (excluding sensitive data)"""
126
- return {
127
- "app_name": self.APP_NAME,
128
- "environment": self.ENVIRONMENT,
129
- "debug": self.DEBUG,
130
- "database_host": self.DB_HOST,
131
- "database_port": self.DB_PORT,
132
- "database_name": self.DB_NAME,
133
- "database_user": self.DB_USER,
134
- "log_level": self.LOG_LEVEL,
135
- "log_format": self.LOG_FORMAT,
136
- "log_to_file": self.LOG_TO_FILE,
137
- "algorithm": self.ALGORITHM,
138
- "access_token_expire_minutes": self.ACCESS_TOKEN_EXPIRE_MINUTES,
139
- "MAIN_TENANTS_TABLE": self.MAIN_TENANTS_TABLE,
140
- "role_permissions_table": self.ROLE_PERMISSIONS_TABLE,
141
- "TENANT_LOGIN_SETTINGS_TABLE": self.TENANT_LOGIN_SETTINGS_TABLE,
142
- "user_groups_table": self.USER_GROUPS_TABLE,
143
- "assign_roles_table": self.ASSIGN_ROLES_TABLE,
144
- }
145
52
 
146
- # Global settings instance
147
- db_settings = Settings()
53
+ port = int(self.DB_PORT) if self.DB_PORT else 5432
54
+ return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{port}/{self.DB_NAME}"
148
55
 
149
- # Validate configuration on import
150
- try:
151
- db_settings.validate_configuration()
152
- except Exception as e:
153
- warnings.warn("Configuration validation failed: %s", str(e), UserWarning)
56
+ # Global settings instance
57
+ db_settings = Settings()
@@ -1,8 +1,8 @@
1
1
  from fastapi import APIRouter
2
- from src.entities.shared.shared_response import Respons
3
- from src.configs.settings import db_settings
4
- from src.configs.database import DatabaseManager
5
- from src.configs.logging import get_logger
2
+ from .sh_response import Respons
3
+ from ..configs.settings import db_settings
4
+ from ..configs.database import DatabaseManager
5
+ from ..configs.logging import get_logger
6
6
 
7
7
  health_check_router = APIRouter(tags=["Health Path"])
8
8
  logger = get_logger("health")
@@ -0,0 +1,14 @@
1
+ """
2
+ TroveSuite Notification Service
3
+
4
+ Provides email and SMS notification capabilities for TroveSuite applications.
5
+ """
6
+
7
+ from .notification_service import NotificationService
8
+ from .notification_write_dto import NotificationEmailServiceWriteDto, NotificationSMSServiceWriteDto
9
+
10
+ __all__ = [
11
+ "NotificationService",
12
+ "NotificationEmailServiceWriteDto",
13
+ "NotificationSMSServiceWriteDto"
14
+ ]
@@ -0,0 +1,13 @@
1
+ from typing import List, Optional, Union
2
+ from pydantic import BaseModel
3
+
4
+ class NotificationEmailBase(BaseModel):
5
+ sender_email: str
6
+ receiver_email: Union[str, List[str]]
7
+ password: str
8
+ subject: str
9
+ text_message: str
10
+ html_message: Optional[str] = None
11
+
12
+ class NotificationSMSBase(BaseModel):
13
+ pass
@@ -0,0 +1,21 @@
1
+ from .notification_write_dto import (
2
+ NotificationEmailControllerWriteDto,
3
+ NotificationSMSControllerWriteDto
4
+ )
5
+ from .notification_read_dto import (
6
+ NotificationEmailControllerReadDto,
7
+ NotificationSMSControllerReadDto
8
+ )
9
+ from .notification_service import NotificationService
10
+ from ..entities.sh_response import Respons
11
+ from fastapi import APIRouter
12
+
13
+ notification_router = APIRouter(tags=["Notification"])
14
+
15
+ @notification_router.post("/send_email", response_model=Respons[NotificationEmailControllerReadDto])
16
+ async def send_email(data: NotificationEmailControllerWriteDto):
17
+ return NotificationService.send_email(data=data)
18
+
19
+ @notification_router.post("/send_sms", response_model=Respons[NotificationSMSControllerReadDto])
20
+ async def send_sms(data: NotificationSMSControllerWriteDto):
21
+ return NotificationService.send_sms(data=data)
@@ -0,0 +1,21 @@
1
+ from .notification_base import (
2
+ NotificationEmailBase,
3
+ NotificationSMSBase
4
+ )
5
+ from pydantic import BaseModel
6
+
7
+ # EMAIL Notification
8
+
9
+ class NotificationEmailControllerReadDto(NotificationEmailBase):
10
+ pass
11
+
12
+ class NotificationEmailServiceReadDto(BaseModel):
13
+ pass
14
+
15
+ # SMS Notification
16
+
17
+ class NotificationSMSControllerReadDto(NotificationSMSBase):
18
+ pass
19
+
20
+ class NotificationSMSServiceReadDto(BaseModel):
21
+ pass
@@ -0,0 +1,73 @@
1
+ import smtplib
2
+ from email.mime.text import MIMEText
3
+ from email.mime.multipart import MIMEMultipart
4
+ from ..entities.sh_response import Respons
5
+ from .notification_read_dto import (
6
+ NotificationEmailServiceReadDto,
7
+ NotificationSMSServiceReadDto
8
+ )
9
+ from .notification_write_dto import (
10
+ NotificationEmailServiceWriteDto,
11
+ NotificationSMSServiceWriteDto
12
+ )
13
+
14
+ class NotificationService:
15
+
16
+ @staticmethod
17
+ def send_email(data: NotificationEmailServiceWriteDto) -> Respons[NotificationEmailServiceReadDto]:
18
+ """
19
+ Send an email (single or multiple recipients) via Gmail SMTP.
20
+ Supports both plain text and HTML email bodies.
21
+ """
22
+
23
+ # Extract input data
24
+ receiver_email = data.receiver_email
25
+ text_message = data.text_message
26
+ html_message = getattr(data, "html_message", None)
27
+ sender_email = data.sender_email
28
+ password = data.password
29
+ subject = data.subject
30
+
31
+ # Allow single email or list
32
+ if isinstance(receiver_email, str):
33
+ receiver_email = [receiver_email]
34
+
35
+ # Create the email container
36
+ message = MIMEMultipart("alternative")
37
+ message["From"] = sender_email
38
+ message["To"] = ", ".join(receiver_email)
39
+ message["Subject"] = subject
40
+
41
+ # Attach plain text
42
+ message.attach(MIMEText(text_message, "plain"))
43
+
44
+ # Attach HTML if provided
45
+ if html_message:
46
+ message.attach(MIMEText(html_message, "html"))
47
+
48
+ try:
49
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
50
+ server.login(sender_email, password)
51
+ server.sendmail(sender_email, receiver_email, message.as_string())
52
+
53
+ return Respons[NotificationEmailServiceReadDto](
54
+ detail=f"Email successfully sent to {len(receiver_email)} recipient(s)",
55
+ error=None,
56
+ data=[],
57
+ status_code=200,
58
+ success=True,
59
+ )
60
+
61
+ except Exception as e:
62
+ print(e)
63
+ return Respons[NotificationEmailServiceReadDto](
64
+ detail="An error occurred while sending the email",
65
+ error=str(e),
66
+ data=[],
67
+ status_code=500,
68
+ success=False,
69
+ )
70
+
71
+ @staticmethod
72
+ def send_sms(data: NotificationSMSServiceWriteDto) -> Respons[NotificationSMSServiceReadDto]:
73
+ pass
@@ -0,0 +1,21 @@
1
+ from pydantic import BaseModel
2
+ from .notification_base import (
3
+ NotificationEmailBase,
4
+ NotificationSMSBase
5
+ )
6
+
7
+ # Email Notification
8
+
9
+ class NotificationEmailControllerWriteDto(NotificationEmailBase):
10
+ pass
11
+
12
+ class NotificationEmailServiceWriteDto(NotificationEmailControllerWriteDto):
13
+ pass
14
+
15
+ # SMS Notification
16
+
17
+ class NotificationSMSControllerWriteDto(NotificationSMSBase):
18
+ pass
19
+
20
+ class NotificationSMSServiceWriteDto(BaseModel):
21
+ pass
@@ -0,0 +1,42 @@
1
+ """
2
+ TroveSuite Storage Service
3
+
4
+ Provides Azure Storage blob management capabilities for TroveSuite applications.
5
+ Includes container creation, file upload/download/update/delete, and presigned URL generation.
6
+ """
7
+
8
+ from .storage_service import StorageService
9
+ from .storage_write_dto import (
10
+ StorageContainerCreateServiceWriteDto,
11
+ StorageFileUploadServiceWriteDto,
12
+ StorageFileUpdateServiceWriteDto,
13
+ StorageFileDeleteServiceWriteDto,
14
+ StorageFileDownloadServiceWriteDto,
15
+ StorageFileUrlServiceWriteDto
16
+ )
17
+ from .storage_read_dto import (
18
+ StorageContainerCreateServiceReadDto,
19
+ StorageFileUploadServiceReadDto,
20
+ StorageFileUpdateServiceReadDto,
21
+ StorageFileDeleteServiceReadDto,
22
+ StorageFileDownloadServiceReadDto,
23
+ StorageFileUrlServiceReadDto
24
+ )
25
+
26
+ __all__ = [
27
+ "StorageService",
28
+ # Write DTOs
29
+ "StorageContainerCreateServiceWriteDto",
30
+ "StorageFileUploadServiceWriteDto",
31
+ "StorageFileUpdateServiceWriteDto",
32
+ "StorageFileDeleteServiceWriteDto",
33
+ "StorageFileDownloadServiceWriteDto",
34
+ "StorageFileUrlServiceWriteDto",
35
+ # Read DTOs
36
+ "StorageContainerCreateServiceReadDto",
37
+ "StorageFileUploadServiceReadDto",
38
+ "StorageFileUpdateServiceReadDto",
39
+ "StorageFileDeleteServiceReadDto",
40
+ "StorageFileDownloadServiceReadDto",
41
+ "StorageFileUrlServiceReadDto",
42
+ ]
@@ -0,0 +1,63 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class StorageConnectionBase(BaseModel):
6
+ """Base model for Azure Storage connection using Managed Identity"""
7
+ storage_account_url: str # e.g., https://<account-name>.blob.core.windows.net
8
+ container_name: str
9
+ managed_identity_client_id: Optional[str] = None # Optional: For user-assigned managed identity
10
+
11
+
12
+ class StorageFileUploadBase(BaseModel):
13
+ """Base model for file upload operations"""
14
+ storage_account_url: str
15
+ container_name: str
16
+ file_content: bytes
17
+ blob_name: str
18
+ directory_path: Optional[str] = None
19
+ content_type: Optional[str] = None
20
+ managed_identity_client_id: Optional[str] = None
21
+
22
+
23
+ class StorageFileUpdateBase(BaseModel):
24
+ """Base model for file update operations"""
25
+ storage_account_url: str
26
+ container_name: str
27
+ blob_name: str
28
+ file_content: bytes
29
+ content_type: Optional[str] = None
30
+ managed_identity_client_id: Optional[str] = None
31
+
32
+
33
+ class StorageFileDeleteBase(BaseModel):
34
+ """Base model for file delete operations"""
35
+ storage_account_url: str
36
+ container_name: str
37
+ blob_name: str
38
+ managed_identity_client_id: Optional[str] = None
39
+
40
+
41
+ class StorageFileDownloadBase(BaseModel):
42
+ """Base model for file download operations"""
43
+ storage_account_url: str
44
+ container_name: str
45
+ blob_name: str
46
+ managed_identity_client_id: Optional[str] = None
47
+
48
+
49
+ class StorageFileUrlBase(BaseModel):
50
+ """Base model for getting presigned URL"""
51
+ storage_account_url: str
52
+ container_name: str
53
+ blob_name: str
54
+ expiry_hours: Optional[int] = 1
55
+ managed_identity_client_id: Optional[str] = None
56
+
57
+
58
+ class StorageContainerCreateBase(BaseModel):
59
+ """Base model for creating a container"""
60
+ storage_account_url: str
61
+ container_name: str
62
+ public_access: Optional[str] = None
63
+ managed_identity_client_id: Optional[str] = None
@@ -0,0 +1,198 @@
1
+ from typing import List
2
+ from fastapi import APIRouter, File, UploadFile, Form
3
+ from fastapi.responses import StreamingResponse
4
+ from io import BytesIO
5
+ from .storage_write_dto import (
6
+ StorageContainerCreateControllerWriteDto,
7
+ StorageFileUploadControllerWriteDto,
8
+ StorageFileUpdateControllerWriteDto,
9
+ StorageFileDeleteControllerWriteDto,
10
+ StorageFileDownloadControllerWriteDto,
11
+ StorageFileUrlControllerWriteDto
12
+ )
13
+ from .storage_read_dto import (
14
+ StorageContainerCreateControllerReadDto,
15
+ StorageFileUploadControllerReadDto,
16
+ StorageFileUpdateControllerReadDto,
17
+ StorageFileDeleteControllerReadDto,
18
+ StorageFileDownloadControllerReadDto,
19
+ StorageFileUrlControllerReadDto
20
+ )
21
+ from .storage_service import StorageService
22
+ from ..entities.sh_response import Respons
23
+
24
+ storage_router = APIRouter(tags=["File Storage"])
25
+
26
+
27
+ @storage_router.post("/create-container", response_model=Respons[StorageContainerCreateControllerReadDto])
28
+ async def create_container(data: StorageContainerCreateControllerWriteDto):
29
+ """
30
+ Create a new Azure Storage container.
31
+
32
+ Example request body:
33
+ {
34
+ "storage_account_url": "https://myaccount.blob.core.windows.net",
35
+ "container_name": "my-container",
36
+ "public_access": null,
37
+ "managed_identity_client_id": "your-client-id" // optional
38
+ }
39
+ """
40
+ return StorageService.create_container(data=data)
41
+
42
+
43
+ @storage_router.post("/upload", response_model=Respons[StorageFileUploadControllerReadDto])
44
+ async def upload_file(
45
+ storage_account_url: str = Form(...),
46
+ container_name: str = Form(...),
47
+ blob_name: str = Form(...),
48
+ file: UploadFile = File(...),
49
+ directory_path: str = Form(None),
50
+ managed_identity_client_id: str = Form(None)
51
+ ):
52
+ """
53
+ Upload a file to Azure Storage.
54
+
55
+ Use form-data with the following fields:
56
+ - storage_account_url: Your Azure storage URL
57
+ - container_name: Container name
58
+ - blob_name: Name for the blob
59
+ - file: The file to upload
60
+ - directory_path: Optional directory path (e.g., "uploads/2024")
61
+ - managed_identity_client_id: Optional client ID for user-assigned managed identity
62
+ """
63
+ content = await file.read()
64
+
65
+ upload_data = StorageFileUploadControllerWriteDto(
66
+ storage_account_url=storage_account_url,
67
+ container_name=container_name,
68
+ file_content=content,
69
+ blob_name=blob_name,
70
+ directory_path=directory_path,
71
+ content_type=file.content_type,
72
+ managed_identity_client_id=managed_identity_client_id
73
+ )
74
+
75
+ return StorageService.upload_file(data=upload_data)
76
+
77
+
78
+ @storage_router.put("/update", response_model=Respons[StorageFileUpdateControllerReadDto])
79
+ async def update_file(
80
+ storage_account_url: str = Form(...),
81
+ container_name: str = Form(...),
82
+ blob_name: str = Form(...),
83
+ file: UploadFile = File(...),
84
+ managed_identity_client_id: str = Form(None)
85
+ ):
86
+ """
87
+ Update an existing file in Azure Storage.
88
+
89
+ Use form-data with the following fields:
90
+ - storage_account_url: Your Azure storage URL
91
+ - container_name: Container name
92
+ - blob_name: Full blob name including path (e.g., "uploads/2024/file.pdf")
93
+ - file: The new file content
94
+ - managed_identity_client_id: Optional client ID for user-assigned managed identity
95
+ """
96
+ content = await file.read()
97
+
98
+ update_data = StorageFileUpdateControllerWriteDto(
99
+ storage_account_url=storage_account_url,
100
+ container_name=container_name,
101
+ blob_name=blob_name,
102
+ file_content=content,
103
+ content_type=file.content_type,
104
+ managed_identity_client_id=managed_identity_client_id
105
+ )
106
+
107
+ return StorageService.update_file(data=update_data)
108
+
109
+
110
+ @storage_router.delete("/delete", response_model=Respons[StorageFileDeleteControllerReadDto])
111
+ async def delete_file(data: StorageFileDeleteControllerWriteDto):
112
+ """
113
+ Delete a file from Azure Storage.
114
+
115
+ Example request body:
116
+ {
117
+ "storage_account_url": "https://myaccount.blob.core.windows.net",
118
+ "container_name": "my-container",
119
+ "blob_name": "uploads/2024/file.pdf",
120
+ "managed_identity_client_id": "your-client-id" // optional
121
+ }
122
+ """
123
+ return StorageService.delete_file(data=data)
124
+
125
+
126
+ @storage_router.delete("/delete-multiple", response_model=Respons[StorageFileDeleteControllerReadDto])
127
+ async def delete_multiple_files(
128
+ storage_account_url: str,
129
+ container_name: str,
130
+ blob_names: List[str],
131
+ managed_identity_client_id: str = None
132
+ ):
133
+ """
134
+ Delete multiple files from Azure Storage.
135
+
136
+ Example request body:
137
+ {
138
+ "storage_account_url": "https://myaccount.blob.core.windows.net",
139
+ "container_name": "my-container",
140
+ "blob_names": ["file1.pdf", "file2.pdf", "folder/file3.jpg"],
141
+ "managed_identity_client_id": "your-client-id" // optional
142
+ }
143
+ """
144
+ return StorageService.delete_multiple_files(
145
+ storage_account_url=storage_account_url,
146
+ container_name=container_name,
147
+ blob_names=blob_names,
148
+ managed_identity_client_id=managed_identity_client_id
149
+ )
150
+
151
+
152
+ @storage_router.post("/download")
153
+ async def download_file(data: StorageFileDownloadControllerWriteDto):
154
+ """
155
+ Download a file from Azure Storage.
156
+
157
+ Returns the file as a streaming response.
158
+
159
+ Example request body:
160
+ {
161
+ "storage_account_url": "https://myaccount.blob.core.windows.net",
162
+ "container_name": "my-container",
163
+ "blob_name": "uploads/2024/file.pdf",
164
+ "managed_identity_client_id": "your-client-id" // optional
165
+ }
166
+ """
167
+ result = StorageService.download_file(data=data)
168
+
169
+ if not result.success:
170
+ return result
171
+
172
+ file_data = result.data[0]
173
+
174
+ # Return as streaming response
175
+ return StreamingResponse(
176
+ BytesIO(file_data.content),
177
+ media_type=file_data.content_type or "application/octet-stream",
178
+ headers={
179
+ "Content-Disposition": f"attachment; filename={data.blob_name.split('/')[-1]}"
180
+ }
181
+ )
182
+
183
+
184
+ @storage_router.post("/get-url", response_model=Respons[StorageFileUrlControllerReadDto])
185
+ async def get_file_url(data: StorageFileUrlControllerWriteDto):
186
+ """
187
+ Generate a presigned URL for a file.
188
+
189
+ Example request body:
190
+ {
191
+ "storage_account_url": "https://myaccount.blob.core.windows.net",
192
+ "container_name": "my-container",
193
+ "blob_name": "uploads/2024/file.pdf",
194
+ "expiry_hours": 2,
195
+ "managed_identity_client_id": "your-client-id" // optional
196
+ }
197
+ """
198
+ return StorageService.get_file_url(data=data)