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.
- trovesuite/__init__.py +10 -5
- trovesuite/auth/__init__.py +3 -5
- trovesuite/auth/auth_base.py +1 -0
- trovesuite/auth/auth_controller.py +6 -5
- trovesuite/auth/auth_read_dto.py +4 -3
- trovesuite/auth/auth_service.py +61 -51
- trovesuite/auth/auth_write_dto.py +2 -1
- trovesuite/configs/database.py +130 -36
- trovesuite/configs/settings.py +36 -132
- trovesuite/entities/health.py +4 -4
- trovesuite/notification/__init__.py +14 -0
- trovesuite/notification/notification_base.py +13 -0
- trovesuite/notification/notification_controller.py +21 -0
- trovesuite/notification/notification_read_dto.py +21 -0
- trovesuite/notification/notification_service.py +73 -0
- trovesuite/notification/notification_write_dto.py +21 -0
- trovesuite/storage/__init__.py +42 -0
- trovesuite/storage/storage_base.py +63 -0
- trovesuite/storage/storage_controller.py +198 -0
- trovesuite/storage/storage_read_dto.py +74 -0
- trovesuite/storage/storage_service.py +529 -0
- trovesuite/storage/storage_write_dto.py +70 -0
- {trovesuite-1.0.9.dist-info → trovesuite-1.0.11.dist-info}/METADATA +188 -11
- trovesuite-1.0.11.dist-info/RECORD +33 -0
- trovesuite-1.0.9.dist-info/RECORD +0 -21
- {trovesuite-1.0.9.dist-info → trovesuite-1.0.11.dist-info}/WHEEL +0 -0
- {trovesuite-1.0.9.dist-info → trovesuite-1.0.11.dist-info}/licenses/LICENSE +0 -0
- {trovesuite-1.0.9.dist-info → trovesuite-1.0.11.dist-info}/top_level.txt +0 -0
trovesuite/configs/settings.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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()
|
|
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
|
-
#
|
|
33
|
+
# SHARED TABLES (main schema)
|
|
49
34
|
# =============================================================================
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
#
|
|
42
|
+
# TENANT-SPECIFIC TABLES (tenant schemas)
|
|
61
43
|
# =============================================================================
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
#
|
|
150
|
-
|
|
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()
|
trovesuite/entities/health.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from fastapi import APIRouter
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
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)
|