├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── workflows │ ├── black.yml │ ├── test_build.yml │ └── unit_tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── check_ports.sh ├── container-storage ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg └── 5.jpg ├── docker-compose.test-github.yml ├── docker-compose.yml ├── docker-grafana ├── Dockerfile ├── configuration.env ├── datasources │ └── influx.json └── entrypoint.sh ├── docker-influxdb ├── Dockerfile ├── configuration.env └── entrypoint.sh ├── docker-mosquitto └── Dockerfile ├── docker-python-pypy └── Dockerfile ├── docker-python └── Dockerfile ├── docker-redis ├── Dockerfile └── redis.conf ├── import.sh ├── krakend.json ├── kubernetes ├── fulltext-search-deplyment.yaml ├── fulltext-serarch-service.yaml ├── mongodb-deplyment.yaml ├── mongodb-service.yaml ├── random-demo-deplyment.yaml └── random-demo-service.yaml ├── python ├── __init__.py ├── baesian │ ├── __init__.py │ └── baesian.py ├── bookcollection │ ├── __init__.py │ ├── bookcollection.py │ └── requirements.txt ├── common │ ├── __init__.py │ └── utils.py ├── diagrams_generator.py ├── fastapidemo │ ├── __init__.py │ ├── requirements.txt │ └── users-fastapi.py ├── fulltextsearch │ ├── __init__.py │ └── fulltext_search.py ├── geolocation │ ├── __init__.py │ ├── geolocation_search.py │ └── requirements-geolocation.txt ├── graphql │ ├── requirements.txt │ ├── schema.graphql │ └── users.py ├── mqtt │ ├── __init__.py │ ├── mqtt.py │ └── requirements-mqtt.txt ├── photo │ ├── __init__.py │ ├── photo_process.py │ └── requirements-photo.txt ├── python_app.log ├── random │ ├── __init__.py │ └── random_demo.py ├── requirements-dev.txt ├── requirements.txt ├── tictactoe │ ├── __init__.py │ ├── template │ │ └── tictactoe.html │ └── tictactoe.py └── users │ ├── __init__.py │ ├── caching.py │ └── users.py ├── resources ├── autogenerated.png ├── diagram.jpg ├── diagram.odp └── grafana.png ├── secrets ├── mqtt_pass.txt ├── mqtt_user.txt └── redis_pass.txt ├── stresstest-locusts ├── baesian.py ├── fulltext_search.py ├── geolocation_search.py ├── random_demo.py └── users.py ├── test.txt ├── tests ├── conftest.py ├── requirements.txt ├── resources │ └── test.jpg ├── test_0_users.py ├── test_baesian.py ├── test_bookcollection.py ├── test_fulltext_search.py ├── test_geolocation_search.py ├── test_mqtt.py ├── test_photo.py ├── test_random_demo.py └── utils.py └── wait_until_up.sh /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-python@v2 11 | - name: install black 12 | run: 'pip install black' 13 | - name: Lint 14 | run: 'black --check .' 15 | -------------------------------------------------------------------------------- /.github/workflows/test_build.yml: -------------------------------------------------------------------------------- 1 | name: test-build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 10 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v1 12 | - name: Test 13 | run: docker network create project-network && docker-compose build && docker-compose up --exit-code-from web-test 14 | -------------------------------------------------------------------------------- /.github/workflows/unit_tests.yml: -------------------------------------------------------------------------------- 1 | name: unit_tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 10 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Creating docker network 13 | run: | 14 | echo ---Creating network--- 15 | docker network create project-network 16 | echo ---Created--- 17 | - name: Build docker containers 18 | run: | 19 | echo ---Building and starting up docker--- 20 | git_hash=$(git rev-parse --short "$GITHUB_SHA") 21 | git_branch=${GITHUB_REF#refs/heads/} 22 | echo "Git hash:" 23 | echo $git_hash 24 | docker-compose -f ./docker-compose.yml up -d --build 25 | echo ---Containers up--- 26 | - name: Wait for services to get online 27 | run: | 28 | echo ---Waiting for web-random to get up --- 29 | sleep 50 30 | echo --- UP!--- 31 | - name: Run tests 32 | run: | 33 | echo --- Running test cases --- 34 | docker-compose -f ./docker-compose.test-github.yml up --build --exit-code-from run-unit-tests 35 | echo --- Completed test cases --- 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \.idea 2 | \docker-python/project/base 3 | python_app.log 4 | *.pyc -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 20.8b1 4 | hooks: 5 | - id: black 6 | language_version: python3 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | ### Check open issues 3 | 4 | https://github.com/danionescu0/docker-flask-mongodb-example/issues consider taking on an issue, or adding a new one. 5 | 6 | ### Working on a issue 7 | 8 | * To start developing locally using anaconda: 9 | ````bash 10 | cd docker--mongodb-example 11 | conda create --name dockerflaskmongodbexample python=3.10.0 12 | conda activate dockerflaskmongodbexample 13 | ```` 14 | 15 | * Install proper requirements.txt coresponding to the module you will be working for, example: 16 | ````bash 17 | pip install -r python/fastapidemo/requirements.txt 18 | ```` 19 | 20 | * Add common package in pythonpath (searching for a solution to that) 21 | ````bash 22 | export PYTHONPATH=/your_project_path/python/common/ 23 | ```` 24 | 25 | * If you created a new service or modified some connexions between services, you may need to **generate a new diagram**. 26 | I have created a diagram using this tool: https://diagrams.mingrammer.com 27 | 28 | 29 | To generate a new one install diagrams and graphviz packages inside your conda env: 30 | ````bash 31 | pip install -r python/requirements-dev.txt 32 | ```` 33 | Now you're ready to modify "diagrams_generator.py" inside python folder then run the generator 34 | ````bash 35 | cd python 36 | python diagrams_generator.py 37 | ```` 38 | It will replace the ./resources/autogenerated.png file 39 | 40 | * Run unit tests with pytest and fix the tests if they are failing, if it's new feature create a new test inside /tests folder 41 | 42 | - first run the app locally 43 | ````bash 44 | docker compose up 45 | ```` 46 | - then install tests requirements in a separate anaconda environment 47 | ````bash 48 | cd docker--mongodb-example 49 | conda create --name dockerflaskmongodbexampletestenv python=3.10.0 50 | conda activate dockerflaskmongodbexampletestenv 51 | pip install -r tests/requirements.txt 52 | ```` 53 | 54 | - run all tests 55 | ````bash 56 | pytest -s 57 | ```` 58 | 59 | - to run just a specific test: 60 | ````bash 61 | pytest -q tests/test_0_users.py -s 62 | ```` 63 | 64 | * Work on branch, modify what you need 65 | * Run black () to format the code properly (if not the build will fail on push) 66 | ````bash 67 | cd docker--mongodb-example 68 | pip install -r python/requirements-dev.txt 69 | black . 70 | ```` 71 | 72 | * Push and create a pull request 73 | 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /check_ports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | arr=("$@") 3 | for i in "${arr[@]}"; 4 | do 5 | echo "$i" 6 | done 7 | isopenport() 8 | { 9 | arr=("$@") 10 | for i in "${arr[@]}"; 11 | do 12 | nc -z 127.0.0.1 $i &> /dev/null 13 | result1=$? 14 | if [ "$result1" != 0 ]; then 15 | echo port $i is free 16 | else 17 | echo port $i is used 18 | fi 19 | done 20 | } 21 | ports=(800 801 81 82 83 84 85 86 88 89 1883 27017 8080 3000 6379) 22 | 23 | isopenport "${ports[@]}" 24 | -------------------------------------------------------------------------------- /container-storage/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/container-storage/1.jpg -------------------------------------------------------------------------------- /container-storage/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/container-storage/2.jpg -------------------------------------------------------------------------------- /container-storage/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/container-storage/3.jpg -------------------------------------------------------------------------------- /container-storage/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/container-storage/4.jpg -------------------------------------------------------------------------------- /container-storage/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/container-storage/5.jpg -------------------------------------------------------------------------------- /docker-compose.test-github.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | run-unit-tests: 5 | build: 6 | context: ./docker-python 7 | args: 8 | requirements: /root/flask-mongodb-example/tests/requirements.txt 9 | image: run-unit-tests 10 | entrypoint: pytest /root/flask-mongodb-example -s --maxfail=50 11 | 12 | networks: 13 | default: 14 | external: 15 | name: project-network 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | ## Section for database and external tools 5 | 6 | # Database for grafana 7 | influxdb: 8 | build: docker-influxdb 9 | env_file: ./docker-influxdb/configuration.env 10 | ports: 11 | - '8086:8086' 12 | volumes: 13 | - influxdb_data:/var/lib/influxdb 14 | 15 | # UI for influxdb 16 | chronograf: 17 | image: chronograf:1.9 18 | command: 19 | - --influxdb-url=http://influxdb:8086 20 | ports: 21 | - '8888:8888' 22 | 23 | # Grafana, used here to show sensors graphs 24 | grafana: 25 | build: docker-grafana 26 | env_file: ./docker-grafana/configuration.env 27 | links: 28 | - influxdb 29 | ports: 30 | - '3000:3000' 31 | volumes: 32 | - grafana_data:/var/lib/grafana 33 | 34 | # Genaral purpose database 35 | mongo: 36 | image: mongo:4.4.5-bionic 37 | hostname: mongodb 38 | ports: 39 | - '27017:27017' 40 | environment: 41 | - MONGO_REPLICA_SET_NAME=dan 42 | command: 43 | - --storageEngine 44 | - wiredTiger 45 | - --replSet 46 | - myrepl 47 | healthcheck: 48 | test: test $$(echo "rs.initiate().ok || rs.status().ok" | mongo --quiet) -eq 1 49 | interval: 10s 50 | start_period: 30s 51 | 52 | 53 | # admin credentials are admin:pass, change them 54 | mongo-express: 55 | image: mongo-express:latest 56 | container_name: mongo-express 57 | environment: 58 | ME_CONFIG_MONGODB_SERVER: mongo 59 | ports: 60 | - "8081:8081" 61 | depends_on: 62 | - mongo 63 | 64 | 65 | # Redis server, used here for caching 66 | # uses healthcheck with redis-cli ping 67 | redis: 68 | build: 69 | context: ./docker-redis 70 | ports: 71 | - '6379:6379' 72 | healthcheck: 73 | test: ["CMD", "redis-cli", "ping"] 74 | interval: 10s 75 | timeout: 3s 76 | start_period: 30s 77 | 78 | # Mosquitto MQTT broker 79 | mqtt: 80 | build: 81 | context: ./docker-mosquitto 82 | ports: 83 | - "1883:1883" 84 | 85 | # An api gateway 86 | krakend: 87 | image: devopsfaith/krakend 88 | volumes: 89 | - ${PWD}/krakend.json:/etc/krakend/krakend.json 90 | ports: 91 | - "8080:8080" 92 | depends_on: 93 | - web-random 94 | - web-users 95 | 96 | 97 | # Section for our containers 98 | 99 | # generates random numbers and lists them 100 | # uses healthcheck by making a curl request to a GET endpoint 101 | web-random: 102 | build: 103 | context: ./docker-python 104 | args: 105 | requirements: /root/flask-mongodb-example/python/requirements.txt 106 | ports: 107 | - "800:5000" 108 | entrypoint: python /root/flask-mongodb-example/python/random/random_demo.py 109 | depends_on: 110 | - mongo 111 | healthcheck: 112 | test: curl --silent --show-error --fail "http://localhost:5000/random?lower=0&upper10" 113 | interval: 10s 114 | start_period: 30s 115 | 116 | # generates random numbers and lists them, it's faster thatn web-random because it's using pypy accelerator 117 | web-random-pypy: 118 | build: 119 | context: ./docker-python-pypy 120 | args: 121 | requirements: /root/flask-mongodb-example/python/requirements.txt 122 | ports: 123 | - "801:5000" 124 | entrypoint: pypy /root/flask-mongodb-example/python/random/random_demo.py 125 | depends_on: 126 | - mongo 127 | 128 | # Create, read, update and delete operations over a user collection 129 | web-users: 130 | build: 131 | context: ./docker-python 132 | args: 133 | requirements: /root/flask-mongodb-example/python/requirements.txt 134 | ports: 135 | - "81:5000" 136 | entrypoint: python /root/flask-mongodb-example/python/users/users.py 137 | environment: 138 | - REDIS_PASSWORD=/run/secrets/redis_password 139 | - PYTHONUNBUFFERED=1 140 | secrets: 141 | - redis_password 142 | depends_on: 143 | - mongo 144 | - redis 145 | 146 | # Uses an MQTT server (Mosquitto) to allow to publish sensor updates over MQTT 147 | background-mqtt: 148 | build: 149 | context: ./docker-python 150 | args: 151 | requirements: /root/flask-mongodb-example/python/mqtt/requirements-mqtt.txt 152 | entrypoint: python /root/flask-mongodb-example/python/mqtt/mqtt.py 153 | environment: 154 | - MQTT_USER=/run/secrets/mqtt_user 155 | - MQTT_PASSWORD=/run/secrets/mqtt_password 156 | - PYTHONUNBUFFERED=1 157 | secrets: 158 | - mqtt_user 159 | - mqtt_password 160 | depends_on: 161 | - mongo 162 | - mqtt 163 | - influxdb 164 | tty: true 165 | 166 | # Fulltext search engine backed by fulltext MongoDb index 167 | web-fulltext-search: 168 | build: 169 | context: ./docker-python 170 | args: 171 | requirements: /root/flask-mongodb-example/python/requirements.txt 172 | ports: 173 | - "82:5000" 174 | entrypoint: python /root/flask-mongodb-example/python/fulltextsearch/fulltext_search.py 175 | depends_on: 176 | - mongo 177 | 178 | # Geospacial search service that supports adding places, and quering the placing by coordonates and distance 179 | web-geolocation-search: 180 | build: 181 | context: ./docker-python 182 | args: 183 | requirements: /root/flask-mongodb-example/python/geolocation/requirements-geolocation.txt 184 | ports: 185 | - "83:5000" 186 | entrypoint: python /root/flask-mongodb-example/python/geolocation/geolocation_search.py 187 | depends_on: 188 | - mongo 189 | 190 | # Baesian average demo (https://en.wikipedia.org/wiki/Bayesian_average) 191 | web-baesian: 192 | build: 193 | context: ./docker-python 194 | args: 195 | requirements: /root/flask-mongodb-example/python/requirements.txt 196 | ports: 197 | - "84:5000" 198 | entrypoint: python /root/flask-mongodb-example/python/baesian/baesian.py 199 | depends_on: 200 | - mongo 201 | 202 | # A demo of working with file photo uploads, hash searching and using docker volumes 203 | web-photo-process: 204 | build: 205 | context: ./docker-python 206 | args: 207 | requirements: /root/flask-mongodb-example/python/photo/requirements-photo.txt 208 | ports: 209 | - "85:5000" 210 | entrypoint: python /root/flask-mongodb-example/python/photo/photo_process.py 211 | volumes: 212 | - ./container-storage:/root/storage 213 | depends_on: 214 | - mongo 215 | 216 | # A virtual book library 217 | web-book-collection: 218 | build: 219 | context: ./docker-python 220 | args: 221 | requirements: /root/flask-mongodb-example/python/bookcollection/requirements.txt 222 | environment: 223 | - PYTHONUNBUFFERED=1 224 | ports: 225 | - "86:5000" 226 | entrypoint: python /root/flask-mongodb-example/python/bookcollection/bookcollection.py 227 | depends_on: 228 | - mongo 229 | - web-users 230 | 231 | # Wame functionality as web-users but build with fastapi 232 | # runs with gunicorn on two processor cores [-w 2] 233 | web-users-fast-api: 234 | build: 235 | context: ./docker-python 236 | args: 237 | requirements: /root/flask-mongodb-example/python/fastapidemo/requirements.txt 238 | ports: 239 | - "88:5000" # port 87 is restricted in browsers 240 | entrypoint: gunicorn -w 2 -k uvicorn.workers.UvicornH11Worker --chdir /root/flask-mongodb-example/python/fastapidemo --bind 0.0.0.0:5000 --log-level debug users-fastapi:app 241 | depends_on: 242 | - mongo 243 | 244 | # A two player tic tac toe game written in flask using flask_session. It has a simple UI 245 | web-tictactoe: 246 | build: 247 | context: ./docker-python 248 | args: 249 | requirements: /root/flask-mongodb-example/python/requirements.txt 250 | ports: 251 | - "89:5000" 252 | entrypoint: python /root/flask-mongodb-example/python/tictactoe/tictactoe.py 253 | 254 | 255 | # GraphQl implementation of CRUD users 256 | web-users-graphql: 257 | build: 258 | context: ./docker-python 259 | args: 260 | requirements: /root/flask-mongodb-example/python/graphql/requirements.txt 261 | ports: 262 | - "90:5000" 263 | entrypoint: python /root/flask-mongodb-example/python/graphql/users.py 264 | 265 | # Used to test build of services 266 | web-test: 267 | image: alpine 268 | depends_on: 269 | - web-random 270 | - web-random-pypy 271 | - web-users 272 | - background-mqtt 273 | - web-fulltext-search 274 | - web-geolocation-search 275 | - web-baesian 276 | - web-photo-process 277 | - web-book-collection 278 | - web-users-fast-api 279 | - web-users-graphql 280 | - influxdb 281 | - chronograf 282 | - grafana 283 | - mongo 284 | - mqtt 285 | - krakend 286 | - web-tictactoe 287 | - redis 288 | 289 | volumes: 290 | grafana_data: {} 291 | influxdb_data: {} 292 | 293 | secrets: 294 | mqtt_user: 295 | file: ./secrets/mqtt_user.txt 296 | mqtt_password: 297 | file: ./secrets/mqtt_pass.txt 298 | redis_password: 299 | file: ./secrets/redis_pass.txt 300 | 301 | networks: 302 | default: 303 | name: project-network 304 | external: true 305 | 306 | -------------------------------------------------------------------------------- /docker-grafana/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM grafana/grafana:8.1.5-ubuntu 2 | 3 | USER root 4 | RUN apt-get update && apt-get install -y curl gettext-base && rm -rf /var/lib/apt/lists/* 5 | 6 | WORKDIR /etc/grafana 7 | COPY datasources ./datasources 8 | 9 | WORKDIR /app 10 | COPY entrypoint.sh ./ 11 | RUN chmod u+x entrypoint.sh 12 | 13 | ENTRYPOINT ["/app/entrypoint.sh"] 14 | -------------------------------------------------------------------------------- /docker-grafana/configuration.env: -------------------------------------------------------------------------------- 1 | GF_SECURITY_ADMIN_USER=admin 2 | GF_SECURITY_ADMIN_PASSWORD=admin 3 | GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-worldmap-panel,grafana-piechart-panel -------------------------------------------------------------------------------- /docker-grafana/datasources/influx.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "InfluxDB", 3 | "type": "influxdb", 4 | "url": "http://influxdb:8086", 5 | "access": "proxy", 6 | "user": "$INFLUX_USER", 7 | "password": "$INFLUX_PASSWORD", 8 | "database": "$INFLUX_DB", 9 | "basicAuth": false 10 | } 11 | -------------------------------------------------------------------------------- /docker-grafana/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | url="http://$GF_SECURITY_ADMIN_USER:$GF_SECURITY_ADMIN_PASSWORD@localhost:3000" 4 | 5 | post() { 6 | curl -s -X POST -d "$1" \ 7 | -H 'Content-Type: application/json;charset=UTF-8' \ 8 | "$url$2" 2> /dev/null 9 | } 10 | 11 | if [ ! -f "/var/lib/grafana/.init" ]; then 12 | exec /run.sh $@ & 13 | 14 | until curl -s "$url/api/datasources" 2> /dev/null; do 15 | sleep 1 16 | done 17 | 18 | for datasource in /etc/grafana/datasources/*; do 19 | post "$(envsubst < $datasource)" "/api/datasources" 20 | done 21 | post '{"meta":{"type":"db","canSave":true,"canEdit":true,"canAdmin":true,"canStar":true,"slug":"sensormetrics","expires":"0001-01-01T00:00:00Z","created":"2019-12-25T17:58:23Z","updated":"2019-12-25T18:04:59Z","updatedBy":"admin","createdBy":"admin","version":6,"hasAcl":false,"isFolder":false,"folderId":0,"folderTitle":"General","folderUrl":"","provisioned":false},"dashboard":{"annotations":{"list":[{"builtIn":1,"datasource":"-- Grafana --","enable":true,"hide":true,"iconColor":"rgba(0, 211, 255, 1)","name":"Annotations & Alerts","type":"dashboard"}]},"editable":true,"gnetId":null,"graphTooltip":0,"iteration":1577296839762,"links":[],"panels":[{"aliasColors":{},"bars":false,"dashLength":10,"dashes":false,"datasource":"InfluxDB","fill":1,"gridPos":{"h":9,"w":12,"x":0,"y":0},"id":2,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":false},"lines":true,"linewidth":1,"links":[],"nullPointMode":"null","percentage":false,"pointradius":5,"points":false,"renderer":"flot","seriesOverrides":[],"spaceLength":10,"stack":false,"steppedLine":false,"targets":[{"groupBy":[{"params":["$__interval"],"type":"time"},{"params":["null"],"type":"fill"}],"measurement":"temperature","orderByTime":"ASC","policy":"default","query":"SELECT \"value\" FROM \"$sensortype\" WHERE $timeFilter ","rawQuery":true,"refId":"A","resultFormat":"time_series","select":[[{"params":["value"],"type":"field"}]],"tags":[]}],"thresholds":[],"timeFrom":null,"timeRegions":[],"timeShift":null,"title":"Sensors","tooltip":{"shared":true,"sort":0,"value_type":"individual"},"type":"graph","xaxis":{"buckets":null,"mode":"time","name":null,"show":true,"values":[]},"yaxes":[{"format":"short","label":null,"logBase":1,"max":null,"min":null,"show":true},{"format":"short","label":null,"logBase":1,"max":null,"min":null,"show":true}],"yaxis":{"align":false,"alignLevel":null}}],"schemaVersion":16,"style":"dark","tags":[],"templating":{"list":[{"current":{"text":"temperature","value":"temperature"},"hide":0,"label":null,"name":"sensortype","options":[{"text":"humidity","value":"humidity"}],"query":"humidity","skipUrlSync":false,"type":"textbox"}]},"time":{"from":"now-6h","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"","title":"SensorMetrics","version":6}}' "/api/dashboards/db" 22 | 23 | touch "/var/lib/grafana/.init" 24 | 25 | kill $(pgrep grafana) 26 | fi 27 | 28 | 29 | exec /run.sh $@ -------------------------------------------------------------------------------- /docker-influxdb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM influxdb:1.8 2 | 3 | WORKDIR /app 4 | COPY entrypoint.sh ./ 5 | RUN chmod u+x entrypoint.sh 6 | 7 | ENTRYPOINT ["/app/entrypoint.sh"] 8 | -------------------------------------------------------------------------------- /docker-influxdb/configuration.env: -------------------------------------------------------------------------------- 1 | INFLUX_USER=admin 2 | INFLUX_PASSWORD=admin 3 | INFLUX_DB=influx -------------------------------------------------------------------------------- /docker-influxdb/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ ! -f "/var/lib/influxdb/.init" ]; then 4 | exec influxd $@ & 5 | 6 | until wget -q "http://localhost:8086/ping" 2> /dev/null; do 7 | sleep 1 8 | done 9 | 10 | influx -host=localhost -port=8086 -execute="CREATE USER ${INFLUX_USER} WITH PASSWORD '${INFLUX_PASSWORD}' WITH ALL PRIVILEGES" 11 | influx -host=localhost -port=8086 -execute="CREATE DATABASE ${INFLUX_DB}" 12 | 13 | touch "/var/lib/influxdb/.init" 14 | 15 | kill -s TERM %1 16 | fi 17 | 18 | exec influxd $@ 19 | -------------------------------------------------------------------------------- /docker-mosquitto/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-mosquitto:1.6.8 2 | 3 | RUN echo 'password_file /mosquitto/config/pwfile' >> /mosquitto/config/mosquitto.conf 4 | RUN echo 'allow_anonymous false' >> /mosquitto/config/mosquitto.conf 5 | RUN touch /mosquitto/config/pwfile 6 | RUN mosquitto_passwd -b /mosquitto/config/pwfile some_user some_pass 7 | 8 | EXPOSE 1883 9 | ENTRYPOINT ["/docker-entrypoint.sh"] 10 | CMD ["/usr/sbin/mosquitto", "-c", "/mosquitto/config/mosquitto.conf"] -------------------------------------------------------------------------------- /docker-python-pypy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pypy:3-slim 2 | ARG requirements 3 | RUN apt-get update 4 | RUN apt install git -y 5 | WORKDIR /root 6 | RUN git clone https://github.com/danionescu0/docker-flask-mongodb-example.git flask-mongodb-example 7 | WORKDIR /root/flask-mongodb-example/python 8 | ENV PYTHONPATH "/root/flask-mongodb-example/python/common" 9 | RUN pip install -qr $requirements 10 | EXPOSE 5000 11 | -------------------------------------------------------------------------------- /docker-python/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-buster as web-base 2 | 3 | #[DEVELOPMENT ONLY] run in shell from root location 4 | # mkdir docker-python/base/project; rsync -av --progress ./ docker-python/base/project --exclude docker-python 5 | 6 | # this is the python base image that contains olny git and the downloaded project 7 | RUN apt-get update 8 | RUN apt install git -y 9 | 10 | WORKDIR /root 11 | 12 | # 1. [DEVELOPMENT ONLY] uncomment the following 2 lines (will copy files from local instead from github) 13 | # RUN mkdir flask-mongodb-example 14 | # COPY ./project ./flask-mongodb-example/ 15 | 16 | # 2. [DEVELOPMENT ONLY] comment the line with git clone 17 | RUN git clone https://github.com/danionescu0/docker-flask-mongodb-example.git flask-mongodb-example 18 | 19 | FROM python:3.10-buster 20 | COPY --from=web-base /root/flask-mongodb-example /root/flask-mongodb-example 21 | 22 | ARG requirements 23 | 24 | WORKDIR /root/flask-mongodb-example/python 25 | ENV PYTHONPATH "/root/flask-mongodb-example/python/common" 26 | RUN pip install -qr $requirements 27 | 28 | EXPOSE 5000 -------------------------------------------------------------------------------- /docker-redis/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM redis:6 2 | 3 | COPY redis.conf /usr/local/etc/redis/redis.conf 4 | CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ] -------------------------------------------------------------------------------- /docker-redis/redis.conf: -------------------------------------------------------------------------------- 1 | requirepass someredispassword -------------------------------------------------------------------------------- /import.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Runing import script" 4 | # random_demo 5 | curl -X PUT "http://localhost:800/random" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "lower=10&upper=100" 6 | curl -X PUT "http://localhost:800/random" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "lower=10&upper=100" 7 | curl -X PUT "http://localhost:800/random" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "lower=10&upper=100" 8 | curl -X PUT "http://localhost:800/random" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "lower=10&upper=100" 9 | curl -X PUT "http://localhost:800/random" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "lower=10&upper=100" 10 | 11 | # users crud 12 | curl -X PUT "http://localhost:81/users/1" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "email=daniel%40gmail.com&name=Daniel" 13 | curl -X PUT "http://localhost:81/users/2" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "email=gimy%40gmail.com&name=Gimy" 14 | curl -X PUT "http://localhost:81/users/3" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "email=tor%40gmail.com&name=Tor" 15 | 16 | # fulltext search 17 | curl -X PUT "http://localhost:82/fulltext" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "expression=Who%20has%20many%20apples%3F" 18 | curl -X PUT "http://localhost:82/fulltext" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "expression=The%20apple%20tree%20grew%20in%20the%20park" 19 | curl -X PUT "http://localhost:82/fulltext" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "expression=Some%20apples%20are%20green%20some%20are%20yellow" 20 | curl -X PUT "http://localhost:82/fulltext" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "expression=How%20many%20trees%20are%20there%20in%20this%20forest%3F" 21 | 22 | # geolocation-search 23 | curl -X POST "http://localhost:83/location" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "name=Bucharest&lat=44.2218653&lng=26.1753655" 24 | curl -X POST "http://localhost:83/location" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "name=Sofia&lat=42.6954108&lng=23.2539076" 25 | curl -X POST "http://localhost:83/location" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "name=Belgrade&lat=44.8152239&lng=20.3525579" 26 | curl -X POST "http://localhost:83/location" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "name=Minsk&lat=53.8846196&lng=27.5233296" 27 | 28 | # baesian 29 | curl -X POST "http://localhost:84/item/1" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "name=Dan" 30 | curl -X POST "http://localhost:84/item/2" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "name=John" 31 | curl -X POST "http://localhost:84/item/3" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "name=Cicero" 32 | 33 | curl -X PUT "http://localhost:84/item/vote/1" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "mark=6&userid=1" 34 | curl -X PUT "http://localhost:84/item/vote/2" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "mark=8&userid=1" 35 | curl -X PUT "http://localhost:84/item/vote/3" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "mark=9&userid=1" 36 | curl -X PUT "http://localhost:84/item/vote/1" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "mark=8&userid=2" 37 | curl -X PUT "http://localhost:84/item/vote/2" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "mark=8&userid=2" 38 | curl -X PUT "http://localhost:84/item/vote/3" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "mark=4&userid=3" 39 | curl -X PUT "http://localhost:84/item/vote/1" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "mark=4&userid=3" 40 | curl -X PUT "http://localhost:84/item/vote/2" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "mark=9&userid=3" 41 | 42 | # bookcollection 43 | curl -X PUT "http://localhost:86/book/978-0735211292" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"isbn\": \"978-0735211292\", \"name\": \"Atomic Habits: An Easy & Proven Way to Build Good Habits & Break Bad Ones\", \"author\": \"James Clear\", \"publisher\": \"Avery; Illustrated Edition (October 16, 2018)\", \"nr_available\": 5}" 44 | curl -X PUT "http://localhost:86/book/978-0525538585" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"isbn\": \"978-0525538585\", \"name\": \"Stillness Is the Key\", \"author\": \"Ryan Holiday\", \"publisher\": \"Portfolio (October 1, 2019)\", \"nr_available\": 3}" 45 | curl -X PUT "http://localhost:86/borrow/1" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"id\": \"string\", \"userid\": 1, \"isbn\": \"978-0735211292\", \"borrow_date\": \"2020-10-13T13:04:37.644Z\", \"max_return_date\": \"2020-11-13T13:04:37.644Z\"}" -------------------------------------------------------------------------------- /krakend.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "extra_config": { 4 | "github_com/devopsfaith/krakend-gologging": { 5 | "level": "INFO", 6 | "prefix": "[KRAKEND]", 7 | "syslog": false, 8 | "stdout": true, 9 | "format": "default" 10 | } 11 | }, 12 | "timeout": "3000ms", 13 | "cache_ttl": "300s", 14 | "output_encoding": "json", 15 | "name": "docker-flask-mongodb-example", 16 | "endpoints": [ 17 | { 18 | "endpoint": "/random", 19 | "method": "GET", 20 | "extra_config": {}, 21 | "output_encoding": "string", 22 | "concurrent_calls": 1, 23 | "querystring_params": [ 24 | "lower", 25 | "upper" 26 | ], 27 | "backend": [ 28 | { 29 | "url_pattern": "/random", 30 | "encoding": "string", 31 | "sd": "static", 32 | "extra_config": {}, 33 | "method": "GET", 34 | "host": [ 35 | "http://web-random:5000" 36 | ], 37 | "disable_host_sanitize": false 38 | } 39 | ] 40 | }, 41 | { 42 | "endpoint": "/users", 43 | "method": "GET", 44 | "extra_config": {}, 45 | "output_encoding": "string", 46 | "concurrent_calls": 1, 47 | "querystring_params": [ 48 | "limit", 49 | "offset" 50 | ], 51 | "backend": [ 52 | { 53 | "url_pattern": "/users", 54 | "encoding": "string", 55 | "sd": "static", 56 | "extra_config": {}, 57 | "method": "GET", 58 | "host": [ 59 | "http://web-users:5000" 60 | ], 61 | "disable_host_sanitize": false 62 | } 63 | ] 64 | }, 65 | { 66 | "endpoint": "/random", 67 | "method": "PUT", 68 | "output_encoding": "string", 69 | "extra_config": {}, 70 | "querystring_params": [ 71 | "lower", 72 | "upper" 73 | ], 74 | "backend": [ 75 | { 76 | "url_pattern": "/random", 77 | "encoding": "string", 78 | "sd": "static", 79 | "method": "PUT", 80 | "extra_config": {}, 81 | "host": [ 82 | "http://web-random:5000" 83 | ], 84 | "disable_host_sanitize": false 85 | } 86 | ] 87 | } 88 | ], 89 | "disable_rest": false 90 | } -------------------------------------------------------------------------------- /kubernetes/fulltext-search-deplyment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: fulltext-search-deployment 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: fulltext-search 10 | replicas: 1 11 | template: 12 | metadata: 13 | labels: 14 | app: fulltext-search 15 | spec: 16 | containers: 17 | - name: fulltext-search-conainer 18 | image: danionescu/docker-flask-mongodb-example-python-default:latest 19 | command: ["python", "/root/flask-mongodb-example/fulltext_search.py", "mongodb-service.default.svc.cluster.local"] 20 | imagePullPolicy: Always 21 | ports: 22 | - containerPort: 5000 -------------------------------------------------------------------------------- /kubernetes/fulltext-serarch-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: fulltext-search-service 5 | spec: 6 | selector: 7 | app: fulltext-search 8 | ports: 9 | - protocol: "TCP" 10 | port: 82 11 | targetPort: 5000 12 | type: LoadBalancer -------------------------------------------------------------------------------- /kubernetes/mongodb-deplyment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mongodb-deployment 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: mongodb 10 | replicas: 1 11 | template: 12 | metadata: 13 | labels: 14 | app: mongodb 15 | spec: 16 | containers: 17 | - name: mongodb-conainer 18 | image: mongo:4.2-bionic 19 | imagePullPolicy: Always 20 | ports: 21 | - containerPort: 27017 -------------------------------------------------------------------------------- /kubernetes/mongodb-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: mongodb-service 5 | spec: 6 | selector: 7 | app: mongodb 8 | ports: 9 | - protocol: "TCP" 10 | port: 27017 11 | targetPort: 27017 12 | type: LoadBalancer -------------------------------------------------------------------------------- /kubernetes/random-demo-deplyment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: random-demo-deployment 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: random-demo 10 | replicas: 1 11 | template: 12 | metadata: 13 | labels: 14 | app: random-demo 15 | spec: 16 | containers: 17 | - name: random-demo-conainer 18 | image: danionescu/docker-flask-mongodb-example-python-default:latest 19 | command: ["python", "/root/flask-mongodb-example/random_demo.py", "mongodb-service.default.svc.cluster.local"] 20 | imagePullPolicy: Always 21 | ports: 22 | - containerPort: 5000 -------------------------------------------------------------------------------- /kubernetes/random-demo-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: random-demo-service 5 | spec: 6 | selector: 7 | app: random-demo 8 | ports: 9 | - protocol: "TCP" 10 | port: 800 11 | targetPort: 5000 12 | type: LoadBalancer -------------------------------------------------------------------------------- /python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/__init__.py -------------------------------------------------------------------------------- /python/baesian/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/baesian/__init__.py -------------------------------------------------------------------------------- /python/baesian/baesian.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask import Flask, request, Response 4 | from pymongo import MongoClient 5 | from flasgger import Swagger 6 | 7 | 8 | app = Flask(__name__) 9 | swagger = Swagger(app) 10 | baesian = MongoClient("mongodb", 27017).demo.baesian 11 | 12 | 13 | @app.route("/item/", methods=["POST"]) 14 | def upsert_item(itemid): 15 | """Create item 16 | --- 17 | parameters: 18 | - name: itemid 19 | in: path 20 | type: string 21 | required: true 22 | - name: name 23 | in: formData 24 | type: string 25 | required: false 26 | responses: 27 | 200: 28 | description: Item added 29 | """ 30 | request_params = request.form 31 | if "name" not in request_params: 32 | return Response( 33 | "Name not present in parameters!", status=404, mimetype="application/json" 34 | ) 35 | baesian.update_one( 36 | {"_id": itemid}, 37 | {"$set": {"name": request_params["name"], "nr_votes": 0}}, 38 | upsert=True, 39 | ) 40 | 41 | return Response( 42 | json.dumps({"_id": itemid, "name": request_params["name"]}), 43 | status=200, 44 | mimetype="application/json", 45 | ) 46 | 47 | 48 | @app.route("/item/vote/", methods=["PUT"]) 49 | def add_vote(itemid): 50 | """Vote an item 51 | --- 52 | parameters: 53 | - name: itemid 54 | in: path 55 | type: string 56 | required: true 57 | - name: mark 58 | in: formData 59 | type: integer 60 | required: false 61 | - name: userid 62 | in: formData 63 | type: integer 64 | required: false 65 | responses: 66 | 200: 67 | description: Update succeded 68 | """ 69 | request_params = request.form 70 | if "mark" not in request_params or "userid" not in request_params: 71 | return Response( 72 | "Mark and userid must be present in form data!", 73 | status=404, 74 | mimetype="application/json", 75 | ) 76 | mark = int(request_params["mark"]) 77 | if mark not in range(0, 10): 78 | return Response( 79 | "Mark must be in range (0, 10) !", status=500, mimetype="application/json" 80 | ) 81 | userid = int(request_params["userid"]) 82 | update_items_data = { 83 | "$push": {"marks": {"userid": userid, "mark": mark}}, 84 | "$inc": {"nr_votes": 1, "sum_votes": mark}, 85 | } 86 | baesian.update_one({"_id": itemid}, update_items_data) 87 | return Response("", status=200, mimetype="application/json") 88 | 89 | 90 | @app.route("/item/", methods=["GET"]) 91 | def get_item(itemid): 92 | """Item details 93 | --- 94 | parameters: 95 | - name: itemid 96 | in: path 97 | type: string 98 | required: true 99 | definitions: 100 | Item: 101 | type: object 102 | properties: 103 | _id: 104 | type: integer 105 | name: 106 | type: string 107 | marks: 108 | type: array 109 | items: 110 | type: integer 111 | sum_votes: 112 | type: integer 113 | nr_votes: 114 | type: integer 115 | baesian_average: 116 | type: float 117 | responses: 118 | 200: 119 | description: Item model 120 | schema: 121 | $ref: '#/definitions/Item' 122 | 404: 123 | description: Item not found 124 | """ 125 | item_data = baesian.find_one({"_id": itemid}) 126 | if None == item_data: 127 | return Response("", status=404, mimetype="application/json") 128 | if "marks" not in item_data: 129 | item_data["nr_votes"] = 0 130 | item_data["sum_votes"] = 0 131 | item_data["baesian_average"] = 0 132 | return Response(json.dumps(item_data), status=200, mimetype="application/json") 133 | 134 | average_nr_votes_pipeline = [ 135 | {"$group": {"_id": "avg_nr_votes", "avg_nr_votes": {"$avg": "$nr_votes"}}}, 136 | ] 137 | average_nr_votes = list(baesian.aggregate(average_nr_votes_pipeline))[0][ 138 | "avg_nr_votes" 139 | ] 140 | average_rating = [ 141 | { 142 | "$group": { 143 | "_id": "avg", 144 | "avg": {"$sum": "$sum_votes"}, 145 | "count": {"$sum": "$nr_votes"}, 146 | } 147 | }, 148 | {"$project": {"result": {"$divide": ["$avg", "$count"]}}}, 149 | ] 150 | average_rating = list(baesian.aggregate(average_rating))[0]["result"] 151 | item_nr_votes = item_data["nr_votes"] 152 | item_average_rating = item_data["sum_votes"] / item_data["nr_votes"] 153 | baesian_average = round( 154 | ((average_nr_votes * average_rating) + (item_nr_votes * item_average_rating)) 155 | / (average_nr_votes + item_nr_votes), 156 | 3, 157 | ) 158 | item_data["baesian_average"] = baesian_average 159 | return Response(json.dumps(item_data), status=200, mimetype="application/json") 160 | 161 | 162 | @app.route("/items", methods=["GET"]) 163 | def get_items(): 164 | """All items with pagination without averages 165 | --- 166 | parameters: 167 | - name: limit 168 | in: query 169 | type: integer 170 | required: false 171 | - name: offset 172 | in: query 173 | type: integer 174 | required: false 175 | definitions: 176 | Items: 177 | type: array 178 | items: 179 | properties: 180 | _id: 181 | type: integer 182 | name: 183 | type: string 184 | marks: 185 | type: array 186 | items: 187 | type: integer 188 | responses: 189 | 200: 190 | description: List of items 191 | schema: 192 | $ref: '#/definitions/Items' 193 | """ 194 | request_args = request.args 195 | limit = int(request_args.get("limit")) if "limit" in request_args else 10 196 | offset = int(request_args.get("offset")) if "offset" in request_args else 0 197 | item_list = baesian.find().limit(limit).skip(offset) 198 | if None == baesian: 199 | return Response(json.dumps([]), status=200, mimetype="application/json") 200 | extracted = [ 201 | { 202 | "_id": d["_id"], 203 | "name": d["name"], 204 | "marks": d["marks"] if "marks" in d else [], 205 | } 206 | for d in item_list 207 | ] 208 | return Response(json.dumps(extracted), status=200, mimetype="application/json") 209 | 210 | 211 | @app.route("/item/", methods=["DELETE"]) 212 | def delete_item(itemid): 213 | """Delete operation for a item 214 | --- 215 | parameters: 216 | - name: itemid 217 | in: path 218 | type: string 219 | required: true 220 | responses: 221 | 200: 222 | description: Item deleted 223 | """ 224 | baesian.delete_one({"_id": itemid}) 225 | return Response("", status=200, mimetype="application/json") 226 | 227 | 228 | if __name__ == "__main__": 229 | # starts the app in debug mode, bind on all ip's and on port 5000 230 | app.run(debug=True, host="0.0.0.0", port=5000) 231 | -------------------------------------------------------------------------------- /python/bookcollection/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/bookcollection/__init__.py -------------------------------------------------------------------------------- /python/bookcollection/bookcollection.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import requests 4 | import dateutil.parser 5 | 6 | from flask import Flask, request, Response 7 | from flask_restx import Api, Resource, fields, reqparse 8 | from pymongo import MongoClient, errors 9 | 10 | from utils import get_logger 11 | 12 | 13 | if len(sys.argv) == 3: 14 | _, users_host, mongo_host = sys.argv 15 | mongo_client = MongoClient(mongo_host, 27017) 16 | else: 17 | users_host = "http://web-users:5000" 18 | mongo_client = MongoClient("mongodb", 27017) 19 | bookcollection = mongo_client.demo.bookcollection 20 | borrowcollection = mongo_client.demo.borrowcollection 21 | logger = get_logger() 22 | 23 | 24 | app = Flask(__name__) 25 | api = Api( 26 | app=app, 27 | title="Book collection", 28 | description="Simulates a book library with users and book borrwing", 29 | ) 30 | book_api = api.namespace("book", description="Book api") 31 | borrow_api = api.namespace("borrow", description="Boorrow, returing api") 32 | 33 | book_model = book_api.model( 34 | "Book", 35 | { 36 | "isbn": fields.String(description="ISBN", required=True), 37 | "name": fields.String(description="Name of the book", required=True), 38 | "author": fields.String(description="Book author", required=True), 39 | "publisher": fields.String(description="Book publisher", required=True), 40 | "nr_available": fields.Integer( 41 | min=0, description="Nr books available for lend", required=True 42 | ), 43 | }, 44 | ) 45 | 46 | borrow_model = borrow_api.model( 47 | "Borrow", 48 | { 49 | "id": fields.String( 50 | min=0, description="Unique uuid for borrowing", required=True 51 | ), 52 | "userid": fields.Integer( 53 | min=0, description="Userid of the borrower", required=True 54 | ), 55 | "isbn": fields.String(description="ISBN", required=True), 56 | "borrow_date": fields.DateTime(required=True), 57 | "return_date": fields.DateTime(required=False), 58 | "max_return_date": fields.DateTime(required=True), 59 | }, 60 | ) 61 | 62 | return_model = borrow_api.model( 63 | "Return", 64 | { 65 | "id": fields.String( 66 | min=0, description="Unique uuid for borrowing", required=True 67 | ), 68 | "return_date": fields.DateTime(required=False), 69 | }, 70 | ) 71 | 72 | 73 | class User: 74 | def __init__(self, exists: bool, userid: int, name: str, email: str) -> None: 75 | self.exists = exists 76 | self.userid = userid 77 | self.name = name 78 | self.email = email 79 | 80 | 81 | pagination_parser = reqparse.RequestParser() 82 | pagination_parser.add_argument("limit", type=int, help="Limit") 83 | pagination_parser.add_argument("offset", type=int, help="Offset") 84 | 85 | 86 | def get_user(id: int) -> User: 87 | try: 88 | response = requests.get(url="{0}/users/{1}".format(users_host, str(id))) 89 | except Exception as e: 90 | logger.error("Error getting user data error: {0}".format(str(e))) 91 | return User(False, id, None, None) 92 | if response.status_code != 200: 93 | return User(False, id, None, None) 94 | try: 95 | result = response.json() 96 | return User(True, id, result["name"], result["email"]) 97 | except: 98 | return User(False, id, None, None) 99 | 100 | 101 | @borrow_api.route("/return/") 102 | class Return(Resource): 103 | @borrow_api.doc(responses={200: "Ok"}) 104 | @borrow_api.expect(return_model) 105 | def put(self, id): 106 | borrow_api.payload["id"] = id 107 | borrow = borrowcollection.find_one({"id": id}) 108 | if None is borrow: 109 | return Response( 110 | json.dumps({"error": "Borrow id not found"}), 111 | status=404, 112 | mimetype="application/json", 113 | ) 114 | if "return_date" in borrow: 115 | return Response( 116 | json.dumps({"error": "Book already returned"}), 117 | status=404, 118 | mimetype="application/json", 119 | ) 120 | del borrow["_id"] 121 | bookcollection.update_one( 122 | {"isbn": borrow["isbn"]}, {"$inc": {"nr_available": 1}} 123 | ) 124 | borrowcollection.update_one( 125 | {"id": borrow_api.payload["id"]}, 126 | { 127 | "$set": { 128 | "return_date": dateutil.parser.parse( 129 | borrow_api.payload["return_date"] 130 | ) 131 | } 132 | }, 133 | ) 134 | return Response( 135 | json.dumps(borrow_api.payload, default=str), 136 | status=200, 137 | mimetype="application/json", 138 | ) 139 | 140 | 141 | @borrow_api.route("/") 142 | class Borrow(Resource): 143 | def get(self, id): 144 | borrow = borrowcollection.find_one({"id": id}) 145 | if None is borrow: 146 | return Response( 147 | json.dumps({"error": "Borrow id not found"}), 148 | status=404, 149 | mimetype="application/json", 150 | ) 151 | del borrow["_id"] 152 | user = get_user(borrow["userid"]) 153 | borrow["user_name"] = user.name 154 | borrow["user_email"] = user.email 155 | book = bookcollection.find_one({"isbn": borrow["isbn"]}) 156 | if None is book: 157 | return Response( 158 | json.dumps({"error": "Book not found"}), 159 | status=404, 160 | mimetype="application/json", 161 | ) 162 | borrow["book_name"] = book["name"] 163 | borrow["book_author"] = book["author"] 164 | return Response( 165 | json.dumps(borrow, default=str), status=200, mimetype="application/json" 166 | ) 167 | 168 | @borrow_api.doc(responses={200: "Ok"}) 169 | @borrow_api.expect(borrow_model) 170 | def put(self, id): 171 | session = mongo_client.start_session() 172 | session.start_transaction() 173 | try: 174 | borrow = borrowcollection.find_one({"id": id}, session=session) 175 | if None is not borrow: 176 | return Response( 177 | json.dumps({"error": "Borrow already used"}), 178 | status=404, 179 | mimetype="application/json", 180 | ) 181 | borrow_api.payload["id"] = id 182 | user = get_user(borrow_api.payload["userid"]) 183 | if not user.exists: 184 | return Response( 185 | json.dumps({"error": "User not found"}), 186 | status=404, 187 | mimetype="application/json", 188 | ) 189 | book = bookcollection.find_one( 190 | {"isbn": borrow_api.payload["isbn"]}, session=session 191 | ) 192 | if book is None: 193 | return Response( 194 | json.dumps({"error": "Book not found"}), 195 | status=404, 196 | mimetype="application/json", 197 | ) 198 | if book["nr_available"] < 1: 199 | return Response( 200 | json.dumps({"error": "Book is not available yet"}), 201 | status=404, 202 | mimetype="application/json", 203 | ) 204 | borrow_api.payload["borrow_date"] = dateutil.parser.parse( 205 | borrow_api.payload["borrow_date"] 206 | ) 207 | borrow_api.payload["max_return_date"] = dateutil.parser.parse( 208 | borrow_api.payload["max_return_date"] 209 | ) 210 | borrow_api.payload.pop("return_date", None) 211 | borrowcollection.insert_one(borrow_api.payload, session=session) 212 | bookcollection.update_one( 213 | {"isbn": borrow_api.payload["isbn"]}, 214 | {"$inc": {"nr_available": -1}}, 215 | session=session, 216 | ) 217 | del borrow_api.payload["_id"] 218 | db_entry = borrowcollection.find_one({"id": id}, session=session) 219 | session.commit_transaction() 220 | except Exception as e: 221 | session.end_session() 222 | return Response( 223 | json.dumps({"error": str(e)}, default=str), 224 | status=500, 225 | mimetype="application/json", 226 | ) 227 | 228 | session.end_session() 229 | return Response( 230 | json.dumps(db_entry, default=str), status=200, mimetype="application/json" 231 | ) 232 | 233 | 234 | @borrow_api.route("") 235 | class BorrowList(Resource): 236 | @borrow_api.marshal_with(borrow_model, as_list=True) 237 | @borrow_api.expect(pagination_parser, validate=True) 238 | def get(self): 239 | args = pagination_parser.parse_args(request) 240 | data = ( 241 | borrowcollection.find() 242 | .sort("id", 1) 243 | .limit(args["limit"]) 244 | .skip(args["offset"]) 245 | ) 246 | extracted = [ 247 | { 248 | "id": d["id"], 249 | "userid": d["userid"], 250 | "isbn": d["isbn"], 251 | "borrow_date": d["borrow_date"], 252 | "return_date": d["return_date"] if "return_date" in d else None, 253 | "max_return_date": d["max_return_date"], 254 | } 255 | for d in data 256 | ] 257 | return extracted 258 | 259 | 260 | @book_api.route("/") 261 | class Book(Resource): 262 | def get(self, isbn): 263 | book = bookcollection.find_one({"isbn": isbn}) 264 | if None is book: 265 | return Response( 266 | json.dumps({"error": "Book not found"}), 267 | status=404, 268 | mimetype="application/json", 269 | ) 270 | del book["_id"] 271 | return Response(json.dumps(book), status=200, mimetype="application/json") 272 | 273 | @book_api.doc(responses={200: "Ok"}) 274 | @book_api.expect(book_model) 275 | def put(self, isbn): 276 | book_api.payload["isbn"] = isbn 277 | try: 278 | bookcollection.insert_one(book_api.payload) 279 | except errors.DuplicateKeyError: 280 | return Response( 281 | json.dumps({"error": "Isbn already exists"}), 282 | status=404, 283 | mimetype="application/json", 284 | ) 285 | del book_api.payload["_id"] 286 | return Response( 287 | json.dumps(book_api.payload), status=200, mimetype="application/json" 288 | ) 289 | 290 | def delete(self, isbn): 291 | bookcollection.delete_one({"isbn": isbn}) 292 | return Response("", status=200, mimetype="application/json") 293 | 294 | 295 | @book_api.route("") 296 | class BookList(Resource): 297 | @book_api.marshal_with(book_model, as_list=True) 298 | @book_api.expect(pagination_parser, validate=True) 299 | def get(self): 300 | args = pagination_parser.parse_args(request) 301 | books = ( 302 | bookcollection.find() 303 | .sort("id", 1) 304 | .limit(args["limit"]) 305 | .skip(args["offset"]) 306 | ) 307 | extracted = [ 308 | { 309 | "isbn": d["isbn"], 310 | "name": d["name"], 311 | "author": d["author"], 312 | "publisher": d["publisher"], 313 | "nr_available": d["nr_available"], 314 | } 315 | for d in books 316 | ] 317 | return extracted 318 | 319 | 320 | if __name__ == "__main__": 321 | try: 322 | mongo_client.admin.command("replSetInitiate") 323 | except errors.OperationFailure as e: 324 | logger.error("Error setting mongodb replSetInitiate error: {0}".format(str(e))) 325 | bookcollection.insert_one({"isbn": 0}) 326 | bookcollection.delete_one({"isbn": 0}) 327 | borrowcollection.insert_one({"id": 0}) 328 | borrowcollection.delete_one({"id": 0}) 329 | 330 | bookcollection.create_index("isbn", unique=True) 331 | # starts the app in debug mode, bind on all ip's and on port 5000 332 | app.run(debug=True, host="0.0.0.0", port=5000) 333 | -------------------------------------------------------------------------------- /python/bookcollection/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.0 2 | Werkzeug==3.0.1 3 | flask-restx==1.3.0 4 | pymongo==3.12.1 5 | flasgger==0.9.7.1 6 | requests==2.31.0 7 | python-dateutil==2.8.1 -------------------------------------------------------------------------------- /python/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/common/__init__.py -------------------------------------------------------------------------------- /python/common/utils.py: -------------------------------------------------------------------------------- 1 | import logging, os 2 | 3 | 4 | def get_logger(): 5 | """Configures the logging module, and returns it 6 | 7 | Writes to a file log, also outputs it in the console 8 | """ 9 | logger = logging.getLogger("python_app") 10 | logger.setLevel(logging.DEBUG) 11 | # create file handler which logs even debug messages 12 | fh = logging.FileHandler("../python_app.log") 13 | fh.setLevel(logging.DEBUG) 14 | # create console handler with a higher log level 15 | ch = logging.StreamHandler() 16 | ch.setLevel(logging.DEBUG) 17 | # create formatter and add it to the handlers 18 | formatter = logging.Formatter( 19 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 20 | ) 21 | fh.setFormatter(formatter) 22 | ch.setFormatter(formatter) 23 | # add the handlers to the logger 24 | logger.addHandler(fh) 25 | logger.addHandler(ch) 26 | return logger 27 | 28 | 29 | def read_docker_secret(name: str) -> str: 30 | """ 31 | Read a secret by name from as a docker configuration 32 | 33 | :param name: name of the secret 34 | :return: the secret as a string 35 | """ 36 | with open(os.environ.get(name), "r") as file: 37 | return file.read() 38 | -------------------------------------------------------------------------------- /python/diagrams_generator.py: -------------------------------------------------------------------------------- 1 | from diagrams import Cluster, Diagram, Edge 2 | from diagrams.onprem.compute import Server 3 | from diagrams.onprem.monitoring import Grafana 4 | from diagrams.aws.iot import IotMqtt 5 | from diagrams.onprem.database import MongoDB 6 | from diagrams.onprem.database import InfluxDB 7 | from diagrams.onprem.network import HAProxy 8 | from diagrams.onprem.inmemory import Redis 9 | 10 | 11 | with Diagram( 12 | name="Docker Flask MongoDB example", 13 | show=True, 14 | filename="../resources/autogenerated", 15 | direction="LR", 16 | ): 17 | with Cluster("Services"): 18 | fulltext_search = Server("Fulltext search") 19 | users = Server("Users") 20 | book_collection = Server("Book collection") 21 | geolocation_search = Server("Geolocation search") 22 | photo_process = Server("Photo process") 23 | random_demo = Server("Random demo") 24 | tic_tac_toe = Server("Tic tac toe") 25 | users_fastapi = Server("Users Fastapi") 26 | webservers = [ 27 | fulltext_search, 28 | book_collection, 29 | geolocation_search, 30 | random_demo, 31 | users, 32 | users_fastapi, 33 | ] 34 | 35 | proxy = HAProxy("Krakend") 36 | mqtt_service = Server("MQTT service") 37 | mongo = MongoDB("MongoDb") 38 | mosquitto = IotMqtt("Mosquitto") 39 | grafana = Grafana("Grafana") 40 | influxdb = InfluxDB("InfluxDB") 41 | redis = Redis("Redis") 42 | 43 | webservers >> Edge(color="brown") >> mongo 44 | users >> Edge(color="brows") >> redis 45 | book_collection >> Edge(color="black") >> users 46 | mqtt_service >> Edge(color="brown") >> mosquitto 47 | mqtt_service >> Edge(color="brown") >> mongo 48 | mqtt_service >> Edge(color="brown") >> influxdb 49 | grafana >> Edge(color="brown") >> influxdb 50 | 51 | proxy >> Edge(color="black") >> random_demo 52 | proxy >> Edge(color="black") >> users 53 | -------------------------------------------------------------------------------- /python/fastapidemo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/fastapidemo/__init__.py -------------------------------------------------------------------------------- /python/fastapidemo/requirements.txt: -------------------------------------------------------------------------------- 1 | motor==2.3.1 2 | python-dateutil==2.8.1 3 | fastapi==0.65.2 4 | uvicorn==0.13.4 5 | gunicorn==20.0.4 6 | email-validator==1.1.2 -------------------------------------------------------------------------------- /python/fastapidemo/users-fastapi.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from datetime import datetime 3 | 4 | from pymongo import errors 5 | import motor.motor_asyncio 6 | from fastapi import FastAPI, HTTPException 7 | from fastapi.encoders import jsonable_encoder 8 | from pydantic import BaseModel, Field, EmailStr 9 | 10 | 11 | app = FastAPI() 12 | users_async = motor.motor_asyncio.AsyncIOMotorClient("localhost", 27017).demo.users 13 | 14 | 15 | class User(BaseModel): 16 | userid: int 17 | email: EmailStr 18 | name: str = Field(..., title="Name of the user", max_length=50) 19 | birth_date: Optional[datetime] 20 | country: Optional[str] = Field(..., title="Country of origin", max_length=50) 21 | 22 | class Config: 23 | json_encoders = {datetime: lambda v: v.strftime("%Y-%m-%d")} 24 | 25 | 26 | class UpdateUser(BaseModel): 27 | email: Optional[EmailStr] 28 | name: Optional[str] = Field(title="Name of the user", max_length=50) 29 | birth_date: Optional[datetime] 30 | country: Optional[str] = Field(title="Country of origin", max_length=50) 31 | 32 | class Config: 33 | json_encoders = {datetime: lambda v: v.strftime("%Y-%m-%d")} 34 | 35 | 36 | @app.post("/users/{userid}", response_model=User) 37 | async def add_user(userid: int, user: User): 38 | if user.email is None and user.name is None: 39 | raise HTTPException( 40 | status_code=500, detail="Email or name not present in user!" 41 | ) 42 | mongo_user = jsonable_encoder(user) 43 | mongo_user["_id"] = userid 44 | try: 45 | await users_async.insert_one(mongo_user) 46 | except errors.DuplicateKeyError as e: 47 | raise HTTPException(status_code=500, detail="Duplicate user id!") 48 | db_item = await users_async.find_one({"_id": userid}) 49 | return format_user(db_item) 50 | 51 | 52 | @app.put("/users/{userid}", response_model=User) 53 | async def update_user(userid: int, user: UpdateUser): 54 | mongo_user = jsonable_encoder(user) 55 | mongo_user_no_none = {k: v for k, v in mongo_user.items() if v is not None} 56 | await users_async.update_one({"_id": userid}, {"$set": mongo_user_no_none}) 57 | return format_user(await users_async.find_one({"_id": userid})) 58 | 59 | 60 | @app.get("/users/{userid}", response_model=User) 61 | async def get_user(userid: int): 62 | user = await users_async.find_one({"_id": userid}) 63 | if None == user: 64 | raise HTTPException(status_code=404, detail="User not found") 65 | return format_user(user) 66 | 67 | 68 | @app.get("/users", response_model=List[User]) 69 | async def get_users(limit: Optional[int] = 10, offset: Optional[int] = 0): 70 | items_cursor = users_async.find().limit(limit).skip(offset) 71 | items = await items_cursor.to_list(limit) 72 | return list(map(format_user, items)) 73 | 74 | 75 | @app.delete("/users/{userid}", response_model=User) 76 | async def delete_user(userid: int): 77 | user = await users_async.find_one({"_id": userid}) 78 | await users_async.delete_one({"_id": userid}) 79 | return format_user(user) 80 | 81 | 82 | def format_user(user: dict) -> dict: 83 | if user is None: 84 | return None 85 | return { 86 | "userid": user["_id"], 87 | "name": user["name"], 88 | "email": user["email"], 89 | "birth_date": datetime.strptime(user["birth_date"], "%Y-%m-%d"), 90 | "country": user["country"], 91 | } 92 | -------------------------------------------------------------------------------- /python/fulltextsearch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/fulltextsearch/__init__.py -------------------------------------------------------------------------------- /python/fulltextsearch/fulltext_search.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json, datetime 3 | 4 | from flask import Flask, request, Response 5 | from flask_httpauth import HTTPBasicAuth 6 | from werkzeug import generate_password_hash, check_password_hash 7 | from flasgger import Swagger 8 | from pymongo import MongoClient, TEXT 9 | from bson import json_util 10 | 11 | 12 | app = Flask(__name__) 13 | auth = HTTPBasicAuth() 14 | swagger_template = {"securityDefinitions": {"basicAuth": {"type": "basic"}}} 15 | users = { 16 | "admin": generate_password_hash("changeme"), 17 | } 18 | 19 | 20 | @auth.verify_password 21 | def verify_password(username, password): 22 | if username in users and check_password_hash(users.get(username), password): 23 | return username 24 | 25 | 26 | swagger = Swagger(app, template=swagger_template) 27 | mongo_host = "mongodb" 28 | if len(sys.argv) == 2: 29 | mongo_host = sys.argv[1] 30 | fulltext_search = MongoClient(mongo_host, 27017).demo.fulltext_search 31 | 32 | 33 | @app.route("/search/") 34 | @auth.login_required 35 | def search(searched_expression: str): 36 | """Search by an expression 37 | --- 38 | parameters: 39 | - name: searched_expression 40 | in: path 41 | type: string 42 | required: true 43 | definitions: 44 | Result: 45 | type: object 46 | properties: 47 | app_text: 48 | type: string 49 | indexed_date: 50 | type: date 51 | responses: 52 | 200: 53 | description: List of results 54 | schema: 55 | $ref: '#/definitions/Result' 56 | """ 57 | results = ( 58 | fulltext_search.find( 59 | {"$text": {"$search": searched_expression}}, 60 | {"score": {"$meta": "textScore"}}, 61 | ) 62 | .sort([("score", {"$meta": "textScore"})]) 63 | .limit(10) 64 | ) 65 | results = [ 66 | {"text": result["app_text"], "date": result["indexed_date"].isoformat()} 67 | for result in results 68 | ] 69 | return Response( 70 | json.dumps(list(results), default=json_util.default), 71 | status=200, 72 | mimetype="application/json", 73 | ) 74 | 75 | 76 | @app.route("/fulltext", methods=["PUT"]) 77 | @auth.login_required 78 | def add_expression(): 79 | """Add an expression to fulltext index 80 | --- 81 | parameters: 82 | - name: expression 83 | in: formData 84 | type: string 85 | required: true 86 | responses: 87 | 200: 88 | description: Creation succeded 89 | """ 90 | request_params = request.form 91 | if "expression" not in request_params: 92 | return Response( 93 | '"Expression" must be present as a POST parameter!', 94 | status=404, 95 | mimetype="application/json", 96 | ) 97 | document = { 98 | "app_text": request_params["expression"], 99 | "indexed_date": datetime.datetime.utcnow(), 100 | } 101 | fulltext_search.save(document) 102 | return Response( 103 | json.dumps(document, default=json_util.default), 104 | status=200, 105 | mimetype="application/json", 106 | ) 107 | 108 | 109 | if __name__ == "__main__": 110 | # create the fulltext index 111 | fulltext_search.create_index( 112 | [("app_text", TEXT)], name="fulltextsearch_index", default_language="english" 113 | ) 114 | # starts the app in debug mode, bind on all ip's and on port 5000 115 | app.run(debug=True, host="0.0.0.0", port=5000) 116 | -------------------------------------------------------------------------------- /python/geolocation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/geolocation/__init__.py -------------------------------------------------------------------------------- /python/geolocation/geolocation_search.py: -------------------------------------------------------------------------------- 1 | import json, sys 2 | 3 | from flask import Flask, request, Response, jsonify 4 | from flask_jwt_extended import create_access_token 5 | from flask_jwt_extended import jwt_required 6 | from flask_jwt_extended import JWTManager 7 | from flasgger import Swagger 8 | from pymongo import MongoClient, GEOSPHERE 9 | from bson import json_util 10 | from flask_restful import Api 11 | 12 | 13 | app = Flask(__name__) 14 | mongo_host = "mongodb" 15 | if len(sys.argv) == 2: 16 | mongo_host = sys.argv[1] 17 | places = MongoClient(mongo_host, 27017).demo.places 18 | 19 | app.config["JWT_AUTH_URL_RULE"] = "/api/auth" 20 | app.config["JWT_SECRET_KEY"] = "super-secret" 21 | 22 | template = { 23 | "swagger": "2.0", 24 | "info": { 25 | "title": "Geolocation search demo", 26 | "description": "A demo of geolocation search using mongodb and flask", 27 | }, 28 | "securityDefinitions": { 29 | "Bearer": { 30 | "type": "apiKey", 31 | "name": "Authorization", 32 | "in": "header", 33 | "description": 'JWT Authorization header using the Bearer scheme. Example: "Authorization: Bearer {token}"', 34 | } 35 | }, 36 | "security": [ 37 | { 38 | "Bearer": [], 39 | } 40 | ], 41 | } 42 | 43 | app.config["SWAGGER"] = { 44 | "title": "Geolocation search demo", 45 | "uiversion": 3, 46 | "specs_route": "/apidocs/", 47 | } 48 | swagger = Swagger(app, template=template) 49 | api = Api(app) 50 | 51 | 52 | class User(object): 53 | def __init__(self, user_id, username, password): 54 | self.id = user_id 55 | self.username = username 56 | self.password = password 57 | 58 | def __str__(self): 59 | return "User(id='%s')" % self.id 60 | 61 | 62 | users = [ 63 | User(1, "admin", "secret"), 64 | ] 65 | 66 | jwt = JWTManager(app) 67 | 68 | 69 | @app.route("/login", methods=["POST"]) 70 | def login(): 71 | """ 72 | User authenticate method. 73 | --- 74 | description: Authenticate user with supplied credentials. 75 | parameters: 76 | - name: username 77 | in: formData 78 | type: string 79 | required: true 80 | - name: password 81 | in: formData 82 | type: string 83 | required: true 84 | responses: 85 | 200: 86 | description: User successfully logged in. 87 | 400: 88 | description: User login failed. 89 | """ 90 | try: 91 | username = request.form.get("username", None) 92 | password = request.form.get("password", None) 93 | authenticated_user = [ 94 | user 95 | for user in users 96 | if username == user.username and password == user.password 97 | ] 98 | if not authenticated_user: 99 | return jsonify({"msg": "Bad username or password"}), 401 100 | 101 | access_token = create_access_token(identity=username) 102 | resp = jsonify(access_token="Bearer {0}".format(access_token)) 103 | except Exception as e: 104 | resp = jsonify({"message": "Bad username and/or password"}) 105 | resp.status_code = 401 106 | return resp 107 | 108 | 109 | @app.route("/location", methods=["POST"]) 110 | @jwt_required() 111 | def new_location(): 112 | """Add a place (name, latitude and longitude) 113 | --- 114 | parameters: 115 | - name: name 116 | in: formData 117 | type: string 118 | required: true 119 | - name: lat 120 | in: formData 121 | type: string 122 | required: true 123 | - name: lng 124 | in: formData 125 | type: string 126 | required: true 127 | responses: 128 | 200: 129 | description: Place added 130 | """ 131 | request_params = request.form 132 | if ( 133 | "name" not in request_params 134 | or "lat" not in request_params 135 | or "lng" not in request_params 136 | ): 137 | return Response( 138 | "Name, lat, lng must be present in parameters!", 139 | status=404, 140 | mimetype="application/json", 141 | ) 142 | latitude = float(request_params["lng"]) 143 | longitude = float(request_params["lat"]) 144 | places.insert_one( 145 | { 146 | "name": request_params["name"], 147 | "location": {"type": "Point", "coordinates": [latitude, longitude]}, 148 | } 149 | ) 150 | return Response( 151 | json.dumps({"name": request_params["name"], "lat": latitude, "lng": longitude}), 152 | status=200, 153 | mimetype="application/json", 154 | ) 155 | 156 | 157 | @app.route("/location//") 158 | @jwt_required() 159 | def get_near(lat: str, lng: str): 160 | """Get all points near a location given coordonates, and radius 161 | --- 162 | parameters: 163 | - name: lat 164 | in: path 165 | type: string 166 | required: true 167 | - name: lng 168 | in: path 169 | type: string 170 | required: true 171 | - name: max_distance 172 | in: query 173 | type: integer 174 | required: false 175 | - name: limit 176 | in: query 177 | type: integer 178 | required: false 179 | definitions: 180 | Place: 181 | type: object 182 | properties: 183 | name: 184 | type: string 185 | lat: 186 | type: double 187 | long: 188 | type: double 189 | responses: 190 | 200: 191 | description: Places list 192 | schema: 193 | $ref: '#/definitions/Place' 194 | type: array 195 | """ 196 | max_distance = int(request.args.get("max_distance", 10000)) 197 | limit = int(request.args.get("limit", 10)) 198 | cursor = places.find( 199 | { 200 | "location": { 201 | "$near": { 202 | "$geometry": { 203 | "type": "Point", 204 | "coordinates": [float(lng), float(lat)], 205 | }, 206 | "$maxDistance": max_distance, 207 | } 208 | } 209 | } 210 | ).limit(limit) 211 | extracted = [ 212 | { 213 | "name": d["name"], 214 | "lat": d["location"]["coordinates"][1], 215 | "lng": d["location"]["coordinates"][0], 216 | } 217 | for d in cursor 218 | ] 219 | return Response( 220 | json.dumps(extracted, default=json_util.default), 221 | status=200, 222 | mimetype="application/json", 223 | ) 224 | 225 | 226 | if __name__ == "__main__": 227 | # cretes a GEOSHPHERE (2dsphere in MongoDb: https://docs.mongodb.com/manual/core/2dsphere/) index 228 | # named "location_index" on "location" field, it's used to search by distance 229 | places.create_index([("location", GEOSPHERE)], name="location_index") 230 | 231 | # starts the app in debug mode, bind on all ip's and on port 5000 232 | app.run(debug=True, host="0.0.0.0", port=5000) 233 | -------------------------------------------------------------------------------- /python/geolocation/requirements-geolocation.txt: -------------------------------------------------------------------------------- 1 | Flask==2.0.1 2 | flask-httpauth==4.3.0 3 | flasgger==0.9.7.1 4 | pymongo==3.12.1 5 | Werkzeug==2.2.2 6 | Flask-JWT-Extended==4.3.1 7 | Flask-RESTful==0.3.9 -------------------------------------------------------------------------------- /python/graphql/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.0 2 | Flask-Cors==3.0.10 3 | pymongo==4.0.1 4 | pydantic==1.9.0 5 | ariadne==0.14.0 6 | -------------------------------------------------------------------------------- /python/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | 3 | schema { 4 | query: Query 5 | mutation: Mutation 6 | } 7 | 8 | type User { 9 | userid: Int 10 | email: String! 11 | name: String! 12 | birth_date: Date, 13 | country: String 14 | } 15 | 16 | type UserResult { 17 | success: Boolean! 18 | errors: [String] 19 | user: User 20 | } 21 | 22 | type UsersResult { 23 | success: Boolean! 24 | errors: [String] 25 | users: [User] 26 | } 27 | 28 | type Query { 29 | listUsers: UsersResult! 30 | getUser(userid: Int): UserResult! 31 | } 32 | 33 | type Mutation { 34 | upsertUser(userid: Int, email: String!, name: String!, birth_date: Date, country: String): UserResult! 35 | deleteUser(userid: Int): UserResult! 36 | } -------------------------------------------------------------------------------- /python/graphql/users.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from pydantic import BaseModel, Field 4 | from typing import Optional 5 | from datetime import datetime 6 | from ariadne import ( 7 | load_schema_from_path, 8 | make_executable_schema, 9 | graphql_sync, 10 | snake_case_fallback_resolvers, 11 | ObjectType, 12 | ) 13 | from ariadne.constants import PLAYGROUND_HTML 14 | from ariadne import ScalarType 15 | from pymongo import MongoClient 16 | from flask import request, jsonify 17 | from flask import Flask 18 | from flask_cors import CORS 19 | 20 | 21 | app = Flask(__name__) 22 | CORS(app) 23 | mongo_host = "mongodb" 24 | if len(sys.argv) == 2: 25 | mongo_host = sys.argv[1] 26 | users = MongoClient(mongo_host, 27017).demo.users 27 | datetime_scalar = ScalarType("Date") 28 | 29 | 30 | @datetime_scalar.serializer 31 | def serialize_datetime(value: str): 32 | return datetime.strptime(value, "%Y-%m-%d") 33 | 34 | 35 | class User(BaseModel): 36 | userid: int 37 | email: str 38 | name: str = Field(..., title="Name of the user", max_length=50) 39 | birth_date: Optional[datetime] 40 | country: Optional[str] = Field(..., title="Country of origin", max_length=50) 41 | 42 | class Config: 43 | json_encoders = {datetime: lambda v: v.strftime("%Y-%m-%d")} 44 | 45 | 46 | def list_users_resolver(obj, info) -> dict: 47 | user_list = users.find() 48 | user_list = user_list if None != user_list else [] 49 | formatted = [ 50 | { 51 | "userid": d["_id"], 52 | "name": d["name"], 53 | "email": d["email"], 54 | "birth_date": d["birth_date"], 55 | "country": d["country"], 56 | } 57 | for d in user_list 58 | ] 59 | return {"success": True, "users": formatted} 60 | 61 | 62 | def upsert_user_resolver( 63 | obj, info, userid: int, email: str, name: str, birth_date: datetime, country: str 64 | ) -> dict: 65 | try: 66 | payload = { 67 | "success": True, 68 | "user": { 69 | "_id": userid, 70 | "email": email, 71 | "name": name, 72 | "birth_date": birth_date, 73 | "country": country, 74 | }, 75 | } 76 | users.update_one( 77 | {"_id": userid}, 78 | { 79 | "$set": { 80 | "email": email, 81 | "name": name, 82 | "birth_date": birth_date, 83 | "country": country, 84 | } 85 | }, 86 | upsert=True, 87 | ) 88 | except ValueError: # date format errors 89 | payload = {"success": False, "errors": ["errors"]} 90 | return payload 91 | 92 | 93 | def get_user_resolver(obj, info, userid) -> dict: 94 | try: 95 | user_data = users.find_one({"_id": userid}) 96 | user_data["userid"] = user_data["_id"] 97 | payload = {"success": True, "user": user_data} 98 | 99 | except AttributeError: # todo not found 100 | payload = { 101 | "success": False, 102 | "errors": [f"Todo item matching id {id} not found"], 103 | } 104 | return payload 105 | 106 | 107 | def delete_user_resolver(obj, info, userid: int) -> dict: 108 | try: 109 | user = users.find_one({"_id": userid}) 110 | users.delete_one({"_id": userid}) 111 | payload = {"success": True, "user": user} 112 | except AttributeError: 113 | payload = {"success": False, "errors": ["Not found"]} 114 | return payload 115 | 116 | 117 | query = ObjectType("Query") 118 | mutation = ObjectType("Mutation") 119 | 120 | query.set_field("listUsers", list_users_resolver) 121 | query.set_field("getUser", get_user_resolver) 122 | mutation.set_field("upsertUser", upsert_user_resolver) 123 | mutation.set_field("deleteUser", delete_user_resolver) 124 | 125 | # for prod 126 | type_defs = load_schema_from_path("graphql/schema.graphql") 127 | # for dev uncomment 128 | # type_defs = load_schema_from_path("python/graphql/schema.graphql") 129 | schema = make_executable_schema( 130 | type_defs, query, mutation, snake_case_fallback_resolvers, datetime_scalar 131 | ) 132 | 133 | 134 | @app.route("/graphql", methods=["GET"]) 135 | def graphql_playground(): 136 | return PLAYGROUND_HTML, 200 137 | 138 | 139 | @app.route("/graphql", methods=["POST"]) 140 | def graphql_server(): 141 | data = request.get_json() 142 | success, result = graphql_sync(schema, data, context_value=request, debug=app.debug) 143 | status_code = 200 if success else 400 144 | return jsonify(result), status_code 145 | 146 | 147 | if __name__ == "__main__": 148 | app.run(debug=True, host="0.0.0.0", port=5000) 149 | -------------------------------------------------------------------------------- /python/mqtt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/mqtt/__init__.py -------------------------------------------------------------------------------- /python/mqtt/mqtt.py: -------------------------------------------------------------------------------- 1 | from logging import RootLogger 2 | import json, time, datetime, statistics, requests 3 | 4 | import paho.mqtt.client 5 | from pymongo import MongoClient, errors 6 | 7 | from utils import get_logger, read_docker_secret 8 | 9 | 10 | influxdb_url = "http://influxdb:8086/write?db=influx" 11 | mongo_host = "mongodb" 12 | mqtt_host = "mqtt" 13 | mqtt_user = read_docker_secret("MQTT_USER") 14 | mqtt_password = read_docker_secret("MQTT_PASSWORD") 15 | 16 | mongo_client = MongoClient(mongo_host, 27017) 17 | sensors = mongo_client.demo.sensors 18 | logger = get_logger() 19 | 20 | 21 | class Mqtt: 22 | def __init__(self, host: str, user: str, password: str, logger: RootLogger) -> None: 23 | self.__host = host 24 | self.__user = user 25 | self.__password = password 26 | self.__logger = logger 27 | self.__topic = None 28 | 29 | def connect(self, topic: str): 30 | self.__topic = topic 31 | client = paho.mqtt.client.Client() 32 | client.username_pw_set(mqtt_user, mqtt_password) 33 | client.on_connect = self.on_connect 34 | client.on_message = self.on_message 35 | client.connect(self.__host, 1883, 60) 36 | client.loop_start() 37 | 38 | def on_connect( 39 | self, client: paho.mqtt.client.Client, userdata, flags: dict, rc: int 40 | ): 41 | client.subscribe(self.__topic) 42 | 43 | def on_message( 44 | self, 45 | client: paho.mqtt.client.Client, 46 | userdata, 47 | msg: paho.mqtt.client.MQTTMessage, 48 | ): 49 | try: 50 | message = msg.payload.decode("utf-8") 51 | decoded_data = json.loads(message) 52 | except Exception as e: 53 | self.__logger.error( 54 | "could not decode message {0}, error: {1}".format(msg, str(e)) 55 | ) 56 | return 57 | 58 | # skip processing if it's averages topic to avoid an infinit loop 59 | sensors.update_one( 60 | {"_id": decoded_data["sensor_id"]}, 61 | { 62 | "$push": { 63 | "items": { 64 | "$each": [ 65 | { 66 | "value": decoded_data["sensor_value"], 67 | "date": datetime.datetime.utcnow(), 68 | } 69 | ], 70 | "$sort": {"date": -1}, 71 | "$slice": 5, 72 | } 73 | } 74 | }, 75 | upsert=True, 76 | ) 77 | # add data to grafana through influxdb 78 | try: 79 | requests.post( 80 | url=influxdb_url, 81 | data="{0} value={1}".format( 82 | decoded_data["sensor_id"], decoded_data["sensor_value"] 83 | ), 84 | ) 85 | except Exception as e: 86 | self.__logger.error( 87 | "Error writing to influxdb {0}, error: {1}".format(msg, str(e)) 88 | ) 89 | # obtain the mongo sensor data by id 90 | sensor_data = list(sensors.find({"_id": decoded_data["sensor_id"]})) 91 | # we extract the sensor last values from sensor_data 92 | sensor_values = [d["value"] for d in sensor_data[0]["items"]] 93 | client.publish( 94 | "averages/{0}".format(decoded_data["sensor_id"]), 95 | statistics.mean(sensor_values), 96 | 2, 97 | ) 98 | 99 | 100 | mqtt = Mqtt(mqtt_host, mqtt_user, mqtt_password, logger) 101 | mqtt.connect("sensors") 102 | 103 | logger.debug("MQTT App started") 104 | try: 105 | mongo_client.admin.command("replSetInitiate") 106 | except errors.OperationFailure as e: 107 | logger.error("Error setting mongodb replSetInitiate error: {0}".format(str(e))) 108 | 109 | while True: 110 | time.sleep(0.05) 111 | -------------------------------------------------------------------------------- /python/mqtt/requirements-mqtt.txt: -------------------------------------------------------------------------------- 1 | pymongo==3.12.1 2 | paho-mqtt==1.2.3 3 | requests==2.31.0 -------------------------------------------------------------------------------- /python/photo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/photo/__init__.py -------------------------------------------------------------------------------- /python/photo/photo_process.py: -------------------------------------------------------------------------------- 1 | # Note: the image search algorithm is a naive implementation and it's for demo purposes only 2 | import os 3 | import sys 4 | import io 5 | import json 6 | import imagehash 7 | 8 | from PIL import Image, ImageEnhance 9 | from flasgger import Swagger 10 | from flask import Flask, Response, request 11 | 12 | 13 | app = Flask(__name__) 14 | swagger = Swagger(app) 15 | storage_path = "/root/storage" if len(sys.argv) == 1 else sys.argv[1] 16 | 17 | 18 | class FileHashSearch: 19 | hashes = {} 20 | 21 | def load_from_path(self, path: str) -> None: 22 | for root, subdirs, files in os.walk(path): 23 | for file in os.listdir(root): 24 | filePath = os.path.join(root, file) 25 | hash = imagehash.average_hash(Image.open(filePath)) 26 | self.hashes[hash] = os.path.splitext(file)[0] 27 | 28 | def add(self, file, id) -> None: 29 | self.hashes[imagehash.average_hash(Image.open(file.stream))] = id 30 | 31 | def delete(self, id: int) -> None: 32 | self.hashes = {k: v for k, v in self.hashes.items() if v != str(id)} 33 | 34 | def get_similar(self, hash, similarity: int = 10): 35 | return [ 36 | self.hashes[current_hash] 37 | for id, current_hash in enumerate(self.hashes) 38 | if hash - current_hash < similarity 39 | ] 40 | 41 | 42 | def get_photo_path(photo_id: str): 43 | return "{0}/{1}.jpg".format(storage_path, str(photo_id)) 44 | 45 | 46 | def get_resized_by_height(img, new_height: int): 47 | width, height = img.size 48 | hpercent = new_height / float(height) 49 | wsize = int((float(width) * float(hpercent))) 50 | return img.resize((wsize, new_height), Image.ANTIALIAS) 51 | 52 | 53 | file_hash_search = FileHashSearch() 54 | file_hash_search.load_from_path(storage_path) 55 | 56 | 57 | @app.route("/photo/", methods=["GET"]) 58 | def get_photo(id): 59 | """Returns the photo by id 60 | --- 61 | parameters: 62 | - name: id 63 | in: path 64 | type: string 65 | required: true 66 | - name: resize 67 | description: Resize by width in pixels 68 | in: query 69 | type: integer 70 | required: false 71 | - name: rotate 72 | description: Rotate left in degrees 73 | in: query 74 | type: integer 75 | required: false 76 | - name: brightness 77 | in: query 78 | type: float 79 | required: false 80 | maximum: 20 81 | responses: 82 | 200: 83 | description: The actual photo 84 | 404: 85 | description: Photo not found 86 | """ 87 | request_args = request.args 88 | resize = int(request_args.get("resize")) if "resize" in request_args else 0 89 | rotate = int(request.args.get("rotate")) if "rotate" in request_args else 0 90 | brightness = ( 91 | float(request.args.get("brightness")) if "brightness" in request_args else 0 92 | ) 93 | if brightness > 20: 94 | return get_response({"error": "Maximum value for brightness is 20"}, 500) 95 | 96 | try: 97 | img = Image.open(get_photo_path(id)) 98 | except IOError: 99 | return get_response({"error": "Error loading image"}, 500) 100 | 101 | if resize > 0: 102 | img = get_resized_by_height(img, resize) 103 | if rotate > 0: 104 | img = img.rotate(rotate) 105 | if brightness > 0: 106 | enhancer = ImageEnhance.Brightness(img) 107 | img = enhancer.enhance(brightness) 108 | output = io.BytesIO() 109 | img.save(output, format="JPEG") 110 | image_data = output.getvalue() 111 | output.close() 112 | return Response(image_data, status=200, mimetype="image/jpeg") 113 | 114 | 115 | @app.route("/photo/similar", methods=["PUT"]) 116 | def get_photos_like_this(): 117 | """Find similar photos: 118 | --- 119 | parameters: 120 | - name: file 121 | required: false 122 | in: formData 123 | type: file 124 | - name: similarity 125 | description: How similar the file should be, minimum 0 maximum 40 126 | in: query 127 | type: integer 128 | required: false 129 | maximum: 40 130 | definitions: 131 | Number: 132 | type: integer 133 | responses: 134 | 200: 135 | description: Found 136 | schema: 137 | $ref: '#/definitions/Number' 138 | type: array 139 | 404: 140 | description: Erros occured 141 | """ 142 | if "file" not in request.files: 143 | return get_response({"error": "File parameter not present!"}, 500) 144 | file = request.files["file"] 145 | if file.mimetype != "image/jpeg": 146 | return get_response({"error": "File mimetype must pe jpeg!"}, 500) 147 | 148 | request_args = request.args 149 | similarity = ( 150 | int(request.args.get("similarity")) if "similarity" in request_args else 10 151 | ) 152 | result = file_hash_search.get_similar( 153 | imagehash.average_hash(Image.open(file.stream)), similarity 154 | ) 155 | 156 | return Response(json.dumps(result), status=200, mimetype="application/json") 157 | 158 | 159 | @app.route("/photo/", methods=["PUT"]) 160 | def set_photo(id): 161 | """Add jpeg photo on disk: 162 | --- 163 | parameters: 164 | - name: id 165 | in: path 166 | type: string 167 | required: true 168 | - name: file 169 | required: false 170 | in: formData 171 | type: file 172 | responses: 173 | 200: 174 | description: Added succesfully 175 | 404: 176 | description: Error saving photo 177 | """ 178 | if "file" not in request.files: 179 | return get_response({"error": "File parameter not present!"}, 500) 180 | 181 | file = request.files["file"] 182 | if file.mimetype != "image/jpeg": 183 | return get_response({"error": "File mimetype must pe jpeg!"}, 500) 184 | 185 | try: 186 | file.save(get_photo_path(id)) 187 | except Exception as e: 188 | return get_response({"error": "Could not save file to disk!"}, 500) 189 | 190 | file_hash_search.add(file, id) 191 | return get_response({"status": "success"}, 200) 192 | 193 | 194 | @app.route("/photo/", methods=["DELETE"]) 195 | def delete_photo(id): 196 | """Delete photo by id: 197 | --- 198 | parameters: 199 | - name: id 200 | in: path 201 | type: string 202 | required: true 203 | responses: 204 | 200: 205 | description: Deleted succesfully 206 | 404: 207 | description: Error deleting 208 | """ 209 | try: 210 | os.remove(get_photo_path(id)) 211 | file_hash_search.delete(id) 212 | except OSError as e: 213 | return get_response({"error": "File does not exists!"}, 500) 214 | 215 | return get_response({"status": "success"}, 200) 216 | 217 | 218 | def get_response(data: dict, status: int) -> Response: 219 | return Response( 220 | json.dumps(data), 221 | status=status, 222 | mimetype="application/json", 223 | ) 224 | 225 | 226 | if __name__ == "__main__": 227 | # starts the app in debug mode, bind on all ip's and on port 5000 228 | app.run(debug=True, host="0.0.0.0", port=5000) 229 | -------------------------------------------------------------------------------- /python/photo/requirements-photo.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | pymongo==4.0.2 3 | #flasgger 0.9.5 is not compatible with Flask 2.1.1 4 | flasgger==0.9.7.1 5 | Pillow==9.3.0 6 | ImageHash==4.2.1 -------------------------------------------------------------------------------- /python/python_app.log: -------------------------------------------------------------------------------- 1 | 2021-01-31 19:01:48,263 - python_app - DEBUG - Random demo app started 2 | 2021-01-31 19:01:48,484 - python_app - DEBUG - Random demo app started 3 | 2021-01-31 19:03:10,004 - python_app - DEBUG - Random demo app started 4 | 2021-01-31 19:03:10,217 - python_app - DEBUG - Random demo app started 5 | 2021-01-31 19:04:11,058 - python_app - DEBUG - Random demo app started 6 | 2021-01-31 19:06:43,742 - python_app - DEBUG - Random demo app started 7 | 2021-01-31 19:06:43,949 - python_app - DEBUG - Random demo app started 8 | 2021-01-31 19:12:37,999 - python_app - DEBUG - Random demo app started 9 | 2021-01-31 19:12:38,203 - python_app - DEBUG - Random demo app started 10 | 2021-01-31 19:58:31,479 - python_app - DEBUG - MQTT App started 11 | 2021-01-31 19:58:46,800 - python_app - DEBUG - MQTT App started 12 | 2021-01-31 22:08:23,694 - python_app - DEBUG - MQTT App started 13 | 2021-01-31 22:24:12,124 - python_app - DEBUG - MQTT App started 14 | 2021-01-31 22:25:11,955 - python_app - DEBUG - MQTT App started 15 | 2021-01-31 22:25:16,675 - python_app - ERROR - Erro writing to grafana , error: HTTPConnectionPool(host='influxdb', port=8086): Max retries exceeded with url: /write?db=influx (Caused by NewConnectionError(': Failed to establish a new connection: [Errno -2] Name or service not known')) 16 | 2021-02-21 15:49:03,502 - python_app - DEBUG - Random demo app started 17 | 2021-02-21 15:49:03,711 - python_app - DEBUG - Random demo app started 18 | 2021-02-21 15:52:54,615 - python_app - ERROR - Error setting mongodb replSetInitiate error: already initialized 19 | 2021-02-21 15:57:12,566 - python_app - ERROR - Error setting mongodb replSetInitiate error: already initialized 20 | -------------------------------------------------------------------------------- /python/random/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/random/__init__.py -------------------------------------------------------------------------------- /python/random/random_demo.py: -------------------------------------------------------------------------------- 1 | import random, json, datetime, sys 2 | 3 | from flask import Flask, Response, request 4 | from flasgger import Swagger 5 | from pymongo import MongoClient 6 | from bson import json_util 7 | 8 | from utils import get_logger 9 | 10 | 11 | app = Flask(__name__) 12 | swagger = Swagger(app) 13 | mongo_host = "mongodb" 14 | if len(sys.argv) == 2: 15 | mongo_host = sys.argv[1] 16 | random_numbers = MongoClient(mongo_host, 27017).demo.random_numbers 17 | logger = get_logger() 18 | 19 | 20 | @app.route("/random", methods=["PUT"]) 21 | def random_insert(): 22 | """Add a number number to the list of last 5 numbers 23 | --- 24 | parameters: 25 | - name: lower 26 | in: formData 27 | type: integer 28 | required: false 29 | - name: upper 30 | in: formData 31 | type: integer 32 | required: false 33 | responses: 34 | 200: 35 | description: Random number added successfully 36 | type: integer 37 | """ 38 | request_params = request.form 39 | number = str( 40 | random.randint(int(request_params["lower"]), int(request_params["upper"])) 41 | ) 42 | random_numbers.update_one( 43 | {"_id": "lasts"}, 44 | { 45 | "$push": { 46 | "items": { 47 | "$each": [{"value": number, "date": datetime.datetime.utcnow()}], 48 | "$sort": {"date": -1}, 49 | "$slice": 5, 50 | } 51 | } 52 | }, 53 | upsert=True, 54 | ) 55 | return Response(number, status=200, mimetype="application/json") 56 | 57 | 58 | @app.route("/random", methods=["GET"]) 59 | def random_generator(): 60 | """Returns a random number in interval 61 | --- 62 | parameters: 63 | - name: lower 64 | in: query 65 | type: integer 66 | required: false 67 | - name: upper 68 | in: query 69 | type: integer 70 | required: false 71 | responses: 72 | 200: 73 | description: Random number generated 74 | type: integer 75 | """ 76 | request_args = request.args 77 | lower = int(request_args.get("lower")) if "lower" in request_args else 10 78 | upper = int(request_args.get("upper")) if "upper" in request_args else 0 79 | if upper < lower: 80 | return Response( 81 | json.dumps( 82 | {"error": "Upper boundary must be greater or equal than lower boundary"} 83 | ), 84 | status=400, 85 | mimetype="application/json", 86 | ) 87 | number = str(random.randint(lower, upper)) 88 | return Response(number, status=200, mimetype="application/json") 89 | 90 | 91 | @app.route("/random-list") 92 | def last_number_list(): 93 | """Gets the latest 5 generated numbers 94 | --- 95 | definitions: 96 | Number: 97 | type: int 98 | responses: 99 | 200: 100 | description: list of results 101 | schema: 102 | $ref: '#/definitions/Number' 103 | type: array 104 | """ 105 | last_numbers = list(random_numbers.find({"_id": "lasts"})) 106 | if len(last_numbers) == 0: 107 | extracted = [] 108 | else: 109 | extracted = [d["value"] for d in last_numbers[0]["items"]] 110 | return Response( 111 | json.dumps(extracted, default=json_util.default), 112 | status=200, 113 | mimetype="application/json", 114 | ) 115 | 116 | 117 | if __name__ == "__main__": 118 | logger.debug("Random demo app started") 119 | # starts the app in debug mode, bind on all ip's and on port 5000 120 | app.run(debug=True, host="0.0.0.0", port=5000) 121 | -------------------------------------------------------------------------------- /python/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==20.8b1 2 | diagrams==0.19.1 3 | graphviz==0.16 4 | Faker==8.1.1 -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.0.1 2 | flask-httpauth==4.3.0 3 | Flask-Session==0.3.2 4 | flasgger==0.9.4 5 | pymongo==3.12.1 6 | requests==2.31.0 7 | python-dateutil==2.8.1 8 | redis==3.5.3 9 | Werkzeug==2.2.2 -------------------------------------------------------------------------------- /python/tictactoe/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/tictactoe/__init__.py -------------------------------------------------------------------------------- /python/tictactoe/template/tictactoe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tic Tac Toe 5 | 7 | 23 | 24 | 25 | 26 |
27 | Reset Game 28 |
29 | {% if draw %} 30 |
31 |

Game Drawn

32 |
33 | {% endif %} 34 | {% if winnerFound %} 35 |
36 |

WINNER is {{winner}}

37 |
38 | {% endif %} 39 | 40 | {% for i in range(0, 3) %} 41 | 42 | {% for j in range(0, 3) %} 43 | 50 | {% endfor %} 51 | 52 | {% endfor %} 53 |
44 | {% if game[j*3+i] %} 45 | {{ game[j*3+i] }} 46 | {% else %} 47 | Play {{turn}} here. 48 | {% endif %} 49 |
54 | 55 | -------------------------------------------------------------------------------- /python/tictactoe/tictactoe.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, session, redirect, url_for 2 | from flask_session import Session 3 | from tempfile import mkdtemp 4 | 5 | 6 | app = Flask(__name__, template_folder="template") 7 | app.config["SESSION_FILE_DIR"] = mkdtemp() 8 | app.config["SESSION_PERMANENT"] = False 9 | app.config["SESSION_TYPE"] = "filesystem" 10 | Session(app) 11 | 12 | 13 | class Game: 14 | WIN_LINES = [ 15 | [1, 2, 3], 16 | [4, 5, 6], 17 | [7, 8, 9], # horiz. 18 | [1, 4, 7], 19 | [2, 5, 8], 20 | [3, 6, 9], # vertical 21 | [1, 5, 9], 22 | [3, 5, 7], # diagonal 23 | ] 24 | 25 | def has_won(self, board: list, turn: str) -> bool: 26 | wins = [all([(board[c - 1] == turn) for c in line]) for line in self.WIN_LINES] 27 | return any(wins) 28 | 29 | def has_moves_left(self, board: list) -> bool: 30 | return all([move is not None for move in board]) 31 | 32 | def get_next_player(self, turn: str): 33 | return {"O": "X", "X": "O"}[turn] 34 | 35 | 36 | game = Game() 37 | 38 | 39 | def initiate_session(session): 40 | session["board"] = [None, None, None, None, None, None, None, None, None] 41 | session["turn"] = "X" 42 | session["winner"] = False 43 | session["draw"] = False 44 | 45 | 46 | @app.route("/") 47 | def index(): 48 | if "board" not in session: 49 | initiate_session(session) 50 | winner_x = game.has_won(session["board"], "X") 51 | winner_O = game.has_won(session["board"], "O") 52 | if winner_x or winner_O: 53 | session["winner"] = True 54 | session["turn"] = "X" if winner_x else "O" 55 | if game.has_moves_left(session["board"]): 56 | session["draw"] = True 57 | return render_template( 58 | "tictactoe.html", 59 | game=session["board"], 60 | turn=session["turn"], 61 | winnerFound=session["winner"], 62 | winner=session["turn"], 63 | draw=session["draw"], 64 | ) 65 | 66 | 67 | @app.route("/play//") 68 | def play(row: int, col: int): 69 | session["board"][col * 3 + row] = session["turn"] 70 | session["turn"] = game.get_next_player(session["turn"]) 71 | return redirect(url_for("index")) 72 | 73 | 74 | @app.route("/reset") 75 | def reset(): 76 | initiate_session(session) 77 | return redirect(url_for("index")) 78 | 79 | 80 | if __name__ == "__main__": 81 | app.run(debug=True, host="0.0.0.0", port=5000) 82 | -------------------------------------------------------------------------------- /python/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/python/users/__init__.py -------------------------------------------------------------------------------- /python/users/caching.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import pickle 3 | from functools import wraps 4 | 5 | 6 | def cache(redis: redis.Redis, key: str): 7 | """ 8 | Caches the result of the function in redis and pickle, used a key to cache it 9 | 10 | :param redis: a redis configured instance 11 | :param key: the key to use as a parameter for the cache 12 | :return: the result of the wrapped function 13 | """ 14 | 15 | def decorator(fn): # define a decorator for a function "fn" 16 | @wraps(fn) 17 | def wrapped( 18 | *args, **kwargs 19 | ): # define a wrapper that will finally call "fn" with all arguments 20 | # if cache exists -> load it and return its content 21 | cached = redis.get(kwargs[key]) 22 | if cached: 23 | return pickle.loads(cached) 24 | # execute the function with all arguments passed 25 | res = fn(*args, **kwargs) 26 | # save cache in redis 27 | redis.set(kwargs[key], pickle.dumps(res)) 28 | return res 29 | 30 | return wrapped 31 | 32 | return decorator 33 | 34 | 35 | def cache_invalidate(redis: redis.Redis, key: str): 36 | """ 37 | Deletes the redis cache by the key specified 38 | 39 | :param redis: a redis configured instance 40 | :param key: the key to use as a parameter for the cache deletion 41 | :return: the result of the wrapped function 42 | """ 43 | 44 | def decorator(fn): # define a decorator for a function "fn" 45 | @wraps(fn) 46 | def wrapped_f( 47 | *args, **kwargs 48 | ): # define a wrapper that will finally call "fn" with all arguments 49 | # execute the function with all arguments passed 50 | res = fn(*args, **kwargs) 51 | # delete cache 52 | redis.delete(kwargs[key]) 53 | return res 54 | 55 | return wrapped_f 56 | 57 | return decorator 58 | -------------------------------------------------------------------------------- /python/users/users.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import redis 4 | from datetime import datetime 5 | from flask import Flask, request, Response 6 | from pymongo import MongoClient, errors 7 | from bson import json_util 8 | from flasgger import Swagger 9 | from utils import read_docker_secret 10 | from caching import cache, cache_invalidate 11 | 12 | 13 | app = Flask(__name__) 14 | swagger = Swagger(app) 15 | users = MongoClient("mongodb", 27017).demo.users 16 | 17 | 18 | redis_cache = redis.Redis( 19 | host="redis", port=6379, db=0, password=read_docker_secret("REDIS_PASSWORD") 20 | ) 21 | 22 | 23 | def serialize_datetime(value: str): 24 | return datetime.strptime(value, "%Y-%m-%d") 25 | 26 | 27 | def format_user(user: dict) -> dict: 28 | if user is None: 29 | return None 30 | return { 31 | "userid": user["_id"], 32 | "name": user["name"], 33 | "email": user["email"], 34 | "birthdate": user["birthdate"].strftime("%Y-%m-%d") 35 | if "birthdate" in user and user["birthdate"] is not None 36 | else None, 37 | "country": user["country"] if "country" in user else None, 38 | } 39 | 40 | 41 | @app.route("/users/", methods=["POST"]) 42 | @cache_invalidate(redis=redis_cache, key="userid") 43 | def add_user(userid: int): 44 | """Create user 45 | --- 46 | parameters: 47 | - name: userid 48 | in: path 49 | type: string 50 | required: true 51 | - name: email 52 | in: formData 53 | type: string 54 | required: true 55 | - name: name 56 | in: formData 57 | type: string 58 | required: true 59 | - name: birthdate 60 | in: formData 61 | type: string 62 | - name: country 63 | in: formData 64 | type: string 65 | required: false 66 | responses: 67 | 200: 68 | description: Creation succeded 69 | """ 70 | request_params = request.form 71 | if "email" not in request_params or "name" not in request_params: 72 | return Response( 73 | "Email and name not present in parameters!", 74 | status=404, 75 | mimetype="application/json", 76 | ) 77 | try: 78 | users.insert_one( 79 | { 80 | "_id": userid, 81 | "email": request_params["email"], 82 | "name": request_params["name"], 83 | "birthdate": serialize_datetime(request_params["birthdate"]) 84 | if "birthdate" in request_params 85 | else None, 86 | "country": request_params["country"], 87 | } 88 | ) 89 | except errors.DuplicateKeyError as e: 90 | return Response("Duplicate user id!", status=404, mimetype="application/json") 91 | return Response( 92 | json.dumps(format_user(users.find_one({"_id": userid}))), 93 | status=200, 94 | mimetype="application/json", 95 | ) 96 | 97 | 98 | @app.route("/users/", methods=["PUT"]) 99 | @cache_invalidate(redis=redis_cache, key="userid") 100 | def update_user(userid: int): 101 | """Update user information 102 | --- 103 | parameters: 104 | - name: userid 105 | in: path 106 | type: string 107 | required: true 108 | - name: email 109 | in: formData 110 | type: string 111 | required: false 112 | - name: name 113 | in: formData 114 | type: string 115 | required: false 116 | - name: birthdate 117 | in: formData 118 | type: string 119 | - name: country 120 | in: formData 121 | type: string 122 | required: false 123 | responses: 124 | 200: 125 | description: Update succeded 126 | """ 127 | request_params = request.form 128 | set = {} 129 | if "email" in request_params: 130 | set["email"] = request_params["email"] 131 | if "name" in request_params: 132 | set["name"] = request_params["name"] 133 | if "birthdate" in request_params: 134 | set["birthdate"] = (serialize_datetime(request_params["birthdate"]),) 135 | if "country" in request_params: 136 | set["country"] = request_params["country"] 137 | 138 | users.update_one({"_id": userid}, {"$set": set}) 139 | return Response( 140 | json.dumps(format_user(users.find_one({"_id": userid}))), 141 | status=200, 142 | mimetype="application/json", 143 | ) 144 | 145 | 146 | @app.route("/users/", methods=["GET"]) 147 | @cache(redis=redis_cache, key="userid") 148 | def get_user(userid: int): 149 | """Details about a user 150 | --- 151 | parameters: 152 | - name: userid 153 | in: path 154 | type: string 155 | required: true 156 | definitions: 157 | User: 158 | type: object 159 | properties: 160 | _id: 161 | type: integer 162 | email: 163 | type: string 164 | name: 165 | type: string 166 | birthdate: 167 | type: string 168 | country: 169 | type: string 170 | responses: 171 | 200: 172 | description: User model 173 | schema: 174 | $ref: '#/definitions/User' 175 | 404: 176 | description: User not found 177 | """ 178 | user = users.find_one({"_id": userid}) 179 | 180 | if None == user: 181 | return Response("", status=404, mimetype="application/json") 182 | return Response( 183 | json.dumps(format_user(user)), status=200, mimetype="application/json" 184 | ) 185 | 186 | 187 | @app.route("/users", methods=["GET"]) 188 | def get_users(): 189 | """Example endpoint returning all users with pagination 190 | --- 191 | parameters: 192 | - name: limit 193 | in: query 194 | type: integer 195 | required: false 196 | - name: offset 197 | in: query 198 | type: integer 199 | required: false 200 | definitions: 201 | Users: 202 | type: array 203 | items: 204 | properties: 205 | _id: 206 | type: integer 207 | email: 208 | type: string 209 | name: 210 | type: string 211 | birthdate: 212 | type: string 213 | country: 214 | type: string 215 | responses: 216 | 200: 217 | description: List of user models 218 | schema: 219 | $ref: '#/definitions/Users' 220 | """ 221 | request_args = request.args 222 | limit = int(request_args.get("limit")) if "limit" in request_args else 10 223 | offset = int(request_args.get("offset")) if "offset" in request_args else 0 224 | user_list = users.find().limit(limit).skip(offset) 225 | if None == users: 226 | return Response(json.dumps([]), status=200, mimetype="application/json") 227 | extracted = [format_user(d) for d in user_list] 228 | 229 | return Response( 230 | json.dumps(extracted, default=json_util.default), 231 | status=200, 232 | mimetype="application/json", 233 | ) 234 | 235 | 236 | @app.route("/users/", methods=["DELETE"]) 237 | @cache_invalidate(redis=redis_cache, key="userid") 238 | def delete_user(userid: int): 239 | """Delete operation for a user 240 | --- 241 | parameters: 242 | - name: userid 243 | in: path 244 | type: string 245 | required: true 246 | responses: 247 | 200: 248 | description: User deleted 249 | """ 250 | users.delete_one({"_id": userid}) 251 | return Response("", status=200, mimetype="application/json") 252 | 253 | 254 | if __name__ == "__main__": 255 | app.run(debug=True, host="0.0.0.0", port=5000) 256 | -------------------------------------------------------------------------------- /resources/autogenerated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/resources/autogenerated.png -------------------------------------------------------------------------------- /resources/diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/resources/diagram.jpg -------------------------------------------------------------------------------- /resources/diagram.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/resources/diagram.odp -------------------------------------------------------------------------------- /resources/grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/resources/grafana.png -------------------------------------------------------------------------------- /secrets/mqtt_pass.txt: -------------------------------------------------------------------------------- 1 | some_pass -------------------------------------------------------------------------------- /secrets/mqtt_user.txt: -------------------------------------------------------------------------------- 1 | some_user -------------------------------------------------------------------------------- /secrets/redis_pass.txt: -------------------------------------------------------------------------------- 1 | someredispassword -------------------------------------------------------------------------------- /stresstest-locusts/baesian.py: -------------------------------------------------------------------------------- 1 | from random import randrange 2 | 3 | from locust import HttpUser, TaskSet, task 4 | 5 | 6 | class RegistredUser(HttpUser): 7 | min_wait = 5000 8 | max_wait = 9000 9 | 10 | @task 11 | class BaesianStresstest(TaskSet): 12 | @task(1) 13 | def create_item(self): 14 | id = self.__get_item_id() 15 | url = "/item/{0}".format(id) 16 | self.client.post(url, {"name": "item_{0}".format(id)}) 17 | 18 | @task(2) 19 | def add_vote(self): 20 | item_id = self.__get_item_id() 21 | user_id = self.__get_user_id() 22 | url = "/item/vote/{0}".format(item_id) 23 | self.client.put(url, {"mark": randrange(0, 10), "userid": user_id}) 24 | 25 | @task(3) 26 | def get_by_id(self): 27 | self.client.get("/item/{0}".format(self.__get_item_id())) 28 | 29 | def __get_item_id(self) -> int: 30 | return randrange(10, 50) 31 | 32 | def __get_user_id(self) -> int: 33 | return randrange(1, 3) 34 | -------------------------------------------------------------------------------- /stresstest-locusts/fulltext_search.py: -------------------------------------------------------------------------------- 1 | from locust import HttpUser, TaskSet, task 2 | from faker import Faker 3 | 4 | 5 | class RegistredUser(HttpUser): 6 | min_wait = 5000 7 | max_wait = 9000 8 | auth = ("admin", "changeme") 9 | 10 | @task 11 | class FulltextSearchStresstest(TaskSet): 12 | def __init__(self, parent): 13 | super().__init__(parent) 14 | self.__faker = Faker("en_US") 15 | 16 | @task(1) 17 | def add_random_text(self): 18 | data = {"expression": self.__faker.text()} 19 | self.client.put("/fulltext", data, auth=RegistredUser.auth) 20 | 21 | @task(2) 22 | def search(self): 23 | self.client.get("/search/" + self.__faker.text(), auth=RegistredUser.auth) 24 | -------------------------------------------------------------------------------- /stresstest-locusts/geolocation_search.py: -------------------------------------------------------------------------------- 1 | from locust import HttpUser, TaskSet, task 2 | from faker import Faker 3 | 4 | 5 | class RegistredUser(HttpUser): 6 | min_wait = 5000 7 | max_wait = 9000 8 | 9 | @task 10 | class GeolocationStresstest(TaskSet): 11 | def __init__(self, parent): 12 | super().__init__(parent) 13 | self.__faker = Faker("en_US") 14 | 15 | def on_start(self): 16 | self.token = self.login() 17 | self.client.headers = {"Authorization": "Bearer " + self.token} 18 | 19 | def login(self): 20 | response = self.client.post( 21 | "/login", data={"username": "admin", "password": "secret"} 22 | ) 23 | return response.headers["jwt-token"] 24 | 25 | @task(1) 26 | def add_location(self): 27 | coordonates = self.__faker.location_on_land() 28 | data = { 29 | "lat": coordonates[0], 30 | "lng": coordonates[1], 31 | "name": coordonates[2], 32 | } 33 | self.client.post("/location", data) 34 | 35 | @task(2) 36 | def search(self): 37 | self.client.get( 38 | "/location/{0}/{1}".format( 39 | self.__faker.latitude(), self.__faker.longitude() 40 | ) 41 | ) 42 | -------------------------------------------------------------------------------- /stresstest-locusts/random_demo.py: -------------------------------------------------------------------------------- 1 | from locust import HttpUser, TaskSet, task 2 | 3 | 4 | class RegistredUser(HttpUser): 5 | min_wait = 5000 6 | max_wait = 9000 7 | 8 | @task 9 | class RandomStresstest(TaskSet): 10 | @task(2) 11 | def list(self): 12 | self.client.get("/random-list") 13 | 14 | @task(1) 15 | def insert_random_value(self): 16 | self.client.put("/random", {"lower": 0, "upper": 10000}) 17 | -------------------------------------------------------------------------------- /stresstest-locusts/users.py: -------------------------------------------------------------------------------- 1 | from random import randrange 2 | 3 | from locust import HttpUser, TaskSet, task 4 | 5 | 6 | class RegistredUser(HttpUser): 7 | min_wait = 5000 8 | max_wait = 9000 9 | 10 | @task 11 | class CrudStresstest(TaskSet): 12 | def __get_random_user(self): 13 | userid = str(randrange(0, 10000)) 14 | username = "testuser_{0}".format(userid) 15 | email = "some-email{0}@yahoo.com".format(userid) 16 | 17 | return userid, username, email 18 | 19 | @task(1) 20 | def add_user(self): 21 | user_data = self.__get_random_user() 22 | user = { 23 | "id": user_data[0], 24 | "name": user_data[1], 25 | "email": user_data[2], 26 | } 27 | self.client.put("/users/" + user_data[0], user) 28 | 29 | @task(2) 30 | def update_user(self): 31 | user_data = self.__get_random_user() 32 | user = { 33 | "id": user_data[0], 34 | "name": "upd_" + user_data[1], 35 | "email": "upd_" + user_data[2], 36 | } 37 | self.client.post("/users/" + user_data[0], user) 38 | -------------------------------------------------------------------------------- /test.txt: -------------------------------------------------------------------------------- 1 | # ---for testing random--- 2 | curl -X GET -i http://localhost:800/random-list 3 | curl --get -X GET -i http://localhost:800/random -d lower=50 -d upper=100 4 | curl -X PUT -i http://localhost:800/random -d upper=100 -d lower=10 5 | 6 | #---user crud test--- 7 | # adding users 8 | curl -X POST -i http://localhost:81/users/1 -d email=john@doe.com -d name=John 9 | curl -X POST -i http://localhost:81/users/2 -d email=steve@rogers.com -d name=Steve 10 | curl -X POST -i http://localhost:81/users/3 -d email=change@user.com -d name=Change 11 | 12 | # change email of user 3 13 | curl -X PUT -i http://localhost:81/users/3 -d email=newuser@user.com 14 | 15 | # check the change 16 | curl -X GET -i http://localhost:81/users/3 17 | 18 | # delete user 3 19 | curl -X DELETE -i http://localhost:81/users/3 20 | 21 | # check if delete works 22 | curl -X GET -i http://localhost:81/users 23 | 24 | 25 | #---fulltext search--- 26 | curl -X PUT -i http://localhost:82/fulltext -d expression="Who has many apples" 27 | curl -X PUT -i http://localhost:82/fulltext -d expression="The apple tree grew in the park" 28 | curl -X PUT -i http://localhost:82/fulltext -d expression="Some apples are green and some are yellow" 29 | curl -X PUT -i http://localhost:82/fulltext -d expression="How many trees are there in this forest" 30 | 31 | curl -X GET -i http://localhost:82/search/apples 32 | 33 | #---geo location search--- 34 | curl -X POST -i http://localhost:83/location \ 35 | -d name=Bucharest \ 36 | -d lat="26.1496616" \ 37 | -d lng="44.4205455" 38 | 39 | curl -X GET -i http://localhost:83/location/26.1/44.4 40 | 41 | curl -X GET -i http://localhost:83/location/26.1/44.4 -d max_distance==50000 42 | 43 | #---Bayesian average--- 44 | curl -X POST -i http://localhost:84/item/1 -d name=Hamlet 45 | curl -X POST -i http://localhost:84/item/2 -d name=Cicero 46 | curl -X POST -i http://localhost:84/item/3 -d name=Alfred 47 | 48 | curl -X PUT -i http://localhost:84/item/vote/1 -d mark=9 -d userid=1 49 | curl -X PUT -i http://localhost:84/item/vote/2 -d mark=9 -d userid=4 50 | curl -X PUT -i http://localhost:84/item/vote/3 -d mark=7 -d userid=6 51 | 52 | curl -X DELETE -i http://localhost:84/item/3 53 | 54 | curl -X GET -i http://localhost:84/item/1 55 | curl -X GET -i http://localhost:84/items 56 | 57 | #---photo process--- 58 | curl -X PUT -F file=@image1.jpeg -i http://localhost:85/photo/1 59 | curl -X PUT -F file=@image2.jpeg -i http://localhost:85/photo/2 60 | 61 | curl -X GET http://localhost:85/photo/1 -d resize==100 > image1resize.jpeg 62 | 63 | curl -X PUT -F file=@image1resize.jpeg -i http://localhost:85/photo/similar 64 | 65 | curl -X DELETE -i http://localhost:85/photo/2 66 | 67 | 68 | #---book collection--- 69 | curl -X PUT -i http://localhost:86/book/978-1607965503 \ 70 | -H "accept: application/json" \ 71 | -H "Content-Type: application/json" \ 72 | -d '{ 73 | "isbn": "978-1607965503", 74 | "name": "Lincoln the Unknown", 75 | "author": "Dale Carnegie", 76 | "publisher": "snowballpublishing", 77 | "nr_available": 5 78 | }' 79 | 80 | curl -X PUT -i http://localhost:86/book/9780262529624 \ 81 | -H "accept: application/json" \ 82 | -H "Content-Type: application/json" \ 83 | -d '{ 84 | "name": "Intro to Computation and Programming using Python", 85 | "isbn": "9780262529624", 86 | "author": "John Guttag", 87 | "publisher": "MIT Press", 88 | "nr_available": 3 89 | }' 90 | 91 | 92 | curl -X GET -i http://localhost:86/book/9780262529624 93 | 94 | curl -X GET -i http://localhost:86/book/9780262529624 95 | 96 | curl -X GET http://localhost:86/book -d limit=5 -d offset=0 97 | 98 | # borrow book 99 | # will have to create user for this to work 100 | curl -X PUT -i http://localhost:86/borrow/978-1607965503 \ 101 | -H "accept: application/json" \ 102 | -H "Content-Type: application/json" \ 103 | -d '{ 104 | "id": 1, 105 | "userid": 1, 106 | "isbn": "978-1607965503", 107 | "borrow_date": "2019-12-12T09:32:51.715Z", 108 | "return_date": "2020-02-12T09:32:51.715Z", 109 | "max_return_date": "2020-03-12T09:32:51.715Z" 110 | }' 111 | 112 | # list a borrowed book 113 | curl -X GET -i http://localhost:86/borrow/978-1607965503 114 | 115 | curl -X PUT -i http://localhost:86/borrow/return/978-1607965503 \ 116 | -H "accept: application/json" \ 117 | -H "Content-Type: application/json" \ 118 | -d '{ 119 | "id": "978-1607965503", 120 | "return_date":"2020-02-12T09:32:51.715Z" 121 | }' 122 | 123 | curl -X GET -i http://localhost:86/borrow -d limit=5 -d offset=0 124 | 125 | 126 | #---fastapi user CRUD--- 127 | curl -X POST -i http://localhost:88/users/1 \ 128 | -H "accept: application/json" \ 129 | -H "Content-Type: application/json" \ 130 | -d '{ 131 | "userid": 1, 132 | "email": "john@doe.com" 133 | "name": "John" 134 | }' 135 | 136 | curl -X POST -i http://localhost:88/users/3 \ 137 | -H "accept: application/json" \ 138 | -H "Content-Type: application/json" \ 139 | -d '{ 140 | "userid": 3, 141 | "email": "change@user.com", 142 | "name": "Change" 143 | }' 144 | 145 | curl -X GET -i http://localhost:88/users/2 146 | 147 | curl -X PUT -i http://localhost:88/users/3 \ 148 | -H "accept: application/json" \ 149 | -H "Content-Type: application/json" \ 150 | -d '{ 151 | "userid": 3, 152 | "email": "user@user.com", 153 | "name": "Change" 154 | }' 155 | 156 | curl -X DELETE -i http://localhost:88/users/1 157 | curl -X GET -i http://localhost:88/users 158 | 159 | 160 | curl -i -XPOST 'http://localhost:8086/write?db=influx' --data-binary 'humidity value=61' 161 | 162 | 163 | # mqtt service 164 | # new terminal 165 | mosquitto_pub -h localhost -u some_user -P some_pass -p 1883 -d -t sensors -m "{\"sensor_id\": \"temperature\", \"sensor_value\": 15.2}" 166 | 167 | # new terminal for sub 168 | mosquitto_sub -h localhost -u some_user -P some_pass -p 1883 -d -t sensors 169 | 170 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from utils import MongoDb 3 | from pymongo import MongoClient 4 | 5 | 6 | @pytest.fixture() 7 | def demo_db() -> MongoClient: 8 | db = MongoDb(host="mongodb") 9 | db.create_connection() 10 | return db.connection 11 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | python-dateutil==2.8.2 3 | paho-mqtt==1.2.3 4 | pytest-sugar==0.9.4 5 | pymongo==3.11.2 6 | requests==2.28.1 7 | Pillow==9.3.0 -------------------------------------------------------------------------------- /tests/resources/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danionescu0/docker-flask-mongodb-example/98857426fa24c3b07978e71a4e0335940f66e25d/tests/resources/test.jpg -------------------------------------------------------------------------------- /tests/test_0_users.py: -------------------------------------------------------------------------------- 1 | import pytest, requests 2 | from datetime import datetime 3 | from utils import Collection 4 | from typing import Generator 5 | 6 | 7 | users_host = "http://web-users:5000" 8 | # @todo run this test on users fastapi too with pytest.mark.parametrize 9 | 10 | 11 | @pytest.fixture 12 | def users(demo_db) -> Generator[Collection, None, None]: 13 | collection = Collection(demo_db, "users") 14 | yield collection 15 | collection.drop() 16 | 17 | 18 | def test_get_user(users): 19 | users.upsert( 20 | 120, 21 | { 22 | "name": "John", 23 | "email": "test@email.eu", 24 | "birthdate": datetime.strptime("1983-11-28", "%Y-%m-%d"), 25 | "country": "Romania", 26 | }, 27 | ) 28 | response = requests.get(url="{0}/users/120".format(users_host)).json() 29 | print(response) 30 | assert response["userid"] == 120 31 | assert response["email"] == "test@email.eu" 32 | assert response["name"] == "John" 33 | assert response["birthdate"] == "1983-11-28" 34 | assert response["country"] == "Romania" 35 | 36 | 37 | def test_create_user(users): 38 | response = requests.post( 39 | url="{0}/users/101".format(users_host), 40 | data={ 41 | "name": "John Doe", 42 | "email": "johny@email.eu", 43 | "birthdate": "1984-11-28", 44 | "country": "Russia", 45 | }, 46 | ) 47 | assert response.status_code == 200 48 | 49 | response = users.get({"_id": 101}) 50 | print(response) 51 | assert len(response) == 1 52 | assert response[0]["_id"] == 101 53 | assert response[0]["email"] == "johny@email.eu" 54 | assert response[0]["name"] == "John Doe" 55 | assert response[0]["birthdate"].strftime("%Y-%m-%d") == "1984-11-28" 56 | assert response[0]["country"] == "Russia" 57 | 58 | 59 | def test_update_user(users): 60 | users.upsert(110, {"name": "John", "email": "test@email.eu"}) 61 | requests.put( 62 | url="{0}/users/110".format(users_host), 63 | data={"name": "John", "email": "john@email.com"}, 64 | ).json() 65 | response = users.get({"_id": 110}) 66 | assert response[0] == {"_id": 110, "name": "John", "email": "john@email.com"} 67 | 68 | 69 | def test_get_and_delete_users(users): 70 | users.upsert(105, {"name": "John", "email": "john@email.com"}) 71 | users.upsert(109, {"name": "Doe", "email": "doe@email.com"}) 72 | response = requests.get(url="{}/users".format(users_host)).json() 73 | # testing get request 74 | print(response) 75 | assert response == [ 76 | { 77 | "userid": 105, 78 | "name": "John", 79 | "email": "john@email.com", 80 | "birthdate": None, 81 | "country": None, 82 | }, 83 | { 84 | "userid": 109, 85 | "name": "Doe", 86 | "email": "doe@email.com", 87 | "birthdate": None, 88 | "country": None, 89 | }, 90 | ] 91 | 92 | requests.delete(url="{}/users/105".format(users_host)) 93 | response = users.get({"_id": 105}) 94 | # asserting the delete has been done 95 | assert response == [] 96 | -------------------------------------------------------------------------------- /tests/test_baesian.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | from typing import Generator 4 | 5 | from utils import Collection 6 | 7 | 8 | baesian_host = "http://web-baesian:5000" 9 | name = "Cicero" 10 | item_id = 1 11 | userid_seven = 7 12 | userid_eight = 8 13 | 14 | upsert_data = { 15 | "marks": [{"mark": 9, "userid": userid_eight}, {"mark": 9, "userid": userid_seven}], 16 | "name": name, 17 | "nr_votes": 2, 18 | "sum_votes": 18, 19 | } 20 | 21 | 22 | @pytest.fixture 23 | def baesian(demo_db) -> Generator[Collection, None, None]: 24 | collection = Collection(demo_db, "baesian") 25 | yield collection 26 | collection.drop() 27 | 28 | 29 | def test_upsert_item(baesian): 30 | requests.post(url="{0}/item/{1}".format(baesian_host, item_id), data={"name": name}) 31 | response = baesian.get({}) 32 | assert response[0]["name"] == name 33 | assert response[0]["nr_votes"] == 0 34 | 35 | 36 | def test_add_vote(baesian): 37 | requests.post(url="{0}/item/{1}".format(baesian_host, item_id), data={"name": name}) 38 | requests.put( 39 | url="{0}/item/vote/{1}".format(baesian_host, item_id), 40 | data={"userid": userid_eight, "mark": 9}, 41 | ) 42 | requests.put( 43 | url="{0}/item/vote/{1}".format(baesian_host, item_id), 44 | data={"userid": userid_seven, "mark": 9}, 45 | ) 46 | 47 | response = baesian.get({}) 48 | assert len(response[0]["marks"]) == response[0]["nr_votes"] 49 | assert response[0]["name"] == name 50 | assert response[0]["sum_votes"] == 18 51 | 52 | 53 | def test_get_item(baesian): 54 | baesian.upsert(key=item_id, data=upsert_data) 55 | 56 | response = requests.get( 57 | url="{0}/item/{1}".format(baesian_host, item_id), 58 | ).json() 59 | assert response["baesian_average"] == 9.0 60 | assert response["sum_votes"] == 18 61 | 62 | 63 | def test_get_items(baesian): 64 | baesian.upsert(key=item_id, data=upsert_data) 65 | response = requests.get( 66 | url="{0}/items".format(baesian_host), 67 | ).json() 68 | 69 | assert response[0]["name"] == name 70 | assert len(response[0]["marks"]) > 0 71 | 72 | 73 | def delete_item(baesian): 74 | baesian.upsert(key=item_id, data=upsert_data) 75 | response = requests.delete( 76 | url="{0}/item/{1}".format(baesian_host, item_id), 77 | ).json() 78 | 79 | assert response.status_code == 200 80 | 81 | db_response = baesian.get({}) 82 | assert db_response == [] 83 | -------------------------------------------------------------------------------- /tests/test_bookcollection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | import json 4 | import datetime 5 | import dateutil.parser 6 | from typing import Generator 7 | from pytest import FixtureRequest 8 | from utils import Collection, get_random_objectid 9 | 10 | 11 | headers = {"accept": "application/json", "Content-Type": "application/json"} 12 | book_collection_host = "http://web-book-collection:5000" 13 | books = [ 14 | { 15 | "isbn": "978-1607965503", 16 | "name": "Lincoln the Unknown", 17 | "author": "Dale Carnegie", 18 | "publisher": "snowballpublishing", 19 | "nr_available": 5, 20 | }, 21 | { 22 | "isbn": "9780262529624", 23 | "name": "Intro to Computation and Programming using Python", 24 | "author": "John Guttag", 25 | "publisher": "MIT Press", 26 | "nr_available": 3, 27 | }, 28 | ] 29 | 30 | 31 | @pytest.fixture 32 | def book_collection( 33 | demo_db, request: FixtureRequest 34 | ) -> Generator[Collection, None, None]: 35 | collection = Collection(demo_db, "bookcollection") 36 | yield collection 37 | collection.delete_many() 38 | 39 | 40 | @pytest.fixture 41 | def load_books(book_collection): 42 | for book in books: 43 | book_collection.upsert(get_random_objectid(), book) 44 | 45 | 46 | def test_book_add(book_collection): 47 | responses = list() 48 | for counter in range(0, len(books)): 49 | response = requests.put( 50 | url="{0}/book/{1}".format(book_collection_host, books[counter]["isbn"]), 51 | headers=headers, 52 | data=json.dumps(books[counter]), 53 | ) 54 | assert response.status_code == 200 55 | responses.append(response) 56 | 57 | assert all([response.status_code == 200 for response in responses]) 58 | 59 | db_response = book_collection.get({}) 60 | assert len(db_response) == len(books) 61 | 62 | # assert authors 63 | authors = [book["author"] for book in books] 64 | expected_authors = [book["author"] for book in db_response] 65 | assert authors == expected_authors 66 | 67 | 68 | def test_get_book(load_books): 69 | response = requests.get( 70 | url="{0}/book/{1}".format(book_collection_host, books[0]["isbn"]), 71 | ) 72 | assert response.status_code == 200 73 | assert response.json() in books 74 | 75 | 76 | def test_list_all_books(load_books): 77 | # check with limit=1 78 | limit, offset = 1, 0 79 | response = requests.get( 80 | url="{0}/book?limit={limit}&offset={offset}".format( 81 | book_collection_host, limit=limit, offset=offset 82 | ), 83 | ) 84 | assert response.status_code == 200 85 | response = response.json() 86 | assert len(response) == 1 87 | assert response[0] == books[0] 88 | 89 | # check with limit=2 90 | limit, offset = 2, 0 91 | response = requests.get( 92 | url="{0}/book?limit={limit}&offset={offset}".format( 93 | book_collection_host, limit=limit, offset=offset 94 | ), 95 | ) 96 | assert response.status_code == 200 97 | response = response.json() 98 | assert len(response) == 2 99 | assert response == books 100 | 101 | 102 | def test_delete_book(load_books, book_collection): 103 | assert len(book_collection.get({})) == len(books) 104 | response = requests.delete( 105 | url="{0}/book/{1}".format(book_collection_host, books[0]["isbn"]), 106 | headers=headers, 107 | ) 108 | assert response.status_code == 200 109 | # after delete 110 | assert len(book_collection.get({})) == len(books) - 1 111 | 112 | 113 | # 114 | # 115 | # 116 | # borrow tests 117 | # 118 | # 119 | # 120 | 121 | users = { 122 | 100: {"name": "John", "email": "john@email.com"}, 123 | 101: {"name": "Doe", "email": "doe@email.com"}, 124 | } 125 | 126 | return_days = 10 127 | max_return_days = 20 128 | today_date = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) 129 | 130 | borrow_data = [ 131 | { 132 | "id": "1", 133 | "userid": 100, 134 | "isbn": books[0]["isbn"], 135 | "borrow_date": today_date, 136 | "return_date": today_date + datetime.timedelta(days=return_days), 137 | "max_return_date": today_date + datetime.timedelta(days=max_return_days), 138 | }, 139 | { 140 | "id": "2", 141 | "userid": 100, 142 | "isbn": books[1]["isbn"], 143 | "borrow_date": today_date, 144 | "return_date": today_date + datetime.timedelta(days=return_days), 145 | "max_return_date": today_date + datetime.timedelta(days=max_return_days), 146 | }, 147 | { 148 | "id": "3", 149 | "userid": 101, 150 | "isbn": books[1]["isbn"], 151 | "borrow_date": today_date, 152 | "max_return_date": today_date + datetime.timedelta(days=max_return_days), 153 | }, 154 | ] 155 | 156 | 157 | @pytest.fixture 158 | def users_collection(demo_db) -> Generator[Collection, None, None]: 159 | collection = Collection(demo_db, "users") 160 | yield collection 161 | collection.drop() 162 | 163 | 164 | @pytest.fixture 165 | def load_users(users_collection): 166 | for user in users: 167 | users_collection.upsert(user, users[user]) 168 | 169 | 170 | @pytest.fixture 171 | def borrow_collection(demo_db) -> Generator[Collection, None, None]: 172 | collection = Collection(demo_db, "borrowcollection") 173 | yield collection 174 | collection.drop() 175 | 176 | 177 | @pytest.fixture 178 | def load_book_borrows(borrow_collection): 179 | for borrow in borrow_data: 180 | borrow_collection.upsert(get_random_objectid(), borrow) 181 | 182 | 183 | def test_borrow_book(load_users, load_books, borrow_collection, book_collection): 184 | data = { 185 | "id": "1", 186 | "userid": 100, 187 | "isbn": books[0]["isbn"], 188 | "borrow_date": str(today_date), 189 | "return_date": str(today_date + datetime.timedelta(days=return_days)), 190 | "max_return_date": str(today_date + datetime.timedelta(days=max_return_days)), 191 | } 192 | response = requests.put( 193 | url="{}/borrow/{}".format(book_collection_host, str(data["userid"])), 194 | headers=headers, 195 | data=json.dumps(data), 196 | ) 197 | assert response.status_code == 200 198 | db_response = borrow_collection.get({})[0] 199 | db_response["isbn"] = data["isbn"] 200 | db_response["userid"] = data["userid"] 201 | db_response["return_date"] = dateutil.parser.parse(data["return_date"]) 202 | db_response["borrow_date"] = dateutil.parser.parse(data["borrow_date"]) 203 | db_response["max_return_date"] = dateutil.parser.parse(data["max_return_date"]) 204 | 205 | # check one less in book collection 206 | assert book_collection.get({})[0]["nr_available"] == books[0]["nr_available"] - 1 207 | 208 | 209 | def test_list_a_book_borrow(load_book_borrows, load_books, load_users): 210 | response = requests.get(url="{}/borrow/{}".format(book_collection_host, "1")) 211 | assert response.status_code == 200 212 | response_json = response.json() 213 | 214 | assert response_json["book_name"] == books[0]["name"] 215 | assert response_json["user_name"] == users[100]["name"] 216 | assert response_json["borrow_date"] == str(borrow_data[0]["borrow_date"]) 217 | assert response_json["book_author"] == books[0]["author"] 218 | 219 | 220 | def test_book_borrows(load_book_borrows, load_books): 221 | limit, offset = 1, 0 222 | response = requests.get( 223 | url="{0}/borrow?limit={limit}&offset={offset}".format( 224 | book_collection_host, limit=limit, offset=offset 225 | ), 226 | ) 227 | assert response.status_code == 200 228 | response = response.json() 229 | assert len(response) == 1 230 | assert response[0]["isbn"] in [book["isbn"] for book in books] 231 | assert isinstance( 232 | dateutil.parser.parse(response[0]["max_return_date"]), (datetime.datetime) 233 | ) 234 | 235 | limit, offset = 2, 0 236 | response = requests.get( 237 | url="{0}/borrow?limit={limit}&offset={offset}".format( 238 | book_collection_host, limit=limit, offset=offset 239 | ), 240 | ) 241 | assert response.status_code == 200 242 | response = response.json() 243 | assert len(response) == 2 244 | 245 | 246 | def test_return_book(load_book_borrows, load_books, book_collection, borrow_collection): 247 | book_collection.get({}) 248 | return_date = str(today_date + datetime.timedelta(days=4)) 249 | response = requests.put( 250 | url="{}/borrow/return/{}".format(book_collection_host, "3"), 251 | headers=headers, 252 | data=json.dumps({"id": "3", "return_date": return_date}), 253 | ) 254 | response_json = response.json() 255 | assert response.status_code == 200 256 | assert response_json["id"] == "3" 257 | assert response_json["return_date"] == return_date 258 | assert book_collection.get({})[1]["nr_available"] == books[1]["nr_available"] + 1 259 | -------------------------------------------------------------------------------- /tests/test_fulltext_search.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest import FixtureRequest 3 | import requests 4 | from requests.auth import HTTPBasicAuth 5 | import datetime 6 | from typing import Generator 7 | from bson.objectid import ObjectId 8 | from utils import Collection 9 | 10 | 11 | fulltext_search_host = "http://web-fulltext-search:5000" 12 | 13 | expression_one = "ana has many more apples" 14 | expression_two = "john has many more apples" 15 | 16 | 17 | @pytest.fixture 18 | def fulltext_search( 19 | demo_db, request: FixtureRequest 20 | ) -> Generator[Collection, None, None]: 21 | collection = Collection(demo_db, "fulltext_search") 22 | yield collection 23 | param = getattr(request, "param", None) 24 | for key in param: 25 | collection.delete_many("app_text", key) 26 | 27 | 28 | @pytest.mark.parametrize("fulltext_search", [expression_one], indirect=True) 29 | def test_add_expression(fulltext_search): 30 | requests.put( 31 | url="{0}/fulltext".format(fulltext_search_host), 32 | data={"expression": expression_one}, 33 | auth=HTTPBasicAuth("admin", "changeme"), 34 | ) 35 | response = fulltext_search.get({"app_text": expression_one}) 36 | assert response[0]["app_text"] == expression_one 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "fulltext_search", [expression_one, expression_two], indirect=True 41 | ) 42 | def test_search(fulltext_search): 43 | fulltext_search.upsert( 44 | ObjectId(b"foo-bar-quux"), 45 | {"app_text": expression_one, "indexed_date": datetime.datetime.utcnow()}, 46 | ) 47 | fulltext_search.upsert( 48 | ObjectId(b"foo-bar-baaz"), 49 | {"app_text": expression_two, "indexed_date": datetime.datetime.utcnow()}, 50 | ) 51 | response = requests.get( 52 | url="{0}/search/apples".format(fulltext_search_host), 53 | auth=HTTPBasicAuth("admin", "changeme"), 54 | ).json() 55 | 56 | assert response[0]["text"].find("apples") > -1 57 | assert response[1]["text"].find("apples") > -1 58 | -------------------------------------------------------------------------------- /tests/test_geolocation_search.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | from typing import Generator 4 | from bson.objectid import ObjectId 5 | 6 | from utils import Collection 7 | 8 | 9 | geolocation_host = "http://web-geolocation-search:5000" 10 | new_york = {"name": "NewYork", "lat": 40.730610, "lng": -73.935242} 11 | jersey_city = {"name": "JerseyCity", "lat": 40.719074, "lng": -74.050552} 12 | print("request to:") 13 | print(geolocation_host + "/login") 14 | authentication_response = requests.post( 15 | geolocation_host + "/login", 16 | headers={"Content-Type": "application/x-www-form-urlencoded"}, 17 | data={"username": "admin", "password": "secret"}, 18 | ) 19 | 20 | assert isinstance(authentication_response.json(), dict) 21 | assert "access_token" in authentication_response.json() 22 | token = authentication_response.json()["access_token"] 23 | auth_header = {"Authorization": token} 24 | 25 | 26 | @pytest.fixture 27 | def places(demo_db, request) -> Generator[Collection, None, None]: 28 | collection = Collection(demo_db, "places") 29 | param = getattr(request, "param", None) 30 | yield collection 31 | if param: 32 | for key in param: 33 | collection.delete_many("name", param["name"]) 34 | 35 | 36 | @pytest.mark.parametrize("places", [new_york], indirect=True) 37 | def test_new_location(places): 38 | response = requests.post( 39 | "{0}/location".format(geolocation_host), data=new_york, headers=auth_header 40 | ) 41 | assert response.status_code == 200 42 | response = places.get({}) 43 | assert response[0]["name"] == new_york["name"] 44 | 45 | coordinates = response[0]["location"]["coordinates"] 46 | assert coordinates == [new_york["lng"], new_york["lat"]] 47 | 48 | 49 | @pytest.mark.parametrize("places", [new_york, jersey_city], indirect=True) 50 | def test_get_near(places): 51 | places.upsert( 52 | ObjectId(b"foo-bar-baaz"), 53 | { 54 | "name": new_york["name"], 55 | "location": { 56 | "type": "Point", 57 | "coordinates": [new_york["lng"], new_york["lat"]], 58 | }, 59 | }, 60 | ) 61 | places.upsert( 62 | ObjectId(b"foo-bar-quux"), 63 | { 64 | "name": jersey_city["name"], 65 | "location": { 66 | "type": "Point", 67 | "coordinates": [jersey_city["lng"], jersey_city["lat"]], 68 | }, 69 | }, 70 | ) 71 | request = requests.get( 72 | url="{0}/location/{1}/{2}".format( 73 | geolocation_host, new_york["lat"], new_york["lng"] 74 | ), 75 | data={"max_distance": 5000}, 76 | headers=auth_header, 77 | ) 78 | assert request.status_code == 200 79 | response = request.json() 80 | 81 | assert response[0]["name"] == new_york["name"] 82 | assert response[1]["name"] == jersey_city["name"] 83 | -------------------------------------------------------------------------------- /tests/test_mqtt.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import pytest 4 | import time 5 | import os 6 | from typing import Generator, Any 7 | 8 | import paho.mqtt.client as mqtt 9 | 10 | from utils import Collection 11 | 12 | 13 | influx_query_url = "http://influxdb:8086/query?db=influx&" 14 | # status for mqtt 15 | SUCCESS = 0 16 | 17 | 18 | @pytest.fixture 19 | def sensors(demo_db) -> Generator[Collection, None, None]: 20 | collection = Collection(demo_db, "sensors") 21 | yield collection 22 | collection.drop() 23 | 24 | 25 | @pytest.fixture 26 | def mqtt_client() -> Generator[mqtt.Client, None, None]: 27 | username = "" 28 | password = "" 29 | parent_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 30 | secrets_path = os.path.join(parent_path, "secrets") 31 | 32 | with open(os.path.join(secrets_path, "mqtt_user.txt"), "r") as file: 33 | username = file.read() 34 | with open(os.path.join(secrets_path, "mqtt_pass.txt"), "r") as file: 35 | password = file.read() 36 | 37 | mqtt_client = mqtt.Client() 38 | mqtt_client.username_pw_set(username, password) 39 | mqtt_client.connect("mqtt", 1883) 40 | yield mqtt_client 41 | mqtt_client.disconnect() 42 | 43 | 44 | @pytest.mark.skip(reason="fix this test under gitlab ci") 45 | def test_db_insert(mqtt_client, sensors): 46 | # publish message 47 | measurement = "temperature" 48 | cleanup_influx(measurement) 49 | mqtt_response = publish_message( 50 | mqtt_client, 51 | "sensors", 52 | json.dumps({"sensor_id": measurement, "sensor_value": 10}), 53 | ) 54 | assert mqtt_response == SUCCESS 55 | 56 | # influx 57 | query = "q=SELECT * FROM {}".format(measurement) 58 | response = requests.get(influx_query_url + query) 59 | results = response.json()["results"] 60 | series = results[0]["series"] 61 | values = series[0]["values"] 62 | name = series[0]["name"] 63 | 64 | assert len(results) == 1 65 | assert name == measurement 66 | assert values[0][1] == 10 67 | mqtt_client.disconnect() 68 | 69 | # mongo 70 | response = sensors.get({}) 71 | items = response[0]["items"] 72 | assert len(items) == 1 73 | assert items[0]["value"] == 10 74 | 75 | # delete data 76 | cleanup_influx(measurement) 77 | 78 | 79 | def test_mqtt_publish(mqtt_client, sensors): 80 | measurement = "temperature" 81 | cleanup_influx(measurement) 82 | 83 | publish_message( 84 | mqtt_client, 85 | "sensors", 86 | json.dumps({"sensor_id": measurement, "sensor_value": 10}), 87 | ) 88 | 89 | mqtt_client.subscribe("averages/{}".format(measurement)) 90 | mqtt_client.on_message = check_message 91 | mqtt_client.loop_start() 92 | cleanup_influx(measurement) 93 | sensors.delete(measurement) 94 | 95 | 96 | def publish_message(mqtt_client, topic: str, data: str) -> int: 97 | mqtt_response = mqtt_client.publish(topic, data) 98 | time.sleep(0.5) 99 | return mqtt_response[0] 100 | 101 | 102 | # fix this on gitlab ci 103 | def cleanup_influx(measurement: str) -> int: 104 | pass 105 | # resp = requests.post( 106 | # 'http://influxdb:8086/query?db=influx&q=DELETE FROM "{}"'.format(measurement) 107 | # ) 108 | # return resp.status_code 109 | 110 | 111 | def check_message(client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage): 112 | message = msg.payload.decode("utf-8") 113 | decoded_data = json.loads(message) 114 | assert decoded_data["sensor_value"] == 10 115 | -------------------------------------------------------------------------------- /tests/test_photo.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | import os 4 | from pathlib import Path 5 | from PIL import Image 6 | 7 | 8 | photo_process_host = "http://web-photo-process:5000" 9 | parent_path = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 10 | image_path = os.path.join(str(parent_path) + "/tests/resources/test.jpg") 11 | storage_path = os.path.join(str(parent_path) + "/container-storage") 12 | image_id = "101" 13 | 14 | 15 | @pytest.fixture 16 | def set_photo(): 17 | response = requests.put( 18 | url="{0}/photo/{1}".format(photo_process_host, image_id), 19 | files={"file": ("test.jpg", open(image_path, "rb"), "image/jpeg")}, 20 | ) 21 | return response 22 | 23 | 24 | # this test must be refactored, it assumes the storage path is accessible directly and that's not true in gitlab CI 25 | # def test_put_photo(set_photo): 26 | # assert set_photo.status_code == 200 27 | # image_storage_path = Path(os.path.join(storage_path, "{}.jpg".format(image_id))) 28 | # assert image_storage_path.exists() 29 | # 30 | # # cleanup 31 | # image_storage_path.unlink() 32 | 33 | 34 | def test_get_photo_and_similar(set_photo): 35 | # get photo resized to 100 36 | response = requests.get( 37 | url="{0}/photo/{1}".format(photo_process_host, image_id), data={"resize": 100} 38 | ) 39 | assert response.status_code == 200 40 | temp_image_path = os.path.join(str(parent_path) + "/tests/resources/temp.jpg") 41 | 42 | # store the resized photo 43 | with open(temp_image_path, "wb") as f: 44 | f.write(response.content) 45 | 46 | im = Image.open(temp_image_path) 47 | assert im.format == "JPEG" 48 | 49 | # search for photo similar to resized one 50 | response = requests.put( 51 | url="{0}/photo/similar".format(photo_process_host), 52 | files={"file": ("temp.jpg", open(temp_image_path, "rb"), "image/jpeg")}, 53 | ) 54 | assert response.status_code == 200 55 | assert response.json() == [int(image_id)] 56 | 57 | # cleanup 58 | os.remove(temp_image_path) 59 | 60 | 61 | # this test must be refactored, it assumes the storage path is accessible directly and that's not true in gitlab CI 62 | # def test_delete_image(set_photo): 63 | # image_storage_path = Path(os.path.join(storage_path, "{}.jpg".format(image_id))) 64 | # assert image_storage_path.exists() 65 | # 66 | # # delete the image 67 | # requests.delete(url="{0}/photo/{1}".format(photo_process_host, image_id)) 68 | # assert image_storage_path.exists() == False 69 | -------------------------------------------------------------------------------- /tests/test_random_demo.py: -------------------------------------------------------------------------------- 1 | import pytest, requests 2 | import datetime 3 | from utils import Collection 4 | from typing import Generator 5 | 6 | random_host = "http://web-random:5000" 7 | 8 | 9 | @pytest.fixture 10 | def random_numbers(demo_db) -> Generator[Collection, None, None]: 11 | collection = Collection(demo_db, "random_numbers") 12 | yield collection 13 | collection.drop() 14 | 15 | 16 | def test_random_insert(random_numbers): 17 | requests.put( 18 | url="{0}/random".format(random_host), 19 | data={"upper": 100, "lower": 10}, 20 | ).json() 21 | 22 | response = random_numbers.get(dict()) 23 | assert len(response) == 1 24 | assert response[0]["_id"] == "lasts" 25 | 26 | items = response[0]["items"] 27 | assert len(items) == 1 28 | 29 | first_item = items[0] 30 | assert isinstance(first_item["date"], datetime.datetime) 31 | assert 10 <= int(first_item["value"]) <= 100 32 | 33 | 34 | def test_random_generator(): 35 | response = requests.get( 36 | url="{0}/random?lower=10&upper=100".format(random_host) 37 | ).json() 38 | assert 10 <= int(response) <= 100 39 | 40 | 41 | def test_last_number_list(random_numbers): 42 | random_numbers.upsert( 43 | "lasts", 44 | { 45 | "items": [ 46 | {"date": datetime.datetime(2021, 3, 1, 0, 0, 000000), "value": 10}, 47 | {"date": datetime.datetime(2021, 3, 2, 0, 0, 000000), "value": 11}, 48 | {"date": datetime.datetime(2021, 3, 3, 0, 0, 000000), "value": 12}, 49 | {"date": datetime.datetime(2021, 3, 4, 0, 0, 000000), "value": 13}, 50 | {"date": datetime.datetime(2021, 3, 5, 0, 0, 000000), "value": 14}, 51 | ] 52 | }, 53 | ) 54 | response = requests.get(url="{0}/random-list".format(random_host)).json() 55 | assert response == [10, 11, 12, 13, 14] 56 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from pymongo import MongoClient, database 3 | from bson.objectid import ObjectId 4 | 5 | 6 | def get_random_objectid(): 7 | return ObjectId(str(uuid.uuid4())[:12].encode("utf-8")) 8 | 9 | 10 | class MongoDb: 11 | def __init__(self, host="mongodb", dbname="demo") -> None: 12 | self.__host = host 13 | self.__dbname = dbname 14 | 15 | def create_connection(self): 16 | self.connection = MongoClient(self.__host, 27017)[self.__dbname] 17 | 18 | 19 | class Collection: 20 | def __init__(self, db: database.Database, collection_name: str): 21 | self.__db = db 22 | self.__collection = collection_name 23 | 24 | def get(self, query: dict, limit: int = 10, offset: int = 0): 25 | return list(self.__db[self.__collection].find(query).limit(limit).skip(offset)) 26 | 27 | def upsert(self, key, data: dict): 28 | self.__db[self.__collection].update_one( 29 | {"_id": key}, {"$set": data}, upsert=True 30 | ) 31 | 32 | def delete(self, key): 33 | self.__db[self.__collection].delete_one({"_id": key}) 34 | 35 | def delete_many(self, index=None, key=None): 36 | if index and key: 37 | self.__db[self.__collection].delete_many({index: key}) 38 | else: 39 | self.__db[self.__collection].delete_many({}) 40 | 41 | def drop(self): 42 | self.__db[self.__collection].drop() 43 | -------------------------------------------------------------------------------- /wait_until_up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | declare HOST=$1 4 | declare TIMEOUT=$2 5 | 6 | echo "Reaching host $1: " 7 | 8 | curl --head -X GET --retry $TIMEOUT --retry-connrefused --retry-delay 1 $HOST --------------------------------------------------------------------------------