├── .gitignore
├── LICENSE
├── README.md
├── deployment
├── .dockerignore
├── deploy.bat
├── deploy.sh
└── docker-compose.yml
├── infra
├── api-gateway
│ ├── .dockerignore
│ └── docker-compose.yml
└── efk-stack
│ ├── .dockerignore
│ ├── docker-compose.yml
│ ├── fluent-bit.conf
│ └── parser_json.conf
└── services
├── iam-service
├── .dockerignore
├── Dockerfile
├── app
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ └── v1
│ │ │ ├── __init__.py
│ │ │ └── endpoints
│ │ │ ├── __init__.py
│ │ │ └── users.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── db
│ │ │ ├── __init__.py
│ │ │ └── database.py
│ │ └── redis
│ │ │ ├── __init__.py
│ │ │ └── redis_client.py
│ ├── domain
│ │ ├── __init__.py
│ │ ├── models
│ │ │ ├── __init__.py
│ │ │ └── user.py
│ │ └── schemas
│ │ │ ├── __init__.py
│ │ │ ├── token_schema.py
│ │ │ └── user_schema.py
│ ├── infrastructure
│ │ ├── __init__.py
│ │ └── repositories
│ │ │ ├── __init__.py
│ │ │ └── user_repository.py
│ ├── logging_service
│ │ ├── __init__.py
│ │ └── logging_config.py
│ ├── main.py
│ └── services
│ │ ├── __init__.py
│ │ ├── auth_services
│ │ ├── __init__.py
│ │ ├── auth_service.py
│ │ ├── hash_sevice.py
│ │ └── otp_service.py
│ │ ├── base_service.py
│ │ ├── register_service.py
│ │ └── user_service.py
├── docker-compose.yml
└── requirements.txt
├── media-service
├── .dockerignore
├── Dockerfile
├── app
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ └── v1
│ │ │ ├── __init__.py
│ │ │ └── endpoints
│ │ │ ├── __init__.py
│ │ │ └── media.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── config.py
│ │ └── db
│ │ │ ├── __init__.py
│ │ │ └── database.py
│ ├── domain
│ │ ├── __init__.py
│ │ ├── models
│ │ │ ├── __init__.py
│ │ │ ├── media_model.py
│ │ │ └── object_id_model.py
│ │ └── schemas
│ │ │ ├── __init__.py
│ │ │ ├── media_schema.py
│ │ │ └── token_schema.py
│ ├── grpc_server.py
│ ├── grpc_service
│ │ ├── __init__.py
│ │ ├── media.proto
│ │ ├── media_pb2.py
│ │ └── media_pb2_grpc.py
│ ├── infrastructure
│ │ ├── __init__.py
│ │ ├── clients
│ │ │ ├── __init__.py
│ │ │ ├── http_client.py
│ │ │ └── iam_client.py
│ │ ├── repositories
│ │ │ ├── __init__.py
│ │ │ └── media_repository.py
│ │ └── storage
│ │ │ ├── __init__.py
│ │ │ └── gridfs_storage.py
│ ├── logging_service
│ │ ├── __init__.py
│ │ └── logging_config.py
│ ├── main.py
│ └── services
│ │ ├── auth_service.py
│ │ └── media_service.py
├── docker-compose.yml
├── requirements.txt
└── supervisord.conf
└── ocr-service
├── .dockerignore
├── .idea
└── .gitignore
├── Dockerfile
├── app
├── __init__.py
├── api
│ ├── __init__.py
│ └── v1
│ │ ├── __init__.py
│ │ └── endpoints
│ │ ├── __init__.py
│ │ └── ocr.py
├── core
│ ├── __init__.py
│ ├── config.py
│ └── db
│ │ ├── __init__.py
│ │ └── database.py
├── domain
│ ├── __init__.py
│ ├── models
│ │ ├── __init__.py
│ │ ├── object_id_model.py
│ │ └── ocr_model.py
│ └── schemas
│ │ ├── __init__.py
│ │ ├── ocr_schema.py
│ │ └── token_schema.py
├── infrastructure
│ ├── __init__.py
│ ├── clients
│ │ ├── __init__.py
│ │ ├── grpc_client.py
│ │ ├── http_client.py
│ │ ├── iam_client.py
│ │ └── media_client.py
│ ├── grpc
│ │ ├── __init__.py
│ │ ├── media.proto
│ │ ├── media_pb2.py
│ │ └── media_pb2_grpc.py
│ └── repositories
│ │ ├── __init__.py
│ │ └── ocr_repository.py
├── logging_service
│ ├── __init__.py
│ └── logging_config.py
├── main.py
└── services
│ ├── auth_service.py
│ └── ocr_service.py
├── docker-compose.yml
└── requirements.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all,linux,windows
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all,linux,windows
3 |
4 | ### Linux ###
5 | *~
6 |
7 | # temporary files which can be created if a process still has a handle open of a deleted file
8 | .fuse_hidden*
9 |
10 | # KDE directory preferences
11 | .directory
12 |
13 | # Linux trash folder which might appear on any partition or disk
14 | .Trash-*
15 |
16 | # .nfs files are created when an open file is removed but is still being accessed
17 | .nfs*
18 |
19 | ### PyCharm+all ###
20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
21 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
22 |
23 | # User-specific stuff
24 | .idea/**/workspace.xml
25 | .idea/**/tasks.xml
26 | .idea/**/usage.statistics.xml
27 | .idea/**/dictionaries
28 | .idea/**/shelf
29 |
30 | # AWS User-specific
31 | .idea/**/aws.xml
32 |
33 | # Generated files
34 | .idea/**/contentModel.xml
35 |
36 | # Sensitive or high-churn files
37 | .idea/**/dataSources/
38 | .idea/**/dataSources.ids
39 | .idea/**/dataSources.local.xml
40 | .idea/**/sqlDataSources.xml
41 | .idea/**/dynamic.xml
42 | .idea/**/uiDesigner.xml
43 | .idea/**/dbnavigator.xml
44 |
45 | # Gradle
46 | .idea/**/gradle.xml
47 | .idea/**/libraries
48 |
49 | # Gradle and Maven with auto-import
50 | # When using Gradle or Maven with auto-import, you should exclude module files,
51 | # since they will be recreated, and may cause churn. Uncomment if using
52 | # auto-import.
53 | # .idea/artifacts
54 | # .idea/compiler.xml
55 | # .idea/jarRepositories.xml
56 | # .idea/modules.xml
57 | # .idea/*.iml
58 | # .idea/modules
59 | # *.iml
60 | # *.ipr
61 |
62 | # CMake
63 | cmake-build-*/
64 |
65 | # Mongo Explorer plugin
66 | .idea/**/mongoSettings.xml
67 |
68 | # File-based project format
69 | *.iws
70 |
71 | # IntelliJ
72 | out/
73 |
74 | # mpeltonen/sbt-idea plugin
75 | .idea_modules/
76 |
77 | # JIRA plugin
78 | atlassian-ide-plugin.xml
79 |
80 | # Cursive Clojure plugin
81 | .idea/replstate.xml
82 |
83 | # SonarLint plugin
84 | .idea/sonarlint/
85 |
86 | # Crashlytics plugin (for Android Studio and IntelliJ)
87 | com_crashlytics_export_strings.xml
88 | crashlytics.properties
89 | crashlytics-build.properties
90 | fabric.properties
91 |
92 | # Editor-based Rest Client
93 | .idea/httpRequests
94 |
95 | # Android studio 3.1+ serialized cache file
96 | .idea/caches/build_file_checksums.ser
97 |
98 | ### PyCharm+all Patch ###
99 | # Ignore everything but code style settings and run configurations
100 | # that are supposed to be shared within teams.
101 |
102 | .idea/*
103 |
104 | !.idea/codeStyles
105 | !.idea/runConfigurations
106 |
107 | ### Python ###
108 | # Byte-compiled / optimized / DLL files
109 | __pycache__/
110 | *.py[cod]
111 | *$py.class
112 |
113 | # C extensions
114 | *.so
115 |
116 | # Distribution / packaging
117 | .Python
118 | build/
119 | develop-eggs/
120 | dist/
121 | downloads/
122 | eggs/
123 | .eggs/
124 | lib/
125 | lib64/
126 | parts/
127 | sdist/
128 | var/
129 | wheels/
130 | share/python-wheels/
131 | *.egg-info/
132 | .installed.cfg
133 | *.egg
134 | MANIFEST
135 |
136 | # PyInstaller
137 | # Usually these files are written by a python script from a template
138 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
139 | *.manifest
140 | *.spec
141 |
142 | # Installer logs
143 | pip-log.txt
144 | pip-delete-this-directory.txt
145 |
146 | # Unit test / coverage reports
147 | htmlcov/
148 | .tox/
149 | .nox/
150 | .coverage
151 | .coverage.*
152 | .cache
153 | nosetests.xml
154 | coverage.xml
155 | *.cover
156 | *.py,cover
157 | .hypothesis/
158 | .pytest_cache/
159 | cover/
160 |
161 | # Translations
162 | *.mo
163 | *.pot
164 |
165 | # Django stuff:
166 | *.log
167 | local_settings.py
168 | db.sqlite3
169 | db.sqlite3-journal
170 |
171 | # Flask stuff:
172 | instance/
173 | .webassets-cache
174 |
175 | # Scrapy stuff:
176 | .scrapy
177 |
178 | # Sphinx documentation
179 | docs/_build/
180 |
181 | # PyBuilder
182 | .pybuilder/
183 | target/
184 |
185 | # Jupyter Notebook
186 | .ipynb_checkpoints
187 |
188 | # IPython
189 | profile_default/
190 | ipython_config.py
191 |
192 | # pyenv
193 | # For a library or package, you might want to ignore these files since the code is
194 | # intended to run in multiple environments; otherwise, check them in:
195 | # .python-version
196 |
197 | # pipenv
198 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
199 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
200 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
201 | # install all needed dependencies.
202 | #Pipfile.lock
203 |
204 | # poetry
205 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
206 | # This is especially recommended for binary packages to ensure reproducibility, and is more
207 | # commonly ignored for libraries.
208 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
209 | #poetry.lock
210 |
211 | # pdm
212 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
213 | #pdm.lock
214 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
215 | # in version control.
216 | # https://pdm.fming.dev/#use-with-ide
217 | .pdm.toml
218 |
219 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
220 | __pypackages__/
221 |
222 | # Celery stuff
223 | celerybeat-schedule
224 | celerybeat.pid
225 |
226 | # SageMath parsed files
227 | *.sage.py
228 |
229 | # Environments
230 | .env
231 | .venv
232 | env/
233 | venv/
234 | ENV/
235 | env.bak/
236 | venv.bak/
237 |
238 | *logs/
239 |
240 | # Spyder project settings
241 | .spyderproject
242 | .spyproject
243 |
244 | # Rope project settings
245 | .ropeproject
246 |
247 | # mkdocs documentation
248 | /site
249 |
250 | # mypy
251 | .mypy_cache/
252 | .dmypy.json
253 | dmypy.json
254 |
255 | # Pyre type checker
256 | .pyre/
257 |
258 | # pytype static type analyzer
259 | .pytype/
260 |
261 | # Cython debug symbols
262 | cython_debug/
263 |
264 | # PyCharm
265 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
266 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
267 | # and can be added to the global gitignore or merged into this file. For a more nuclear
268 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
269 | #.idea/
270 |
271 | ### Python Patch ###
272 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
273 | poetry.toml
274 |
275 | # ruff
276 | .ruff_cache/
277 |
278 | # LSP config files
279 | pyrightconfig.json
280 |
281 | ### Windows ###
282 | # Windows thumbnail cache files
283 | Thumbs.db
284 | Thumbs.db:encryptable
285 | ehthumbs.db
286 | ehthumbs_vista.db
287 |
288 | # Dump file
289 | *.stackdump
290 |
291 | # Folder config file
292 | [Dd]esktop.ini
293 |
294 | # Recycle Bin used on file shares
295 | $RECYCLE.BIN/
296 |
297 | # Windows Installer files
298 | *.cab
299 | *.msi
300 | *.msix
301 | *.msm
302 | *.msp
303 |
304 | # Windows shortcuts
305 | *.lnk
306 |
307 | # End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all,linux,windows
308 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 MohammadAmin Eskandari
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AIToolsBox
2 |
3 | Welcome to **AIToolsBox**! This project is a comprehensive example of building and deploying AI-powered microservices using FastAPI. It demonstrates clean architecture, dependency injection, and advanced microservices practices. The project currently includes an OCR service leveraging Tesseract OCR, and will be expanded to include additional AI services in the future. Additionally, AIToolsBox comes with a React-based web application and a Flutter-based Android client.
4 |
5 | ## Table of Contents
6 |
7 | - [Overview](#overview)
8 | - [Features](#features)
9 | - [Clean Architecture](#clean-architecture)
10 | - [Prerequisites](#prerequisites)
11 | - [Setup and Deployment](#setup-and-deployment)
12 | - [Usage](#usage)
13 | - [Contributing](#contributing)
14 | - [License](#license)
15 | - [Contact](#contact)
16 |
17 | ## Overview
18 |
19 | **AIToolsBox** is designed to be a learning resource and a practical example of implementing AI-powered microservices with a focus on clean architecture. It features:
20 |
21 | - A microservice architecture using FastAPI.
22 | - A Tesseract OCR service for extracting text from images.
23 | - Centralized logging and monitoring using the EFK stack (Elasticsearch, Fluent Bit, Kibana).
24 | - API management and routing through Traefik.
25 | - HTTP & gRPC communication between services.
26 |
27 | ## Features
28 |
29 | - **Microservices with FastAPI**: Each service is implemented using FastAPI with clean architecture principles and dependency injection for maintainability and scalability.
30 | - **Tesseract OCR Service**: Utilizes Tesseract OCR to extract text from images.
31 | - **Traefik API Gateway**: Manages routing and load balancing, accessible via `http://localhost:8080`.
32 | - **EFK Stack for Logging**: Centralized logging using Elasticsearch, Fluent Bit, and Kibana, accessible via `http://localhost:5601`.
33 | - **Simplified Deployment**: Easy setup and deployment scripts for both Windows and Linux users.
34 |
35 | ## Clean Architecture
36 |
37 | AIToolsBox follows the principles of Clean Architecture in each service to ensure that services are:
38 |
39 | - **Independent of frameworks**: The core application logic is not dependent on any specific framework or external technology.
40 | - **Testable**: Each component is isolated and can be tested independently.
41 | - **Independent of UI**: The user interface can change without altering the business logic.
42 | - **Independent of Database**: The database can be swapped without changing the business logic.
43 |
44 | ## Prerequisites
45 |
46 | To run this project, ensure you have the following installed:
47 |
48 | - [Docker](https://www.docker.com/get-started)
49 | - [Docker Compose](https://docs.docker.com/compose/install/)
50 | - [Python 3.10+](https://www.python.org/downloads/)
51 |
52 | ## Setup and Deployment
53 |
54 | ### Cloning the Repository
55 |
56 | Clone the repository and navigate to the project directory:
57 |
58 | ```bash
59 | git clone https://github.com/aminupy/AIToolsBox.git
60 | cd AIToolsBox
61 | ```
62 |
63 | ### Running the Services
64 | #### Windows
65 | To run the services on Windows, execute the following command:
66 |
67 | ```bash
68 | cd deployment
69 | deply.bat
70 | ```
71 |
72 | #### Linux
73 | To run the services on Linux, execute the following command:
74 |
75 | ```bash
76 | cd deployment
77 | ./deploy.sh
78 | ```
79 |
80 | These scripts will build and start all microservices, the Traefik API gateway, and the EFK stack.
81 |
82 | ### Accessing the Services
83 |
84 | - **IAM Service**: 'http://iam.localhost'
85 | - **Media Service**: 'http://media.localhost'
86 | - **OCR Service**: 'http://ocr.localhost'
87 | - **Traefik Dashboard**: 'http://localhost:8080'
88 | - **Kibana Dashboard**: 'http://localhost:5601'
89 |
90 |
91 | ## Usage
92 |
93 | - **IAM Service**: Handles user registration, authentication and authorization.
94 | - **Media Service**: Manages operations related to media files.
95 | - **OCR Service**: Extracts text from images using Tesseract OCR.
96 | - **Traefik Dashboard**: Provides an overview of the services and routing configuration.
97 | - **Kibana Dashboard**: Displays logs and metrics for monitoring.
98 |
99 |
100 | ## Contributing
101 | Contributions are welcome! To contribute, follow these steps:
102 |
103 | 1. Fork this repository.
104 | 2. Create a new branch (`git checkout -b feature-branch`).
105 | 3. Make your changes.
106 | 4. Commit your changes (`git commit -am 'Add new feature'`).
107 | 5. Push to the branch (`git push origin feature-branch`).
108 | 6. Create a new Pull Request.
109 | 7. Sit back and relax while your PR is reviewed.
110 |
111 |
112 | ## License
113 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more information.
114 |
115 | ## Contact
116 | If you have any questions or suggestions, feel free to reach out to me at:
117 | - Email: esaminu.py@gmail.com
118 | - LinkedIn: [linkedin.com/aminupy](www.linkedin.com/in/mohamadamin-eskandari-28377b225)
119 | - GitHub: [github.com/aminupy](https://github.com/aminupy)
120 |
--------------------------------------------------------------------------------
/deployment/.dockerignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
4 |
5 | # User-specific stuff
6 | .idea/**/workspace.xml
7 | .idea/**/tasks.xml
8 | .idea/**/usage.statistics.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # AWS User-specific
13 | .idea/**/aws.xml
14 |
15 | # Generated files
16 | .idea/**/contentModel.xml
17 |
18 | # Sensitive or high-churn files
19 | .idea/**/dataSources/
20 | .idea/**/dataSources.ids
21 | .idea/**/dataSources.local.xml
22 | .idea/**/sqlDataSources.xml
23 | .idea/**/dynamic.xml
24 | .idea/**/uiDesigner.xml
25 | .idea/**/dbnavigator.xml
26 |
27 | # Gradle
28 | .idea/**/gradle.xml
29 | .idea/**/libraries
30 |
31 | # Gradle and Maven with auto-import
32 | # When using Gradle or Maven with auto-import, you should exclude module files,
33 | # since they will be recreated, and may cause churn. Uncomment if using
34 | # auto-import.
35 | # .idea/artifacts
36 | # .idea/compiler.xml
37 | # .idea/jarRepositories.xml
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 | # *.iml
42 | # *.ipr
43 |
44 | # CMake
45 | cmake-build-*/
46 |
47 | # Mongo Explorer plugin
48 | .idea/**/mongoSettings.xml
49 |
50 | # File-based project format
51 | *.iws
52 |
53 | # IntelliJ
54 | out/
55 |
56 | # mpeltonen/sbt-idea plugin
57 | .idea_modules/
58 |
59 | # JIRA plugin
60 | atlassian-ide-plugin.xml
61 |
62 | # Cursive Clojure plugin
63 | .idea/replstate.xml
64 |
65 | # SonarLint plugin
66 | .idea/sonarlint/
67 |
68 | # Crashlytics plugin (for Android Studio and IntelliJ)
69 | com_crashlytics_export_strings.xml
70 | crashlytics.properties
71 | crashlytics-build.properties
72 | fabric.properties
73 |
74 | # Editor-based Rest Client
75 | .idea/httpRequests
76 |
77 | # Android studio 3.1+ serialized cache file
78 | .idea/caches/build_file_checksums.ser
79 |
80 | ### Linux template
81 | *~
82 |
83 | # temporary files which can be created if a process still has a handle open of a deleted file
84 | .fuse_hidden*
85 |
86 | # KDE directory preferences
87 | .directory
88 |
89 | # Linux trash folder which might appear on any partition or disk
90 | .Trash-*
91 |
92 | # .nfs files are created when an open file is removed but is still being accessed
93 | .nfs*
94 |
95 | ### Redis template
96 | # Ignore redis binary dump (dump.rdb) files
97 |
98 | *.rdb
99 |
100 | ### Python template
101 | # Byte-compiled / optimized / DLL files
102 | __pycache__/
103 | *.py[cod]
104 | *$py.class
105 |
106 | # C extensions
107 | *.so
108 |
109 | # Distribution / packaging
110 | .Python
111 | build/
112 | develop-eggs/
113 | dist/
114 | downloads/
115 | eggs/
116 | .eggs/
117 | lib/
118 | lib64/
119 | parts/
120 | sdist/
121 | var/
122 | wheels/
123 | share/python-wheels/
124 | *.egg-info/
125 | .installed.cfg
126 | *.egg
127 | MANIFEST
128 |
129 | # PyInstaller
130 | # Usually these files are written by a python script from a template
131 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
132 | *.manifest
133 | *.spec
134 |
135 | # Installer logs
136 | pip-log.txt
137 | pip-delete-this-directory.txt
138 |
139 | # Unit test / coverage reports
140 | htmlcov/
141 | .tox/
142 | .nox/
143 | .coverage
144 | .coverage.*
145 | .cache
146 | nosetests.xml
147 | coverage.xml
148 | *.cover
149 | *.py,cover
150 | .hypothesis/
151 | .pytest_cache/
152 | cover/
153 |
154 | # Translations
155 | *.mo
156 | *.pot
157 |
158 | # Django stuff:
159 | *.log
160 | local_settings.py
161 | db.sqlite3
162 | db.sqlite3-journal
163 |
164 | # Flask stuff:
165 | instance/
166 | .webassets-cache
167 |
168 | # Scrapy stuff:
169 | .scrapy
170 |
171 | # Sphinx documentation
172 | docs/_build/
173 |
174 | # PyBuilder
175 | .pybuilder/
176 | target/
177 |
178 | # Jupyter Notebook
179 | .ipynb_checkpoints
180 |
181 | # IPython
182 | profile_default/
183 | ipython_config.py
184 |
185 | # pyenv
186 | # For a library or package, you might want to ignore these files since the code is
187 | # intended to run in multiple environments; otherwise, check them in:
188 | # .python-version
189 |
190 | # pipenv
191 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
192 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
193 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
194 | # install all needed dependencies.
195 | #Pipfile.lock
196 |
197 | # poetry
198 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
199 | # This is especially recommended for binary packages to ensure reproducibility, and is more
200 | # commonly ignored for libraries.
201 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
202 | #poetry.lock
203 |
204 | # pdm
205 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
206 | #pdm.lock
207 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
208 | # in version control.
209 | # https://pdm.fming.dev/#use-with-ide
210 | .pdm.toml
211 |
212 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
213 | __pypackages__/
214 |
215 | # Celery stuff
216 | celerybeat-schedule
217 | celerybeat.pid
218 |
219 | # SageMath parsed files
220 | *.sage.py
221 |
222 | # Environments
223 | .env
224 | .venv
225 | env/
226 | venv/
227 | ENV/
228 | env.bak/
229 | venv.bak/
230 |
231 | # Spyder project settings
232 | .spyderproject
233 | .spyproject
234 |
235 | # Rope project settings
236 | .ropeproject
237 |
238 | # mkdocs documentation
239 | /site
240 |
241 | # mypy
242 | .mypy_cache/
243 | .dmypy.json
244 | dmypy.json
245 |
246 | # Pyre type checker
247 | .pyre/
248 |
249 | # pytype static type analyzer
250 | .pytype/
251 |
252 | # Cython debug symbols
253 | cython_debug/
254 |
255 | # PyCharm
256 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
257 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
258 | # and can be added to the global gitignore or merged into this file. For a more nuclear
259 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
260 | #.idea/
261 |
262 | data
263 | redis.conf
--------------------------------------------------------------------------------
/deployment/deploy.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | REM Deploy services using Docker Compose
3 |
4 | REM Combine Docker Compose files and build the services
5 | docker compose -f docker-compose.yml ^
6 | -f ..\infra\api-gateway\docker-compose.yml ^
7 | -f ..\services\iam-service\docker-compose.yml ^
8 | -f ..\services\media-service\docker-compose.yml ^
9 | -f ..\services\ocr-service\docker-compose.yml ^
10 | up -d --build
11 |
12 | REM Check the exit code of the last command and print result
13 | if %ERRORLEVEL% EQU 0 (
14 | echo Deployment succeeded.
15 | ) else (
16 | echo Deployment failed with error code %ERRORLEVEL%.
17 | )
18 |
19 | REM Wait for user input before closing
20 | echo.
21 | echo Press any key to exit...
22 | pause >nul
23 |
--------------------------------------------------------------------------------
/deployment/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Use multiple -f flags to include all service-specific and infrastructure Compose files
4 | docker compose -f docker-compose.yml \
5 | -f ../infra/api-gateway/docker-compose.yml \
6 | -f ../services/iam-service/docker-compose.yml \
7 | -f ../services/media-service/docker-compose.yml \
8 | -f ../services/ocr-service/docker-compose.yml \
9 | -f ../infra/efk-stack/docker-compose.yml \
10 | up -d --build
11 | # -f ../client/docker-compose.yml \
12 |
13 |
14 |
--------------------------------------------------------------------------------
/deployment/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | networks:
4 | app-network:
5 | driver: bridge
6 |
--------------------------------------------------------------------------------
/infra/api-gateway/.dockerignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
4 |
5 | # User-specific stuff
6 | .idea/**/workspace.xml
7 | .idea/**/tasks.xml
8 | .idea/**/usage.statistics.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # AWS User-specific
13 | .idea/**/aws.xml
14 |
15 | # Generated files
16 | .idea/**/contentModel.xml
17 |
18 | # Sensitive or high-churn files
19 | .idea/**/dataSources/
20 | .idea/**/dataSources.ids
21 | .idea/**/dataSources.local.xml
22 | .idea/**/sqlDataSources.xml
23 | .idea/**/dynamic.xml
24 | .idea/**/uiDesigner.xml
25 | .idea/**/dbnavigator.xml
26 |
27 | # Gradle
28 | .idea/**/gradle.xml
29 | .idea/**/libraries
30 |
31 | # Gradle and Maven with auto-import
32 | # When using Gradle or Maven with auto-import, you should exclude module files,
33 | # since they will be recreated, and may cause churn. Uncomment if using
34 | # auto-import.
35 | # .idea/artifacts
36 | # .idea/compiler.xml
37 | # .idea/jarRepositories.xml
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 | # *.iml
42 | # *.ipr
43 |
44 | # CMake
45 | cmake-build-*/
46 |
47 | # Mongo Explorer plugin
48 | .idea/**/mongoSettings.xml
49 |
50 | # File-based project format
51 | *.iws
52 |
53 | # IntelliJ
54 | out/
55 |
56 | # mpeltonen/sbt-idea plugin
57 | .idea_modules/
58 |
59 | # JIRA plugin
60 | atlassian-ide-plugin.xml
61 |
62 | # Cursive Clojure plugin
63 | .idea/replstate.xml
64 |
65 | # SonarLint plugin
66 | .idea/sonarlint/
67 |
68 | # Crashlytics plugin (for Android Studio and IntelliJ)
69 | com_crashlytics_export_strings.xml
70 | crashlytics.properties
71 | crashlytics-build.properties
72 | fabric.properties
73 |
74 | # Editor-based Rest Client
75 | .idea/httpRequests
76 |
77 | # Android studio 3.1+ serialized cache file
78 | .idea/caches/build_file_checksums.ser
79 |
80 | ### Linux template
81 | *~
82 |
83 | # temporary files which can be created if a process still has a handle open of a deleted file
84 | .fuse_hidden*
85 |
86 | # KDE directory preferences
87 | .directory
88 |
89 | # Linux trash folder which might appear on any partition or disk
90 | .Trash-*
91 |
92 | # .nfs files are created when an open file is removed but is still being accessed
93 | .nfs*
94 |
95 | ### Redis template
96 | # Ignore redis binary dump (dump.rdb) files
97 |
98 | *.rdb
99 |
100 | ### Python template
101 | # Byte-compiled / optimized / DLL files
102 | __pycache__/
103 | *.py[cod]
104 | *$py.class
105 |
106 | # C extensions
107 | *.so
108 |
109 | # Distribution / packaging
110 | .Python
111 | build/
112 | develop-eggs/
113 | dist/
114 | downloads/
115 | eggs/
116 | .eggs/
117 | lib/
118 | lib64/
119 | parts/
120 | sdist/
121 | var/
122 | wheels/
123 | share/python-wheels/
124 | *.egg-info/
125 | .installed.cfg
126 | *.egg
127 | MANIFEST
128 |
129 | # PyInstaller
130 | # Usually these files are written by a python script from a template
131 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
132 | *.manifest
133 | *.spec
134 |
135 | # Installer logs
136 | pip-log.txt
137 | pip-delete-this-directory.txt
138 |
139 | # Unit test / coverage reports
140 | htmlcov/
141 | .tox/
142 | .nox/
143 | .coverage
144 | .coverage.*
145 | .cache
146 | nosetests.xml
147 | coverage.xml
148 | *.cover
149 | *.py,cover
150 | .hypothesis/
151 | .pytest_cache/
152 | cover/
153 |
154 | # Translations
155 | *.mo
156 | *.pot
157 |
158 | # Django stuff:
159 | *.log
160 | local_settings.py
161 | db.sqlite3
162 | db.sqlite3-journal
163 |
164 | # Flask stuff:
165 | instance/
166 | .webassets-cache
167 |
168 | # Scrapy stuff:
169 | .scrapy
170 |
171 | # Sphinx documentation
172 | docs/_build/
173 |
174 | # PyBuilder
175 | .pybuilder/
176 | target/
177 |
178 | # Jupyter Notebook
179 | .ipynb_checkpoints
180 |
181 | # IPython
182 | profile_default/
183 | ipython_config.py
184 |
185 | # pyenv
186 | # For a library or package, you might want to ignore these files since the code is
187 | # intended to run in multiple environments; otherwise, check them in:
188 | # .python-version
189 |
190 | # pipenv
191 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
192 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
193 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
194 | # install all needed dependencies.
195 | #Pipfile.lock
196 |
197 | # poetry
198 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
199 | # This is especially recommended for binary packages to ensure reproducibility, and is more
200 | # commonly ignored for libraries.
201 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
202 | #poetry.lock
203 |
204 | # pdm
205 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
206 | #pdm.lock
207 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
208 | # in version control.
209 | # https://pdm.fming.dev/#use-with-ide
210 | .pdm.toml
211 |
212 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
213 | __pypackages__/
214 |
215 | # Celery stuff
216 | celerybeat-schedule
217 | celerybeat.pid
218 |
219 | # SageMath parsed files
220 | *.sage.py
221 |
222 | # Environments
223 | .env
224 | .venv
225 | env/
226 | venv/
227 | ENV/
228 | env.bak/
229 | venv.bak/
230 |
231 | # Spyder project settings
232 | .spyderproject
233 | .spyproject
234 |
235 | # Rope project settings
236 | .ropeproject
237 |
238 | # mkdocs documentation
239 | /site
240 |
241 | # mypy
242 | .mypy_cache/
243 | .dmypy.json
244 | dmypy.json
245 |
246 | # Pyre type checker
247 | .pyre/
248 |
249 | # pytype static type analyzer
250 | .pytype/
251 |
252 | # Cython debug symbols
253 | cython_debug/
254 |
255 | # PyCharm
256 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
257 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
258 | # and can be added to the global gitignore or merged into this file. For a more nuclear
259 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
260 | #.idea/
261 |
262 | data
263 | redis.conf
--------------------------------------------------------------------------------
/infra/api-gateway/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | traefik:
5 | image: traefik:v2.5
6 | container_name: traefik
7 | command:
8 | - "--api.insecure=true" # Enable Traefik dashboard
9 | - "--providers.docker=true" # Enable Docker provider
10 | - "--entrypoints.web.address=:80" # Define HTTP entrypoint
11 | - "--entrypoints.websecure.address=:443" # Define HTTPS entrypoint
12 | - "--certificatesresolvers.myresolver.acme.tlschallenge=true" # Enable TLS challenge for Let's Encrypt
13 | - "--certificatesresolvers.myresolver.acme.email=esaminu.py@gmail.com" # Email for Let's Encrypt notifications
14 | - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" # Storage for certificates
15 | ports:
16 | - "80:80" # Expose HTTP port
17 | - "443:443" # Expose HTTPS port
18 | - "8080:8080" # Expose Traefik dashboard
19 | volumes:
20 | - "/var/run/docker.sock:/var/run/docker.sock:ro" # Docker socket for service discovery
21 | - "./letsencrypt:/letsencrypt" # Volume for certificates storage
22 | networks:
23 | - app-network
24 |
25 | networks:
26 | app-network:
27 | driver: bridge
28 |
29 | volumes:
30 | letsencrypt:
31 | driver: local
--------------------------------------------------------------------------------
/infra/efk-stack/.dockerignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
4 |
5 | # User-specific stuff
6 | .idea/**/workspace.xml
7 | .idea/**/tasks.xml
8 | .idea/**/usage.statistics.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # AWS User-specific
13 | .idea/**/aws.xml
14 |
15 | # Generated files
16 | .idea/**/contentModel.xml
17 |
18 | # Sensitive or high-churn files
19 | .idea/**/dataSources/
20 | .idea/**/dataSources.ids
21 | .idea/**/dataSources.local.xml
22 | .idea/**/sqlDataSources.xml
23 | .idea/**/dynamic.xml
24 | .idea/**/uiDesigner.xml
25 | .idea/**/dbnavigator.xml
26 |
27 | # Gradle
28 | .idea/**/gradle.xml
29 | .idea/**/libraries
30 |
31 | # Gradle and Maven with auto-import
32 | # When using Gradle or Maven with auto-import, you should exclude module files,
33 | # since they will be recreated, and may cause churn. Uncomment if using
34 | # auto-import.
35 | # .idea/artifacts
36 | # .idea/compiler.xml
37 | # .idea/jarRepositories.xml
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 | # *.iml
42 | # *.ipr
43 |
44 | # CMake
45 | cmake-build-*/
46 |
47 | # Mongo Explorer plugin
48 | .idea/**/mongoSettings.xml
49 |
50 | # File-based project format
51 | *.iws
52 |
53 | # IntelliJ
54 | out/
55 |
56 | # mpeltonen/sbt-idea plugin
57 | .idea_modules/
58 |
59 | # JIRA plugin
60 | atlassian-ide-plugin.xml
61 |
62 | # Cursive Clojure plugin
63 | .idea/replstate.xml
64 |
65 | # SonarLint plugin
66 | .idea/sonarlint/
67 |
68 | # Crashlytics plugin (for Android Studio and IntelliJ)
69 | com_crashlytics_export_strings.xml
70 | crashlytics.properties
71 | crashlytics-build.properties
72 | fabric.properties
73 |
74 | # Editor-based Rest Client
75 | .idea/httpRequests
76 |
77 | # Android studio 3.1+ serialized cache file
78 | .idea/caches/build_file_checksums.ser
79 |
80 | ### Linux template
81 | *~
82 |
83 | # temporary files which can be created if a process still has a handle open of a deleted file
84 | .fuse_hidden*
85 |
86 | # KDE directory preferences
87 | .directory
88 |
89 | # Linux trash folder which might appear on any partition or disk
90 | .Trash-*
91 |
92 | # .nfs files are created when an open file is removed but is still being accessed
93 | .nfs*
94 |
95 | ### Redis template
96 | # Ignore redis binary dump (dump.rdb) files
97 |
98 | *.rdb
99 |
100 | ### Python template
101 | # Byte-compiled / optimized / DLL files
102 | __pycache__/
103 | *.py[cod]
104 | *$py.class
105 |
106 | # C extensions
107 | *.so
108 |
109 | # Distribution / packaging
110 | .Python
111 | build/
112 | develop-eggs/
113 | dist/
114 | downloads/
115 | eggs/
116 | .eggs/
117 | lib/
118 | lib64/
119 | parts/
120 | sdist/
121 | var/
122 | wheels/
123 | share/python-wheels/
124 | *.egg-info/
125 | .installed.cfg
126 | *.egg
127 | MANIFEST
128 |
129 | # PyInstaller
130 | # Usually these files are written by a python script from a template
131 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
132 | *.manifest
133 | *.spec
134 |
135 | # Installer logs
136 | pip-log.txt
137 | pip-delete-this-directory.txt
138 |
139 | # Unit test / coverage reports
140 | htmlcov/
141 | .tox/
142 | .nox/
143 | .coverage
144 | .coverage.*
145 | .cache
146 | nosetests.xml
147 | coverage.xml
148 | *.cover
149 | *.py,cover
150 | .hypothesis/
151 | .pytest_cache/
152 | cover/
153 |
154 | # Translations
155 | *.mo
156 | *.pot
157 |
158 | # Django stuff:
159 | *.log
160 | local_settings.py
161 | db.sqlite3
162 | db.sqlite3-journal
163 |
164 | # Flask stuff:
165 | instance/
166 | .webassets-cache
167 |
168 | # Scrapy stuff:
169 | .scrapy
170 |
171 | # Sphinx documentation
172 | docs/_build/
173 |
174 | # PyBuilder
175 | .pybuilder/
176 | target/
177 |
178 | # Jupyter Notebook
179 | .ipynb_checkpoints
180 |
181 | # IPython
182 | profile_default/
183 | ipython_config.py
184 |
185 | # pyenv
186 | # For a library or package, you might want to ignore these files since the code is
187 | # intended to run in multiple environments; otherwise, check them in:
188 | # .python-version
189 |
190 | # pipenv
191 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
192 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
193 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
194 | # install all needed dependencies.
195 | #Pipfile.lock
196 |
197 | # poetry
198 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
199 | # This is especially recommended for binary packages to ensure reproducibility, and is more
200 | # commonly ignored for libraries.
201 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
202 | #poetry.lock
203 |
204 | # pdm
205 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
206 | #pdm.lock
207 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
208 | # in version control.
209 | # https://pdm.fming.dev/#use-with-ide
210 | .pdm.toml
211 |
212 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
213 | __pypackages__/
214 |
215 | # Celery stuff
216 | celerybeat-schedule
217 | celerybeat.pid
218 |
219 | # SageMath parsed files
220 | *.sage.py
221 |
222 | # Environments
223 | .env
224 | .venv
225 | env/
226 | venv/
227 | ENV/
228 | env.bak/
229 | venv.bak/
230 |
231 | # Spyder project settings
232 | .spyderproject
233 | .spyproject
234 |
235 | # Rope project settings
236 | .ropeproject
237 |
238 | # mkdocs documentation
239 | /site
240 |
241 | # mypy
242 | .mypy_cache/
243 | .dmypy.json
244 | dmypy.json
245 |
246 | # Pyre type checker
247 | .pyre/
248 |
249 | # pytype static type analyzer
250 | .pytype/
251 |
252 | # Cython debug symbols
253 | cython_debug/
254 |
255 | # PyCharm
256 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
257 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
258 | # and can be added to the global gitignore or merged into this file. For a more nuclear
259 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
260 | #.idea/
261 |
262 | data
263 | redis.conf
--------------------------------------------------------------------------------
/infra/efk-stack/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | elasticsearch:
5 | image: docker.elastic.co/elasticsearch/elasticsearch:8.14.1
6 | container_name: elasticsearch_container
7 | environment:
8 | - discovery.type=single-node
9 | - ES_JAVA_OPTS=-Xms512m -Xmx512m
10 | - xpack.security.enabled=false
11 | ports:
12 | - "9200:9200"
13 | volumes:
14 | - esdata:/usr/share/elasticsearch/data
15 | networks:
16 | - efk-network
17 |
18 | kibana:
19 | image: docker.elastic.co/kibana/kibana:8.14.1
20 | container_name: kibana_container
21 | environment:
22 | - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
23 | ports:
24 | - "5601:5601"
25 | networks:
26 | - efk-network
27 | depends_on:
28 | - elasticsearch
29 |
30 | fluentbit:
31 | image: fluent/fluent-bit:latest
32 | container_name: fluentbit
33 | volumes:
34 | - ../infra/efk-stack/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
35 | - ../infra/efk-stack/parser_json.conf:/fluent-bit/etc/parser_json.conf
36 | - ../services/iam-service/logs:/fluent-bit/logs/iam
37 | - ../services/media-service/logs:/fluent-bit/logs/media
38 | - ../services/ocr-service/logs:/fluent-bit/logs/ocr
39 | networks:
40 | - efk-network
41 | - app-network
42 |
43 | networks:
44 | app-network:
45 | driver: bridge
46 | efk-network:
47 | driver: bridge
48 |
49 | volumes:
50 | esdata:
51 |
--------------------------------------------------------------------------------
/infra/efk-stack/fluent-bit.conf:
--------------------------------------------------------------------------------
1 | [SERVICE]
2 | Flush 1
3 | Log_Level info
4 | Daemon Off
5 | Parsers_File parser_json.conf
6 |
7 | [INPUT]
8 | Name tail
9 | Path /fluent-bit/logs/iam/*.log
10 | Parser json_parser
11 | Tag iam.*
12 | DB /fluent-bit/logs/iam/fluentbit.db
13 | Mem_Buf_Limit 5MB
14 | Skip_Long_Lines On
15 |
16 | [INPUT]
17 | Name tail
18 | Path /fluent-bit/logs/media/*.log
19 | Parser json_parser
20 | Tag media.*
21 | DB /fluent-bit/logs/media/fluentbit.db
22 | Mem_Buf_Limit 5MB
23 | Skip_Long_Lines On
24 |
25 |
26 | [INPUT]
27 | Name tail
28 | Path /fluent-bit/logs/ocr/*.log
29 | Parser json_parser
30 | Tag ocr.*
31 | DB /fluent-bit/logs/ocr/fluentbit.db
32 | Mem_Buf_Limit 5MB
33 | Skip_Long_Lines On
34 |
35 |
36 | [OUTPUT]
37 | Name es
38 | Match iam.*
39 | Host elasticsearch
40 | Port 9200
41 | Index iam-service-logs
42 | Type _doc
43 | Suppress_Type_Name On
44 | Logstash_Format Off
45 | Retry_Limit False
46 |
47 | [OUTPUT]
48 | Name es
49 | Match media.*
50 | Host elasticsearch
51 | Port 9200
52 | Index media-service-logs
53 | Type _doc
54 | Suppress_Type_Name On
55 | Logstash_Format Off
56 | Retry_Limit False
57 |
58 |
59 | [OUTPUT]
60 | Name es
61 | Match ocr.*
62 | Host elasticsearch
63 | Port 9200
64 | Index ocr-service-logs
65 | Type _doc
66 | Suppress_Type_Name On
67 | Logstash_Format Off
68 | Retry_Limit False
69 |
70 |
--------------------------------------------------------------------------------
/infra/efk-stack/parser_json.conf:
--------------------------------------------------------------------------------
1 | [PARSER]
2 | Name json_parser
3 | Format json
4 | Time_Key time
5 | Time_Format %Y-%m-%dT%H:%M:%S
6 |
--------------------------------------------------------------------------------
/services/iam-service/.dockerignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
4 |
5 | # User-specific stuff
6 | .idea/**/workspace.xml
7 | .idea/**/tasks.xml
8 | .idea/**/usage.statistics.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # AWS User-specific
13 | .idea/**/aws.xml
14 |
15 | # Generated files
16 | .idea/**/contentModel.xml
17 |
18 | # Sensitive or high-churn files
19 | .idea/**/dataSources/
20 | .idea/**/dataSources.ids
21 | .idea/**/dataSources.local.xml
22 | .idea/**/sqlDataSources.xml
23 | .idea/**/dynamic.xml
24 | .idea/**/uiDesigner.xml
25 | .idea/**/dbnavigator.xml
26 |
27 | # Gradle
28 | .idea/**/gradle.xml
29 | .idea/**/libraries
30 |
31 | # Gradle and Maven with auto-import
32 | # When using Gradle or Maven with auto-import, you should exclude module files,
33 | # since they will be recreated, and may cause churn. Uncomment if using
34 | # auto-import.
35 | # .idea/artifacts
36 | # .idea/compiler.xml
37 | # .idea/jarRepositories.xml
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 | # *.iml
42 | # *.ipr
43 |
44 | # CMake
45 | cmake-build-*/
46 |
47 | # Mongo Explorer plugin
48 | .idea/**/mongoSettings.xml
49 |
50 | # File-based project format
51 | *.iws
52 |
53 | # IntelliJ
54 | out/
55 |
56 | # mpeltonen/sbt-idea plugin
57 | .idea_modules/
58 |
59 | # JIRA plugin
60 | atlassian-ide-plugin.xml
61 |
62 | # Cursive Clojure plugin
63 | .idea/replstate.xml
64 |
65 | # SonarLint plugin
66 | .idea/sonarlint/
67 |
68 | # Crashlytics plugin (for Android Studio and IntelliJ)
69 | com_crashlytics_export_strings.xml
70 | crashlytics.properties
71 | crashlytics-build.properties
72 | fabric.properties
73 |
74 | # Editor-based Rest Client
75 | .idea/httpRequests
76 |
77 | # Android studio 3.1+ serialized cache file
78 | .idea/caches/build_file_checksums.ser
79 |
80 | ### Linux template
81 | *~
82 |
83 | # temporary files which can be created if a process still has a handle open of a deleted file
84 | .fuse_hidden*
85 |
86 | # KDE directory preferences
87 | .directory
88 |
89 | # Linux trash folder which might appear on any partition or disk
90 | .Trash-*
91 |
92 | # .nfs files are created when an open file is removed but is still being accessed
93 | .nfs*
94 |
95 | ### Redis template
96 | # Ignore redis binary dump (dump.rdb) files
97 |
98 | *.rdb
99 |
100 | ### Python template
101 | # Byte-compiled / optimized / DLL files
102 | __pycache__/
103 | *.py[cod]
104 | *$py.class
105 |
106 | # C extensions
107 | *.so
108 |
109 | # Distribution / packaging
110 | .Python
111 | build/
112 | develop-eggs/
113 | dist/
114 | downloads/
115 | eggs/
116 | .eggs/
117 | lib/
118 | lib64/
119 | parts/
120 | sdist/
121 | var/
122 | wheels/
123 | share/python-wheels/
124 | *.egg-info/
125 | .installed.cfg
126 | *.egg
127 | MANIFEST
128 |
129 | # PyInstaller
130 | # Usually these files are written by a python script from a template
131 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
132 | *.manifest
133 | *.spec
134 |
135 | # Installer logs
136 | pip-log.txt
137 | pip-delete-this-directory.txt
138 |
139 | # Unit test / coverage reports
140 | htmlcov/
141 | .tox/
142 | .nox/
143 | .coverage
144 | .coverage.*
145 | .cache
146 | nosetests.xml
147 | coverage.xml
148 | *.cover
149 | *.py,cover
150 | .hypothesis/
151 | .pytest_cache/
152 | cover/
153 |
154 | # Translations
155 | *.mo
156 | *.pot
157 |
158 | # Django stuff:
159 | *.log
160 | local_settings.py
161 | db.sqlite3
162 | db.sqlite3-journal
163 |
164 | # Flask stuff:
165 | instance/
166 | .webassets-cache
167 |
168 | # Scrapy stuff:
169 | .scrapy
170 |
171 | # Sphinx documentation
172 | docs/_build/
173 |
174 | # PyBuilder
175 | .pybuilder/
176 | target/
177 |
178 | # Jupyter Notebook
179 | .ipynb_checkpoints
180 |
181 | # IPython
182 | profile_default/
183 | ipython_config.py
184 |
185 | # pyenv
186 | # For a library or package, you might want to ignore these files since the code is
187 | # intended to run in multiple environments; otherwise, check them in:
188 | # .python-version
189 |
190 | # pipenv
191 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
192 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
193 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
194 | # install all needed dependencies.
195 | #Pipfile.lock
196 |
197 | # poetry
198 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
199 | # This is especially recommended for binary packages to ensure reproducibility, and is more
200 | # commonly ignored for libraries.
201 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
202 | #poetry.lock
203 |
204 | # pdm
205 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
206 | #pdm.lock
207 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
208 | # in version control.
209 | # https://pdm.fming.dev/#use-with-ide
210 | .pdm.toml
211 |
212 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
213 | __pypackages__/
214 |
215 | # Celery stuff
216 | celerybeat-schedule
217 | celerybeat.pid
218 |
219 | # SageMath parsed files
220 | *.sage.py
221 |
222 | # Environments
223 | app/core/.env
224 | .venv
225 | env/
226 | venv/
227 | ENV/
228 | env.bak/
229 | venv.bak/
230 |
231 | # Spyder project settings
232 | .spyderproject
233 | .spyproject
234 |
235 | # Rope project settings
236 | .ropeproject
237 |
238 | # mkdocs documentation
239 | /site
240 |
241 | # mypy
242 | .mypy_cache/
243 | .dmypy.json
244 | dmypy.json
245 |
246 | # Pyre type checker
247 | .pyre/
248 |
249 | # pytype static type analyzer
250 | .pytype/
251 |
252 | # Cython debug symbols
253 | cython_debug/
254 |
255 | # PyCharm
256 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
257 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
258 | # and can be added to the global gitignore or merged into this file. For a more nuclear
259 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
260 | #.idea/
261 |
262 | data
263 | redis.conf
--------------------------------------------------------------------------------
/services/iam-service/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11
2 |
3 | COPY ./requirements.txt /app/requirements.txt
4 |
5 | RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
6 |
7 | COPY ./app /app/app
8 |
--------------------------------------------------------------------------------
/services/iam-service/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/api/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/api/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/api/v1/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/api/v1/endpoints/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/api/v1/endpoints/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/api/v1/endpoints/users.py:
--------------------------------------------------------------------------------
1 | from fastapi import Depends, HTTPException, status, APIRouter
2 | from fastapi.security import OAuth2PasswordRequestForm
3 | from typing import Annotated
4 | from loguru import logger
5 |
6 | from app.domain.models.user import User
7 | from app.domain.schemas.token_schema import TokenSchema, TokenDataSchema
8 | from app.domain.schemas.user_schema import (
9 | UserCreateSchema,
10 | UserCreateResponseSchema,
11 | UserSchema,
12 | UserLoginSchema,
13 | VerifyOTPSchema,
14 | VerifyOTPResponseSchema,
15 | ResendOTPSchema,
16 | ResendOTPResponseSchema,
17 | )
18 | from app.services.auth_services.auth_service import AuthService, get_current_user
19 | from app.services.register_service import RegisterService
20 | from app.services.user_service import UserService
21 |
22 | user_router = APIRouter()
23 |
24 |
25 | @user_router.post(
26 | "/Register",
27 | response_model=UserCreateResponseSchema,
28 | status_code=status.HTTP_201_CREATED,
29 | )
30 | async def register(
31 | user: UserCreateSchema, register_service: Annotated[RegisterService, Depends()]
32 | ) -> UserCreateResponseSchema:
33 | logger.info(f"Registering user with mobile number {user.mobile_number}")
34 | return await register_service.register_user(user)
35 |
36 |
37 | @user_router.post("/Token", response_model=TokenSchema, status_code=status.HTTP_200_OK)
38 | async def login_for_access_token(
39 | form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
40 | auth_service: Annotated[AuthService, Depends()],
41 | ) -> TokenSchema:
42 |
43 | logger.info(f"Logging in user with mobile number {form_data.username}")
44 | return await auth_service.authenticate_user(
45 | UserLoginSchema(mobile_number=form_data.username, password=form_data.password)
46 | )
47 |
48 |
49 | @user_router.post(
50 | "/VerifyOTP", response_model=VerifyOTPResponseSchema, status_code=status.HTTP_200_OK
51 | )
52 | async def verify_otp(
53 | verify_user_schema: VerifyOTPSchema,
54 | register_service: Annotated[RegisterService, Depends()],
55 | ) -> VerifyOTPResponseSchema:
56 | logger.info(f"Verifying OTP for user with mobile number {verify_user_schema.mobile_number}")
57 | return await register_service.verify_user(verify_user_schema)
58 |
59 |
60 | @user_router.post(
61 | "/ResendOTP",
62 | response_model=ResendOTPResponseSchema,
63 | status_code=status.HTTP_200_OK,
64 | )
65 | async def resend_otp(
66 | resend_otp_schema: ResendOTPSchema,
67 | register_service: Annotated[RegisterService, Depends()],
68 | ) -> ResendOTPResponseSchema:
69 | logger.info(f"Resending OTP for user with mobile number {resend_otp_schema.mobile_number}")
70 | return await register_service.resend_otp(resend_otp_schema)
71 |
72 |
73 | @user_router.get("/Me", response_model=UserSchema, status_code=status.HTTP_200_OK)
74 | async def read_users_me(current_user: User = Depends(get_current_user)) -> UserSchema:
75 | logger.info(f"Getting user with mobile number {current_user.mobile_number}")
76 | return current_user
77 |
--------------------------------------------------------------------------------
/services/iam-service/app/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/core/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/core/config.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 | from pathlib import Path
3 |
4 | from loguru import logger
5 |
6 | from pydantic_settings import BaseSettings, SettingsConfigDict
7 |
8 |
9 | class Settings(BaseSettings):
10 | DATABASE_DIALECT: str
11 | DATABASE_HOSTNAME: str
12 | DATABASE_NAME: str
13 | DATABASE_PASSWORD: str
14 | DATABASE_PORT: int
15 | DATABASE_USERNAME: str
16 | DEBUG_MODE: bool
17 | REDIS_URL: str
18 | JWT_SECRET_KEY: str
19 | JWT_ALGORITHM: str
20 | ACCESS_TOKEN_EXPIRE_MINUTES: int
21 | OTP_EXPIRE_TIME: int
22 |
23 | # model_config = SettingsConfigDict(env_file=str(Path(__file__).resolve().parent / ".env"))
24 |
25 |
26 | @lru_cache
27 | @logger.catch
28 | def get_settings():
29 | return Settings()
30 |
31 |
32 |
--------------------------------------------------------------------------------
/services/iam-service/app/core/db/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/core/db/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/core/db/database.py:
--------------------------------------------------------------------------------
1 | from typing import Generator
2 | from sqlalchemy import create_engine
3 | from sqlalchemy.exc import SQLAlchemyError
4 | from sqlalchemy.orm import sessionmaker, Session, declarative_base
5 | from sqlalchemy_utils import database_exists, create_database
6 | from loguru import logger
7 |
8 | from app.core.config import get_settings
9 |
10 | config = get_settings()
11 |
12 | # Generate Database URL
13 | DATABASE_URL = (
14 | f"{config.DATABASE_DIALECT}://"
15 | f"{config.DATABASE_USERNAME}:"
16 | f"{config.DATABASE_PASSWORD}@"
17 | f"{config.DATABASE_HOSTNAME}:"
18 | f"{config.DATABASE_PORT}/"
19 | f"{config.DATABASE_NAME}"
20 | )
21 |
22 | engine = create_engine(DATABASE_URL, echo=config.DEBUG_MODE, future=True)
23 |
24 | EntityBase = declarative_base()
25 |
26 |
27 | def init_db() -> bool:
28 | EntityBase.metadata.create_all(bind=engine)
29 | logger.info("Database Initialized")
30 | return True
31 |
32 |
33 | try:
34 | if not database_exists(engine.url):
35 | logger.info("Creating Database")
36 | create_database(engine.url)
37 | logger.info("Database Created")
38 |
39 | except Exception as e:
40 | logger.error(f"Error: {e}")
41 |
42 | session_local = sessionmaker(autoflush=False, autocommit=False, bind=engine)
43 | logger.info("Database Session Created")
44 |
45 |
46 | def get_entitybase():
47 | return EntityBase
48 |
49 |
50 | def get_db() -> Generator[Session, None, None]:
51 | db = session_local()
52 | try:
53 | yield db
54 | except SQLAlchemyError as ex:
55 | logger.error(f"Database error during session: {ex}")
56 | db.rollback() # Roll back any uncommitted transactions
57 | raise # Re-raise the original exception for further handling
58 | finally:
59 | db.close()
60 |
--------------------------------------------------------------------------------
/services/iam-service/app/core/redis/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/core/redis/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/core/redis/redis_client.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 | from loguru import logger
3 |
4 | from app.core.config import get_settings
5 |
6 | config = get_settings()
7 |
8 | try:
9 | redis_client = Redis(
10 | host=config.REDIS_URL,
11 | port=6379,
12 | charset="utf-8",
13 | decode_responses=True
14 | )
15 | logger.info("Redis Client Created")
16 |
17 | except Exception as e:
18 | logger.error(f"Redis Client Creation Failed: {e}")
19 | redis_client = None
20 |
21 |
22 | @logger.catch
23 | def get_redis_client():
24 | return redis_client
25 |
--------------------------------------------------------------------------------
/services/iam-service/app/domain/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/domain/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/domain/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/domain/models/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/domain/models/user.py:
--------------------------------------------------------------------------------
1 | from fastapi_restful.guid_type import GUID_SERVER_DEFAULT_POSTGRESQL
2 | from sqlalchemy import Column, String, TIMESTAMP, Boolean
3 | from sqlalchemy.dialects.postgresql import UUID
4 | from sqlalchemy.sql import func
5 |
6 | from app.core.db.database import get_entitybase
7 |
8 | EntityBase = get_entitybase()
9 |
10 |
11 | class User(EntityBase):
12 | __tablename__ = "users"
13 |
14 | id = Column(
15 | UUID,
16 | primary_key=True,
17 | index=True,
18 | unique=True,
19 | nullable=False,
20 | server_default=GUID_SERVER_DEFAULT_POSTGRESQL
21 | )
22 | first_name = Column(String, nullable=False)
23 | last_name = Column(String, nullable=False)
24 | mobile_number = Column(String, unique=True, nullable=False)
25 | hashed_password = Column(String, nullable=False)
26 | is_verified = Column(Boolean, nullable=False, default=False)
27 | created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
28 | updated_at = Column(TIMESTAMP(timezone=True), nullable=True, default=None, onupdate=func.now())
29 |
--------------------------------------------------------------------------------
/services/iam-service/app/domain/schemas/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/domain/schemas/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/domain/schemas/token_schema.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class TokenSchema(BaseModel):
7 | access_token: str
8 | token_type: str
9 |
10 |
11 | class TokenDataSchema(BaseModel):
12 | mobile_number: Optional[str] = None
13 |
14 |
--------------------------------------------------------------------------------
/services/iam-service/app/domain/schemas/user_schema.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional
3 | from uuid import UUID
4 |
5 | from pydantic import BaseModel, Field, EmailStr
6 |
7 | from app.domain.schemas.token_schema import TokenSchema
8 |
9 |
10 | class UserBaseSchema(BaseModel):
11 | first_name: str
12 | last_name: str
13 | mobile_number: str = Field(..., pattern=r"^\+?[1-9]\d{1,14}$")
14 |
15 |
16 | class UserCreateSchema(UserBaseSchema):
17 | password: str
18 |
19 |
20 | class UserLoginSchema(BaseModel):
21 | mobile_number: str
22 | password: str
23 |
24 |
25 | class UserSchema(UserBaseSchema):
26 | id: UUID
27 | created_at: Optional[datetime]
28 | updated_at: Optional[datetime]
29 | is_verified: bool
30 |
31 | class Config:
32 | from_attributes = True
33 |
34 |
35 | class VerifyOTPSchema(BaseModel):
36 | mobile_number: str
37 | OTP: str
38 |
39 |
40 | class ResendOTPSchema(BaseModel):
41 | mobile_number: str
42 |
43 |
44 | class ResendOTPResponseSchema(BaseModel):
45 | mobile_number: str
46 | OTP: str
47 | message: str
48 |
49 |
50 | class VerifyOTPResponseSchema(BaseModel):
51 | verified: bool
52 | message: str
53 |
54 |
55 | class UserCreateResponseSchema(BaseModel):
56 | user: UserSchema
57 | OTP: str
58 | message: str
59 |
60 |
61 | class UserLoginResponseSchema(BaseModel):
62 | user: UserSchema
63 | access_token: TokenSchema
64 |
--------------------------------------------------------------------------------
/services/iam-service/app/infrastructure/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/infrastructure/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/infrastructure/repositories/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/infrastructure/repositories/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/infrastructure/repositories/user_repository.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated, Dict
2 | from uuid import UUID
3 | from loguru import logger
4 | from fastapi import Depends
5 | from sqlalchemy.orm import Session
6 |
7 | from app.core.db.database import get_db
8 | from app.domain.models.user import User
9 |
10 |
11 | class UserRepository:
12 | def __init__(self, db: Annotated[Session, Depends(get_db)]):
13 | self.db = db
14 |
15 | def create_user(self, user: User) -> User:
16 | self.db.add(user)
17 | self.db.commit()
18 | self.db.refresh(user)
19 | logger.info(f"User {user.id} created")
20 | return user
21 |
22 | def update_user(self, user_id: int, updated_user: Dict) -> User:
23 | # Update user with the given id
24 | user_query = self.db.query(User).filter(User.id == user_id)
25 | db_user = user_query.first()
26 | user_query.filter(User.id == user_id).update(
27 | updated_user, synchronize_session=False
28 | )
29 | self.db.commit()
30 | self.db.refresh(db_user)
31 | logger.info(f"User {user_id} updated")
32 | return db_user
33 |
34 | def delete_user(self, user: User) -> None:
35 | self.db.delete(user)
36 | self.db.commit()
37 | self.db.flush()
38 | logger.info(f"User {user.id} deleted")
39 |
40 | def get_user(self, user_id: UUID) -> User:
41 | logger.info(f"Fetching user {user_id}")
42 | return self.db.get(User, user_id)
43 |
44 | def get_user_by_mobile_number(self, mobile_number: str) -> User:
45 | logger.info(f"Fetching user with mobile number {mobile_number}")
46 | return self.db.query(User).filter(User.mobile_number == mobile_number).first()
47 |
--------------------------------------------------------------------------------
/services/iam-service/app/logging_service/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/logging_service/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/logging_service/logging_config.py:
--------------------------------------------------------------------------------
1 | from loguru import logger
2 | import sys
3 | import os
4 |
5 |
6 | def configure_logger():
7 | # Ensure log directory exists
8 | os.makedirs("logs", exist_ok=True)
9 |
10 | # Remove default logger
11 | logger.remove()
12 |
13 | # JSON serialization is handled internally by loguru when serialize=True, hence no need for a custom format.
14 | json_logging_format = {
15 | "rotation": "10 MB",
16 | "retention": "10 days",
17 | "serialize": True,
18 | }
19 |
20 | # Add file logging for JSON logs
21 | logger.add("logs/iam_service_info.log", level="INFO", **json_logging_format)
22 | logger.add("logs/iam_service_error.log", level="ERROR", **json_logging_format)
23 |
24 | # Custom log format for console and stderr
25 | log_format = "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:" \
26 | "{function}:{line} - {message}"
27 |
28 | # Add console logging
29 | logger.add(sys.stdout, level="INFO", format=log_format)
30 |
31 | # Add stderr logging
32 | logger.add(sys.stderr, level="ERROR", backtrace=True, diagnose=True, format=log_format)
33 |
--------------------------------------------------------------------------------
/services/iam-service/app/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 | from fastapi.middleware.cors import CORSMiddleware
3 | from loguru import logger
4 |
5 | from app.api.v1.endpoints.users import user_router
6 | from app.core.db.database import init_db
7 | from app.logging_service.logging_config import configure_logger
8 |
9 |
10 | configure_logger()
11 |
12 | init_db()
13 | app = FastAPI()
14 |
15 | origins = ["*"]
16 |
17 | app.add_middleware(
18 | CORSMiddleware,
19 | allow_origins=origins,
20 | allow_credentials=True,
21 | allow_methods=["*"],
22 | allow_headers=["*"],
23 | )
24 |
25 | app.include_router(user_router, prefix="/api/v1/users", tags=["users"])
26 |
27 | logger.info("IAM Service Started")
28 |
29 | @app.get("/")
30 | async def root():
31 | return {"message": "Hello Dear !"}
32 |
33 |
34 |
--------------------------------------------------------------------------------
/services/iam-service/app/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/services/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/services/auth_services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/iam-service/app/services/auth_services/__init__.py
--------------------------------------------------------------------------------
/services/iam-service/app/services/auth_services/auth_service.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta, timezone
2 | from typing import Annotated
3 | from loguru import logger
4 | import jwt
5 | from fastapi import Depends, HTTPException, status
6 | from fastapi.security import OAuth2PasswordBearer
7 |
8 | from app.domain.models.user import User
9 | from app.domain.schemas.token_schema import TokenSchema
10 | from app.domain.schemas.user_schema import UserLoginSchema
11 | from app.services.auth_services.hash_sevice import HashService
12 | from app.services.base_service import BaseService
13 | from app.services.user_service import UserService
14 |
15 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/users/Token")
16 |
17 |
18 | class AuthService(BaseService):
19 | def __init__(
20 | self,
21 | hash_service: Annotated[HashService, Depends()],
22 | user_service: Annotated[UserService, Depends()],
23 | ) -> None:
24 | super().__init__()
25 | self.user_service = user_service
26 | self.hash_service = hash_service
27 |
28 | async def authenticate_user(self, user: UserLoginSchema) -> TokenSchema:
29 | existing_user = await self.user_service.get_user_by_mobile_number(
30 | user.mobile_number
31 | )
32 | logger.info(f"Authenticating user with mobile number {user.mobile_number}")
33 |
34 | if not existing_user:
35 | logger.error(f"User with mobile number {user.mobile_number} does not exist")
36 | raise HTTPException(
37 | status_code=status.HTTP_400_BAD_REQUEST, detail="User does not exist"
38 | )
39 |
40 | if not existing_user.is_verified:
41 | logger.error(f"User with mobile number {user.mobile_number} is not verified")
42 | raise HTTPException(
43 | status_code=status.HTTP_400_BAD_REQUEST, detail="User is not verified"
44 | )
45 |
46 | if not self.hash_service.verify_password(
47 | user.password, existing_user.hashed_password
48 | ):
49 | logger.error(f"Invalid password for user with mobile number {user.mobile_number}")
50 | raise HTTPException(
51 | status_code=status.HTTP_401_UNAUTHORIZED,
52 | detail="Incorrect username or password",
53 | headers={"WWW-Authenticate": "Bearer"},
54 | )
55 | access_token = self.create_access_token(data={"sub": str(existing_user.id)})
56 |
57 | logger.info(f"User with mobile number {user.mobile_number} authenticated successfully")
58 | return TokenSchema(access_token=access_token, token_type="bearer")
59 |
60 | def create_access_token(self, data: dict) -> str:
61 | logger.info("Creating access token")
62 | to_encode = data.copy()
63 | expire = datetime.now(timezone.utc) + timedelta(
64 | self.config.ACCESS_TOKEN_EXPIRE_MINUTES
65 | )
66 | to_encode.update({"exp": expire})
67 | encoded_jwt = jwt.encode(
68 | to_encode, self.config.JWT_SECRET_KEY, algorithm=self.config.JWT_ALGORITHM
69 | )
70 | return encoded_jwt
71 |
72 |
73 | async def get_current_user(
74 | token: Annotated[str, Depends(oauth2_scheme)],
75 | user_service: Annotated[UserService, Depends()],
76 | ) -> User:
77 | credentials_exception = HTTPException(
78 | status_code=status.HTTP_401_UNAUTHORIZED,
79 | detail="Could not validate credentials",
80 | headers={"WWW-Authenticate": "Bearer"},
81 | )
82 | logger.info(f"Validating token {token}")
83 | try:
84 | payload = jwt.decode(
85 | token,
86 | user_service.config.JWT_SECRET_KEY,
87 | algorithms=[user_service.config.JWT_ALGORITHM],
88 | )
89 | user_id: str = payload.get("sub")
90 | user = await user_service.get_user(user_id)
91 | if user_id is None:
92 | logger.error("Could not validate credentials")
93 | raise credentials_exception
94 | except jwt.PyJWTError:
95 | logger.error("Error decoding token")
96 | raise credentials_exception
97 |
98 | logger.info(f"User with id {user_id} validated successfully")
99 | return user
100 |
--------------------------------------------------------------------------------
/services/iam-service/app/services/auth_services/hash_sevice.py:
--------------------------------------------------------------------------------
1 | from passlib.context import CryptContext
2 |
3 | from app.services.base_service import BaseService
4 |
5 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
6 |
7 |
8 | class HashService(BaseService):
9 | def __init__(self) -> None:
10 | super().__init__()
11 |
12 | @staticmethod
13 | def hash_password(password: str) -> str:
14 | return pwd_context.hash(password)
15 |
16 | @staticmethod
17 | def verify_password(plain_password: str, hashed_password: str) -> bool:
18 | return pwd_context.verify(plain_password, hashed_password)
19 |
--------------------------------------------------------------------------------
/services/iam-service/app/services/auth_services/otp_service.py:
--------------------------------------------------------------------------------
1 | import random
2 | from typing import Annotated
3 | from loguru import logger
4 | from fastapi import Depends
5 | from redis import Redis
6 |
7 | from app.core.redis.redis_client import get_redis_client
8 | from app.services.base_service import BaseService
9 |
10 |
11 | class OTPService(BaseService):
12 | def __init__(
13 | self, redis_client: Annotated[Redis, Depends(get_redis_client)]
14 | ) -> None:
15 | super().__init__()
16 | self.redis_client = redis_client
17 |
18 | @staticmethod
19 | def __generate_otp() -> str:
20 | return str(random.randint(100000, 999999))
21 |
22 | def send_otp(self, mobile_number: str):
23 | otp = self.__generate_otp()
24 | self.redis_client.setex(mobile_number, self.config.OTP_EXPIRE_TIME, otp)
25 | logger.info(f"OTP {otp} sent to mobile number {mobile_number}")
26 | return otp
27 |
28 | def verify_otp(self, mobile_number: str, otp: str) -> bool:
29 | stored_otp = self.redis_client.get(mobile_number)
30 | return stored_otp is not None and stored_otp == otp
31 |
32 | def check_exist(self, mobile_number: str) -> bool:
33 | stored_otp = self.redis_client.get(mobile_number)
34 | return stored_otp is not None
35 |
--------------------------------------------------------------------------------
/services/iam-service/app/services/base_service.py:
--------------------------------------------------------------------------------
1 | from app.core.config import get_settings, Settings
2 |
3 |
4 | class BaseService:
5 | def __init__(self, config: Settings = get_settings()) -> None:
6 | self.config = config
7 |
8 |
--------------------------------------------------------------------------------
/services/iam-service/app/services/register_service.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 | from loguru import logger
3 | from fastapi import Depends, HTTPException, status
4 |
5 | from app.domain.schemas.user_schema import (
6 | UserCreateSchema,
7 | UserSchema,
8 | UserCreateResponseSchema,
9 | VerifyOTPSchema,
10 | VerifyOTPResponseSchema,
11 | ResendOTPSchema,
12 | ResendOTPResponseSchema,
13 | )
14 | from app.services.auth_services.auth_service import AuthService
15 | from app.services.auth_services.otp_service import OTPService
16 | from app.services.base_service import BaseService
17 | from app.services.user_service import UserService
18 |
19 |
20 | class RegisterService(BaseService):
21 | def __init__(
22 | self,
23 | user_service: Annotated[UserService, Depends()],
24 | otp_service: Annotated[OTPService, Depends()],
25 | auth_service: Annotated[AuthService, Depends()],
26 | ) -> None:
27 | super().__init__()
28 |
29 | self.user_service = user_service
30 | self.otp_service = otp_service
31 | self.auth_service = auth_service
32 |
33 | async def register_user(self, user: UserCreateSchema) -> UserCreateResponseSchema:
34 | existing_mobile_number = await self.user_service.get_user_by_mobile_number(
35 | user.mobile_number
36 | )
37 |
38 | if existing_mobile_number:
39 | logger.error(f"User with mobile number {user.mobile_number} already exists")
40 | raise HTTPException(
41 | status_code=status.HTTP_400_BAD_REQUEST, detail="User already exists"
42 | )
43 |
44 | new_user = await self.user_service.create_user(user)
45 | otp = self.otp_service.send_otp(new_user.mobile_number)
46 |
47 | logger.info(f"User with mobile number {user.mobile_number} created successfully")
48 | return UserCreateResponseSchema(
49 | user=UserSchema.from_orm(new_user),
50 | OTP=otp,
51 | message="User created successfully, OTP sent to mobile number",
52 | )
53 |
54 | async def verify_user(
55 | self, verify_user_schema: VerifyOTPSchema
56 | ) -> VerifyOTPResponseSchema:
57 | if not self.otp_service.verify_otp(
58 | verify_user_schema.mobile_number, verify_user_schema.OTP
59 | ):
60 | logger.error(f"Invalid OTP for mobile number {verify_user_schema.mobile_number}")
61 | raise HTTPException(
62 | status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid OTP"
63 | )
64 |
65 | user = await self.user_service.get_user_by_mobile_number(
66 | verify_user_schema.mobile_number
67 | )
68 |
69 | await self.user_service.update_user(user.id, {"is_verified": True})
70 |
71 | logger.info(f"User with mobile number {verify_user_schema.mobile_number} verified")
72 | return VerifyOTPResponseSchema(
73 | verified=True, message="User verified successfully"
74 | )
75 |
76 | async def resend_otp(
77 | self, resend_otp_schema: ResendOTPSchema
78 | ) -> ResendOTPResponseSchema:
79 | existing_user = await self.user_service.get_user_by_mobile_number(
80 | resend_otp_schema.mobile_number
81 | )
82 | if not existing_user:
83 | logger.error(f"User with mobile number {resend_otp_schema.mobile_number} does not exist")
84 | raise HTTPException(
85 | status_code=status.HTTP_400_BAD_REQUEST, detail="User does not exist"
86 | )
87 |
88 | if existing_user.is_verified:
89 | logger.error(f"User with mobile number {resend_otp_schema.mobile_number} already verified")
90 | raise HTTPException(
91 | status_code=status.HTTP_400_BAD_REQUEST, detail="User already verified"
92 | )
93 |
94 | if self.otp_service.check_exist(resend_otp_schema.mobile_number):
95 | logger.error(f"OTP for mobile number {resend_otp_schema.mobile_number} already exists")
96 | raise HTTPException(
97 | status_code=status.HTTP_400_BAD_REQUEST, detail="OTP already exists"
98 | )
99 |
100 | otp = self.otp_service.send_otp(resend_otp_schema.mobile_number)
101 |
102 | logger.info(f"OTP resent to mobile number {resend_otp_schema.mobile_number}")
103 | return ResendOTPResponseSchema(
104 | mobile_number=resend_otp_schema.mobile_number,
105 | OTP=otp,
106 | message="OTP sent to mobile number",
107 | )
108 |
--------------------------------------------------------------------------------
/services/iam-service/app/services/user_service.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated, Dict
2 | from uuid import UUID
3 | from loguru import logger
4 | from fastapi import Depends
5 |
6 | from app.domain.models.user import User
7 | from app.domain.schemas.user_schema import UserCreateSchema
8 | from app.infrastructure.repositories.user_repository import UserRepository
9 | from app.services.auth_services.hash_sevice import HashService
10 | from app.services.base_service import BaseService
11 |
12 |
13 | class UserService(BaseService):
14 | def __init__(
15 | self,
16 | user_repository: Annotated[UserRepository, Depends()],
17 | hash_service: Annotated[HashService, Depends()],
18 | ) -> None:
19 | super().__init__()
20 | self.user_repository = user_repository
21 | self.hash_service = hash_service
22 |
23 | async def create_user(self, user_body: UserCreateSchema) -> User:
24 | logger.info(f"Creating user with mobile number {user_body.mobile_number}")
25 | return self.user_repository.create_user(
26 | User(
27 | first_name=user_body.first_name,
28 | last_name=user_body.last_name,
29 | mobile_number=user_body.mobile_number,
30 | hashed_password=self.hash_service.hash_password(user_body.password),
31 | )
32 | )
33 |
34 | async def update_user(self, user_id: int, update_fields: Dict) -> User:
35 | logger.info(f"Updating user with id {user_id}")
36 | return self.user_repository.update_user(user_id, update_fields)
37 |
38 | async def delete_user(self, user: User) -> None:
39 | logger.info(f"Deleting user with id {user.id}")
40 | return self.user_repository.delete_user(user)
41 |
42 | async def get_user(self, user_id: UUID) -> User:
43 | logger.info(f"Fetching user with id {user_id}")
44 | return self.user_repository.get_user(user_id)
45 |
46 | async def get_user_by_mobile_number(self, mobile_number: str) -> User:
47 | logger.info(f"Fetching user with mobile number {mobile_number}")
48 | return self.user_repository.get_user_by_mobile_number(mobile_number)
49 |
--------------------------------------------------------------------------------
/services/iam-service/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 |
5 | postgres:
6 | container_name: postgres_container
7 | image: postgres
8 | environment:
9 | POSTGRES_USER: ${POSTGRES_USER:-postgres}
10 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin}
11 | PGDATA: /data/postgres
12 | volumes:
13 | - postgres:/data/postgres
14 | ports:
15 | - "5432:5432"
16 | networks:
17 | - app-network
18 | restart: unless-stopped
19 | expose:
20 | - 5432
21 |
22 | pgadmin:
23 | container_name: pgadmin_container
24 | image: dpage/pgadmin4
25 | environment:
26 | PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org}
27 | PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
28 | PGADMIN_CONFIG_SERVER_MODE: 'False'
29 | volumes:
30 | - pgadmin:/var/lib/pgadmin
31 | ports:
32 | - "${PGADMIN_PORT:-5050}:80"
33 | networks:
34 | - app-network
35 | restart: unless-stopped
36 |
37 | redis:
38 | image: redis
39 | container_name: redis
40 | command: redis-server /usr/local/etc/redis/redis.conf
41 | ports:
42 | - "6379:6379"
43 | volumes:
44 | - ./data:/data
45 | - ./redis.conf:/usr/local/etc/redis/redis.conf
46 | networks:
47 | - app-network
48 | restart: unless-stopped
49 |
50 |
51 | iam:
52 | build:
53 | context: ../services/iam-service
54 | dockerfile: Dockerfile
55 | container_name: iam_service
56 | environment:
57 | - DATABASE_DIALECT=postgresql+psycopg2
58 | - DATABASE_HOSTNAME=postgres_container
59 | - DATABASE_NAME=IAM-DB
60 | - DATABASE_PASSWORD=admin
61 | - DATABASE_PORT=5432
62 | - DATABASE_USERNAME=postgres
63 | - DEBUG_MODE=False
64 | - REDIS_URL=redis
65 | - JWT_SECRET_KEY=1807372bcbf0963ebe30a1df3669690b8f0e4f83a1b52e7579cfee9ff08db230
66 | - JWT_ALGORITHM=HS256
67 | - ACCESS_TOKEN_EXPIRE_MINUTES=30
68 | - OTP_EXPIRE_TIME=60
69 | # ports:
70 | # - "80:80"
71 | networks:
72 | - app-network
73 | labels:
74 | - "traefik.enable=true"
75 | - "traefik.http.routers.iam-service.rule=Host(`iam.localhost`)"
76 | - "traefik.http.routers.iam-service.entrypoints=web"
77 | - "traefik.http.services.iam-service.loadbalancer.server.port=80"
78 |
79 | restart: unless-stopped
80 |
81 | depends_on:
82 | - postgres
83 | - redis
84 |
85 |
86 |
87 |
88 | networks:
89 | app-network:
90 | # external: true
91 | driver: bridge
92 |
93 | volumes:
94 | postgres:
95 | pgadmin:
96 |
97 |
--------------------------------------------------------------------------------
/services/iam-service/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | fastapi-restful
3 | uvicorn
4 | sqlalchemy
5 | databases
6 | pydantic
7 | pydantic-settings
8 | typing-inspect
9 | psycopg2-binary
10 | redis
11 | python-jose
12 | pyjwt
13 | passlib[bcrypt]
14 | bcrypt==4.0.1
15 | sqlalchemy-utils
16 | loguru
17 |
--------------------------------------------------------------------------------
/services/media-service/.dockerignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
4 |
5 | # User-specific stuff
6 | .idea/**/workspace.xml
7 | .idea/**/tasks.xml
8 | .idea/**/usage.statistics.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # AWS User-specific
13 | .idea/**/aws.xml
14 |
15 | # Generated files
16 | .idea/**/contentModel.xml
17 |
18 | # Sensitive or high-churn files
19 | .idea/**/dataSources/
20 | .idea/**/dataSources.ids
21 | .idea/**/dataSources.local.xml
22 | .idea/**/sqlDataSources.xml
23 | .idea/**/dynamic.xml
24 | .idea/**/uiDesigner.xml
25 | .idea/**/dbnavigator.xml
26 |
27 | # Gradle
28 | .idea/**/gradle.xml
29 | .idea/**/libraries
30 |
31 | # Gradle and Maven with auto-import
32 | # When using Gradle or Maven with auto-import, you should exclude module files,
33 | # since they will be recreated, and may cause churn. Uncomment if using
34 | # auto-import.
35 | # .idea/artifacts
36 | # .idea/compiler.xml
37 | # .idea/jarRepositories.xml
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 | # *.iml
42 | # *.ipr
43 |
44 | # CMake
45 | cmake-build-*/
46 |
47 | # Mongo Explorer plugin
48 | .idea/**/mongoSettings.xml
49 |
50 | # File-based project format
51 | *.iws
52 |
53 | # IntelliJ
54 | out/
55 |
56 | # mpeltonen/sbt-idea plugin
57 | .idea_modules/
58 |
59 | # JIRA plugin
60 | atlassian-ide-plugin.xml
61 |
62 | # Cursive Clojure plugin
63 | .idea/replstate.xml
64 |
65 | # SonarLint plugin
66 | .idea/sonarlint/
67 |
68 | # Crashlytics plugin (for Android Studio and IntelliJ)
69 | com_crashlytics_export_strings.xml
70 | crashlytics.properties
71 | crashlytics-build.properties
72 | fabric.properties
73 |
74 | # Editor-based Rest Client
75 | .idea/httpRequests
76 |
77 | # Android studio 3.1+ serialized cache file
78 | .idea/caches/build_file_checksums.ser
79 |
80 | ### Linux template
81 | *~
82 |
83 | # temporary files which can be created if a process still has a handle open of a deleted file
84 | .fuse_hidden*
85 |
86 | # KDE directory preferences
87 | .directory
88 |
89 | # Linux trash folder which might appear on any partition or disk
90 | .Trash-*
91 |
92 | # .nfs files are created when an open file is removed but is still being accessed
93 | .nfs*
94 |
95 | ### Redis template
96 | # Ignore redis binary dump (dump.rdb) files
97 |
98 | *.rdb
99 |
100 | ### Python template
101 | # Byte-compiled / optimized / DLL files
102 | __pycache__/
103 | *.py[cod]
104 | *$py.class
105 |
106 | # C extensions
107 | *.so
108 |
109 | # Distribution / packaging
110 | .Python
111 | build/
112 | develop-eggs/
113 | dist/
114 | downloads/
115 | eggs/
116 | .eggs/
117 | lib/
118 | lib64/
119 | parts/
120 | sdist/
121 | var/
122 | wheels/
123 | share/python-wheels/
124 | *.egg-info/
125 | .installed.cfg
126 | *.egg
127 | MANIFEST
128 |
129 | # PyInstaller
130 | # Usually these files are written by a python script from a template
131 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
132 | *.manifest
133 | *.spec
134 |
135 | # Installer logs
136 | pip-log.txt
137 | pip-delete-this-directory.txt
138 |
139 | # Unit test / coverage reports
140 | htmlcov/
141 | .tox/
142 | .nox/
143 | .coverage
144 | .coverage.*
145 | .cache
146 | nosetests.xml
147 | coverage.xml
148 | *.cover
149 | *.py,cover
150 | .hypothesis/
151 | .pytest_cache/
152 | cover/
153 |
154 | # Translations
155 | *.mo
156 | *.pot
157 |
158 | # Django stuff:
159 | *.log
160 | local_settings.py
161 | db.sqlite3
162 | db.sqlite3-journal
163 |
164 | # Flask stuff:
165 | instance/
166 | .webassets-cache
167 |
168 | # Scrapy stuff:
169 | .scrapy
170 |
171 | # Sphinx documentation
172 | docs/_build/
173 |
174 | # PyBuilder
175 | .pybuilder/
176 | target/
177 |
178 | # Jupyter Notebook
179 | .ipynb_checkpoints
180 |
181 | # IPython
182 | profile_default/
183 | ipython_config.py
184 |
185 | # pyenv
186 | # For a library or package, you might want to ignore these files since the code is
187 | # intended to run in multiple environments; otherwise, check them in:
188 | # .python-version
189 |
190 | # pipenv
191 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
192 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
193 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
194 | # install all needed dependencies.
195 | #Pipfile.lock
196 |
197 | # poetry
198 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
199 | # This is especially recommended for binary packages to ensure reproducibility, and is more
200 | # commonly ignored for libraries.
201 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
202 | #poetry.lock
203 |
204 | # pdm
205 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
206 | #pdm.lock
207 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
208 | # in version control.
209 | # https://pdm.fming.dev/#use-with-ide
210 | .pdm.toml
211 |
212 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
213 | __pypackages__/
214 |
215 | # Celery stuff
216 | celerybeat-schedule
217 | celerybeat.pid
218 |
219 | # SageMath parsed files
220 | *.sage.py
221 |
222 | # Environments
223 | .env
224 | .venv
225 | env/
226 | venv/
227 | ENV/
228 | env.bak/
229 | venv.bak/
230 |
231 | # Spyder project settings
232 | .spyderproject
233 | .spyproject
234 |
235 | # Rope project settings
236 | .ropeproject
237 |
238 | # mkdocs documentation
239 | /site
240 |
241 | # mypy
242 | .mypy_cache/
243 | .dmypy.json
244 | dmypy.json
245 |
246 | # Pyre type checker
247 | .pyre/
248 |
249 | # pytype static type analyzer
250 | .pytype/
251 |
252 | # Cython debug symbols
253 | cython_debug/
254 |
255 | # PyCharm
256 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
257 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
258 | # and can be added to the global gitignore or merged into this file. For a more nuclear
259 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
260 | #.idea/
261 |
262 | data
263 | redis.conf
--------------------------------------------------------------------------------
/services/media-service/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11
2 |
3 | # Install supervisor
4 | RUN apt-get -o Acquire::Check-Valid-Until=false update && apt-get install -y supervisor
5 |
6 | # Copy the supervisor configuration file
7 | COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
8 |
9 | COPY ./requirements.txt /app/requirements.txt
10 |
11 | RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
12 |
13 | COPY ./app /app/app
14 |
15 | # Use supervisor to run both processes
16 | CMD ["/usr/bin/supervisord"]
--------------------------------------------------------------------------------
/services/media-service/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/api/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/api/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/api/v1/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/api/v1/endpoints/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/api/v1/endpoints/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/api/v1/endpoints/media.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 | from loguru import logger
3 | from fastapi import APIRouter, Depends, UploadFile, status, Form
4 | from fastapi.responses import StreamingResponse
5 |
6 | from app.domain.schemas.media_schema import MediaGetSchema, MediaSchema
7 | from app.domain.schemas.token_schema import TokenDataSchema
8 | from app.services.media_service import MediaService
9 | from app.services.auth_service import get_current_user
10 |
11 | media_router = APIRouter()
12 |
13 |
14 | @media_router.post(
15 | "/UploadMedia", response_model=MediaSchema, status_code=status.HTTP_201_CREATED
16 | )
17 | async def upload_media(
18 | media_service: Annotated[MediaService, Depends()],
19 | file: UploadFile,
20 | current_user: Annotated[TokenDataSchema, Depends(get_current_user)],
21 | ):
22 | logger.info(f"Uploading media file {file.filename}")
23 | return await media_service.create_media(file, current_user.id)
24 |
25 |
26 | @media_router.post(
27 | "/GetMedia", response_class=StreamingResponse, status_code=status.HTTP_200_OK
28 | )
29 | async def get_media(
30 | media_get: MediaGetSchema,
31 | media_service: Annotated[MediaService, Depends()],
32 | current_user: Annotated[TokenDataSchema, Depends(get_current_user)],
33 | ):
34 | media_schema, file_stream = await media_service.get_media(
35 | media_get.mongo_id, current_user.id
36 | )
37 |
38 | logger.info(f"Retrieving media file {media_schema.filename}")
39 |
40 | return StreamingResponse(
41 | content=file_stream(),
42 | media_type=media_schema.content_type,
43 | headers={
44 | "Content-Disposition": f"attachment; filename={media_schema.filename}"
45 | },
46 | )
47 |
--------------------------------------------------------------------------------
/services/media-service/app/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/core/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/core/config.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 | from pathlib import Path
3 | from loguru import logger
4 |
5 | from pydantic_settings import BaseSettings, SettingsConfigDict
6 |
7 |
8 | class Settings(BaseSettings):
9 | DATABASE_URL: str
10 | DATABASE_NAME: str
11 | FILE_STORAGE_PATH: str
12 | IAM_URL: str
13 | GRPC_PORT: int
14 |
15 | # model_config = SettingsConfigDict(env_file=str(Path(__file__).resolve().parent / ".env"))
16 |
17 |
18 | @lru_cache
19 | @logger.catch
20 | def get_settings():
21 | return Settings()
22 |
--------------------------------------------------------------------------------
/services/media-service/app/core/db/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/core/db/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/core/db/database.py:
--------------------------------------------------------------------------------
1 | from motor.motor_asyncio import AsyncIOMotorClient
2 | from loguru import logger
3 | from app.core.config import get_settings
4 |
5 | try:
6 | config = get_settings()
7 | client = AsyncIOMotorClient(config.DATABASE_URL)
8 | db = client[config.DATABASE_NAME]
9 | logger.info("Connected to database")
10 | except Exception as e:
11 | logger.error(f"Error connecting to database: {e}")
12 |
13 |
14 | async def get_db():
15 | yield db
16 |
--------------------------------------------------------------------------------
/services/media-service/app/domain/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/domain/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/domain/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/domain/models/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/domain/models/media_model.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Annotated
3 | from uuid import UUID
4 |
5 | from bson import ObjectId
6 | from pydantic import BaseModel
7 |
8 | from app.domain.models.object_id_model import ObjectIdPydanticAnnotation
9 |
10 |
11 | class MediaModel(BaseModel):
12 | mongo_id: Annotated[ObjectId, ObjectIdPydanticAnnotation] = None
13 | filename: str
14 | content_type: str
15 | size: int
16 | upload_date: datetime = datetime.now()
17 | metadata: dict = {}
18 | user_id: str
19 |
20 |
21 | class MediaGridFSModel(MediaModel):
22 | storage_id: Annotated[ObjectId, ObjectIdPydanticAnnotation]
23 |
24 | class Config:
25 | from_attributes = True
26 | json_encoders = {ObjectId: str, datetime: lambda v: v.isoformat()}
27 | arbitrary_types_allowed = True
28 |
--------------------------------------------------------------------------------
/services/media-service/app/domain/models/object_id_model.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from bson import ObjectId
4 | from pydantic.json_schema import JsonSchemaValue
5 | from pydantic_core import core_schema
6 |
7 |
8 | class ObjectIdPydanticAnnotation:
9 | @classmethod
10 | def validate_object_id(cls, v: Any, handler) -> ObjectId:
11 | if isinstance(v, ObjectId):
12 | return v
13 |
14 | s = handler(v)
15 | if ObjectId.is_valid(s):
16 | return ObjectId(s)
17 | else:
18 | raise ValueError("Invalid ObjectId")
19 |
20 | @classmethod
21 | def __get_pydantic_core_schema__(
22 | cls, source_type, _handler
23 | ) -> core_schema.CoreSchema:
24 | assert source_type is ObjectId
25 | return core_schema.no_info_wrap_validator_function(
26 | cls.validate_object_id,
27 | core_schema.str_schema(),
28 | serialization=core_schema.to_string_ser_schema(),
29 | )
30 |
31 | @classmethod
32 | def __get_pydantic_json_schema__(cls, _core_schema, handler) -> JsonSchemaValue:
33 | return handler(core_schema.str_schema())
34 |
--------------------------------------------------------------------------------
/services/media-service/app/domain/schemas/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/domain/schemas/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/domain/schemas/media_schema.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Annotated
3 |
4 | from bson import ObjectId
5 | from pydantic import BaseModel
6 |
7 | from app.domain.models.media_model import MediaModel
8 | from app.domain.models.object_id_model import ObjectIdPydanticAnnotation
9 |
10 |
11 | class MediaSchema(MediaModel):
12 | message: str
13 |
14 | class Config:
15 | from_attributes = True
16 | json_encoders = {ObjectId: str, datetime: lambda v: v.isoformat()}
17 | arbitrary_types_allowed = True
18 |
19 |
20 | class MediaGetSchema(BaseModel):
21 | mongo_id: Annotated[ObjectId, ObjectIdPydanticAnnotation]
22 | # mongo_id: str
23 |
--------------------------------------------------------------------------------
/services/media-service/app/domain/schemas/token_schema.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional
3 | from uuid import UUID
4 |
5 | from pydantic import BaseModel, Field
6 |
7 |
8 | class TokenDataSchema(BaseModel):
9 | id: str
10 | first_name: str
11 | last_name: str
12 | mobile_number: str = Field(..., pattern=r"^\+?[1-9]\d{1,14}$")
13 | created_at: Optional[datetime]
14 | updated_at: Optional[datetime]
15 | is_verified: bool
16 |
17 | class Config:
18 | from_attributes = True
19 |
--------------------------------------------------------------------------------
/services/media-service/app/grpc_server.py:
--------------------------------------------------------------------------------
1 | from concurrent import futures
2 | from loguru import logger
3 | import grpc
4 | from app.core.config import get_settings
5 | from app.grpc_service import media_pb2_grpc, media_pb2
6 | from app.infrastructure.clients.iam_client import IAMClient
7 | from app.services.media_service import MediaService
8 | from app.infrastructure.repositories.media_repository import MediaRepository
9 | from app.infrastructure.storage.gridfs_storage import GridFsStorage
10 | from app.core.db.database import db
11 |
12 |
13 | config = get_settings()
14 |
15 |
16 | class MediaServiceServicer(media_pb2_grpc.MediaServiceServicer):
17 | def __init__(self, media_service: MediaService, iam_client: IAMClient):
18 | self.media_service = media_service
19 | self.iam_client = iam_client
20 |
21 | async def DownloadMedia(self, request, context):
22 | # Extract the user filed from the metadata
23 | user_id = dict(context.invocation_metadata()).get("user")
24 |
25 | try:
26 | media_data = await self.media_service.get_media_data(request.media_id, user_id)
27 | if media_data is None:
28 | logger.error(f"Media not found")
29 | context.set_code(grpc.StatusCode.NOT_FOUND)
30 | context.set_details("Media not found")
31 | return media_pb2.MediaResponse()
32 |
33 | logger.info(f"Media {request.media_id} retrieved")
34 | return media_pb2.MediaResponse(
35 | media_data=media_data, media_type=request.media_type
36 | )
37 | except Exception as e:
38 | logger.error(f"Error retrieving media: {e}")
39 | context.set_code(grpc.StatusCode.INTERNAL)
40 | context.set_details(str(e))
41 | return media_pb2.MediaResponse()
42 |
43 |
44 | async def serve():
45 | options = [('grpc.max_message_length', 100 * 1024 * 1024),
46 | ('grpc.max_receive_message_length', 100 * 1024 * 1024),
47 | ('grpc.max_send_message_length', 100 * 1024 * 1024)]
48 | server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=10), options=options)
49 | media_pb2_grpc.add_MediaServiceServicer_to_server(
50 | MediaServiceServicer(MediaService(
51 | MediaRepository(db), GridFsStorage(db)
52 | ), IAMClient(config)),
53 | server,
54 | )
55 | server.add_insecure_port(f"[::]:{config.GRPC_PORT}")
56 | logger.info(f"Starting Media Service gRPC server on port {config.GRPC_PORT}")
57 | await server.start()
58 | await server.wait_for_termination()
59 |
60 |
61 | if __name__ == "__main__":
62 | import asyncio
63 |
64 | asyncio.run(serve())
65 |
--------------------------------------------------------------------------------
/services/media-service/app/grpc_service/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/grpc_service/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/grpc_service/media.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package media;
4 |
5 | service MediaService {
6 | rpc DownloadMedia (MediaRequest) returns (MediaResponse);
7 | }
8 |
9 | message MediaRequest {
10 | string media_id = 1;
11 | string media_type = 2;
12 | }
13 |
14 | message MediaResponse {
15 | bytes media_data = 1;
16 | string media_type = 2;
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/services/media-service/app/grpc_service/media_pb2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by the protocol buffer compiler. DO NOT EDIT!
3 | # source: media.proto
4 | # Protobuf Python Version: 5.26.1
5 | """Generated protocol buffer code."""
6 | from google.protobuf import descriptor as _descriptor
7 | from google.protobuf import descriptor_pool as _descriptor_pool
8 | from google.protobuf import symbol_database as _symbol_database
9 | from google.protobuf.internal import builder as _builder
10 | # @@protoc_insertion_point(imports)
11 |
12 | _sym_db = _symbol_database.Default()
13 |
14 |
15 |
16 |
17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bmedia.proto\x12\x05media\"4\n\x0cMediaRequest\x12\x10\n\x08media_id\x18\x01 \x01(\t\x12\x12\n\nmedia_type\x18\x02 \x01(\t\"7\n\rMediaResponse\x12\x12\n\nmedia_data\x18\x01 \x01(\x0c\x12\x12\n\nmedia_type\x18\x02 \x01(\t2J\n\x0cMediaService\x12:\n\rDownloadMedia\x12\x13.media.MediaRequest\x1a\x14.media.MediaResponseb\x06proto3')
18 |
19 | _globals = globals()
20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'media_pb2', _globals)
22 | if not _descriptor._USE_C_DESCRIPTORS:
23 | DESCRIPTOR._loaded_options = None
24 | _globals['_MEDIAREQUEST']._serialized_start=22
25 | _globals['_MEDIAREQUEST']._serialized_end=74
26 | _globals['_MEDIARESPONSE']._serialized_start=76
27 | _globals['_MEDIARESPONSE']._serialized_end=131
28 | _globals['_MEDIASERVICE']._serialized_start=133
29 | _globals['_MEDIASERVICE']._serialized_end=207
30 | # @@protoc_insertion_point(module_scope)
31 |
--------------------------------------------------------------------------------
/services/media-service/app/grpc_service/media_pb2_grpc.py:
--------------------------------------------------------------------------------
1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2 | """Client and server classes corresponding to protobuf-defined services."""
3 | import grpc
4 | import warnings
5 |
6 | import app.grpc_service.media_pb2 as media__pb2
7 |
8 | GRPC_GENERATED_VERSION = "1.64.1"
9 | GRPC_VERSION = grpc.__version__
10 | EXPECTED_ERROR_RELEASE = "1.65.0"
11 | SCHEDULED_RELEASE_DATE = "June 25, 2024"
12 | _version_not_supported = False
13 |
14 | try:
15 | from grpc._utilities import first_version_is_lower
16 |
17 | _version_not_supported = first_version_is_lower(
18 | GRPC_VERSION, GRPC_GENERATED_VERSION
19 | )
20 | except ImportError:
21 | _version_not_supported = True
22 |
23 | if _version_not_supported:
24 | warnings.warn(
25 | f"The grpc_service package installed is at version {GRPC_VERSION},"
26 | + f" but the generated code in media_pb2_grpc.py depends on"
27 | + f" grpcio>={GRPC_GENERATED_VERSION}."
28 | + f" Please upgrade your grpc_service module to grpcio>={GRPC_GENERATED_VERSION}"
29 | + f" or downgrade your generated code using grpcio-tools<={GRPC_VERSION}."
30 | + f" This warning will become an error in {EXPECTED_ERROR_RELEASE},"
31 | + f" scheduled for release on {SCHEDULED_RELEASE_DATE}.",
32 | RuntimeWarning,
33 | )
34 |
35 |
36 | class MediaServiceStub(object):
37 | """Missing associated documentation comment in .proto file."""
38 |
39 | def __init__(self, channel):
40 | """Constructor.
41 |
42 | Args:
43 | channel: A grpc_service.Channel.
44 | """
45 | self.DownloadMedia = channel.unary_unary(
46 | "/media.MediaService/DownloadMedia",
47 | request_serializer=media__pb2.MediaRequest.SerializeToString,
48 | response_deserializer=media__pb2.MediaResponse.FromString,
49 | _registered_method=True,
50 | )
51 |
52 |
53 | class MediaServiceServicer(object):
54 | """Missing associated documentation comment in .proto file."""
55 |
56 | def DownloadMedia(self, request, context):
57 | """Missing associated documentation comment in .proto file."""
58 | context.set_code(grpc.StatusCode.UNIMPLEMENTED)
59 | context.set_details("Method not implemented!")
60 | raise NotImplementedError("Method not implemented!")
61 |
62 |
63 | def add_MediaServiceServicer_to_server(servicer, server):
64 | rpc_method_handlers = {
65 | "DownloadMedia": grpc.unary_unary_rpc_method_handler(
66 | servicer.DownloadMedia,
67 | request_deserializer=media__pb2.MediaRequest.FromString,
68 | response_serializer=media__pb2.MediaResponse.SerializeToString,
69 | ),
70 | }
71 | generic_handler = grpc.method_handlers_generic_handler(
72 | "media.MediaService", rpc_method_handlers
73 | )
74 | server.add_generic_rpc_handlers((generic_handler,))
75 | server.add_registered_method_handlers("media.MediaService", rpc_method_handlers)
76 |
77 |
78 | # This class is part of an EXPERIMENTAL API.
79 | class MediaService(object):
80 | """Missing associated documentation comment in .proto file."""
81 |
82 | @staticmethod
83 | def DownloadMedia(
84 | request,
85 | target,
86 | options=(),
87 | channel_credentials=None,
88 | call_credentials=None,
89 | insecure=False,
90 | compression=None,
91 | wait_for_ready=None,
92 | timeout=None,
93 | metadata=None,
94 | ):
95 | return grpc.experimental.unary_unary(
96 | request,
97 | target,
98 | "/media.MediaService/DownloadMedia",
99 | media__pb2.MediaRequest.SerializeToString,
100 | media__pb2.MediaResponse.FromString,
101 | options,
102 | channel_credentials,
103 | insecure,
104 | call_credentials,
105 | compression,
106 | wait_for_ready,
107 | timeout,
108 | metadata,
109 | _registered_method=True,
110 | )
111 |
--------------------------------------------------------------------------------
/services/media-service/app/infrastructure/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/infrastructure/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/infrastructure/clients/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/infrastructure/clients/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/infrastructure/clients/http_client.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from typing import Annotated
3 | from loguru import logger
4 | import httpx
5 | import tenacity
6 | from fastapi import Depends, HTTPException, status
7 | from app.core.config import get_settings, Settings
8 | from tenacity import retry, stop_after_attempt, wait_fixed
9 | from aiobreaker import CircuitBreaker
10 |
11 | breaker = CircuitBreaker(fail_max=3, timeout_duration=timedelta(seconds=60))
12 |
13 |
14 | class HTTPClient:
15 | def __init__(self, config: Settings = Depends(get_settings)):
16 | self.config = config
17 | self.http_client = httpx.AsyncClient()
18 |
19 | @retry(stop=stop_after_attempt(3), wait=wait_fixed(2))
20 | @breaker
21 | async def _request(
22 | self, method: str, url: str, headers: dict = None, data: dict = None
23 | ):
24 | async with httpx.AsyncClient(
25 | timeout=10
26 | ) as client: # Using async with to manage lifecycle
27 | try:
28 | response = await client.request(method, url, headers=headers, json=data)
29 | response.raise_for_status()
30 | logger.info(f"HTTP request to {url} successful")
31 | return response
32 | except httpx.RequestError as e:
33 | logger.error(f"Error making request to {url} - {e}")
34 | raise HTTPException(
35 | status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
36 | detail=f"Service unavailable - {e}",
37 | )
38 | except httpx.HTTPStatusError as e:
39 | logger.error(f"Error making request to {url} - {e}")
40 | raise HTTPException(
41 | status_code=e.response.status_code,
42 | detail=e.response.json(),
43 | )
44 |
45 | async def get(self, url: str, headers: dict = None):
46 | try:
47 | logger.info(f"Making GET request to {url}")
48 | return await self._request("GET", url, headers=headers)
49 | except tenacity.RetryError:
50 | logger.error(f"HTTP Service unavailable")
51 | raise HTTPException(
52 | status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
53 | detail=f"Service unavailable",
54 | )
55 |
56 | async def post(self, url: str, headers: dict = None, data: dict = None):
57 | try:
58 | logger.info(f"Making POST request to {url}")
59 | return await self._request("POST", url, headers=headers, data=data)
60 | except tenacity.RetryError:
61 | logger.error(f"HTTP Service unavailable")
62 | raise HTTPException(
63 | status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
64 | detail=f"Service unavailable",
65 | )
66 |
67 | async def __aenter__(self):
68 | return self
69 |
70 | async def __aexit__(self, exc_type, exc_val, exc_tb):
71 | await self.http_client.aclose()
72 |
--------------------------------------------------------------------------------
/services/media-service/app/infrastructure/clients/iam_client.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 | from loguru import logger
3 | from fastapi import Depends, HTTPException, status, Request
4 | from app.core.config import get_settings, Settings
5 | from app.domain.schemas.token_schema import TokenDataSchema
6 | from app.infrastructure.clients.http_client import HTTPClient
7 |
8 |
9 | class IAMClient:
10 | def __init__(
11 | self,
12 | http_client: Annotated[HTTPClient, Depends()],
13 | config: Settings = Depends(get_settings),
14 | ):
15 | self.config = config
16 | self.http_client = http_client
17 |
18 | async def validate_token(self, token: str) -> TokenDataSchema:
19 | headers = {"Authorization": f"Bearer {token}"}
20 | async with self.http_client as client:
21 | response = await client.get(
22 | f"{self.config.IAM_URL}/api/v1/users/Me", headers=headers
23 | )
24 | response.raise_for_status()
25 | logger.info(f"Token {token} validated")
26 | return TokenDataSchema(**response.json())
27 |
--------------------------------------------------------------------------------
/services/media-service/app/infrastructure/repositories/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/infrastructure/repositories/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/infrastructure/repositories/media_repository.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 | from loguru import logger
3 | from bson import ObjectId
4 | from fastapi import Depends
5 | from motor.motor_asyncio import AsyncIOMotorClient
6 |
7 | from app.core.db.database import get_db
8 | from app.domain.models.media_model import MediaGridFSModel
9 |
10 |
11 | class MediaRepository:
12 | def __init__(self, db: Annotated[AsyncIOMotorClient, Depends(get_db)]):
13 | self.collection = db["media"]
14 |
15 | async def create_media(self, media: MediaGridFSModel) -> MediaGridFSModel:
16 | result = await self.collection.insert_one(media.dict())
17 | media.mongo_id = result.inserted_id
18 | logger.info(f"Media {media.filename} created")
19 | return media
20 |
21 | async def get_media(self, media_id: ObjectId) -> MediaGridFSModel:
22 | media = await self.collection.find_one({"_id": media_id})
23 | return (
24 | MediaGridFSModel(
25 | mongo_id=str(media["_id"]),
26 | storage_id=str(media["storage_id"]),
27 | filename=media["filename"],
28 | content_type=media["content_type"],
29 | size=media["size"],
30 | upload_date=media["upload_date"],
31 | metadata=media["metadata"],
32 | user_id=media["user_id"],
33 | )
34 | if media
35 | else None
36 | )
37 |
--------------------------------------------------------------------------------
/services/media-service/app/infrastructure/storage/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/infrastructure/storage/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/infrastructure/storage/gridfs_storage.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from typing import Annotated
3 | from loguru import logger
4 | from bson import ObjectId
5 | from fastapi import Depends, UploadFile
6 | from motor.motor_asyncio import (
7 | AsyncIOMotorClient,
8 | AsyncIOMotorGridFSBucket,
9 | AsyncIOMotorGridOut,
10 | )
11 |
12 | from app.core.db.database import get_db, db
13 |
14 |
15 | class GridFsStorage:
16 | def __init__(self, db: Annotated[AsyncIOMotorClient, Depends(get_db)]):
17 | self.db = db
18 | self.fs = None
19 |
20 | async def init_fs(self):
21 | if not self.fs:
22 | self.fs = AsyncIOMotorGridFSBucket(self.db, bucket_name="media")
23 | logger.info("GridFS initialized")
24 |
25 | async def save_file(self, file: UploadFile) -> ObjectId:
26 | await self.init_fs()
27 | grid_in = self.fs.open_upload_stream(
28 | file.filename, metadata={"content_type": file.content_type}
29 | )
30 | await grid_in.write(await file.read())
31 | await grid_in.close()
32 | logger.info(f"File {file.filename} saved")
33 | return grid_in._id
34 |
35 | async def get_file(self, file_id: ObjectId) -> AsyncIOMotorGridOut:
36 | await self.init_fs()
37 | file_obj = await self.fs.open_download_stream(file_id)
38 | logger.info(f"File {file_id} retrieved")
39 | return await file_obj.read()
40 |
--------------------------------------------------------------------------------
/services/media-service/app/logging_service/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/media-service/app/logging_service/__init__.py
--------------------------------------------------------------------------------
/services/media-service/app/logging_service/logging_config.py:
--------------------------------------------------------------------------------
1 | from loguru import logger
2 | import sys
3 | import os
4 |
5 |
6 | def configure_logger():
7 | # Ensure log directory exists
8 | os.makedirs("logs", exist_ok=True)
9 |
10 | # Remove default logger
11 | logger.remove()
12 |
13 | # JSON serialization is handled internally by loguru when serialize=True, hence no need for a custom format.
14 | json_logging_format = {
15 | "rotation": "10 MB",
16 | "retention": "10 days",
17 | "serialize": True,
18 | }
19 |
20 | # Add file logging for JSON logs
21 | logger.add("logs/media_service_info.log", level="INFO", **json_logging_format)
22 | logger.add("logs/media_service_error.log", level="ERROR", **json_logging_format)
23 |
24 | # Custom log format for console and stderr
25 | log_format = "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:" \
26 | "{function}:{line} - {message}"
27 |
28 | # Add console logging
29 | logger.add(sys.stdout, level="INFO", format=log_format)
30 |
31 | # Add stderr logging
32 | logger.add(sys.stderr, level="ERROR", backtrace=True, diagnose=True, format=log_format)
33 |
--------------------------------------------------------------------------------
/services/media-service/app/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 | from fastapi.middleware.cors import CORSMiddleware
3 | from loguru import logger
4 |
5 | from app.api.v1.endpoints.media import media_router
6 | from app.logging_service.logging_config import configure_logger
7 |
8 | configure_logger()
9 |
10 | app = FastAPI()
11 |
12 | origins = ["*"]
13 |
14 | app.add_middleware(
15 | CORSMiddleware,
16 | allow_origins=origins,
17 | allow_credentials=True,
18 | allow_methods=["*"],
19 | allow_headers=["*"],
20 | )
21 |
22 |
23 | app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
24 |
25 | logger.info("Media Service Started")
26 |
27 |
28 | @app.get("/")
29 | async def root():
30 | return {"message": "Hello From Media Service !"}
31 |
--------------------------------------------------------------------------------
/services/media-service/app/services/auth_service.py:
--------------------------------------------------------------------------------
1 | from fastapi import Depends, HTTPException, Request, status
2 | from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3 | from typing import Annotated
4 | from loguru import logger
5 | from app.infrastructure.clients.iam_client import IAMClient
6 | from app.domain.schemas.token_schema import TokenDataSchema
7 | from app.core.config import get_settings
8 |
9 |
10 | config = get_settings()
11 |
12 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"http://iam.localhost/api/v1/users/Token")
13 |
14 |
15 | async def get_current_user(
16 | token: Annotated[str, Depends(oauth2_scheme)],
17 | client: Annotated[IAMClient, Depends()],
18 | ) -> TokenDataSchema:
19 |
20 | if not token:
21 | logger.error("No token provided")
22 | raise HTTPException(
23 | status_code=status.HTTP_401_UNAUTHORIZED,
24 | detail="Unauthorized",
25 | )
26 |
27 | logger.info(f"Validating token {token}")
28 | return await client.validate_token(token)
29 |
--------------------------------------------------------------------------------
/services/media-service/app/services/media_service.py:
--------------------------------------------------------------------------------
1 | from uuid import UUID
2 |
3 | from bson import ObjectId
4 | from fastapi import Depends, UploadFile, HTTPException, status
5 | from loguru import logger
6 | from typing import Annotated, Generator, Callable, Any, Tuple
7 |
8 | from fastapi import Depends, UploadFile, HTTPException, status
9 | from motor.motor_asyncio import AsyncIOMotorGridOut
10 |
11 | from app.domain.models.media_model import MediaGridFSModel
12 | from app.domain.schemas.media_schema import MediaSchema
13 | from app.infrastructure.repositories.media_repository import MediaRepository
14 | from app.infrastructure.storage.gridfs_storage import GridFsStorage
15 |
16 |
17 | class MediaService:
18 | def __init__(
19 | self,
20 | media_repository: Annotated[MediaRepository, Depends()],
21 | storage: Annotated[GridFsStorage, Depends()],
22 | ):
23 | self.media_repository = media_repository
24 | self.storage = storage
25 |
26 | async def create_media(self, file: UploadFile, user_id: str) -> MediaSchema:
27 | storage_id = await self.storage.save_file(file)
28 | media = MediaGridFSModel(
29 | storage_id=storage_id,
30 | filename=file.filename,
31 | content_type=file.content_type,
32 | size=file.size,
33 | user_id=user_id,
34 | )
35 | await self.media_repository.create_media(media)
36 | logger.info(f"Media {media.filename} created")
37 | return MediaSchema(
38 | mongo_id=str(media.mongo_id),
39 | filename=media.filename,
40 | content_type=media.content_type,
41 | size=media.size,
42 | upload_date=media.upload_date,
43 | user_id=media.user_id,
44 | message="Media uploaded successfully",
45 | )
46 |
47 | async def __get_media_model(self, media_id: ObjectId, user_id: str) -> Tuple[MediaGridFSModel, AsyncIOMotorGridOut]:
48 | media = await self.media_repository.get_media(media_id)
49 | if not media:
50 | raise HTTPException(
51 | status_code=status.HTTP_404_NOT_FOUND, detail="Media not found"
52 | )
53 | if media.user_id != user_id:
54 | raise HTTPException(
55 | status_code=status.HTTP_403_FORBIDDEN,
56 | detail="User does not have permission to access this media",
57 | )
58 | file = await self.storage.get_file(media.storage_id)
59 |
60 | logger.info(f"Media {media.filename} retrieved")
61 |
62 | return media, file
63 |
64 | async def get_media(
65 | self, media_id: ObjectId, user_id: str
66 | ) -> tuple[MediaSchema, Callable[[], Generator[Any, Any, None]]]:
67 | media, file = await self.__get_media_model(media_id, user_id)
68 |
69 | def file_stream():
70 | yield file
71 |
72 | logger.info(f"Retrieving media file {media.filename}")
73 | return (
74 | MediaSchema(
75 | mongo_id=media.mongo_id,
76 | filename=media.filename,
77 | content_type=media.content_type,
78 | size=media.size,
79 | upload_date=media.upload_date,
80 | user_id=media.user_id,
81 | message="Media downloaded successfully",
82 | ),
83 | file_stream,
84 | )
85 |
86 | async def get_media_data(self, media_id: str, user_id: str) -> bytes:
87 | _, file = await self.__get_media_model(ObjectId(media_id), user_id)
88 | logger.info(f"Retrieving media file {media_id}")
89 | return file
90 |
--------------------------------------------------------------------------------
/services/media-service/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 |
5 | mongo:
6 | image: mongo:latest
7 | container_name: mongo_container
8 | ports:
9 | - "27017:27017"
10 | networks:
11 | - app-network
12 |
13 | media:
14 | build:
15 | context: ../services/media-service
16 | dockerfile: Dockerfile
17 | container_name: media_service
18 | environment:
19 | - DATABASE_URL=mongodb://mongo:27017
20 | - DATABASE_NAME=AIToolsBoxMediaDB
21 | - FILE_STORAGE_PATH=app/media
22 | - IAM_URL=http://iam
23 | - GRPC_PORT=50051
24 | networks:
25 | - app-network
26 | labels:
27 | - "traefik.enable=true"
28 | - "traefik.http.routers.media.rule=Host(`media.localhost`)"
29 | - "traefik.http.services.media.loadbalancer.server.port=80"
30 | restart: unless-stopped
31 | ports:
32 | - "50051:50051" # grpc port
33 | depends_on:
34 | - mongo
35 |
36 | networks:
37 | app-network:
38 | driver: bridge
39 |
40 |
41 |
42 | volumes:
43 | postgres:
44 | pgadmin:
45 | letsencrypt:
46 | esdata:
47 |
--------------------------------------------------------------------------------
/services/media-service/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | pymongo
3 | motor
4 | python-multipart
5 | grpcio>=1.64.1
6 | grpcio-tools
7 | pydantic
8 | pydantic-settings
9 | pydantic_core
10 | httpx
11 | tenacity
12 | aiobreaker
13 | loguru
--------------------------------------------------------------------------------
/services/media-service/supervisord.conf:
--------------------------------------------------------------------------------
1 | [supervisord]
2 | nodaemon=true
3 |
4 | [program:fastapi]
5 | command=uvicorn app.main:app --host 0.0.0.0 --port 80
6 | directory=/app
7 | autostart=true
8 | autorestart=true
9 | stdout_logfile=/var/log/fastapi.log
10 | stderr_logfile=/var/log/fastapi_err.log
11 |
12 | [program:grpc_server]
13 | command=python3 /app/app/grpc_server.py
14 | directory=/app
15 | autostart=true
16 | autorestart=true
17 | stdout_logfile=/var/log/grpc_server.log
18 | stderr_logfile=/var/log/grpc_server_err.log
19 |
--------------------------------------------------------------------------------
/services/ocr-service/.dockerignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
4 |
5 | # User-specific stuff
6 | .idea/**/workspace.xml
7 | .idea/**/tasks.xml
8 | .idea/**/usage.statistics.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # AWS User-specific
13 | .idea/**/aws.xml
14 |
15 | # Generated files
16 | .idea/**/contentModel.xml
17 |
18 | # Sensitive or high-churn files
19 | .idea/**/dataSources/
20 | .idea/**/dataSources.ids
21 | .idea/**/dataSources.local.xml
22 | .idea/**/sqlDataSources.xml
23 | .idea/**/dynamic.xml
24 | .idea/**/uiDesigner.xml
25 | .idea/**/dbnavigator.xml
26 |
27 | # Gradle
28 | .idea/**/gradle.xml
29 | .idea/**/libraries
30 |
31 | # Gradle and Maven with auto-import
32 | # When using Gradle or Maven with auto-import, you should exclude module files,
33 | # since they will be recreated, and may cause churn. Uncomment if using
34 | # auto-import.
35 | # .idea/artifacts
36 | # .idea/compiler.xml
37 | # .idea/jarRepositories.xml
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 | # *.iml
42 | # *.ipr
43 |
44 | # CMake
45 | cmake-build-*/
46 |
47 | # Mongo Explorer plugin
48 | .idea/**/mongoSettings.xml
49 |
50 | # File-based project format
51 | *.iws
52 |
53 | # IntelliJ
54 | out/
55 |
56 | # mpeltonen/sbt-idea plugin
57 | .idea_modules/
58 |
59 | # JIRA plugin
60 | atlassian-ide-plugin.xml
61 |
62 | # Cursive Clojure plugin
63 | .idea/replstate.xml
64 |
65 | # SonarLint plugin
66 | .idea/sonarlint/
67 |
68 | # Crashlytics plugin (for Android Studio and IntelliJ)
69 | com_crashlytics_export_strings.xml
70 | crashlytics.properties
71 | crashlytics-build.properties
72 | fabric.properties
73 |
74 | # Editor-based Rest Client
75 | .idea/httpRequests
76 |
77 | # Android studio 3.1+ serialized cache file
78 | .idea/caches/build_file_checksums.ser
79 |
80 | ### Linux template
81 | *~
82 |
83 | # temporary files which can be created if a process still has a handle open of a deleted file
84 | .fuse_hidden*
85 |
86 | # KDE directory preferences
87 | .directory
88 |
89 | # Linux trash folder which might appear on any partition or disk
90 | .Trash-*
91 |
92 | # .nfs files are created when an open file is removed but is still being accessed
93 | .nfs*
94 |
95 | ### Redis template
96 | # Ignore redis binary dump (dump.rdb) files
97 |
98 | *.rdb
99 |
100 | ### Python template
101 | # Byte-compiled / optimized / DLL files
102 | __pycache__/
103 | *.py[cod]
104 | *$py.class
105 |
106 | # C extensions
107 | *.so
108 |
109 | # Distribution / packaging
110 | .Python
111 | build/
112 | develop-eggs/
113 | dist/
114 | downloads/
115 | eggs/
116 | .eggs/
117 | lib/
118 | lib64/
119 | parts/
120 | sdist/
121 | var/
122 | wheels/
123 | share/python-wheels/
124 | *.egg-info/
125 | .installed.cfg
126 | *.egg
127 | MANIFEST
128 |
129 | # PyInstaller
130 | # Usually these files are written by a python script from a template
131 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
132 | *.manifest
133 | *.spec
134 |
135 | # Installer logs
136 | pip-log.txt
137 | pip-delete-this-directory.txt
138 |
139 | # Unit test / coverage reports
140 | htmlcov/
141 | .tox/
142 | .nox/
143 | .coverage
144 | .coverage.*
145 | .cache
146 | nosetests.xml
147 | coverage.xml
148 | *.cover
149 | *.py,cover
150 | .hypothesis/
151 | .pytest_cache/
152 | cover/
153 |
154 | # Translations
155 | *.mo
156 | *.pot
157 |
158 | # Django stuff:
159 | *.log
160 | local_settings.py
161 | db.sqlite3
162 | db.sqlite3-journal
163 |
164 | # Flask stuff:
165 | instance/
166 | .webassets-cache
167 |
168 | # Scrapy stuff:
169 | .scrapy
170 |
171 | # Sphinx documentation
172 | docs/_build/
173 |
174 | # PyBuilder
175 | .pybuilder/
176 | target/
177 |
178 | # Jupyter Notebook
179 | .ipynb_checkpoints
180 |
181 | # IPython
182 | profile_default/
183 | ipython_config.py
184 |
185 | # pyenv
186 | # For a library or package, you might want to ignore these files since the code is
187 | # intended to run in multiple environments; otherwise, check them in:
188 | # .python-version
189 |
190 | # pipenv
191 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
192 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
193 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
194 | # install all needed dependencies.
195 | #Pipfile.lock
196 |
197 | # poetry
198 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
199 | # This is especially recommended for binary packages to ensure reproducibility, and is more
200 | # commonly ignored for libraries.
201 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
202 | #poetry.lock
203 |
204 | # pdm
205 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
206 | #pdm.lock
207 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
208 | # in version control.
209 | # https://pdm.fming.dev/#use-with-ide
210 | .pdm.toml
211 |
212 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
213 | __pypackages__/
214 |
215 | # Celery stuff
216 | celerybeat-schedule
217 | celerybeat.pid
218 |
219 | # SageMath parsed files
220 | *.sage.py
221 |
222 | # Environments
223 | .env
224 | .venv
225 | env/
226 | venv/
227 | ENV/
228 | env.bak/
229 | venv.bak/
230 |
231 | # Spyder project settings
232 | .spyderproject
233 | .spyproject
234 |
235 | # Rope project settings
236 | .ropeproject
237 |
238 | # mkdocs documentation
239 | /site
240 |
241 | # mypy
242 | .mypy_cache/
243 | .dmypy.json
244 | dmypy.json
245 |
246 | # Pyre type checker
247 | .pyre/
248 |
249 | # pytype static type analyzer
250 | .pytype/
251 |
252 | # Cython debug symbols
253 | cython_debug/
254 |
255 | # PyCharm
256 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
257 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
258 | # and can be added to the global gitignore or merged into this file. For a more nuclear
259 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
260 | #.idea/
261 |
262 | data
263 | redis.conf
--------------------------------------------------------------------------------
/services/ocr-service/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/services/ocr-service/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11
2 |
3 | # Install tesseract OCR
4 | RUN apt-get -o Acquire::Check-Valid-Until=false update && apt-get install -y tesseract-ocr libtesseract-dev
5 |
6 | # Copy application requirements
7 | COPY ./requirements.txt /app/requirements.txt
8 |
9 | # Install Python dependencies
10 | RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
11 |
12 | # Copy application code
13 | COPY ./app /app/app
14 |
15 |
16 |
--------------------------------------------------------------------------------
/services/ocr-service/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/api/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/api/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/api/v1/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/api/v1/endpoints/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/api/v1/endpoints/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/api/v1/endpoints/ocr.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 |
3 | from fastapi import APIRouter, Depends, status, Form
4 | from fastapi.responses import StreamingResponse
5 | from loguru import logger
6 |
7 | from app.domain.schemas.ocr_schema import OCRCreateRequest, OCRCreateResponse, OCRRequest, OCRResponse
8 | from app.domain.schemas.token_schema import TokenDataSchema
9 | from app.services.auth_service import get_current_user
10 | from app.services.ocr_service import OCRService
11 |
12 | ocr_router = APIRouter()
13 |
14 |
15 | @ocr_router.post(
16 | "/process", response_model=OCRCreateResponse, status_code=status.HTTP_201_CREATED
17 | )
18 | async def process_image(
19 | ocr_create: OCRCreateRequest,
20 | current_user: Annotated[TokenDataSchema, Depends(get_current_user)],
21 | ocr_service: Annotated[OCRService, Depends()]
22 | ):
23 | logger.info(f'Processing image {ocr_create.image_id} for user {current_user.id}')
24 | return await ocr_service.process_image(ocr_create, current_user.id)
25 |
26 |
27 | @ocr_router.post(
28 | "/get_ocr_result", response_model=OCRResponse, status_code=status.HTTP_200_OK
29 | )
30 | async def get_ocr_result(
31 | ocr_request: OCRRequest,
32 | current_user: Annotated[TokenDataSchema, Depends(get_current_user)],
33 | ocr_service: Annotated[OCRService, Depends()]
34 | ):
35 | return await ocr_service.get_ocr_result(ocr_request, current_user.id)
36 |
37 |
38 | @ocr_router.get(
39 | "/get_ocr_history", response_model=list[OCRResponse], status_code=status.HTTP_200_OK
40 | )
41 | async def get_ocr_history(
42 | current_user: Annotated[TokenDataSchema, Depends(get_current_user)],
43 | ocr_service: Annotated[OCRService, Depends()]
44 | ):
45 | return await ocr_service.get_ocr_history(current_user.id)
--------------------------------------------------------------------------------
/services/ocr-service/app/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/core/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/core/config.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 | from pathlib import Path
3 | from loguru import logger
4 | from pydantic_settings import BaseSettings, SettingsConfigDict
5 |
6 |
7 | class Settings(BaseSettings):
8 | DATABASE_URL: str
9 | DATABASE_NAME: str
10 | TESSERACT_CMD: str
11 | MEDIA_SERVICE_GRPC: str
12 | IAM_URL: str
13 |
14 | # model_config = SettingsConfigDict(env_file=str(Path(__file__).resolve().parent / ".env"))
15 |
16 |
17 | @lru_cache
18 | @logger.catch
19 | def get_settings():
20 | return Settings()
21 |
--------------------------------------------------------------------------------
/services/ocr-service/app/core/db/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/core/db/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/core/db/database.py:
--------------------------------------------------------------------------------
1 | from motor.motor_asyncio import AsyncIOMotorClient
2 |
3 | from app.core.config import get_settings
4 | from loguru import logger
5 |
6 | try:
7 | config = get_settings()
8 | client = AsyncIOMotorClient(config.DATABASE_URL)
9 | db = client[config.DATABASE_NAME]
10 | logger.info("MongoDB Database connected")
11 |
12 | except Exception as e:
13 | logger.error(f"Error connecting to MongoDB: {e}")
14 | raise e
15 |
16 |
17 | async def get_db():
18 | yield db
19 |
--------------------------------------------------------------------------------
/services/ocr-service/app/domain/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/domain/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/domain/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/domain/models/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/domain/models/object_id_model.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from bson import ObjectId
4 | from pydantic.json_schema import JsonSchemaValue
5 | from pydantic_core import core_schema
6 |
7 |
8 | class ObjectIdPydanticAnnotation:
9 | @classmethod
10 | def validate_object_id(cls, v: Any, handler) -> ObjectId:
11 | if isinstance(v, ObjectId):
12 | return v
13 |
14 | s = handler(v)
15 | if ObjectId.is_valid(s):
16 | return ObjectId(s)
17 | else:
18 | raise ValueError("Invalid ObjectId")
19 |
20 | @classmethod
21 | def __get_pydantic_core_schema__(
22 | cls, source_type, _handler
23 | ) -> core_schema.CoreSchema:
24 | assert source_type is ObjectId
25 | return core_schema.no_info_wrap_validator_function(
26 | cls.validate_object_id,
27 | core_schema.str_schema(),
28 | serialization=core_schema.to_string_ser_schema(),
29 | )
30 |
31 | @classmethod
32 | def __get_pydantic_json_schema__(cls, _core_schema, handler) -> JsonSchemaValue:
33 | return handler(core_schema.str_schema())
34 |
--------------------------------------------------------------------------------
/services/ocr-service/app/domain/models/ocr_model.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 |
4 | from pydantic import BaseModel
5 | from app.domain.models.object_id_model import ObjectIdPydanticAnnotation, ObjectId
6 | from typing import Annotated
7 |
8 |
9 | class OCRModel(BaseModel):
10 | ocr_id: Annotated[ObjectId, ObjectIdPydanticAnnotation] = None
11 | image_id: str
12 | user_id: str
13 | text: str
14 | created_at: datetime = datetime.now()
15 |
16 | class Config:
17 | from_attributes = True
18 | json_encoders = {
19 | ObjectId: str
20 | }
--------------------------------------------------------------------------------
/services/ocr-service/app/domain/schemas/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/domain/schemas/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/domain/schemas/ocr_schema.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from pydantic import BaseModel
4 | from typing import Annotated
5 | from app.domain.models.object_id_model import ObjectIdPydanticAnnotation, ObjectId
6 | from app.domain.models.ocr_model import OCRModel
7 |
8 |
9 | class OCRRequest(BaseModel):
10 | ocr_id: Annotated[ObjectId, ObjectIdPydanticAnnotation]
11 |
12 |
13 | class OCRResponse(OCRModel):
14 | pass
15 |
16 |
17 |
18 | class OCRCreateRequest(BaseModel):
19 | image_id: str
20 |
21 | class OCRCreateResponse(OCRModel):
22 | pass
23 |
24 |
25 |
--------------------------------------------------------------------------------
/services/ocr-service/app/domain/schemas/token_schema.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional
3 | from uuid import UUID
4 |
5 | from pydantic import BaseModel, Field
6 |
7 |
8 | class TokenDataSchema(BaseModel):
9 | id: str
10 | first_name: str
11 | last_name: str
12 | mobile_number: str = Field(..., pattern=r"^\+?[1-9]\d{1,14}$")
13 | created_at: Optional[datetime]
14 | updated_at: Optional[datetime]
15 | is_verified: bool
16 |
17 | class Config:
18 | from_attributes = True
19 |
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/infrastructure/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/clients/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/infrastructure/clients/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/clients/grpc_client.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from typing import Annotated, Tuple
3 | import grpc
4 | from loguru import logger
5 | import tenacity
6 | from fastapi import Depends, HTTPException, status
7 | from aiobreaker import CircuitBreaker
8 | from tenacity import retry, stop_after_attempt, wait_fixed
9 |
10 | from app.core.config import get_settings, Settings
11 | from app.infrastructure.grpc import media_pb2, media_pb2_grpc
12 |
13 | breaker = CircuitBreaker(fail_max=3, timeout_duration=timedelta(seconds=60))
14 |
15 |
16 | class GRPCClient:
17 | def __init__(self, config: Annotated[Settings, Depends(get_settings)]):
18 | self.config = config
19 | self.channel = None
20 | self.stub = None
21 |
22 | async def __initialize(self) -> None:
23 | if self.channel is None or self.stub is None:
24 | options = [('grpc.max_message_length', 100 * 1024 * 1024),
25 | ('grpc.max_receive_message_length', 100 * 1024 * 1024),
26 | ('grpc.max_send_message_length', 100 * 1024 * 1024)]
27 | self.channel = grpc.aio.insecure_channel(self.config.MEDIA_SERVICE_GRPC, options=options)
28 | self.stub = media_pb2_grpc.MediaServiceStub(self.channel)
29 | logger.info(f"GRPC Client connected to {self.config.MEDIA_SERVICE_GRPC}")
30 |
31 | @retry(stop=stop_after_attempt(3), wait=wait_fixed(2))
32 | @breaker
33 | async def download_media(self, media_id: str, media_type: str, user_id: str) -> bytes:
34 | try:
35 | request = media_pb2.MediaRequest(media_id=media_id, media_type=media_type)
36 | metadata = [('user', user_id)]
37 | response = await self.stub.DownloadMedia(request, metadata=metadata)
38 | logger.info(f"Media {media_id} downloaded")
39 | return response.media_data
40 | except grpc.aio.AioRpcError as e:
41 | logger.error(f"Error downloading media {media_id} - {e}")
42 | raise HTTPException(
43 | status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
44 | detail=f"Service unavailable - {e}",
45 | )
46 |
47 | async def request_media(self, media_id: str, media_type: str, user_id: str) -> bytes:
48 | try:
49 | logger.info(f"Requesting media {media_id}")
50 | return await self.download_media(media_id, media_type, user_id)
51 | except tenacity.RetryError:
52 | logger.error(f"GRPC Media Service unavailable")
53 | raise HTTPException(
54 | status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
55 | detail=f"Service unavailable",
56 | )
57 |
58 | async def __aenter__(self):
59 | await self.__initialize()
60 | return self
61 |
62 | async def __aexit__(self, exc_type, exc_val, exc_tb):
63 | await self.channel.close()
64 |
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/clients/http_client.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from typing import Annotated
3 | from loguru import logger
4 | import httpx
5 | import tenacity
6 | from fastapi import Depends, HTTPException, status
7 | from app.core.config import get_settings, Settings
8 | from tenacity import retry, stop_after_attempt, wait_fixed
9 | from aiobreaker import CircuitBreaker
10 |
11 | breaker = CircuitBreaker(fail_max=3, timeout_duration=timedelta(seconds=60))
12 |
13 |
14 | class HTTPClient:
15 | def __init__(self, config: Settings = Depends(get_settings)):
16 | self.config = config
17 | self.http_client = httpx.AsyncClient()
18 |
19 | @retry(stop=stop_after_attempt(3), wait=wait_fixed(2))
20 | @breaker
21 | async def _request(
22 | self, method: str, url: str, headers: dict = None, data: dict = None
23 | ):
24 | async with httpx.AsyncClient(
25 | timeout=10
26 | ) as client: # Using async with to manage lifecycle
27 | try:
28 | response = await client.request(method, url, headers=headers, json=data)
29 | response.raise_for_status()
30 | logger.info(f"HTTP request to {url} successful")
31 | return response
32 | except httpx.RequestError as e:
33 | logger.error(f"Error making request to {url} - {e}")
34 | raise HTTPException(
35 | status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
36 | detail=f"Service unavailable - {e}",
37 | )
38 | except httpx.HTTPStatusError as e:
39 | logger.error(f"Error making request to {url} - {e}")
40 | raise HTTPException(
41 | status_code=e.response.status_code,
42 | detail=e.response.json(),
43 | )
44 |
45 | async def get(self, url: str, headers: dict = None):
46 | try:
47 | logger.info(f"Making GET request to {url}")
48 | return await self._request("GET", url, headers=headers)
49 | except tenacity.RetryError:
50 | logger.error(f"HTTP Service unavailable")
51 | raise HTTPException(
52 | status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
53 | detail=f"Service unavailable",
54 | )
55 |
56 | async def post(self, url: str, headers: dict = None, data: dict = None):
57 | try:
58 | logger.info(f"Making POST request to {url}")
59 | return await self._request("POST", url, headers=headers, data=data)
60 | except tenacity.RetryError:
61 | logger.error(f"HTTP Service unavailable")
62 | raise HTTPException(
63 | status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
64 | detail=f"Service unavailable",
65 | )
66 |
67 | async def __aenter__(self):
68 | return self
69 |
70 | async def __aexit__(self, exc_type, exc_val, exc_tb):
71 | await self.http_client.aclose()
72 |
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/clients/iam_client.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 | from loguru import logger
3 | from fastapi import Depends, HTTPException, status, Request
4 | from app.core.config import get_settings, Settings
5 | from app.domain.schemas.token_schema import TokenDataSchema
6 | from app.infrastructure.clients.http_client import HTTPClient
7 |
8 |
9 | class IAMClient:
10 | def __init__(
11 | self,
12 | http_client: Annotated[HTTPClient, Depends()],
13 | config: Settings = Depends(get_settings),
14 | ):
15 | self.config = config
16 | self.http_client = http_client
17 |
18 | async def validate_token(self, token: str) -> TokenDataSchema:
19 | headers = {"Authorization": f"Bearer {token}"}
20 | async with self.http_client as client:
21 | response = await client.get(
22 | f"{self.config.IAM_URL}/api/v1/users/Me", headers=headers
23 | )
24 | response.raise_for_status()
25 | logger.info(f"Token {token} validated")
26 | return TokenDataSchema(**response.json())
27 |
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/clients/media_client.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from typing import Annotated
3 | import grpc
4 | from fastapi import Depends
5 | from aiobreaker import CircuitBreaker
6 | from tenacity import retry, stop_after_attempt, wait_fixed
7 |
8 | from app.core.config import get_settings, Settings
9 | from app.infrastructure.clients.grpc_client import GRPCClient
10 |
11 |
12 | class MediaClient:
13 | def __init__(self, grpc_client: Annotated[GRPCClient, Depends()]):
14 | self.grpc_client = grpc_client
15 |
16 | async def request_media(self, media_id: str, media_type: str, user_id: str) -> bytes:
17 | async with self.grpc_client as client:
18 | return await client.request_media(media_id, media_type, user_id)
19 |
20 |
21 |
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/grpc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/infrastructure/grpc/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/grpc/media.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package media;
4 |
5 | // Define the service for media operations
6 | service MediaService {
7 | // Method to download media by ID
8 | rpc DownloadMedia (MediaRequest) returns (MediaResponse);
9 | }
10 |
11 | // Message to request media download
12 | message MediaRequest {
13 | string media_id = 1;
14 | string media_type = 2; // e.g., "image", "video", "audio"
15 | }
16 |
17 | // Message to respond with media data
18 | message MediaResponse {
19 | bytes media_data = 1;
20 | string media_type = 2;
21 | }
22 |
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/grpc/media_pb2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by the protocol buffer compiler. DO NOT EDIT!
3 | # source: media.proto
4 | # Protobuf Python Version: 5.26.1
5 | """Generated protocol buffer code."""
6 | from google.protobuf import descriptor as _descriptor
7 | from google.protobuf import descriptor_pool as _descriptor_pool
8 | from google.protobuf import symbol_database as _symbol_database
9 | from google.protobuf.internal import builder as _builder
10 | # @@protoc_insertion_point(imports)
11 |
12 | _sym_db = _symbol_database.Default()
13 |
14 |
15 |
16 |
17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bmedia.proto\x12\x05media\"4\n\x0cMediaRequest\x12\x10\n\x08media_id\x18\x01 \x01(\t\x12\x12\n\nmedia_type\x18\x02 \x01(\t\"7\n\rMediaResponse\x12\x12\n\nmedia_data\x18\x01 \x01(\x0c\x12\x12\n\nmedia_type\x18\x02 \x01(\t2J\n\x0cMediaService\x12:\n\rDownloadMedia\x12\x13.media.MediaRequest\x1a\x14.media.MediaResponseb\x06proto3')
18 |
19 | _globals = globals()
20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'media_pb2', _globals)
22 | if not _descriptor._USE_C_DESCRIPTORS:
23 | DESCRIPTOR._loaded_options = None
24 | _globals['_MEDIAREQUEST']._serialized_start=22
25 | _globals['_MEDIAREQUEST']._serialized_end=74
26 | _globals['_MEDIARESPONSE']._serialized_start=76
27 | _globals['_MEDIARESPONSE']._serialized_end=131
28 | _globals['_MEDIASERVICE']._serialized_start=133
29 | _globals['_MEDIASERVICE']._serialized_end=207
30 | # @@protoc_insertion_point(module_scope)
31 |
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/grpc/media_pb2_grpc.py:
--------------------------------------------------------------------------------
1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2 | """Client and server classes corresponding to protobuf-defined services."""
3 | import grpc
4 | import warnings
5 |
6 | import app.infrastructure.grpc.media_pb2 as media__pb2
7 |
8 | GRPC_GENERATED_VERSION = '1.64.1'
9 | GRPC_VERSION = grpc.__version__
10 | EXPECTED_ERROR_RELEASE = '1.65.0'
11 | SCHEDULED_RELEASE_DATE = 'June 25, 2024'
12 | _version_not_supported = False
13 |
14 | try:
15 | from grpc._utilities import first_version_is_lower
16 | _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
17 | except ImportError:
18 | _version_not_supported = True
19 |
20 | if _version_not_supported:
21 | warnings.warn(
22 | f'The grpc package installed is at version {GRPC_VERSION},'
23 | + f' but the generated code in media_pb2_grpc.py depends on'
24 | + f' grpcio>={GRPC_GENERATED_VERSION}.'
25 | + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
26 | + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
27 | + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},'
28 | + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.',
29 | RuntimeWarning
30 | )
31 |
32 |
33 | class MediaServiceStub(object):
34 | """Define the service for media operations
35 | """
36 |
37 | def __init__(self, channel):
38 | """Constructor.
39 |
40 | Args:
41 | channel: A grpc.Channel.
42 | """
43 | self.DownloadMedia = channel.unary_unary(
44 | '/media.MediaService/DownloadMedia',
45 | request_serializer=media__pb2.MediaRequest.SerializeToString,
46 | response_deserializer=media__pb2.MediaResponse.FromString,
47 | _registered_method=True)
48 |
49 |
50 | class MediaServiceServicer(object):
51 | """Define the service for media operations
52 | """
53 |
54 | def DownloadMedia(self, request, context):
55 | """Method to download media by ID
56 | """
57 | context.set_code(grpc.StatusCode.UNIMPLEMENTED)
58 | context.set_details('Method not implemented!')
59 | raise NotImplementedError('Method not implemented!')
60 |
61 |
62 | def add_MediaServiceServicer_to_server(servicer, server):
63 | rpc_method_handlers = {
64 | 'DownloadMedia': grpc.unary_unary_rpc_method_handler(
65 | servicer.DownloadMedia,
66 | request_deserializer=media__pb2.MediaRequest.FromString,
67 | response_serializer=media__pb2.MediaResponse.SerializeToString,
68 | ),
69 | }
70 | generic_handler = grpc.method_handlers_generic_handler(
71 | 'media.MediaService', rpc_method_handlers)
72 | server.add_generic_rpc_handlers((generic_handler,))
73 | server.add_registered_method_handlers('media.MediaService', rpc_method_handlers)
74 |
75 |
76 | # This class is part of an EXPERIMENTAL API.
77 | class MediaService(object):
78 | """Define the service for media operations
79 | """
80 |
81 | @staticmethod
82 | def DownloadMedia(request,
83 | target,
84 | options=(),
85 | channel_credentials=None,
86 | call_credentials=None,
87 | insecure=False,
88 | compression=None,
89 | wait_for_ready=None,
90 | timeout=None,
91 | metadata=None):
92 | return grpc.experimental.unary_unary(
93 | request,
94 | target,
95 | '/media.MediaService/DownloadMedia',
96 | media__pb2.MediaRequest.SerializeToString,
97 | media__pb2.MediaResponse.FromString,
98 | options,
99 | channel_credentials,
100 | insecure,
101 | call_credentials,
102 | compression,
103 | wait_for_ready,
104 | timeout,
105 | metadata,
106 | _registered_method=True)
107 |
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/repositories/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/infrastructure/repositories/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/infrastructure/repositories/ocr_repository.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated, Dict, Any
2 | from loguru import logger
3 | from bson import ObjectId
4 | from fastapi import Depends
5 | from motor.motor_asyncio import AsyncIOMotorClient
6 |
7 | from app.core.db.database import get_db
8 | from app.domain.models.ocr_model import OCRModel
9 |
10 |
11 | class OCRRepository:
12 | def __init__(self, db: Annotated[AsyncIOMotorClient, Depends(get_db)]):
13 | self.collection = db["ocr"]
14 |
15 | async def create_ocr(self, ocr: OCRModel) -> OCRModel:
16 | result = await self.collection.insert_one(ocr.dict())
17 | ocr.ocr_id = result.inserted_id
18 | logger.info(f'OCR result created for image {ocr.image_id} by user {ocr.user_id}')
19 | return ocr
20 |
21 | async def get_ocr(self, ocr_id: ObjectId) -> OCRModel:
22 | ocr = await self.collection.find_one({"_id": ocr_id})
23 | ocr['ocr_id'] = ocr['_id']
24 | logger.info(f'OCR result retrieved for id {ocr_id}')
25 | return (
26 | OCRModel(**ocr)
27 | if ocr
28 | else None
29 | )
30 |
31 | async def get_ocr_history(self, user_id: str) -> list[Dict[str, Any]]:
32 | logger.info(f'OCR history retrieved for user {user_id}')
33 | return self.collection.find({"user_id": user_id})
34 |
--------------------------------------------------------------------------------
/services/ocr-service/app/logging_service/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aminupy/AIToolsBox/349ce3eda716feaf0cff5436abfa5bed581fa572/services/ocr-service/app/logging_service/__init__.py
--------------------------------------------------------------------------------
/services/ocr-service/app/logging_service/logging_config.py:
--------------------------------------------------------------------------------
1 | from loguru import logger
2 | import sys
3 | import os
4 |
5 |
6 | def configure_logger():
7 | # Ensure log directory exists
8 | os.makedirs("logs", exist_ok=True)
9 |
10 | # Remove default logger
11 | logger.remove()
12 |
13 | # JSON serialization is handled internally by loguru when serialize=True, hence no need for a custom format.
14 | json_logging_format = {
15 | "rotation": "10 MB",
16 | "retention": "10 days",
17 | "serialize": True,
18 | }
19 |
20 | # Add file logging for JSON logs
21 | logger.add("logs/media_service_info.log", level="INFO", **json_logging_format)
22 | logger.add("logs/media_service_error.log", level="ERROR", **json_logging_format)
23 |
24 | # Custom log format for console and stderr
25 | log_format = "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:" \
26 | "{function}:{line} - {message}"
27 |
28 | # Add console logging
29 | logger.add(sys.stdout, level="INFO", format=log_format)
30 |
31 | # Add stderr logging
32 | logger.add(sys.stderr, level="ERROR", backtrace=True, diagnose=True, format=log_format)
33 |
--------------------------------------------------------------------------------
/services/ocr-service/app/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 | from fastapi.middleware.cors import CORSMiddleware
3 | from loguru import logger
4 |
5 | from app.api.v1.endpoints.ocr import ocr_router
6 | from app.logging_service.logging_config import configure_logger
7 |
8 |
9 | configure_logger()
10 |
11 | app = FastAPI()
12 |
13 | origins = ["*"]
14 |
15 | app.add_middleware(
16 | CORSMiddleware,
17 | allow_origins=origins,
18 | allow_credentials=True,
19 | allow_methods=["*"],
20 | allow_headers=["*"],
21 | )
22 |
23 | app.include_router(ocr_router, prefix="/api/v1/ocr", tags=["ocr"])
24 |
25 | logger.info("OCR Service Started")
26 |
27 |
28 | @app.get("/")
29 | async def root():
30 | return {"message": "Hello From OCR Service !"}
31 |
--------------------------------------------------------------------------------
/services/ocr-service/app/services/auth_service.py:
--------------------------------------------------------------------------------
1 | from fastapi import Depends, HTTPException, Request, status
2 | from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3 | from typing import Annotated
4 | from loguru import logger
5 | from app.infrastructure.clients.iam_client import IAMClient
6 | from app.domain.schemas.token_schema import TokenDataSchema
7 | from app.core.config import get_settings
8 |
9 |
10 | config = get_settings()
11 |
12 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="http://iam.localhost/api/v1/users/Token")
13 |
14 |
15 | async def get_current_user(
16 | token: Annotated[str, Depends(oauth2_scheme)],
17 | client: Annotated[IAMClient, Depends()],
18 | ) -> TokenDataSchema:
19 |
20 | if not token:
21 | logger.error("No token provided")
22 | raise HTTPException(
23 | status_code=status.HTTP_401_UNAUTHORIZED,
24 | detail="Unauthorized",
25 | )
26 |
27 | logger.info(f"Validating token {token}")
28 | return await client.validate_token(token)
29 |
--------------------------------------------------------------------------------
/services/ocr-service/app/services/ocr_service.py:
--------------------------------------------------------------------------------
1 | import pytesseract
2 | from PIL import Image
3 | from io import BytesIO
4 | from datetime import datetime
5 | from loguru import logger
6 | from bson import ObjectId
7 | from fastapi import Depends, HTTPException, status
8 | from typing import Annotated, Generator, Callable, Any
9 |
10 | from app.infrastructure.repositories.ocr_repository import OCRRepository
11 | from app.infrastructure.clients.media_client import MediaClient
12 |
13 | from app.domain.models.ocr_model import OCRModel
14 | from app.domain.schemas.ocr_schema import OCRResponse, OCRRequest, OCRCreateRequest, OCRCreateResponse
15 |
16 |
17 | class OCRService:
18 | def __init__(self,
19 | ocr_repository: Annotated[OCRRepository, Depends()],
20 | media_client: Annotated[MediaClient, Depends()]
21 | ):
22 | self.ocr_repository = ocr_repository
23 | self.media_client = media_client
24 |
25 | async def process_image(self, ocr_create: OCRCreateRequest, user_id: str) -> OCRCreateResponse:
26 | image_data = await self.media_client.request_media(ocr_create.image_id, 'image', user_id)
27 | image = Image.open(BytesIO(image_data))
28 | extracted_text = pytesseract.image_to_string(image)
29 |
30 | ocr_result = OCRModel(
31 | image_id=ocr_create.image_id,
32 | user_id=user_id,
33 | text=extracted_text
34 | )
35 | ocr_model = await self.ocr_repository.create_ocr(ocr_result)
36 | logger.info(f'OCR result created for image {ocr_create.image_id} by user {user_id}')
37 | return OCRCreateResponse(**ocr_model.dict())
38 |
39 | async def get_ocr_result(self, ocr_request: OCRRequest, user_id: str) -> OCRResponse:
40 | ocr = await self.ocr_repository.get_ocr(ocr_request.ocr_id)
41 | if not ocr:
42 | logger.error(f'OCR not found for id {ocr_request.ocr_id}')
43 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="OCR not found")
44 | if ocr.user_id != user_id:
45 | logger.error(f'User {user_id} does not have permission to access OCR {ocr_request.ocr_id}')
46 | raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
47 | detail="User does not have permission to access this OCR")
48 | logger.info(f'OCR result retrieved for id {ocr_request.ocr_id}')
49 | return OCRResponse(**ocr.dict())
50 |
51 | async def get_ocr_history(self, user_id: str) -> list[OCRResponse]:
52 | ocrs = await self.ocr_repository.get_ocr_history(user_id)
53 |
54 | async def map_ocr(ocr: dict) -> OCRResponse:
55 | return OCRResponse(
56 | ocr_id=ocr['_id'],
57 | image_id=ocr['image_id'],
58 | user_id=ocr['user_id'],
59 | text=ocr['text'],
60 | created_at=ocr['created_at']
61 | )
62 |
63 | logger.info(f'OCR history retrieved for user {user_id}')
64 | return [await map_ocr(ocr) async for ocr in ocrs]
65 |
--------------------------------------------------------------------------------
/services/ocr-service/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 |
5 | mongo:
6 | image: mongo:latest
7 | container_name: mongo_container
8 | ports:
9 | - "27017:27017"
10 | networks:
11 | - app-network
12 |
13 | ocr:
14 | build:
15 | context: ../services/ocr-service
16 | dockerfile: Dockerfile
17 | container_name: ocr_service
18 | environment:
19 | - DATABASE_URL=mongodb://mongo:27017
20 | - DATABASE_NAME=AIToolsBoxOCRDB
21 | - TESSERACT_CMD=/usr/bin/tesseract
22 | - MEDIA_SERVICE_GRPC=media_service:50051
23 | - IAM_URL=http://iam
24 | networks:
25 | - app-network
26 | labels:
27 | - "traefik.enable=true"
28 | - "traefik.http.routers.ocr.rule=Host(`ocr.localhost`)"
29 | - "traefik.http.services.ocr.loadbalancer.server.port=80"
30 | restart: unless-stopped
31 | depends_on:
32 | - mongo
33 |
34 |
35 | networks:
36 | app-network:
37 | driver: bridge
38 |
39 |
--------------------------------------------------------------------------------
/services/ocr-service/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | pymongo
3 | motor
4 | grpcio
5 | grpcio-tools
6 | pytesseract
7 | opencv-python-headless
8 |
9 | pydantic
10 | pydantic_core
11 |
12 |
13 | python-multipart
14 | pydantic
15 | pydantic-settings
16 | pydantic_core
17 | httpx
18 |
19 | pillow
20 | tenacity
21 | aiobreaker
22 | loguru
--------------------------------------------------------------------------------