fast-backend-builder 0.1.0__tar.gz

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 (93) hide show
  1. fast_backend_builder-0.1.0/LICENSE +8 -0
  2. fast_backend_builder-0.1.0/PKG-INFO +81 -0
  3. fast_backend_builder-0.1.0/README.md +64 -0
  4. fast_backend_builder-0.1.0/fast_backend_builder/__init__.py +0 -0
  5. fast_backend_builder-0.1.0/fast_backend_builder/attach/__init__.py +0 -0
  6. fast_backend_builder-0.1.0/fast_backend_builder/attach/api.py +72 -0
  7. fast_backend_builder-0.1.0/fast_backend_builder/attach/gql_controller.py +219 -0
  8. fast_backend_builder-0.1.0/fast_backend_builder/attach/request.py +17 -0
  9. fast_backend_builder-0.1.0/fast_backend_builder/attach/response.py +16 -0
  10. fast_backend_builder-0.1.0/fast_backend_builder/attach/service.py +162 -0
  11. fast_backend_builder-0.1.0/fast_backend_builder/attach/templates/__init__.py +0 -0
  12. fast_backend_builder-0.1.0/fast_backend_builder/attach/templates/attachment_mutations.py +6 -0
  13. fast_backend_builder-0.1.0/fast_backend_builder/attach/templates/attachment_queries.py +7 -0
  14. fast_backend_builder-0.1.0/fast_backend_builder/attach/templates/gql_input_template.py +9 -0
  15. fast_backend_builder-0.1.0/fast_backend_builder/attach/templates/gql_type_template.py +9 -0
  16. fast_backend_builder-0.1.0/fast_backend_builder/attach/templates/graphql_api_template.py +217 -0
  17. fast_backend_builder-0.1.0/fast_backend_builder/auth/__init__.py +0 -0
  18. fast_backend_builder-0.1.0/fast_backend_builder/auth/auth.py +217 -0
  19. fast_backend_builder-0.1.0/fast_backend_builder/auth/context.py +108 -0
  20. fast_backend_builder-0.1.0/fast_backend_builder/auth/jwt_handler.py +108 -0
  21. fast_backend_builder-0.1.0/fast_backend_builder/auth/middleware.py +95 -0
  22. fast_backend_builder-0.1.0/fast_backend_builder/auth/redis.py +265 -0
  23. fast_backend_builder-0.1.0/fast_backend_builder/commands/__init__.py +0 -0
  24. fast_backend_builder-0.1.0/fast_backend_builder/commands/graphql.py +76 -0
  25. fast_backend_builder-0.1.0/fast_backend_builder/common/__init__.py +0 -0
  26. fast_backend_builder-0.1.0/fast_backend_builder/common/models.py +0 -0
  27. fast_backend_builder-0.1.0/fast_backend_builder/common/request/__init__.py +0 -0
  28. fast_backend_builder-0.1.0/fast_backend_builder/common/request/schemas.py +43 -0
  29. fast_backend_builder-0.1.0/fast_backend_builder/common/response/__init__.py +0 -0
  30. fast_backend_builder-0.1.0/fast_backend_builder/common/response/codes.py +35 -0
  31. fast_backend_builder-0.1.0/fast_backend_builder/common/response/schemas.py +27 -0
  32. fast_backend_builder-0.1.0/fast_backend_builder/common/schemas.py +9 -0
  33. fast_backend_builder-0.1.0/fast_backend_builder/common/templates/module_init_.py +11 -0
  34. fast_backend_builder-0.1.0/fast_backend_builder/common/validation/__init__.py +0 -0
  35. fast_backend_builder-0.1.0/fast_backend_builder/common/validation/decorators.py +103 -0
  36. fast_backend_builder-0.1.0/fast_backend_builder/common/validation/field_validator.py +789 -0
  37. fast_backend_builder-0.1.0/fast_backend_builder/common/validation/rules.py +536 -0
  38. fast_backend_builder-0.1.0/fast_backend_builder/crud/__init__.py +0 -0
  39. fast_backend_builder-0.1.0/fast_backend_builder/crud/controller.py +278 -0
  40. fast_backend_builder-0.1.0/fast_backend_builder/crud/gql_controller.py +694 -0
  41. fast_backend_builder-0.1.0/fast_backend_builder/crud/graphql_api_gen.py +301 -0
  42. fast_backend_builder-0.1.0/fast_backend_builder/crud/graphql_schema_gen.py +152 -0
  43. fast_backend_builder-0.1.0/fast_backend_builder/crud/rest_api.py +66 -0
  44. fast_backend_builder-0.1.0/fast_backend_builder/crud/templates/__init__.py +0 -0
  45. fast_backend_builder-0.1.0/fast_backend_builder/crud/templates/controller_template.py +8 -0
  46. fast_backend_builder-0.1.0/fast_backend_builder/crud/templates/gql_input_template.py +11 -0
  47. fast_backend_builder-0.1.0/fast_backend_builder/crud/templates/gql_schemas_template.py +11 -0
  48. fast_backend_builder-0.1.0/fast_backend_builder/crud/templates/gql_type_template.py +8 -0
  49. fast_backend_builder-0.1.0/fast_backend_builder/crud/templates/graphql_api_template.py +132 -0
  50. fast_backend_builder-0.1.0/fast_backend_builder/crud/templates/model_template.py +6 -0
  51. fast_backend_builder-0.1.0/fast_backend_builder/esb/__init__.py +0 -0
  52. fast_backend_builder-0.1.0/fast_backend_builder/esb/esb.py +366 -0
  53. fast_backend_builder-0.1.0/fast_backend_builder/esb/schemas.py +39 -0
  54. fast_backend_builder-0.1.0/fast_backend_builder/models/__init__.py +325 -0
  55. fast_backend_builder-0.1.0/fast_backend_builder/models/attachment.py +22 -0
  56. fast_backend_builder-0.1.0/fast_backend_builder/models/base_models.py +12 -0
  57. fast_backend_builder-0.1.0/fast_backend_builder/models/group_permissions.py +43 -0
  58. fast_backend_builder-0.1.0/fast_backend_builder/models/headship.py +30 -0
  59. fast_backend_builder-0.1.0/fast_backend_builder/models/notification.py +71 -0
  60. fast_backend_builder-0.1.0/fast_backend_builder/models/workflow.py +307 -0
  61. fast_backend_builder-0.1.0/fast_backend_builder/notifications/__init__.py +0 -0
  62. fast_backend_builder-0.1.0/fast_backend_builder/notifications/service.py +105 -0
  63. fast_backend_builder-0.1.0/fast_backend_builder/utils/__init__.py +0 -0
  64. fast_backend_builder-0.1.0/fast_backend_builder/utils/config.py +29 -0
  65. fast_backend_builder-0.1.0/fast_backend_builder/utils/enums.py +17 -0
  66. fast_backend_builder-0.1.0/fast_backend_builder/utils/env.py +41 -0
  67. fast_backend_builder-0.1.0/fast_backend_builder/utils/error_logging.py +87 -0
  68. fast_backend_builder-0.1.0/fast_backend_builder/utils/file.py +37 -0
  69. fast_backend_builder-0.1.0/fast_backend_builder/utils/helpers/__init__.py +0 -0
  70. fast_backend_builder-0.1.0/fast_backend_builder/utils/helpers/log_activity.py +26 -0
  71. fast_backend_builder-0.1.0/fast_backend_builder/utils/helpers/request.py +17 -0
  72. fast_backend_builder-0.1.0/fast_backend_builder/utils/metrics/__init__.py +1 -0
  73. fast_backend_builder-0.1.0/fast_backend_builder/utils/metrics/db_metrics.py +109 -0
  74. fast_backend_builder-0.1.0/fast_backend_builder/utils/mutable_settings.py +192 -0
  75. fast_backend_builder-0.1.0/fast_backend_builder/utils/reflection.py +61 -0
  76. fast_backend_builder-0.1.0/fast_backend_builder/utils/sentry.py +64 -0
  77. fast_backend_builder-0.1.0/fast_backend_builder/utils/str_helpers.py +190 -0
  78. fast_backend_builder-0.1.0/fast_backend_builder/workflow/__init__.py +0 -0
  79. fast_backend_builder-0.1.0/fast_backend_builder/workflow/exceptions.py +47 -0
  80. fast_backend_builder-0.1.0/fast_backend_builder/workflow/gql_controller.py +171 -0
  81. fast_backend_builder-0.1.0/fast_backend_builder/workflow/request.py +8 -0
  82. fast_backend_builder-0.1.0/fast_backend_builder/workflow/response.py +16 -0
  83. fast_backend_builder-0.1.0/fast_backend_builder/workflow/templates/__init__.py +0 -0
  84. fast_backend_builder-0.1.0/fast_backend_builder/workflow/templates/gql_type_template.py +8 -0
  85. fast_backend_builder-0.1.0/fast_backend_builder.egg-info/PKG-INFO +81 -0
  86. fast_backend_builder-0.1.0/fast_backend_builder.egg-info/SOURCES.txt +91 -0
  87. fast_backend_builder-0.1.0/fast_backend_builder.egg-info/dependency_links.txt +1 -0
  88. fast_backend_builder-0.1.0/fast_backend_builder.egg-info/not-zip-safe +1 -0
  89. fast_backend_builder-0.1.0/fast_backend_builder.egg-info/requires.txt +2 -0
  90. fast_backend_builder-0.1.0/fast_backend_builder.egg-info/top_level.txt +1 -0
  91. fast_backend_builder-0.1.0/pyproject.toml +19 -0
  92. fast_backend_builder-0.1.0/setup.cfg +4 -0
  93. fast_backend_builder-0.1.0/setup.py +57 -0
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2025 Japhary Juma, Shija Ntula and Juma Nassoro Ludanga
2
+
3
+ All rights reserved.
4
+
5
+ This software and associated documentation files (the "Software") are
6
+ provided for use only by licensed users. You may not copy, modify,
7
+ merge, publish, distribute, sublicense, or sell copies of the Software
8
+ without express written permission from the author.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: fast-backend-builder
3
+ Version: 0.1.0
4
+ Summary: A FastAPI helper/builder package
5
+ Home-page: https://github.com/jay-ludanga/fast-api-builder
6
+ Author: Japhary Juma Hamisi
7
+ Author-email: Japhary Juma <japharyjuma@gmail.com>, Shija Ntula <abuukauthar13@gmail.com>, Juma Ludanga <ludangajn@gmail.com>
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: fastapi>=0.100
12
+ Requires-Dist: uvicorn>=0.23
13
+ Dynamic: author
14
+ Dynamic: home-page
15
+ Dynamic: license-file
16
+ Dynamic: requires-python
17
+
18
+ Building The package:
19
+
20
+ python setup.py sdist bdist_wheel
21
+
22
+ install the package to your app:
23
+
24
+ pip install path/to/wheel file
25
+
26
+ generating Graphql schemas
27
+
28
+ graphql gen:crud-api <module-name> --model Model1,Model2,Model3
29
+
30
+ generating graphql schemas with attachments
31
+
32
+ graphql gen:crud-api <module-name> --model ModelName --with-attachment
33
+
34
+ for attachments you must have minio server and these .env configs:
35
+
36
+ MINIO_SERVER=ip_address:api_port
37
+ MINIO_BUCKET=backet-name
38
+ MINIO_ACCESS_KEY=minio-username
39
+ MINIO_SECRETE_KEY=minio-password
40
+ MINIO_SECURE=False
41
+
42
+ You must initialize the MinioService before using it by:
43
+
44
+ @app.on_event("startup")
45
+ async def startup_event():
46
+ # Run the init_minio_service in the background without blocking the app startup
47
+ asyncio.create_task(init_minio_service())
48
+
49
+ async def init_minio_service():
50
+ minio_service = MinioService()
51
+ try:
52
+ await minio_service.init(
53
+ server=config('MINIO_SERVER'),
54
+ access_key=config('MINIO_ACCESS_KEY'),
55
+ secret_key=config('MINIO_SECRETE_KEY'),
56
+ bucket_name=config('MINIO_BUCKET'),
57
+ secure=config('MINIO_SECURE', cast=bool)
58
+ )
59
+ except Exception as e:
60
+ print(f"Error initializing MinIO service: {e}")
61
+
62
+ Making it async will easy your app booting process.
63
+
64
+ If you need to use the service out of generated crud apis yo do:
65
+
66
+ For uploading:
67
+
68
+ file_location, upload_error = await MinioService.get_instance().upload_file(
69
+ file_name=f"path/to/file.extension",
70
+ file_data=base64_decoded_file,
71
+ content_type=attachment.file.content_type
72
+ )
73
+
74
+ For downloading:
75
+
76
+ base64_content = await MinioService.get_instance().download_file("path/to/file.extension")
77
+
78
+ For deleting:
79
+
80
+ result = MinioService.get_instance().delete_file("path/to/file.extension")
81
+
@@ -0,0 +1,64 @@
1
+ Building The package:
2
+
3
+ python setup.py sdist bdist_wheel
4
+
5
+ install the package to your app:
6
+
7
+ pip install path/to/wheel file
8
+
9
+ generating Graphql schemas
10
+
11
+ graphql gen:crud-api <module-name> --model Model1,Model2,Model3
12
+
13
+ generating graphql schemas with attachments
14
+
15
+ graphql gen:crud-api <module-name> --model ModelName --with-attachment
16
+
17
+ for attachments you must have minio server and these .env configs:
18
+
19
+ MINIO_SERVER=ip_address:api_port
20
+ MINIO_BUCKET=backet-name
21
+ MINIO_ACCESS_KEY=minio-username
22
+ MINIO_SECRETE_KEY=minio-password
23
+ MINIO_SECURE=False
24
+
25
+ You must initialize the MinioService before using it by:
26
+
27
+ @app.on_event("startup")
28
+ async def startup_event():
29
+ # Run the init_minio_service in the background without blocking the app startup
30
+ asyncio.create_task(init_minio_service())
31
+
32
+ async def init_minio_service():
33
+ minio_service = MinioService()
34
+ try:
35
+ await minio_service.init(
36
+ server=config('MINIO_SERVER'),
37
+ access_key=config('MINIO_ACCESS_KEY'),
38
+ secret_key=config('MINIO_SECRETE_KEY'),
39
+ bucket_name=config('MINIO_BUCKET'),
40
+ secure=config('MINIO_SECURE', cast=bool)
41
+ )
42
+ except Exception as e:
43
+ print(f"Error initializing MinIO service: {e}")
44
+
45
+ Making it async will easy your app booting process.
46
+
47
+ If you need to use the service out of generated crud apis yo do:
48
+
49
+ For uploading:
50
+
51
+ file_location, upload_error = await MinioService.get_instance().upload_file(
52
+ file_name=f"path/to/file.extension",
53
+ file_data=base64_decoded_file,
54
+ content_type=attachment.file.content_type
55
+ )
56
+
57
+ For downloading:
58
+
59
+ base64_content = await MinioService.get_instance().download_file("path/to/file.extension")
60
+
61
+ For deleting:
62
+
63
+ result = MinioService.get_instance().delete_file("path/to/file.extension")
64
+
@@ -0,0 +1,72 @@
1
+ # from typing import List, Optional
2
+ # from fastapi import APIRouter, File, Form, UploadFile, Depends
3
+ # from fastapi.params import Query
4
+
5
+ # from fast_backend_builder.common.response.codes import ResponseCode
6
+ # from fast_backend_builder.common.response.schemas import ApiResponse
7
+ # from src.modules.auth.permission_middleware import get_current_user, authorize
8
+ # from src.modules.cash.schema import AttachmentCreate
9
+
10
+
11
+ # def build_attach_endpoints(router: APIRouter, path: str, controller, parent_id_name: str,
12
+ # model_verbose: Optional[str] = None,
13
+ # security_dependency=Depends(get_current_user)):
14
+ # @router.post(f"{path}/attachments/single/upload")
15
+ # @authorize([f"src.add_{model_verbose}"])
16
+ # def upload_single_attachment(parent_id: int, file: UploadFile = File(...), title: str = Form(...),
17
+ # current_user: dict = security_dependency):
18
+ # try:
19
+ # attachments: List[AttachmentCreate] = [
20
+ # AttachmentCreate(
21
+ # parent_id=parent_id,
22
+ # title=title,
23
+ # content=file
24
+ # )
25
+ # ]
26
+
27
+ # return controller.upload_attachments(attachments, parent_id_name)
28
+ # except Exception as e:
29
+ # return ApiResponse(
30
+ # status=False,
31
+ # code=ResponseCode.FAILURE,
32
+ # message=f"An error occured while uploading attachment. Retry",
33
+ # )
34
+
35
+ # @router.post(f"{path}/attachments/upload")
36
+ # @authorize([f"src.add_{model_verbose}"])
37
+ # def upload_attachments(parent_id: int, files: List[UploadFile] = File(...), titles: List[str] = Form(...),
38
+ # current_user: dict = security_dependency):
39
+ # try:
40
+ # attachments: List[AttachmentCreate] = []
41
+ # for index, file in enumerate(files):
42
+ # attachments.append(
43
+ # AttachmentCreate(
44
+ # parent_id=parent_id,
45
+ # title=titles[index],
46
+ # content=file
47
+ # )
48
+ # )
49
+ # return controller.upload_attachments(attachments, parent_id_name)
50
+ # except IndexError as e:
51
+ # return ApiResponse(
52
+ # status=False,
53
+ # code=ResponseCode.BAD_REQUEST,
54
+ # message=f"Titles and Files do not match {len(files)} - {len(titles)}",
55
+ # )
56
+
57
+ # @router.get(f"{path}/attachments/get")
58
+ # @authorize([f"src.view_{model_verbose}"])
59
+ # def get_attachments(parent_id: int, current_user: dict = security_dependency):
60
+ # return controller.get_attachments(parent_id, parent_id_name)
61
+
62
+ # @router.get(f"{path}/attachments/download")
63
+ # @authorize([f"src.change_{model_verbose}"])
64
+ # def download_attachment(attachment_id: int, current_user: dict = security_dependency):
65
+ # return controller.download_attachment(attachment_id)
66
+
67
+ # @router.delete(f"{path}/attachments/remove")
68
+ # @authorize([f"src.delete_{model_verbose}"])
69
+ # def delete_attachment(attachment_id: int, current_user: dict = security_dependency):
70
+ # return controller.remove_attachment(attachment_id)
71
+
72
+ # return router
@@ -0,0 +1,219 @@
1
+ import base64
2
+ from io import BytesIO
3
+ import os
4
+ from typing import Generic, Type
5
+
6
+ from tortoise.exceptions import DoesNotExist
7
+ from decouple import config
8
+ from fast_backend_builder.attach.request import AttachmentUpload
9
+ from fast_backend_builder.attach.service import MinioService
10
+ from fast_backend_builder.models.attachment import Attachment
11
+ from fast_backend_builder.utils.error_logging import log_exception
12
+ from minio import Minio
13
+ from minio.error import S3Error
14
+
15
+ from fast_backend_builder.common.response.codes import ResponseCode
16
+ from fast_backend_builder.common.response.schemas import ApiResponse, PaginatedResponse
17
+ from fast_backend_builder.common.schemas import ModelType
18
+
19
+
20
+ # MinIO setup
21
+ class AttachmentBaseController(Generic[ModelType]):
22
+ def __init__(self, model: Type[ModelType]):
23
+ self.model = model
24
+
25
+ async def upload_attachment(self, attachment_type_id, attachment: AttachmentUpload) -> ApiResponse:
26
+ try:
27
+ # Check if model exists
28
+ obj = await self.model.get(id=attachment_type_id)
29
+ # Decode the base64 string into binary data (bytes)
30
+ try:
31
+ decoded_file = base64.b64decode(attachment.file.content)
32
+ except Exception as decode_error:
33
+ return ApiResponse(
34
+ status=False,
35
+ code=ResponseCode.FAILURE,
36
+ message=f"Failed to decode base64 file: {decode_error}",
37
+ data=None
38
+ )
39
+
40
+ # Create Minio FIle name
41
+ file_name = f"{attachment.file.name}_{os.urandom(4).hex()}.{attachment.file.extension}"
42
+
43
+ file_location, upload_error = await MinioService.get_instance().upload_file(
44
+ file_name=f"{self.model.__name__}/{file_name}",
45
+ file_data=decoded_file,
46
+ content_type=attachment.file.content_type
47
+ )
48
+
49
+ if not file_location:
50
+ return ApiResponse(
51
+ status=False,
52
+ code=ResponseCode.FAILURE,
53
+ message=f"File upload failed: {upload_error}",
54
+ data=None
55
+ )
56
+
57
+ # Only after a successful upload, store the file path in the database
58
+ try:
59
+ attachment = await Attachment.create(
60
+ title=attachment.title,
61
+ description=attachment.description,
62
+ file_path=file_name,
63
+ mem_type=attachment.file.content_type,
64
+ attachment_type=self.model.__name__,
65
+ attachment_type_id=attachment_type_id
66
+ )
67
+ except Exception as db_error:
68
+ log_exception(Exception(db_error))
69
+ return ApiResponse(
70
+ status=False,
71
+ code=ResponseCode.BAD_REQUEST,
72
+ message=f"Database error: {db_error}",
73
+ data=None
74
+ )
75
+
76
+ # Return success response after successful upload and database insertion
77
+ return ApiResponse(
78
+ status=True,
79
+ code=ResponseCode.SUCCESS,
80
+ message="File uploaded and saved successfully!",
81
+ data=attachment
82
+ )
83
+ except DoesNotExist:
84
+ return ApiResponse(
85
+ status=False,
86
+ code=ResponseCode.NO_RECORD_FOUND,
87
+ message=f"{self.model.Meta.verbose_name} does not exist",
88
+ data=None
89
+ )
90
+ except Exception as e:
91
+ # Handle general exceptions
92
+ log_exception(Exception(e))
93
+ return ApiResponse(
94
+ status=False,
95
+ code=ResponseCode.FAILURE,
96
+ message=f"Unexpected error occurred, try again!",
97
+ data=None
98
+ )
99
+
100
+ async def delete_attachment(self, attachment_id: str) -> ApiResponse:
101
+ try:
102
+ attachment = await Attachment.get(id=attachment_id)
103
+ # Retrieve the file from MinIO
104
+ result = MinioService.get_instance().delete_file(f"{self.model.__name__}/{attachment.file_path}")
105
+
106
+ await attachment.delete()
107
+ # Return success response with file content
108
+ return ApiResponse(
109
+ status=True,
110
+ code=ResponseCode.SUCCESS,
111
+ message="Attachment deleted successfully!",
112
+ data=result
113
+ )
114
+ except DoesNotExist:
115
+ return ApiResponse(
116
+ status=False,
117
+ code=ResponseCode.NO_RECORD_FOUND,
118
+ message=f"Attachment does not exist",
119
+ data=None
120
+ )
121
+ except Exception as e:
122
+ # Handle errors in file retrieval
123
+ return ApiResponse(
124
+ status=False,
125
+ code=ResponseCode.NO_RECORD_FOUND,
126
+ message=f"An error occurred while deleting the attachment: {e}",
127
+ data=None
128
+ )
129
+
130
+ async def get_attachments(self, model_id: str) -> ApiResponse:
131
+ try:
132
+ attachments = await Attachment.filter(attachment_type_id=model_id, attachment_type=self.model.__name__)
133
+
134
+ return ApiResponse(
135
+ status=True,
136
+ code=ResponseCode.SUCCESS,
137
+ message=f"{self.model.Meta.verbose_name} attachments fetched successfully",
138
+ data=PaginatedResponse(
139
+ items=attachments,
140
+ item_count=len(attachments),
141
+ )
142
+ )
143
+ except Exception as e:
144
+ log_exception(Exception(e))
145
+ return ApiResponse(
146
+ status=False,
147
+ code=ResponseCode.BAD_REQUEST,
148
+ message=f"Failed to fetch {self.model.Meta.verbose_name} attachments. Try again",
149
+ data=None
150
+ )
151
+
152
+ async def download_attachment(self, file_path: str) -> ApiResponse:
153
+ try:
154
+ # Call the async download_file method to get the base64 content
155
+ base64_content = await MinioService.get_instance().download_file(f"{self.model.__name__}/{file_path}")
156
+
157
+ if base64_content is False:
158
+ return ApiResponse(
159
+ status=False,
160
+ code=ResponseCode.NO_RECORD_FOUND,
161
+ message="File not found or an error occurred while retrieving the file.",
162
+ data=None
163
+ )
164
+
165
+ # Return success response with base64 content
166
+ return ApiResponse(
167
+ status=True,
168
+ code=ResponseCode.SUCCESS,
169
+ message="File retrieved successfully!",
170
+ data=base64_content.decode('utf-8') # Convert bytes to string
171
+ )
172
+
173
+ except Exception as e:
174
+ # Handle errors in file retrieval
175
+ return ApiResponse(
176
+ status=False,
177
+ code=ResponseCode.NO_RECORD_FOUND,
178
+ message=f"An error occurred while retrieving the file: {e}",
179
+ data=None
180
+ )
181
+
182
+ async def download_attachment_url(self, file_path: str, expiry_seconds: int = 3600) -> ApiResponse:
183
+ """
184
+ Generate a signed URL for downloading a file from MinIO.
185
+
186
+ :param file_path: Relative path to the file in the bucket.
187
+ :param expiry_seconds: Time in seconds for which the signed URL will be valid.
188
+ :return: ApiResponse containing the signed URL or error.
189
+ """
190
+ try:
191
+ full_path = f"{self.model.__name__}/{file_path}" if hasattr(self, 'model') else file_path
192
+
193
+ signed_url = await MinioService.get_instance().get_signed_url(full_path, expiry_seconds=expiry_seconds)
194
+
195
+ if not signed_url:
196
+ return ApiResponse(
197
+ status=False,
198
+ code=ResponseCode.NO_RECORD_FOUND,
199
+ message="Failed to generate signed URL or file does not exist.",
200
+ data=None
201
+ )
202
+
203
+ return ApiResponse(
204
+ status=True,
205
+ code=ResponseCode.SUCCESS,
206
+ message="Signed URL generated successfully!",
207
+ data=signed_url
208
+ )
209
+
210
+ except Exception as e:
211
+ return ApiResponse(
212
+ status=False,
213
+ code=ResponseCode.FAILURE,
214
+ message=f"An error occurred while generating signed URL: {e}",
215
+ data=None
216
+ )
217
+
218
+
219
+
@@ -0,0 +1,17 @@
1
+ import strawberry
2
+ from typing import Optional
3
+
4
+ @strawberry.input
5
+ class AttachmentFile:
6
+ content: str
7
+ content_type: str
8
+ name: str
9
+ extension: str
10
+
11
+ @strawberry.input
12
+ class AttachmentUpload:
13
+ title: Optional[str] = None
14
+ description: Optional[str] = None
15
+ attachment_type_category: Optional[str] = None
16
+ file: AttachmentFile
17
+
@@ -0,0 +1,16 @@
1
+ from typing import Optional
2
+ import strawberry
3
+
4
+ from typing import Optional
5
+ import strawberry
6
+
7
+ @strawberry.type
8
+ class AttachmentResponse:
9
+ id: Optional[int] = None
10
+ title: Optional[str] = None
11
+ description: Optional[str] = None
12
+ file_path: Optional[str] = None
13
+ mem_type: Optional[str] = None
14
+ attachment_type: Optional[str] = None
15
+ attachment_type_category: Optional[str] = None
16
+ attachment_type_id: Optional[int] = None
@@ -0,0 +1,162 @@
1
+
2
+ import base64
3
+ from minio import Minio
4
+ from minio.error import S3Error
5
+ import asyncio
6
+ import threading
7
+ import aiofiles
8
+ from io import BytesIO
9
+ from datetime import timedelta
10
+
11
+ from fast_backend_builder.utils.error_logging import log_exception
12
+
13
+ class MinioService:
14
+ _instance = None
15
+ _lock = threading.Lock()
16
+
17
+ def __new__(cls):
18
+ if cls._instance is None:
19
+ with cls._lock:
20
+ if cls._instance is None:
21
+ cls._instance = super(MinioService, cls).__new__(cls)
22
+ return cls._instance
23
+
24
+ def __init__(self):
25
+ if not hasattr(self, 'initialized'):
26
+ self.minio_client = None
27
+ self.bucket_name = None
28
+ self.initialized = False
29
+
30
+ @classmethod
31
+ def get_instance(cls):
32
+ """Get the initialized instance of MinioService."""
33
+ if cls._instance is None or not cls._instance.initialized:
34
+ raise Exception("MinIO service is not initialized.")
35
+ return cls._instance
36
+
37
+ async def init(self, server: str, access_key: str, secret_key: str, bucket_name: str, secure: bool = True):
38
+ """Initialize the MinIO service asynchronously with the given configuration."""
39
+ try:
40
+ if not self.initialized:
41
+ self.minio_client = Minio(
42
+ endpoint=server,
43
+ access_key=access_key,
44
+ secret_key=secret_key,
45
+ secure=secure
46
+ )
47
+ self.bucket_name = bucket_name
48
+
49
+ # Ensure bucket exists or create it
50
+ if not self.minio_client.bucket_exists(bucket_name):
51
+ self.minio_client.make_bucket(bucket_name)
52
+
53
+ self.initialized = True
54
+ print(f"MinIO initialized with bucket '{bucket_name}'")
55
+ else:
56
+ print("MinIO service is already initialized.")
57
+ return True
58
+ except S3Error as e:
59
+ log_exception(e)
60
+ return False
61
+
62
+ async def upload_file(self, file_name: str, file_data: bytes, content_type: str):
63
+ """Save a file to the MinIO bucket asynchronously and return the file path when successful or False when failed."""
64
+ # Check if MinIO service is initialized
65
+ if not self.initialized:
66
+ print("MinIO service not initialized.")
67
+ return False, 'Files service not initialized'
68
+
69
+ try:
70
+ # Use BytesIO to simulate async operation (Minio SDK is not natively async)
71
+ data = BytesIO(file_data)
72
+ await asyncio.to_thread(
73
+ self.minio_client.put_object,
74
+ self.bucket_name,
75
+ file_name,
76
+ data,
77
+ len(file_data),
78
+ content_type=content_type
79
+ )
80
+ print(f"File '{file_name}' saved successfully.")
81
+ file_url = f"{self.bucket_name}/{file_name}"
82
+ return file_url, None # Return the file path
83
+ except S3Error as e:
84
+ log_exception(e)
85
+ return False, f"Error uploading file '{file_name}'"
86
+
87
+ async def delete_file(self, file_name: str):
88
+ """Delete a file from the MinIO bucket asynchronously."""
89
+ if not self.initialized:
90
+ print("MinIO service not initialized.")
91
+ return False, 'Files service not initialized'
92
+
93
+ try:
94
+ await asyncio.to_thread(self.minio_client.remove_object, self.bucket_name, file_name)
95
+ print(f"File '{file_name}' deleted successfully.")
96
+ return True
97
+ except S3Error as e:
98
+ log_exception(e)
99
+ return False # Return False if failed
100
+
101
+ async def download_file(self, file_name: str):
102
+ """Download a file from the MinIO bucket asynchronously and return it as base64 bytes."""
103
+ self._ensure_initialized()
104
+
105
+ try:
106
+ data = await asyncio.to_thread(self.minio_client.get_object, self.bucket_name, file_name)
107
+ file_bytes = await asyncio.to_thread(data.read) # Read the entire file into memory
108
+
109
+ # Encode the bytes to base64
110
+ base64_bytes = base64.b64encode(file_bytes)
111
+
112
+ print(f"File '{file_name}' downloaded successfully as base64.")
113
+ return base64_bytes # Return the base64-bytes
114
+ except S3Error as e:
115
+ log_exception(e)
116
+ return False # Return False if failed
117
+
118
+
119
+ async def download_byte_file(self, file_name: str):
120
+ """
121
+ Download a file from the MinIO bucket asynchronously and return raw bytes.
122
+ """
123
+ self._ensure_initialized()
124
+ try:
125
+ # Fetch file from MinIO
126
+ data = await asyncio.to_thread(self.minio_client.get_object, self.bucket_name, file_name)
127
+ # Read raw bytes
128
+ file_bytes = await asyncio.to_thread(data.read)
129
+ print(f"File '{file_name}' downloaded successfully")
130
+ return file_bytes # Return raw bytes
131
+ except S3Error as e:
132
+ print(f"Error downloading file: {e}")
133
+ log_exception(e)
134
+ return None
135
+
136
+ async def get_signed_url(self, file_name: str, expiry_seconds: int = 3600):
137
+ """
138
+ Generate a pre-signed URL for a file stored in MinIO.
139
+
140
+ :param file_name: The name of the file in the bucket.
141
+ :param expiry_seconds: Time in seconds until the URL expires (default is 1 hour).
142
+ :return: A signed URL string or None if an error occurred.
143
+ """
144
+ self._ensure_initialized()
145
+
146
+ try:
147
+ url = await asyncio.to_thread(
148
+ self.minio_client.presigned_get_object,
149
+ self.bucket_name,
150
+ file_name,
151
+ expires=timedelta(seconds=expiry_seconds)
152
+ )
153
+ print(f"Generated signed URL for '{file_name}': {url}")
154
+ return url
155
+ except S3Error as e:
156
+ log_exception(e)
157
+ return None
158
+
159
+ def _ensure_initialized(self):
160
+ """Check if the MinIO service is initialized, else raise an error."""
161
+ if not self.initialized:
162
+ raise Exception("MinIO service is not initialized. Call `init()` first.")
@@ -0,0 +1,6 @@
1
+
2
+ def upload_attachment():
3
+ pass
4
+
5
+ def delete_attachment():
6
+ pass
@@ -0,0 +1,7 @@
1
+ @strawberry.field
2
+ async def get_attachments():
3
+ pass
4
+
5
+ @strawberry.field
6
+ def download_attachment():
7
+ pass
@@ -0,0 +1,9 @@
1
+ from typing import Optional
2
+ import strawberry
3
+
4
+ from fast_backend_builder.attach.request import AttachmentUpload
5
+
6
+ '''ATTACHMENT_UPLOAD'''
7
+ @strawberry.input
8
+ class _MODEL_Attachment(AttachmentUpload):
9
+ pass
@@ -0,0 +1,9 @@
1
+ from typing import Optional
2
+ import strawberry
3
+
4
+ from fast_backend_builder.attach.response import AttachmentResponse
5
+
6
+ '''ATTACHMENT_RESPONSE'''
7
+ @strawberry.type
8
+ class _MODEL_AttachmentResponse(AttachmentResponse):
9
+ pass