├── .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 --------------------------------------------------------------------------------