fast-mu-builder 0.1.0.3__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.
- fast_mu_builder-0.1.0.3/LICENSE +19 -0
- fast_mu_builder-0.1.0.3/PKG-INFO +102 -0
- fast_mu_builder-0.1.0.3/README.md +64 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/api.py +72 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/gql_controller.py +219 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/request.py +17 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/response.py +16 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/service.py +230 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/templates/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/templates/attachment_mutations.py +6 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/templates/attachment_queries.py +7 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/templates/gql_input_template.py +9 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/templates/gql_type_template.py +9 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/attach/templates/graphql_api_template.py +217 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/auth/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/auth/auth.py +190 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/auth/context.py +114 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/auth/jwt_handler.py +96 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/auth/middleware.py +142 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/auth/mixings/HeadshipMixing.py +81 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/auth/mixings/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/auth/redis.py +260 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/auth/session_handler.py +58 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/commands/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/commands/graphql.py +76 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/models.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/request/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/request/schemas.py +43 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/response/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/response/codes.py +35 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/response/schemas.py +27 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/schemas.py +9 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/templates/module_init_.py +11 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/validation/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/validation/decorators.py +103 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/validation/field_validator.py +789 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/common/validation/rules.py +536 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/controller.py +278 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/gql_controller.py +709 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/graphql_api_gen.py +301 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/graphql_schema_gen.py +152 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/rest_api.py +66 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/templates/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/templates/controller_template.py +8 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/templates/gql_input_template.py +11 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/templates/gql_schemas_template.py +11 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/templates/gql_type_template.py +8 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/templates/graphql_api_template.py +132 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/crud/templates/model_template.py +6 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/esb/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/esb/esb.py +366 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/esb/schemas.py +39 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/models/__init__.py +305 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/models/attachment.py +20 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/models/notification.py +84 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/models/report_logs/__init__.py +1 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/models/report_logs/audit_log.py +21 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/models/uaa.py +252 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/models/workflow.py +359 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/notifications/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/notifications/service.py +105 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/enums.py +25 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/env.py +41 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/error_logging.py +87 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/file.py +37 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/helpers/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/helpers/log_activity.py +26 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/helpers/request.py +17 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/metrics/__init__.py +1 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/metrics/db_metrics.py +109 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/mutable_settings.py +192 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/reflection.py +61 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/sentry.py +64 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/utils/str_helpers.py +190 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/workflow/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/workflow/exceptions.py +47 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/workflow/gql_controller.py +162 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/workflow/request.py +8 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/workflow/response.py +16 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/workflow/templates/__init__.py +0 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder/workflow/templates/gql_type_template.py +8 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder.egg-info/PKG-INFO +102 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder.egg-info/SOURCES.txt +95 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder.egg-info/dependency_links.txt +1 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder.egg-info/entry_points.txt +2 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder.egg-info/not-zip-safe +1 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder.egg-info/requires.txt +18 -0
- fast_mu_builder-0.1.0.3/fast_mu_builder.egg-info/top_level.txt +1 -0
- fast_mu_builder-0.1.0.3/pyproject.toml +59 -0
- fast_mu_builder-0.1.0.3/setup.cfg +20 -0
- fast_mu_builder-0.1.0.3/setup.py +55 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fast-mu-builder
|
|
3
|
+
Version: 0.1.0.3
|
|
4
|
+
Summary: FastAPI Builder with Tortoise ORM support for MU
|
|
5
|
+
Home-page: https://bitbucket.org/external-dev-mzumbe/fast-mu-builder
|
|
6
|
+
Author: Japhary Juma Hamisi
|
|
7
|
+
Author-email: Japhary Juma Hamisi <japharyjuma@gmail.com>, Shija Ntula <abuukauthar13@gmail.com>, Juma Ludanga <ludangajn@gmail.com>
|
|
8
|
+
License: Other License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Framework :: FastAPI
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: fastapi==0.116.1
|
|
17
|
+
Requires-Dist: tortoise-orm[asyncpg]==0.20.1
|
|
18
|
+
Requires-Dist: pydantic==2.11.7
|
|
19
|
+
Requires-Dist: httpx==0.28.1
|
|
20
|
+
Requires-Dist: strawberry-graphql[fastapi]==0.278.1
|
|
21
|
+
Requires-Dist: jinja2==3.1.6
|
|
22
|
+
Requires-Dist: pluralize==20240519.3
|
|
23
|
+
Requires-Dist: PyJWT==2.10.1
|
|
24
|
+
Requires-Dist: redis==5.2.1
|
|
25
|
+
Requires-Dist: minio==7.2.16
|
|
26
|
+
Requires-Dist: aiofiles==24.1.0
|
|
27
|
+
Requires-Dist: bullmq==2.15.0
|
|
28
|
+
Requires-Dist: sentry_sdk==2.34.1
|
|
29
|
+
Requires-Dist: cryptography==45.0.6
|
|
30
|
+
Requires-Dist: python-decouple==3.8
|
|
31
|
+
Requires-Dist: celery[redis]==5.5.3
|
|
32
|
+
Requires-Dist: xlsxwriter==3.2.5
|
|
33
|
+
Requires-Dist: fpdf2==2.8.3
|
|
34
|
+
Dynamic: author
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
|
|
39
|
+
Building The package:
|
|
40
|
+
|
|
41
|
+
python setup.py sdist bdist_wheel
|
|
42
|
+
|
|
43
|
+
install the package to your app:
|
|
44
|
+
|
|
45
|
+
pip install path/to/wheel file
|
|
46
|
+
|
|
47
|
+
generating Graphql schemas
|
|
48
|
+
|
|
49
|
+
graphql gen:crud-api <module-name> --model Model1,Model2,Model3
|
|
50
|
+
|
|
51
|
+
generating graphql schemas with attachments
|
|
52
|
+
|
|
53
|
+
graphql gen:crud-api <module-name> --model ModelName --with-attachment
|
|
54
|
+
|
|
55
|
+
for attachments you must have minio server and these .env configs:
|
|
56
|
+
|
|
57
|
+
MINIO_SERVER=ip_address:api_port
|
|
58
|
+
MINIO_BUCKET=backet-name
|
|
59
|
+
MINIO_ACCESS_KEY=minio-username
|
|
60
|
+
MINIO_SECRETE_KEY=minio-password
|
|
61
|
+
MINIO_SECURE=False
|
|
62
|
+
|
|
63
|
+
You must initialize the MinioService before using it by:
|
|
64
|
+
|
|
65
|
+
@app.on_event("startup")
|
|
66
|
+
async def startup_event():
|
|
67
|
+
# Run the init_minio_service in the background without blocking the app startup
|
|
68
|
+
asyncio.create_task(init_minio_service())
|
|
69
|
+
|
|
70
|
+
async def init_minio_service():
|
|
71
|
+
minio_service = MinioService()
|
|
72
|
+
try:
|
|
73
|
+
await minio_service.init(
|
|
74
|
+
server=config('MINIO_SERVER'),
|
|
75
|
+
access_key=config('MINIO_ACCESS_KEY'),
|
|
76
|
+
secret_key=config('MINIO_SECRETE_KEY'),
|
|
77
|
+
bucket_name=config('MINIO_BUCKET'),
|
|
78
|
+
secure=config('MINIO_SECURE', cast=bool)
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"Error initializing MinIO service: {e}")
|
|
82
|
+
|
|
83
|
+
Making it async will easy your app booting process.
|
|
84
|
+
|
|
85
|
+
If you need to use the service out of generated crud apis yo do:
|
|
86
|
+
|
|
87
|
+
For uploading:
|
|
88
|
+
|
|
89
|
+
file_location, upload_error = await MinioService.get_instance().upload_file(
|
|
90
|
+
file_name=f"path/to/file.extension",
|
|
91
|
+
file_data=base64_decoded_file,
|
|
92
|
+
content_type=attachment.file.content_type
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
For downloading:
|
|
96
|
+
|
|
97
|
+
base64_content = await MinioService.get_instance().download_file("path/to/file.extension")
|
|
98
|
+
|
|
99
|
+
For deleting:
|
|
100
|
+
|
|
101
|
+
result = MinioService.get_instance().delete_file("path/to/file.extension")
|
|
102
|
+
|
|
@@ -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
|
+
|
|
File without changes
|
|
File without changes
|
|
@@ -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_mu_builder.common.response.codes import ResponseCode
|
|
6
|
+
# from fast_mu_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_mu_builder.attach.request import AttachmentUpload
|
|
9
|
+
from fast_mu_builder.attach.service import MinioService
|
|
10
|
+
from fast_mu_builder.models.attachment import Attachment
|
|
11
|
+
from fast_mu_builder.utils.error_logging import log_exception
|
|
12
|
+
from minio import Minio
|
|
13
|
+
from minio.error import S3Error
|
|
14
|
+
|
|
15
|
+
from fast_mu_builder.common.response.codes import ResponseCode
|
|
16
|
+
from fast_mu_builder.common.response.schemas import ApiResponse, PaginatedResponse
|
|
17
|
+
from fast_mu_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[str] = 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[str] = None
|