Compare commits
	
		
			4 Commits
		
	
	
		
			4cf11798aa
			...
			publicapi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| cffa40c0e3 | |||
| 6dbae94bd6 | |||
| 42edcd1c33 | |||
| b27d5b3c5b | 
							
								
								
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							| @ -9,7 +9,6 @@ blinker = "==1.4" | ||||
| click = "==7.1.2" | ||||
| Flask = "==1.1.2" | ||||
| Flask-Mail = "==0.9.1" | ||||
| Flask-Testing = "==0.8.1" | ||||
| gunicorn = "==20.0.4" | ||||
| isort = "==4.3.21" | ||||
| itsdangerous = "==1.1.0" | ||||
|  | ||||
							
								
								
									
										534
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										534
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| { | ||||
|     "_meta": { | ||||
|         "hash": { | ||||
|             "sha256": "b1ac4a00fca97c0174ff7c0637fc1b9c0c2264b805bf51a2281016397e2a319e" | ||||
|             "sha256": "12572a60fdc1c3246935e0ce5c4fee31cf89e9dc01a03d0fab87ce2b0633fc89" | ||||
|         }, | ||||
|         "pipfile-spec": 6, | ||||
|         "requires": { | ||||
| @ -18,46 +18,46 @@ | ||||
|     "default": { | ||||
|         "aiohttp": { | ||||
|             "hashes": [ | ||||
|                 "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", | ||||
|                 "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", | ||||
|                 "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", | ||||
|                 "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", | ||||
|                 "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", | ||||
|                 "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", | ||||
|                 "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", | ||||
|                 "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", | ||||
|                 "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", | ||||
|                 "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", | ||||
|                 "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", | ||||
|                 "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", | ||||
|                 "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", | ||||
|                 "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", | ||||
|                 "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", | ||||
|                 "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", | ||||
|                 "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", | ||||
|                 "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", | ||||
|                 "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", | ||||
|                 "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", | ||||
|                 "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", | ||||
|                 "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", | ||||
|                 "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", | ||||
|                 "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", | ||||
|                 "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", | ||||
|                 "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", | ||||
|                 "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", | ||||
|                 "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", | ||||
|                 "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", | ||||
|                 "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", | ||||
|                 "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", | ||||
|                 "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", | ||||
|                 "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", | ||||
|                 "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", | ||||
|                 "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", | ||||
|                 "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", | ||||
|                 "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" | ||||
|                 "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", | ||||
|                 "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", | ||||
|                 "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", | ||||
|                 "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", | ||||
|                 "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", | ||||
|                 "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", | ||||
|                 "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", | ||||
|                 "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", | ||||
|                 "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", | ||||
|                 "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", | ||||
|                 "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", | ||||
|                 "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", | ||||
|                 "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", | ||||
|                 "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", | ||||
|                 "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", | ||||
|                 "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", | ||||
|                 "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", | ||||
|                 "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", | ||||
|                 "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", | ||||
|                 "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", | ||||
|                 "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", | ||||
|                 "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", | ||||
|                 "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", | ||||
|                 "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", | ||||
|                 "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", | ||||
|                 "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", | ||||
|                 "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", | ||||
|                 "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", | ||||
|                 "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", | ||||
|                 "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", | ||||
|                 "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", | ||||
|                 "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", | ||||
|                 "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", | ||||
|                 "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", | ||||
|                 "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", | ||||
|                 "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", | ||||
|                 "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.7.4.post0" | ||||
|             "version": "==3.7.3" | ||||
|         }, | ||||
|         "apscheduler": { | ||||
|             "hashes": [ | ||||
| @ -84,10 +84,10 @@ | ||||
|         }, | ||||
|         "attrs": { | ||||
|             "hashes": [ | ||||
|                 "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", | ||||
|                 "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" | ||||
|                 "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", | ||||
|                 "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" | ||||
|             ], | ||||
|             "version": "==21.2.0" | ||||
|             "version": "==20.3.0" | ||||
|         }, | ||||
|         "blinker": { | ||||
|             "hashes": [ | ||||
| @ -98,25 +98,17 @@ | ||||
|         }, | ||||
|         "certifi": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", | ||||
|                 "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" | ||||
|                 "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", | ||||
|                 "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" | ||||
|             ], | ||||
|             "version": "==2021.5.30" | ||||
|             "version": "==2020.12.5" | ||||
|         }, | ||||
|         "chardet": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", | ||||
|                 "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" | ||||
|                 "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", | ||||
|                 "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" | ||||
|             ], | ||||
|             "version": "==4.0.0" | ||||
|         }, | ||||
|         "charset-normalizer": { | ||||
|             "hashes": [ | ||||
|                 "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1", | ||||
|                 "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12" | ||||
|             ], | ||||
|             "markers": "python_version >= '3'", | ||||
|             "version": "==2.0.3" | ||||
|             "version": "==3.0.4" | ||||
|         }, | ||||
|         "click": { | ||||
|             "hashes": [ | ||||
| @ -135,11 +127,11 @@ | ||||
|         }, | ||||
|         "ecdsa": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676", | ||||
|                 "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa" | ||||
|                 "sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747", | ||||
|                 "sha256:cfc046a2ddd425adbd1a78b3c46f0d1325c657811c0f45ecc3a0a6236c1e50ff" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.17.0" | ||||
|             "version": "==0.16.1" | ||||
|         }, | ||||
|         "flask": { | ||||
|             "hashes": [ | ||||
| @ -156,13 +148,6 @@ | ||||
|             "index": "pypi", | ||||
|             "version": "==0.9.1" | ||||
|         }, | ||||
|         "flask-testing": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0a734d7b68e63a9410b413cd7b1f96456f9a858bd09a6222d465650cc782eb01" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.8.1" | ||||
|         }, | ||||
|         "gevent": { | ||||
|             "hashes": [ | ||||
|                 "sha256:16574e4aa902ebc7bad564e25aa9740a82620fdeb61e0bbf5cbc32e84c13cb6a", | ||||
| @ -199,58 +184,52 @@ | ||||
|         }, | ||||
|         "greenlet": { | ||||
|             "hashes": [ | ||||
|                 "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c", | ||||
|                 "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832", | ||||
|                 "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08", | ||||
|                 "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e", | ||||
|                 "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22", | ||||
|                 "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f", | ||||
|                 "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c", | ||||
|                 "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea", | ||||
|                 "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8", | ||||
|                 "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad", | ||||
|                 "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc", | ||||
|                 "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16", | ||||
|                 "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8", | ||||
|                 "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5", | ||||
|                 "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99", | ||||
|                 "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e", | ||||
|                 "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a", | ||||
|                 "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56", | ||||
|                 "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c", | ||||
|                 "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed", | ||||
|                 "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959", | ||||
|                 "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922", | ||||
|                 "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927", | ||||
|                 "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e", | ||||
|                 "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a", | ||||
|                 "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131", | ||||
|                 "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919", | ||||
|                 "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319", | ||||
|                 "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae", | ||||
|                 "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535", | ||||
|                 "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505", | ||||
|                 "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11", | ||||
|                 "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47", | ||||
|                 "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821", | ||||
|                 "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857", | ||||
|                 "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da", | ||||
|                 "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc", | ||||
|                 "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5", | ||||
|                 "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb", | ||||
|                 "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05", | ||||
|                 "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5", | ||||
|                 "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee", | ||||
|                 "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e", | ||||
|                 "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831", | ||||
|                 "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f", | ||||
|                 "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3", | ||||
|                 "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6", | ||||
|                 "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3", | ||||
|                 "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f" | ||||
|                 "sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196", | ||||
|                 "sha256:0ddd77586553e3daf439aa88b6642c5f252f7ef79a39271c25b1d4bf1b7cbb85", | ||||
|                 "sha256:111cfd92d78f2af0bc7317452bd93a477128af6327332ebf3c2be7df99566683", | ||||
|                 "sha256:122c63ba795fdba4fc19c744df6277d9cfd913ed53d1a286f12189a0265316dd", | ||||
|                 "sha256:181300f826625b7fd1182205b830642926f52bd8cdb08b34574c9d5b2b1813f7", | ||||
|                 "sha256:1a1ada42a1fd2607d232ae11a7b3195735edaa49ea787a6d9e6a53afaf6f3476", | ||||
|                 "sha256:1bb80c71de788b36cefb0c3bb6bfab306ba75073dbde2829c858dc3ad70f867c", | ||||
|                 "sha256:1d1d4473ecb1c1d31ce8fd8d91e4da1b1f64d425c1dc965edc4ed2a63cfa67b2", | ||||
|                 "sha256:292e801fcb3a0b3a12d8c603c7cf340659ea27fd73c98683e75800d9fd8f704c", | ||||
|                 "sha256:2c65320774a8cd5fdb6e117c13afa91c4707548282464a18cf80243cf976b3e6", | ||||
|                 "sha256:4365eccd68e72564c776418c53ce3c5af402bc526fe0653722bc89efd85bf12d", | ||||
|                 "sha256:5352c15c1d91d22902582e891f27728d8dac3bd5e0ee565b6a9f575355e6d92f", | ||||
|                 "sha256:58ca0f078d1c135ecf1879d50711f925ee238fe773dfe44e206d7d126f5bc664", | ||||
|                 "sha256:5d4030b04061fdf4cbc446008e238e44936d77a04b2b32f804688ad64197953c", | ||||
|                 "sha256:5d69bbd9547d3bc49f8a545db7a0bd69f407badd2ff0f6e1a163680b5841d2b0", | ||||
|                 "sha256:5f297cb343114b33a13755032ecf7109b07b9a0020e841d1c3cedff6602cc139", | ||||
|                 "sha256:62afad6e5fd70f34d773ffcbb7c22657e1d46d7fd7c95a43361de979f0a45aef", | ||||
|                 "sha256:647ba1df86d025f5a34043451d7c4a9f05f240bee06277a524daad11f997d1e7", | ||||
|                 "sha256:719e169c79255816cdcf6dccd9ed2d089a72a9f6c42273aae12d55e8d35bdcf8", | ||||
|                 "sha256:7cd5a237f241f2764324396e06298b5dee0df580cf06ef4ada0ff9bff851286c", | ||||
|                 "sha256:875d4c60a6299f55df1c3bb870ebe6dcb7db28c165ab9ea6cdc5d5af36bb33ce", | ||||
|                 "sha256:90b6a25841488cf2cb1c8623a53e6879573010a669455046df5f029d93db51b7", | ||||
|                 "sha256:94620ed996a7632723a424bccb84b07e7b861ab7bb06a5aeb041c111dd723d36", | ||||
|                 "sha256:b5f1b333015d53d4b381745f5de842f19fe59728b65f0fbb662dafbe2018c3a5", | ||||
|                 "sha256:c5b22b31c947ad8b6964d4ed66776bcae986f73669ba50620162ba7c832a6b6a", | ||||
|                 "sha256:c93d1a71c3fe222308939b2e516c07f35a849c5047f0197442a4d6fbcb4128ee", | ||||
|                 "sha256:cdb90267650c1edb54459cdb51dab865f6c6594c3a47ebd441bc493360c7af70", | ||||
|                 "sha256:cfd06e0f0cc8db2a854137bd79154b61ecd940dce96fad0cba23fe31de0b793c", | ||||
|                 "sha256:d3789c1c394944084b5e57c192889985a9f23bd985f6d15728c745d380318128", | ||||
|                 "sha256:da7d09ad0f24270b20f77d56934e196e982af0d0a2446120cb772be4e060e1a2", | ||||
|                 "sha256:df3e83323268594fa9755480a442cabfe8d82b21aba815a71acf1bb6c1776218", | ||||
|                 "sha256:df8053867c831b2643b2c489fe1d62049a98566b1646b194cc815f13e27b90df", | ||||
|                 "sha256:e1128e022d8dce375362e063754e129750323b67454cac5600008aad9f54139e", | ||||
|                 "sha256:e6e9fdaf6c90d02b95e6b0709aeb1aba5affbbb9ccaea5502f8638e4323206be", | ||||
|                 "sha256:eac8803c9ad1817ce3d8d15d1bb82c2da3feda6bee1153eec5c58fa6e5d3f770", | ||||
|                 "sha256:eb333b90036358a0e2c57373f72e7648d7207b76ef0bd00a4f7daad1f79f5203", | ||||
|                 "sha256:ed1d1351f05e795a527abc04a0d82e9aecd3bdf9f46662c36ff47b0b00ecaf06", | ||||
|                 "sha256:f3dc68272990849132d6698f7dc6df2ab62a88b0d36e54702a8fd16c0490e44f", | ||||
|                 "sha256:f59eded163d9752fd49978e0bab7a1ff21b1b8d25c05f0995d140cc08ac83379", | ||||
|                 "sha256:f5e2d36c86c7b03c94b8459c3bd2c9fe2c7dab4b258b8885617d44a22e453fb7", | ||||
|                 "sha256:f6f65bf54215e4ebf6b01e4bb94c49180a589573df643735107056f7a910275b", | ||||
|                 "sha256:f8450d5ef759dbe59f84f2c9f77491bb3d3c44bc1a573746daf086e70b14c243", | ||||
|                 "sha256:f97d83049715fd9dec7911860ecf0e17b48d8725de01e45de07d8ac0bd5bc378" | ||||
|             ], | ||||
|             "markers": "platform_python_implementation == 'CPython'", | ||||
|             "version": "==1.1.0" | ||||
|             "version": "==1.0.0" | ||||
|         }, | ||||
|         "gunicorn": { | ||||
|             "hashes": [ | ||||
| @ -262,11 +241,10 @@ | ||||
|         }, | ||||
|         "idna": { | ||||
|             "hashes": [ | ||||
|                 "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", | ||||
|                 "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" | ||||
|                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", | ||||
|                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" | ||||
|             ], | ||||
|             "markers": "python_version >= '3'", | ||||
|             "version": "==3.2" | ||||
|             "version": "==2.10" | ||||
|         }, | ||||
|         "isort": { | ||||
|             "hashes": [ | ||||
| @ -416,28 +394,34 @@ | ||||
|         }, | ||||
|         "matplotlib": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0bea5ec5c28d49020e5d7923c2725b837e60bc8be99d3164af410eb4b4c827da", | ||||
|                 "sha256:1c1779f7ab7d8bdb7d4c605e6ffaa0614b3e80f1e3c8ccf7b9269a22dbc5986b", | ||||
|                 "sha256:21b31057bbc5e75b08e70a43cefc4c0b2c2f1b1a850f4a0f7af044eb4163086c", | ||||
|                 "sha256:32fa638cc10886885d1ca3d409d4473d6a22f7ceecd11322150961a70fab66dd", | ||||
|                 "sha256:3a5c18dbd2c7c366da26a4ad1462fe3e03a577b39e3b503bbcf482b9cdac093c", | ||||
|                 "sha256:5826f56055b9b1c80fef82e326097e34dc4af8c7249226b7dd63095a686177d1", | ||||
|                 "sha256:6382bc6e2d7e481bcd977eb131c31dee96e0fb4f9177d15ec6fb976d3b9ace1a", | ||||
|                 "sha256:6475d0209024a77f869163ec3657c47fed35d9b6ed8bccba8aa0f0099fbbdaa8", | ||||
|                 "sha256:6a6a44f27aabe720ec4fd485061e8a35784c2b9ffa6363ad546316dfc9cea04e", | ||||
|                 "sha256:7a58f3d8fe8fac3be522c79d921c9b86e090a59637cb88e3bc51298d7a2c862a", | ||||
|                 "sha256:7ad19f3fb6145b9eb41c08e7cbb9f8e10b91291396bee21e9ce761bb78df63ec", | ||||
|                 "sha256:85f191bb03cb1a7b04b5c2cca4792bef94df06ef473bc49e2818105671766fee", | ||||
|                 "sha256:956c8849b134b4a343598305a3ca1bdd3094f01f5efc8afccdebeffe6b315247", | ||||
|                 "sha256:a9d8cb5329df13e0cdaa14b3b43f47b5e593ec637f13f14db75bb16e46178b05", | ||||
|                 "sha256:b1d5a2cedf5de05567c441b3a8c2651fbde56df08b82640e7f06c8cd91e201f6", | ||||
|                 "sha256:b26535b9de85326e6958cdef720ecd10bcf74a3f4371bf9a7e5b2e659c17e153", | ||||
|                 "sha256:c541ee5a3287efe066bbe358320853cf4916bc14c00c38f8f3d8d75275a405a9", | ||||
|                 "sha256:d8d994cefdff9aaba45166eb3de4f5211adb4accac85cbf97137e98f26ea0219", | ||||
|                 "sha256:df815378a754a7edd4559f8c51fc7064f779a74013644a7f5ac7a0c31f875866" | ||||
|                 "sha256:1de0bb6cbfe460725f0e97b88daa8643bcf9571c18ba90bb8e41432aaeca91d6", | ||||
|                 "sha256:1e850163579a8936eede29fad41e202b25923a0a8d5ffd08ce50fc0a97dcdc93", | ||||
|                 "sha256:215e2a30a2090221a9481db58b770ce56b8ef46f13224ae33afe221b14b24dc1", | ||||
|                 "sha256:348e6032f666ffd151b323342f9278b16b95d4a75dfacae84a11d2829a7816ae", | ||||
|                 "sha256:3d2eb9c1cc254d0ffa90bc96fde4b6005d09c2228f99dfd493a4219c1af99644", | ||||
|                 "sha256:3e477db76c22929e4c6876c44f88d790aacdf3c3f8f3a90cb1975c0bf37825b0", | ||||
|                 "sha256:451cc89cb33d6652c509fc6b588dc51c41d7246afdcc29b8624e256b7663ed1f", | ||||
|                 "sha256:46b1a60a04e6d884f0250d5cc8dc7bd21a9a96c584a7acdaab44698a44710bab", | ||||
|                 "sha256:5f571b92a536206f7958f7cb2d367ff6c9a1fa8229dc35020006e4cdd1ca0acd", | ||||
|                 "sha256:672960dd114e342b7c610bf32fb99d14227f29919894388b41553217457ba7ef", | ||||
|                 "sha256:7310e353a4a35477c7f032409966920197d7df3e757c7624fd842f3eeb307d3d", | ||||
|                 "sha256:746a1df55749629e26af7f977ea426817ca9370ad1569436608dc48d1069b87c", | ||||
|                 "sha256:7c155437ae4fd366e2700e2716564d1787700687443de46bcb895fe0f84b761d", | ||||
|                 "sha256:9265ae0fb35e29f9b8cc86c2ab0a2e3dcddc4dd9de4b85bf26c0f63fe5c1c2ca", | ||||
|                 "sha256:94bdd1d55c20e764d8aea9d471d2ae7a7b2c84445e0fa463f02e20f9730783e1", | ||||
|                 "sha256:9a79e5dd7bb797aa611048f5b70588b23c5be05b63eefd8a0d152ac77c4243db", | ||||
|                 "sha256:a17f0a10604fac7627ec82820439e7db611722e80c408a726cd00d8c974c2fb3", | ||||
|                 "sha256:a1acb72f095f1d58ecc2538ed1b8bca0b57df313b13db36ed34b8cdf1868e674", | ||||
|                 "sha256:aa49571d8030ad0b9ac39708ee77bd2a22f87815e12bdee52ecaffece9313ed8", | ||||
|                 "sha256:c24c05f645aef776e8b8931cb81e0f1632d229b42b6d216e30836e2e145a2b40", | ||||
|                 "sha256:cf3a7e54eff792f0815dbbe9b85df2f13d739289c93d346925554f71d484be78", | ||||
|                 "sha256:d738acfdfb65da34c91acbdb56abed46803db39af259b7f194dc96920360dbe4", | ||||
|                 "sha256:e15fa23d844d54e7b3b7243afd53b7567ee71c721f592deb0727ee85e668f96a", | ||||
|                 "sha256:ed4a9e6dcacba56b17a0a9ac22ae2c72a35b7f0ef0693aa68574f0b2df607a89", | ||||
|                 "sha256:f44149a0ef5b4991aaef12a93b8e8d66d6412e762745fea1faa61d98524e0ba9" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.4.2" | ||||
|             "version": "==3.3.4" | ||||
|         }, | ||||
|         "mccabe": { | ||||
|             "hashes": [ | ||||
| @ -499,80 +483,69 @@ | ||||
|         }, | ||||
|         "numpy": { | ||||
|             "hashes": [ | ||||
|                 "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33", | ||||
|                 "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5", | ||||
|                 "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1", | ||||
|                 "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1", | ||||
|                 "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac", | ||||
|                 "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4", | ||||
|                 "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50", | ||||
|                 "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6", | ||||
|                 "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267", | ||||
|                 "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172", | ||||
|                 "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af", | ||||
|                 "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8", | ||||
|                 "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2", | ||||
|                 "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63", | ||||
|                 "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1", | ||||
|                 "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8", | ||||
|                 "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16", | ||||
|                 "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214", | ||||
|                 "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd", | ||||
|                 "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68", | ||||
|                 "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062", | ||||
|                 "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e", | ||||
|                 "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f", | ||||
|                 "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b", | ||||
|                 "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd", | ||||
|                 "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671", | ||||
|                 "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a", | ||||
|                 "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a" | ||||
|                 "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29", | ||||
|                 "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab", | ||||
|                 "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04", | ||||
|                 "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d", | ||||
|                 "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590", | ||||
|                 "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f", | ||||
|                 "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a", | ||||
|                 "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845", | ||||
|                 "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090", | ||||
|                 "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b", | ||||
|                 "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e", | ||||
|                 "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc", | ||||
|                 "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257", | ||||
|                 "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e", | ||||
|                 "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff", | ||||
|                 "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9", | ||||
|                 "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b", | ||||
|                 "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e", | ||||
|                 "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0", | ||||
|                 "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb", | ||||
|                 "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f", | ||||
|                 "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2", | ||||
|                 "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14", | ||||
|                 "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9" | ||||
|             ], | ||||
|             "version": "==1.21.1" | ||||
|             "version": "==1.20.1" | ||||
|         }, | ||||
|         "pillow": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc", | ||||
|                 "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63", | ||||
|                 "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d", | ||||
|                 "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e", | ||||
|                 "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd", | ||||
|                 "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4", | ||||
|                 "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77", | ||||
|                 "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723", | ||||
|                 "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba", | ||||
|                 "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792", | ||||
|                 "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae", | ||||
|                 "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e", | ||||
|                 "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367", | ||||
|                 "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77", | ||||
|                 "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856", | ||||
|                 "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4", | ||||
|                 "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de", | ||||
|                 "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8", | ||||
|                 "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a", | ||||
|                 "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636", | ||||
|                 "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab", | ||||
|                 "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79", | ||||
|                 "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d", | ||||
|                 "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229", | ||||
|                 "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf", | ||||
|                 "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500", | ||||
|                 "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04", | ||||
|                 "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093", | ||||
|                 "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844", | ||||
|                 "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8", | ||||
|                 "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82", | ||||
|                 "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf", | ||||
|                 "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83", | ||||
|                 "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0", | ||||
|                 "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c", | ||||
|                 "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8", | ||||
|                 "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37", | ||||
|                 "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24", | ||||
|                 "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14" | ||||
|                 "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6", | ||||
|                 "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded", | ||||
|                 "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865", | ||||
|                 "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174", | ||||
|                 "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032", | ||||
|                 "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a", | ||||
|                 "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e", | ||||
|                 "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378", | ||||
|                 "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17", | ||||
|                 "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c", | ||||
|                 "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913", | ||||
|                 "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7", | ||||
|                 "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0", | ||||
|                 "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820", | ||||
|                 "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba", | ||||
|                 "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2", | ||||
|                 "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b", | ||||
|                 "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9", | ||||
|                 "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234", | ||||
|                 "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d", | ||||
|                 "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5", | ||||
|                 "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206", | ||||
|                 "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9", | ||||
|                 "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8", | ||||
|                 "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59", | ||||
|                 "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d", | ||||
|                 "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7", | ||||
|                 "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a", | ||||
|                 "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0", | ||||
|                 "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b", | ||||
|                 "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d", | ||||
|                 "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae" | ||||
|             ], | ||||
|             "version": "==8.3.1" | ||||
|             "version": "==8.1.0" | ||||
|         }, | ||||
|         "psycopg2": { | ||||
|             "hashes": [ | ||||
| @ -610,18 +583,18 @@ | ||||
|         }, | ||||
|         "python-dateutil": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", | ||||
|                 "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" | ||||
|                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", | ||||
|                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" | ||||
|             ], | ||||
|             "version": "==2.8.2" | ||||
|             "version": "==2.8.1" | ||||
|         }, | ||||
|         "python-dotenv": { | ||||
|             "hashes": [ | ||||
|                 "sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d", | ||||
|                 "sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d" | ||||
|                 "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e", | ||||
|                 "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.18.0" | ||||
|             "version": "==0.15.0" | ||||
|         }, | ||||
|         "pytz": { | ||||
|             "hashes": [ | ||||
| @ -632,12 +605,12 @@ | ||||
|         }, | ||||
|         "requests": { | ||||
|             "hashes": [ | ||||
|                 "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", | ||||
|                 "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" | ||||
|                 "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", | ||||
|                 "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "markers": "python_version >= '3.0'", | ||||
|             "version": "==2.26.0" | ||||
|             "version": "==2.25.1" | ||||
|         }, | ||||
|         "six": { | ||||
|             "hashes": [ | ||||
| @ -649,11 +622,11 @@ | ||||
|         }, | ||||
|         "stripe": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0050763cb67df6745973bd9757f7a765bed1b82b5d5261fb8908cfc6ec9e5200", | ||||
|                 "sha256:8966b7793014380f60c6f121ba333d6f333a55818edaf79c8d70464ce0a7a808" | ||||
|                 "sha256:97431dbc6d25b94816816a3606f75045d79f101db4cf27e79ac4e039e4971d73", | ||||
|                 "sha256:e32c68194a47522a10945eb893218e5cb5ee65e3a3c2c4df7efca117a6bf1902" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2.60.0" | ||||
|             "version": "==2.55.2" | ||||
|         }, | ||||
|         "toml": { | ||||
|             "hashes": [ | ||||
| @ -702,11 +675,11 @@ | ||||
|         }, | ||||
|         "typing-extensions": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", | ||||
|                 "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", | ||||
|                 "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" | ||||
|                 "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", | ||||
|                 "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", | ||||
|                 "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" | ||||
|             ], | ||||
|             "version": "==3.10.0.0" | ||||
|             "version": "==3.7.4.3" | ||||
|         }, | ||||
|         "tzlocal": { | ||||
|             "hashes": [ | ||||
| @ -717,10 +690,10 @@ | ||||
|         }, | ||||
|         "urllib3": { | ||||
|             "hashes": [ | ||||
|                 "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", | ||||
|                 "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" | ||||
|                 "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", | ||||
|                 "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" | ||||
|             ], | ||||
|             "version": "==1.26.6" | ||||
|             "version": "==1.26.3" | ||||
|         }, | ||||
|         "werkzeug": { | ||||
|             "hashes": [ | ||||
| @ -788,59 +761,60 @@ | ||||
|         }, | ||||
|         "zope.interface": { | ||||
|             "hashes": [ | ||||
|                 "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192", | ||||
|                 "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702", | ||||
|                 "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09", | ||||
|                 "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4", | ||||
|                 "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a", | ||||
|                 "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3", | ||||
|                 "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf", | ||||
|                 "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c", | ||||
|                 "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d", | ||||
|                 "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78", | ||||
|                 "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83", | ||||
|                 "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531", | ||||
|                 "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46", | ||||
|                 "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021", | ||||
|                 "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94", | ||||
|                 "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc", | ||||
|                 "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63", | ||||
|                 "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54", | ||||
|                 "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117", | ||||
|                 "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25", | ||||
|                 "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05", | ||||
|                 "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e", | ||||
|                 "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1", | ||||
|                 "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004", | ||||
|                 "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2", | ||||
|                 "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e", | ||||
|                 "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f", | ||||
|                 "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f", | ||||
|                 "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120", | ||||
|                 "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f", | ||||
|                 "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1", | ||||
|                 "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9", | ||||
|                 "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e", | ||||
|                 "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7", | ||||
|                 "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8", | ||||
|                 "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b", | ||||
|                 "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155", | ||||
|                 "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7", | ||||
|                 "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c", | ||||
|                 "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325", | ||||
|                 "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d", | ||||
|                 "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb", | ||||
|                 "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e", | ||||
|                 "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959", | ||||
|                 "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7", | ||||
|                 "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920", | ||||
|                 "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e", | ||||
|                 "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48", | ||||
|                 "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8", | ||||
|                 "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", | ||||
|                 "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" | ||||
|                 "sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1", | ||||
|                 "sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d", | ||||
|                 "sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123", | ||||
|                 "sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232", | ||||
|                 "sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549", | ||||
|                 "sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102", | ||||
|                 "sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5", | ||||
|                 "sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45", | ||||
|                 "sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00", | ||||
|                 "sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc", | ||||
|                 "sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7", | ||||
|                 "sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104", | ||||
|                 "sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034", | ||||
|                 "sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3", | ||||
|                 "sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3", | ||||
|                 "sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4", | ||||
|                 "sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86", | ||||
|                 "sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96", | ||||
|                 "sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546", | ||||
|                 "sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb", | ||||
|                 "sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3", | ||||
|                 "sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b", | ||||
|                 "sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b", | ||||
|                 "sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec", | ||||
|                 "sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae", | ||||
|                 "sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e", | ||||
|                 "sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386", | ||||
|                 "sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2", | ||||
|                 "sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a", | ||||
|                 "sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d", | ||||
|                 "sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a", | ||||
|                 "sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24", | ||||
|                 "sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d", | ||||
|                 "sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b", | ||||
|                 "sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50", | ||||
|                 "sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523", | ||||
|                 "sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a", | ||||
|                 "sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095", | ||||
|                 "sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a", | ||||
|                 "sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520", | ||||
|                 "sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65", | ||||
|                 "sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11", | ||||
|                 "sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c", | ||||
|                 "sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7", | ||||
|                 "sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332", | ||||
|                 "sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e", | ||||
|                 "sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c", | ||||
|                 "sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7", | ||||
|                 "sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20", | ||||
|                 "sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc", | ||||
|                 "sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd", | ||||
|                 "sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537" | ||||
|             ], | ||||
|             "version": "==5.4.0" | ||||
|             "version": "==5.2.0" | ||||
|         } | ||||
|     }, | ||||
|     "develop": {} | ||||
|  | ||||
| @ -29,7 +29,7 @@ pipenv run flask run | ||||
|  | ||||
| Interested in learning more? How about a trip to the the `docs/` folder: | ||||
|  | ||||
| - [**Setting up `capsul-flask` locally**](./docs/local-set-up.md) | ||||
| - [**Setting up capsul-flask locally**](./docs/local-set-up.md) | ||||
|   - [Manually](./docs/local-set-up.md#manually) | ||||
|   - [With docker-compose](./docs/local-set-up.md#docker_compose) | ||||
| - [**Configuring `capsul-flask`**](./docs/configuration.md) | ||||
| @ -40,7 +40,6 @@ Interested in learning more? How about a trip to the the `docs/` folder: | ||||
|   - [Database schema management (schema versions)](./docs/database.md#schema_management) | ||||
|   - [Running manual database queries](./docs/database.md#manual_queries) | ||||
| - [**`capsul-flask`'s hub-and-spoke architecture**](./docs/architecture.md) | ||||
| - [**Running the automated tests**](./docs/testing.md) | ||||
| - [**Deploying capsul-flask on a server**](./docs/deployment.md) | ||||
|   - [Installing prerequisites for Spoke Mode](./docs/deployment.md#spoke_mode_prerequisites) | ||||
|   - [Deploying capsul-flask manually](./docs/deployment.md#deploy_manually) | ||||
|  | ||||
							
								
								
									
										4
									
								
								app.py
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								app.py
									
									
									
									
									
								
							| @ -1,4 +1,2 @@ | ||||
|  | ||||
| from capsulflask import create_app | ||||
|  | ||||
| app = create_app() | ||||
| from capsulflask import app | ||||
|  | ||||
| @ -9,7 +9,7 @@ import requests | ||||
| import sys | ||||
|  | ||||
| import stripe | ||||
| from dotenv import find_dotenv, dotenv_values | ||||
| from dotenv import load_dotenv, find_dotenv | ||||
| from flask import Flask | ||||
| from flask_mail import Mail, Message | ||||
| from flask import render_template | ||||
| @ -23,200 +23,231 @@ from capsulflask import hub_model, spoke_model, cli | ||||
| from capsulflask.btcpay import client as btcpay | ||||
| from capsulflask.http_client import MyHTTPClient | ||||
|  | ||||
| class StdoutMockFlaskMail: | ||||
|     def send(self, message: Message): | ||||
|       current_app.logger.info(f"Email would have been sent if configured:\n\nto: {','.join(message.recipients)}\nsubject: {message.subject}\nbody:\n\n{message.body}\n\n") | ||||
|  | ||||
|  | ||||
| def create_app(): | ||||
|   for var_name in [ | ||||
|     "SPOKE_HOST_TOKEN", "HUB_TOKEN", "STRIPE_SECRET_KEY", | ||||
|     "BTCPAY_PRIVATE_KEY", "MAIL_PASSWORD" | ||||
|   ]: | ||||
|     var = os.environ.get(f"{var_name}_FILE") | ||||
|     if not var: | ||||
|       continue | ||||
| load_dotenv(find_dotenv()) | ||||
|  | ||||
|     if not os.path.isfile(var): | ||||
|       continue | ||||
| for var_name in [ | ||||
|   "SPOKE_HOST_TOKEN", "HUB_TOKEN", "STRIPE_SECRET_KEY", | ||||
|   "BTCPAY_PRIVATE_KEY", "MAIL_PASSWORD" | ||||
| ]: | ||||
|   var = os.environ.get(f"{var_name}_FILE") | ||||
|   if not var: | ||||
|     continue | ||||
|  | ||||
|     with open(var) as secret_file: | ||||
|       os.environ[var_name] = secret_file.read().rstrip('\n') | ||||
|     del os.environ[f"{var_name}_FILE"] | ||||
|   if not os.path.isfile(var): | ||||
|     continue | ||||
|  | ||||
|   with open(var) as secret_file: | ||||
|     os.environ[var_name] = secret_file.read().rstrip('\n') | ||||
|   del os.environ[f"{var_name}_FILE"] | ||||
|  | ||||
|   config = { | ||||
|       **dotenv_values(find_dotenv()), | ||||
|       **os.environ,  # override loaded values with environment variables | ||||
| app = Flask(__name__) | ||||
|  | ||||
| app.config.from_mapping( | ||||
|   BASE_URL=os.environ.get("BASE_URL", default="http://localhost:5000"), | ||||
|   SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"), | ||||
|   HUB_MODE_ENABLED=os.environ.get("HUB_MODE_ENABLED", default="True").lower() in ['true', '1', 't', 'y', 'yes'], | ||||
|   SPOKE_MODE_ENABLED=os.environ.get("SPOKE_MODE_ENABLED", default="True").lower() in ['true', '1', 't', 'y', 'yes'], | ||||
|   INTERNAL_HTTP_TIMEOUT_SECONDS=os.environ.get("INTERNAL_HTTP_TIMEOUT_SECONDS", default="300"), | ||||
|   HUB_MODEL=os.environ.get("HUB_MODEL", default="capsul-flask"), | ||||
|   SPOKE_MODEL=os.environ.get("SPOKE_MODEL", default="mock"), | ||||
|   LOG_LEVEL=os.environ.get("LOG_LEVEL", default="INFO"), | ||||
|   SPOKE_HOST_ID=os.environ.get("SPOKE_HOST_ID", default="baikal"), | ||||
|   SPOKE_HOST_TOKEN=os.environ.get("SPOKE_HOST_TOKEN", default="changeme"), | ||||
|   HUB_TOKEN=os.environ.get("HUB_TOKEN", default="changeme"), | ||||
|  | ||||
|   # https://www.postgresql.org/docs/9.1/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS | ||||
|   # https://stackoverflow.com/questions/56332906/where-to-put-ssl-certificates-when-trying-to-connect-to-a-remote-database-using | ||||
|   # TLS example: sslmode=verify-full sslrootcert=letsencrypt-root-ca.crt host=db.example.com port=5432 user=postgres password=dev dbname=postgres | ||||
|   POSTGRES_CONNECTION_PARAMETERS=os.environ.get( | ||||
|     "POSTGRES_CONNECTION_PARAMETERS",  | ||||
|     default="host=localhost port=5432 user=postgres password=dev dbname=postgres" | ||||
|   ), | ||||
|  | ||||
|   DATABASE_SCHEMA=os.environ.get("DATABASE_SCHEMA", default="public"), | ||||
|  | ||||
|   MAIL_SERVER=os.environ.get("MAIL_SERVER", default=""), | ||||
|   MAIL_PORT=os.environ.get("MAIL_PORT", default="465"), | ||||
|   MAIL_USE_TLS=os.environ.get("MAIL_USE_TLS", default="False").lower() in ['true', '1', 't', 'y', 'yes'], | ||||
|   MAIL_USE_SSL=os.environ.get("MAIL_USE_SSL", default="True").lower() in ['true', '1', 't', 'y', 'yes'], | ||||
|   MAIL_USERNAME=os.environ.get("MAIL_USERNAME", default=""), | ||||
|   MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""), | ||||
|   MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="no-reply@capsul.org"), | ||||
|   ADMIN_EMAIL_ADDRESSES=os.environ.get("ADMIN_EMAIL_ADDRESSES", default="ops@cyberia.club"), | ||||
|   ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES=os.environ.get("ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES", default="forest.n.johnson@gmail.com,capsul@cyberia.club"), | ||||
|  | ||||
|   PROMETHEUS_URL=os.environ.get("PROMETHEUS_URL", default="https://prometheus.cyberia.club"), | ||||
|  | ||||
|   STRIPE_API_VERSION=os.environ.get("STRIPE_API_VERSION", default="2020-03-02"), | ||||
|   STRIPE_SECRET_KEY=os.environ.get("STRIPE_SECRET_KEY", default=""), | ||||
|   STRIPE_PUBLISHABLE_KEY=os.environ.get("STRIPE_PUBLISHABLE_KEY", default=""), | ||||
|   #STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="") | ||||
|  | ||||
|   BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default="").replace("\\n", "\n"), | ||||
|   BTCPAY_URL=os.environ.get("BTCPAY_URL", default=""), | ||||
|  | ||||
|   THEME=os.environ.get("THEME", default="") | ||||
| ) | ||||
|  | ||||
| app.config['HUB_URL'] = os.environ.get("HUB_URL", default=app.config['BASE_URL']) | ||||
|  | ||||
| class SetLogLevelToDebugForHeartbeatRelatedMessagesFilter(logging.Filter): | ||||
|   def isHeartbeatRelatedString(self, thing): | ||||
|     # thing_string = "<error>" | ||||
|     is_in_string = False | ||||
|     try: | ||||
|       thing_string = "%s" % thing | ||||
|       is_in_string = 'heartbeat-task' in thing_string or 'hub/heartbeat' in thing_string or 'spoke/heartbeat' in thing_string | ||||
|     except: | ||||
|       pass | ||||
|     # self.warning("isHeartbeatRelatedString(%s): %s", thing_string, is_in_string ) | ||||
|     return is_in_string | ||||
|  | ||||
|   def filter(self, record): | ||||
|     if app.config['LOG_LEVEL'] == "DEBUG": | ||||
|       return True | ||||
|  | ||||
|     if self.isHeartbeatRelatedString(record.msg): | ||||
|       return False | ||||
|     for arg in record.args: | ||||
|       if self.isHeartbeatRelatedString(arg): | ||||
|         return False | ||||
|  | ||||
|     return True | ||||
|  | ||||
| logging_dict_config({ | ||||
|   'version': 1, | ||||
|   'formatters': {'default': { | ||||
|     'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', | ||||
|   }}, | ||||
|   'filters': { | ||||
|     'setLogLevelToDebugForHeartbeatRelatedMessages': { | ||||
|         '()': SetLogLevelToDebugForHeartbeatRelatedMessagesFilter, | ||||
|     } | ||||
|   }, | ||||
|   'handlers': {'wsgi': { | ||||
|     'class': 'logging.StreamHandler', | ||||
|     'stream': 'ext://flask.logging.wsgi_errors_stream', | ||||
|     'formatter': 'default', | ||||
|     'filters': ['setLogLevelToDebugForHeartbeatRelatedMessages'] | ||||
|   }}, | ||||
|   'root': { | ||||
|     'level': app.config['LOG_LEVEL'], | ||||
|     'handlers': ['wsgi'] | ||||
|   } | ||||
| }) | ||||
|  | ||||
|   app = Flask(__name__) | ||||
| # app.logger.critical("critical") | ||||
| # app.logger.error("error") | ||||
| # app.logger.warning("warning") | ||||
| # app.logger.info("info") | ||||
| # app.logger.debug("debug") | ||||
|  | ||||
|   app.config.from_mapping( | ||||
|     TESTING=config.get("TESTING", False), | ||||
|     BASE_URL=config.get("BASE_URL", "http://localhost:5000"), | ||||
|     SECRET_KEY=config.get("SECRET_KEY", "dev"), | ||||
|     HUB_MODE_ENABLED=config.get("HUB_MODE_ENABLED", "True").lower() in ['true', '1', 't', 'y', 'yes'], | ||||
|     SPOKE_MODE_ENABLED=config.get("SPOKE_MODE_ENABLED", "True").lower() in ['true', '1', 't', 'y', 'yes'], | ||||
|     INTERNAL_HTTP_TIMEOUT_SECONDS=config.get("INTERNAL_HTTP_TIMEOUT_SECONDS", "300"), | ||||
|     HUB_MODEL=config.get("HUB_MODEL", "capsul-flask"), | ||||
|     SPOKE_MODEL=config.get("SPOKE_MODEL", "mock"), | ||||
|     LOG_LEVEL=config.get("LOG_LEVEL", "INFO"), | ||||
|     SPOKE_HOST_ID=config.get("SPOKE_HOST_ID", "baikal"), | ||||
|     SPOKE_HOST_TOKEN=config.get("SPOKE_HOST_TOKEN", "changeme"), | ||||
|     HUB_TOKEN=config.get("HUB_TOKEN", "changeme"), | ||||
| stripe.api_key = app.config['STRIPE_SECRET_KEY'] | ||||
| stripe.api_version = app.config['STRIPE_API_VERSION'] | ||||
|  | ||||
|     # https://www.postgresql.org/docs/9.1/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS | ||||
|     # https://stackoverflow.com/questions/56332906/where-to-put-ssl-certificates-when-trying-to-connect-to-a-remote-database-using | ||||
|     # TLS example: sslmode=verify-full sslrootcert=letsencrypt-root-ca.crt host=db.example.com port=5432 user=postgres password=dev dbname=postgres | ||||
|     POSTGRES_CONNECTION_PARAMETERS=config.get( | ||||
|       "POSTGRES_CONNECTION_PARAMETERS",  | ||||
|       "host=localhost port=5432 user=postgres password=dev dbname=postgres" | ||||
|     ), | ||||
| if app.config['MAIL_SERVER'] != "": | ||||
|   app.config['FLASK_MAIL_INSTANCE'] = Mail(app) | ||||
| else: | ||||
|   app.logger.warning("No MAIL_SERVER configured. capsul will simply print emails to stdout.") | ||||
|   app.config['FLASK_MAIL_INSTANCE'] = StdoutMockFlaskMail() | ||||
|  | ||||
|     DATABASE_SCHEMA=config.get("DATABASE_SCHEMA", "public"), | ||||
| app.config['HTTP_CLIENT'] = MyHTTPClient(timeout_seconds=int(app.config['INTERNAL_HTTP_TIMEOUT_SECONDS'])) | ||||
|  | ||||
|     MAIL_SERVER=config.get("MAIL_SERVER", ""), | ||||
|     MAIL_PORT=config.get("MAIL_PORT", "465"), | ||||
|     MAIL_USE_TLS=config.get("MAIL_USE_TLS", "False").lower() in ['true', '1', 't', 'y', 'yes'], | ||||
|     MAIL_USE_SSL=config.get("MAIL_USE_SSL", "True").lower() in ['true', '1', 't', 'y', 'yes'], | ||||
|     MAIL_USERNAME=config.get("MAIL_USERNAME", ""), | ||||
|     MAIL_PASSWORD=config.get("MAIL_PASSWORD", ""), | ||||
|     MAIL_DEFAULT_SENDER=config.get("MAIL_DEFAULT_SENDER", "no-reply@capsul.org"), | ||||
|     ADMIN_EMAIL_ADDRESSES=config.get("ADMIN_EMAIL_ADDRESSES", "ops@cyberia.club"), | ||||
|     ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES=config.get("ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES", "forest.n.johnson@gmail.com,capsul@cyberia.club"), | ||||
| app.config['BTCPAY_ENABLED'] = False | ||||
| if app.config['BTCPAY_URL'] != "": | ||||
|   try: | ||||
|     app.config['BTCPAY_CLIENT'] = btcpay.Client(api_uri=app.config['BTCPAY_URL'], pem=app.config['BTCPAY_PRIVATE_KEY']) | ||||
|     app.config['BTCPAY_ENABLED'] = True | ||||
|   except: | ||||
|     app.logger.warning("unable to create btcpay client. Capsul will work fine except cryptocurrency payments will not work. The error was: " + my_exec_info_message(sys.exc_info())) | ||||
|  | ||||
|     PROMETHEUS_URL=config.get("PROMETHEUS_URL", "https://prometheus.cyberia.club"), | ||||
| # only start the scheduler and attempt to migrate the database if we are running the app. | ||||
| # otherwise we are running a CLI command. | ||||
| command_line = ' '.join(sys.argv) | ||||
| is_running_server = ('flask run' in command_line) or ('gunicorn' in command_line) | ||||
|  | ||||
|     STRIPE_API_VERSION=config.get("STRIPE_API_VERSION", "2020-03-02"), | ||||
|     STRIPE_SECRET_KEY=config.get("STRIPE_SECRET_KEY", ""), | ||||
|     STRIPE_PUBLISHABLE_KEY=config.get("STRIPE_PUBLISHABLE_KEY", ""), | ||||
|     #STRIPE_WEBHOOK_SECRET=config.get("STRIPE_WEBHOOK_SECRET", "") | ||||
| app.logger.info(f"is_running_server: {is_running_server}") | ||||
|  | ||||
|     BTCPAY_PRIVATE_KEY=config.get("BTCPAY_PRIVATE_KEY", "").replace("\\n", "\n"), | ||||
|     BTCPAY_URL=config.get("BTCPAY_URL", "https://btcpay.cyberia.club") | ||||
| if app.config['THEME'] != "": | ||||
|   my_loader = jinja2.ChoiceLoader([ | ||||
|     jinja2.FileSystemLoader( | ||||
|       [os.path.join('capsulflask', 'theme', app.config['THEME']), | ||||
|        'capsulflask/templates']), | ||||
|   ]) | ||||
|   app.jinja_loader = my_loader | ||||
|  | ||||
| if app.config['HUB_MODE_ENABLED']: | ||||
|   if app.config['HUB_MODEL'] == "capsul-flask": | ||||
|     app.config['HUB_MODEL'] = hub_model.CapsulFlaskHub() | ||||
|  | ||||
|     # debug mode (flask reloader) runs two copies of the app. When running in debug mode, | ||||
|     # we only want to start the scheduler one time. | ||||
|     if is_running_server and (not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true'): | ||||
|       scheduler = BackgroundScheduler() | ||||
|       heartbeat_task_url = f"{app.config['HUB_URL']}/hub/heartbeat-task" | ||||
|       heartbeat_task_headers = {'Authorization': f"Bearer {app.config['HUB_TOKEN']}"} | ||||
|       heartbeat_task = lambda: requests.post(heartbeat_task_url, headers=heartbeat_task_headers) | ||||
|       scheduler.add_job(name="heartbeat-task", func=heartbeat_task, trigger="interval", seconds=5) | ||||
|       scheduler.start() | ||||
|  | ||||
|       atexit.register(lambda: scheduler.shutdown()) | ||||
|  | ||||
|   else: | ||||
|     app.config['HUB_MODEL'] = hub_model.MockHub() | ||||
|  | ||||
|   from capsulflask import db | ||||
|   db.init_app(app, is_running_server) | ||||
|  | ||||
|   from capsulflask import ( | ||||
|     auth, landing, console, payment, metrics, cli, hub_api, publicapi, admin | ||||
|   ) | ||||
|  | ||||
|   app.config['HUB_URL'] = config.get("HUB_URL", app.config['BASE_URL']) | ||||
|   app.register_blueprint(landing.bp) | ||||
|   app.register_blueprint(auth.bp) | ||||
|   app.register_blueprint(console.bp) | ||||
|   app.register_blueprint(payment.bp) | ||||
|   app.register_blueprint(metrics.bp) | ||||
|   app.register_blueprint(cli.bp) | ||||
|   app.register_blueprint(hub_api.bp) | ||||
|   app.register_blueprint(admin.bp) | ||||
|   app.register_blueprint(publicapi.bp) | ||||
|  | ||||
|   logging_dict_config({ | ||||
|     'version': 1, | ||||
|     'formatters': {'default': { | ||||
|       'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', | ||||
|     }}, | ||||
|     'filters': { | ||||
|       'setLogLevelToDebugForHeartbeatRelatedMessages': { | ||||
|           '()': SetLogLevelToDebugForHeartbeatRelatedMessagesFilter, | ||||
|       } | ||||
|     }, | ||||
|     'handlers': {'wsgi': { | ||||
|       'class': 'logging.StreamHandler', | ||||
|       'stream': 'ext://flask.logging.wsgi_errors_stream', | ||||
|       'formatter': 'default', | ||||
|       'filters': ['setLogLevelToDebugForHeartbeatRelatedMessages'] | ||||
|     }}, | ||||
|     'root': { | ||||
|       'level': app.config['LOG_LEVEL'], | ||||
|       'handlers': ['wsgi'] | ||||
|     } | ||||
|   }) | ||||
|   app.add_url_rule("/", endpoint="index") | ||||
|  | ||||
|   # app.logger.critical("critical") | ||||
|   # app.logger.error("error") | ||||
|   # app.logger.warning("warning") | ||||
|   # app.logger.info("info") | ||||
|   # app.logger.debug("debug") | ||||
|  | ||||
|   stripe.api_key = app.config['STRIPE_SECRET_KEY'] | ||||
|   stripe.api_version = app.config['STRIPE_API_VERSION'] | ||||
|  | ||||
|   if app.config['MAIL_SERVER'] != "": | ||||
|     app.config['FLASK_MAIL_INSTANCE'] = Mail(app) | ||||
| if app.config['SPOKE_MODE_ENABLED']: | ||||
|   if app.config['SPOKE_MODEL'] == "shell-scripts": | ||||
|     app.config['SPOKE_MODEL'] = spoke_model.ShellScriptSpoke() | ||||
|   else: | ||||
|     app.logger.warning("No MAIL_SERVER configured. capsul will simply print emails to stdout.") | ||||
|     app.config['FLASK_MAIL_INSTANCE'] = StdoutMockFlaskMail() | ||||
|     app.config['SPOKE_MODEL'] = spoke_model.MockSpoke() | ||||
|  | ||||
|   app.config['HTTP_CLIENT'] = MyHTTPClient(timeout_seconds=int(app.config['INTERNAL_HTTP_TIMEOUT_SECONDS'])) | ||||
|   from capsulflask import spoke_api | ||||
|  | ||||
|   app.config['BTCPAY_ENABLED'] = False | ||||
|   if app.config['BTCPAY_URL'] != "": | ||||
|     try: | ||||
|       app.config['BTCPAY_CLIENT'] = btcpay.Client(api_uri=app.config['BTCPAY_URL'], pem=app.config['BTCPAY_PRIVATE_KEY']) | ||||
|       app.config['BTCPAY_ENABLED'] = True | ||||
|     except: | ||||
|       app.logger.warning("unable to create btcpay client. Capsul will work fine except cryptocurrency payments will not work. The error was: " + my_exec_info_message(sys.exc_info())) | ||||
|   app.register_blueprint(spoke_api.bp) | ||||
|  | ||||
|     # only start the scheduler and attempt to migrate the database if we are running the app. | ||||
|     # otherwise we are running a CLI command. | ||||
|     command_line = ' '.join(sys.argv) | ||||
|     is_running_server = ( | ||||
|       ('flask run' in command_line) or | ||||
|       ('gunicorn' in command_line) or | ||||
|       ('test' in command_line) | ||||
|     ) | ||||
|  | ||||
|     app.logger.info(f"is_running_server: {is_running_server}") | ||||
|  | ||||
|     if app.config['HUB_MODE_ENABLED']: | ||||
|       if app.config['HUB_MODEL'] == "capsul-flask": | ||||
|         app.config['HUB_MODEL'] = hub_model.CapsulFlaskHub() | ||||
|  | ||||
|         # debug mode (flask reloader) runs two copies of the app. When running in debug mode, | ||||
|         # we only want to start the scheduler one time. | ||||
|         if is_running_server and (not app.debug or config.get('WERKZEUG_RUN_MAIN') == 'true'): | ||||
|           scheduler = BackgroundScheduler() | ||||
|           heartbeat_task_url = f"{app.config['HUB_URL']}/hub/heartbeat-task" | ||||
|           heartbeat_task_headers = {'Authorization': f"Bearer {app.config['HUB_TOKEN']}"} | ||||
|           heartbeat_task = lambda: requests.post(heartbeat_task_url, headers=heartbeat_task_headers) | ||||
|           scheduler.add_job(name="heartbeat-task", func=heartbeat_task, trigger="interval", seconds=5) | ||||
|           scheduler.start() | ||||
|  | ||||
|           atexit.register(lambda: scheduler.shutdown()) | ||||
|  | ||||
|       else: | ||||
|         app.config['HUB_MODEL'] = hub_model.MockHub() | ||||
|  | ||||
|       from capsulflask import db | ||||
|       db.init_app(app, is_running_server) | ||||
|  | ||||
|       from capsulflask import auth, landing, console, payment, metrics, cli, hub_api, admin | ||||
|  | ||||
|       app.register_blueprint(landing.bp) | ||||
|       app.register_blueprint(auth.bp) | ||||
|       app.register_blueprint(console.bp) | ||||
|       app.register_blueprint(payment.bp) | ||||
|       app.register_blueprint(metrics.bp) | ||||
|       app.register_blueprint(cli.bp) | ||||
|       app.register_blueprint(hub_api.bp) | ||||
|       app.register_blueprint(admin.bp) | ||||
|  | ||||
|       app.add_url_rule("/", endpoint="index") | ||||
|  | ||||
|     if app.config['SPOKE_MODE_ENABLED']: | ||||
|       if app.config['SPOKE_MODEL'] == "shell-scripts": | ||||
|         app.config['SPOKE_MODEL'] = spoke_model.ShellScriptSpoke() | ||||
|       else: | ||||
|         app.config['SPOKE_MODEL'] = spoke_model.MockSpoke() | ||||
|  | ||||
|       from capsulflask import spoke_api | ||||
|  | ||||
|       app.register_blueprint(spoke_api.bp) | ||||
|  | ||||
|     @app.after_request | ||||
|     def security_headers(response): | ||||
|       response.headers['X-Frame-Options'] = 'SAMEORIGIN' | ||||
|       if 'Content-Security-Policy' not in response.headers: | ||||
|         response.headers['Content-Security-Policy'] = "default-src 'self'" | ||||
|       response.headers['X-Content-Type-Options'] = 'nosniff' | ||||
|       return response | ||||
| @app.after_request | ||||
| def security_headers(response): | ||||
|   response.headers['X-Frame-Options'] = 'SAMEORIGIN' | ||||
|   if 'Content-Security-Policy' not in response.headers: | ||||
|     response.headers['Content-Security-Policy'] = "default-src 'self'" | ||||
|   response.headers['X-Content-Type-Options'] = 'nosniff' | ||||
|   return response | ||||
|  | ||||
|  | ||||
|     @app.context_processor | ||||
|     def override_url_for(): | ||||
|       """ | ||||
|       override the url_for function built into flask  | ||||
|       with our own custom implementation that busts the cache correctly when files change  | ||||
|       """ | ||||
|       return dict(url_for=url_for_with_cache_bust) | ||||
| @app.context_processor | ||||
| def override_url_for(): | ||||
|   """ | ||||
|   override the url_for function built into flask  | ||||
|   with our own custom implementation that busts the cache correctly when files change  | ||||
|   """ | ||||
|   return dict(url_for=url_for_with_cache_bust) | ||||
|  | ||||
|     return app | ||||
|  | ||||
| def url_for_with_cache_bust(endpoint, **values): | ||||
|   """ | ||||
| @ -242,31 +273,3 @@ def url_for_with_cache_bust(endpoint, **values): | ||||
|       values['q'] = current_app.config['STATIC_FILE_HASH_CACHE'][filename] | ||||
|  | ||||
|   return url_for(endpoint, **values) | ||||
|  | ||||
| class StdoutMockFlaskMail: | ||||
|   def send(self, message: Message): | ||||
|     current_app.logger.info(f"Email would have been sent if configured:\n\nto: {','.join(message.recipients)}\nsubject: {message.subject}\nbody:\n\n{message.body}\n\n") | ||||
|  | ||||
| class SetLogLevelToDebugForHeartbeatRelatedMessagesFilter(logging.Filter): | ||||
|   def isHeartbeatRelatedString(self, thing): | ||||
|     # thing_string = "<error>" | ||||
|     is_in_string = False | ||||
|     try: | ||||
|       thing_string = "%s" % thing | ||||
|       is_in_string = 'heartbeat-task' in thing_string or 'hub/heartbeat' in thing_string or 'spoke/heartbeat' in thing_string | ||||
|     except: | ||||
|       pass | ||||
|     # self.warning("isHeartbeatRelatedString(%s): %s", thing_string, is_in_string ) | ||||
|     return is_in_string | ||||
|  | ||||
|   def filter(self, record): | ||||
|     if not current_app or current_app.config['LOG_LEVEL'] == "DEBUG": | ||||
|       return True | ||||
|  | ||||
|     if self.isHeartbeatRelatedString(record.msg): | ||||
|       return False | ||||
|     for arg in record.args: | ||||
|       if self.isHeartbeatRelatedString(arg): | ||||
|         return False | ||||
|  | ||||
|     return True | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| from base64 import b64decode | ||||
| import functools | ||||
| import re | ||||
|  | ||||
| @ -24,6 +25,15 @@ def account_required(view): | ||||
|  | ||||
|     @functools.wraps(view) | ||||
|     def wrapped_view(**kwargs): | ||||
|         api_token = request.headers.get('authorization', None) | ||||
|         if api_token is not None: | ||||
|             email = get_model().authenticate_token(b64decode(api_token).decode('utf-8')) | ||||
|  | ||||
|             if email is not None: | ||||
|                 session.clear() | ||||
|                 session["account"] = email | ||||
|                 session["csrf-token"] = generate() | ||||
|  | ||||
|         if session.get("account") is None or session.get("csrf-token") is None : | ||||
|             return redirect(url_for("auth.login")) | ||||
|  | ||||
| @ -31,6 +41,7 @@ def account_required(view): | ||||
|  | ||||
|     return wrapped_view | ||||
|  | ||||
|  | ||||
| def admin_account_required(view): | ||||
|     """View decorator that redirects non-admin users to the login page.""" | ||||
|  | ||||
| @ -55,7 +66,7 @@ def login(): | ||||
|         if not email: | ||||
|             errors.append("email is required") | ||||
|         elif len(email.strip()) < 6 or email.count('@') != 1 or email.count('.') == 0:  | ||||
| 	        errors.append("enter a valid email address") | ||||
|             errors.append("enter a valid email address") | ||||
|  | ||||
|         if len(errors) == 0: | ||||
|             result = get_model().login(email) | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| from base64 import b64encode | ||||
| from datetime import datetime, timedelta | ||||
| import json | ||||
| import re | ||||
| import sys | ||||
| import json | ||||
| from datetime import datetime, timedelta | ||||
|  | ||||
| from flask import Blueprint | ||||
| from flask import flash | ||||
| from flask import current_app | ||||
| @ -98,7 +100,6 @@ def index(): | ||||
| @bp.route("/<string:id>", methods=("GET", "POST")) | ||||
| @account_required | ||||
| def detail(id): | ||||
|  | ||||
|   duration=request.args.get('duration') | ||||
|   if not duration: | ||||
|     duration = "5m" | ||||
| @ -188,6 +189,70 @@ def detail(id): | ||||
|       duration=duration | ||||
|     ) | ||||
|  | ||||
| def _create(vm_sizes, operating_systems, public_keys_for_account, affordable_vm_sizes, server_data): | ||||
|   errors = list() | ||||
|  | ||||
|   size = server_data.get("size") | ||||
|   os = server_data.get("os") | ||||
|   posted_keys_count = int(server_data.get("ssh_authorized_key_count")) | ||||
|  | ||||
|   if not size: | ||||
|     errors.append("Size is required") | ||||
|   elif size not in vm_sizes: | ||||
|     errors.append(f"Invalid size {size}") | ||||
|   elif size not in affordable_vm_sizes: | ||||
|     errors.append(f"Your account must have enough credit to run an {size} for 1 month before you will be allowed to create it") | ||||
|  | ||||
|  | ||||
|   if not os: | ||||
|     errors.append("OS is required") | ||||
|   elif os not in operating_systems: | ||||
|     errors.append(f"Invalid os {os}") | ||||
|  | ||||
|   posted_keys = list() | ||||
|  | ||||
|   if posted_keys_count > 1000: | ||||
|     errors.append("something went wrong with ssh keys") | ||||
|   else: | ||||
|     for i in range(0, posted_keys_count): | ||||
|       if f"ssh_key_{i}" in server_data: | ||||
|         posted_name = server_data.get(f"ssh_key_{i}") | ||||
|         key = None | ||||
|         for x in public_keys_for_account: | ||||
|           if x['name'] == posted_name: | ||||
|             key = x | ||||
|         if key: | ||||
|           posted_keys.append(key) | ||||
|         else: | ||||
|           errors.append(f"SSH Key \"{posted_name}\" doesn't exist") | ||||
|  | ||||
|   if len(posted_keys) == 0: | ||||
|     errors.append("At least one SSH Public Key is required") | ||||
|  | ||||
|   capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable( | ||||
|     vm_sizes[size]['memory_mb']*1024*1024 | ||||
|   ) | ||||
|  | ||||
|   if not capacity_avaliable: | ||||
|     errors.append(""" | ||||
|       host(s) at capacity. no capsuls can be created at this time. sorry.  | ||||
|     """) | ||||
|  | ||||
|   if len(errors) == 0: | ||||
|     id = make_capsul_id() | ||||
|     current_app.config["HUB_MODEL"].create( | ||||
|       email = session["account"], | ||||
|       id=id, | ||||
|       os=os, | ||||
|       size=size, | ||||
|       template_image_file_name=operating_systems[os]['template_image_file_name'], | ||||
|       vcpus=vm_sizes[size]['vcpus'], | ||||
|       memory_mb=vm_sizes[size]['memory_mb'], | ||||
|       ssh_authorized_keys=posted_keys | ||||
|     ) | ||||
|     return id, errors | ||||
|  | ||||
|   return None, errors | ||||
|  | ||||
| @bp.route("/create", methods=("GET", "POST")) | ||||
| @account_required | ||||
| @ -210,66 +275,16 @@ def create(): | ||||
|   if request.method == "POST": | ||||
|     if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: | ||||
|       return abort(418, f"u want tea") | ||||
|  | ||||
|     size = request.form["size"] | ||||
|     os = request.form["os"] | ||||
|     if not size: | ||||
|       errors.append("Size is required") | ||||
|     elif size not in vm_sizes: | ||||
|       errors.append(f"Invalid size {size}") | ||||
|     elif size not in affordable_vm_sizes: | ||||
|       errors.append(f"Your account must have enough credit to run an {size} for 1 month before you will be allowed to create it") | ||||
|  | ||||
|     if not os: | ||||
|       errors.append("OS is required") | ||||
|     elif os not in operating_systems: | ||||
|       errors.append(f"Invalid os {os}") | ||||
|  | ||||
|     posted_keys_count = int(request.form["ssh_authorized_key_count"]) | ||||
|     posted_keys = list() | ||||
|  | ||||
|     if posted_keys_count > 1000: | ||||
|       errors.append("something went wrong with ssh keys") | ||||
|     else: | ||||
|       for i in range(0, posted_keys_count): | ||||
|         if f"ssh_key_{i}" in request.form: | ||||
|           posted_name = request.form[f"ssh_key_{i}"] | ||||
|           key = None | ||||
|           for x in public_keys_for_account: | ||||
|             if x['name'] == posted_name: | ||||
|               key = x | ||||
|           if key: | ||||
|             posted_keys.append(key) | ||||
|           else: | ||||
|             errors.append(f"SSH Key \"{posted_name}\" doesn't exist") | ||||
|  | ||||
|     if len(posted_keys) == 0: | ||||
|       errors.append("At least one SSH Public Key is required") | ||||
|  | ||||
|     capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(vm_sizes[size]['memory_mb']*1024*1024) | ||||
|  | ||||
|     if not capacity_avaliable: | ||||
|       errors.append(""" | ||||
|         host(s) at capacity. no capsuls can be created at this time. sorry.  | ||||
|       """) | ||||
|  | ||||
|     id, errors = _create( | ||||
|       vm_sizes,  | ||||
|       operating_systems, | ||||
|       public_keys_for_account, | ||||
|       affordable_vm_sizes, | ||||
|       request.form) | ||||
|     if len(errors) == 0:  | ||||
|       id = make_capsul_id() | ||||
|       # we can't create the vm record in the DB yet because its IP address needs to be allocated first. | ||||
|       # so it will be created when the allocation happens inside the hub_api. | ||||
|       current_app.config["HUB_MODEL"].create( | ||||
|         email = session["account"], | ||||
|         id=id, | ||||
|         os=os, | ||||
|         size=size, | ||||
|         template_image_file_name=operating_systems[os]['template_image_file_name'], | ||||
|         vcpus=vm_sizes[size]['vcpus'], | ||||
|         memory_mb=vm_sizes[size]['memory_mb'], | ||||
|         ssh_authorized_keys=list(map(lambda x: dict(name=x['name'], content=x['content']), posted_keys))  | ||||
|       ) | ||||
|        | ||||
|       return redirect(f"{url_for('console.index')}?created={id}") | ||||
|    | ||||
|  | ||||
|   for error in errors: | ||||
|     flash(error) | ||||
|  | ||||
| @ -289,23 +304,25 @@ def create(): | ||||
|     vm_sizes=affordable_vm_sizes | ||||
|   ) | ||||
|  | ||||
| @bp.route("/ssh", methods=("GET", "POST")) | ||||
| @bp.route("/keys", methods=("GET", "POST")) | ||||
| @account_required | ||||
| def ssh_public_keys(): | ||||
| def ssh_api_keys(): | ||||
|   errors = list() | ||||
|  | ||||
|   token = None | ||||
|  | ||||
|   if request.method == "POST": | ||||
|     if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: | ||||
|       return abort(418, f"u want tea") | ||||
|        | ||||
|     method = request.form["method"] | ||||
|     content = None | ||||
|     if method == "POST": | ||||
|     action = request.form["action"] | ||||
|  | ||||
|     if action == 'upload_ssh_key': | ||||
|       content = None | ||||
|       content = request.form["content"].replace("\r", " ").replace("\n", " ").strip() | ||||
|          | ||||
|     name = request.form["name"] | ||||
|     if not name or len(name.strip()) < 1: | ||||
|       if method == "POST": | ||||
|       name = request.form["name"] | ||||
|       if not name or len(name.strip()) < 1: | ||||
|         parts = re.split(" +", content) | ||||
|         if len(parts) > 2 and len(parts[2].strip()) > 0: | ||||
|           name = parts[2].strip() | ||||
| @ -313,10 +330,9 @@ def ssh_public_keys(): | ||||
|           name = parts[0].strip() | ||||
|       else: | ||||
|         errors.append("Name is required") | ||||
|     if not re.match(r"^[0-9A-Za-z_@:. -]+$", name): | ||||
|       errors.append(f"Key name '{name}' must match \"^[0-9A-Za-z_@:. -]+$\"") | ||||
|       if not re.match(r"^[0-9A-Za-z_@:. -]+$", name): | ||||
|         errors.append(f"Key name '{name}' must match \"^[0-9A-Za-z_@:. -]+$\"") | ||||
|  | ||||
|     if method == "POST": | ||||
|       if not content or len(content.strip()) < 1: | ||||
|         errors.append("Content is required") | ||||
|       else: | ||||
| @ -329,31 +345,47 @@ def ssh_public_keys(): | ||||
|       if len(errors) == 0: | ||||
|         get_model().create_ssh_public_key(session["account"], name, content) | ||||
|  | ||||
|     elif method == "DELETE": | ||||
|     elif action == "delete_ssh_key": | ||||
|       get_model().delete_ssh_public_key(session["account"], name) | ||||
|  | ||||
|       if len(errors) == 0: | ||||
|         get_model().delete_ssh_public_key(session["account"], name) | ||||
|     elif action == "generate_api_token": | ||||
|       name = request.form["name"] | ||||
|       if name == '': | ||||
|           name = datetime.utcnow().strftime('%y-%m-%d %H:%M:%S') | ||||
|       token = b64encode( | ||||
|         get_model().generate_api_token(session["account"], name).encode('utf-8') | ||||
|       ).decode('utf-8') | ||||
|  | ||||
|     elif action == "delete_api_token": | ||||
|       get_model().delete_api_token(session["account"], request.form["id"]) | ||||
|  | ||||
|   for error in errors: | ||||
|     flash(error) | ||||
|  | ||||
|   keys_list=list(map( | ||||
|   ssh_keys_list=list(map( | ||||
|     lambda x: dict(name=x['name'], content=f"{x['content'][:20]}...{x['content'][len(x['content'])-20:]}"),  | ||||
|     get_model().list_ssh_public_keys_for_account(session["account"]) | ||||
|   )) | ||||
|  | ||||
|   api_tokens_list = get_model().list_api_tokens(session["account"]) | ||||
|  | ||||
|   return render_template( | ||||
|     "ssh-public-keys.html",  | ||||
|     "keys.html",  | ||||
|     csrf_token = session["csrf-token"], | ||||
|     ssh_public_keys=keys_list,  | ||||
|     has_ssh_public_keys=len(keys_list) > 0 | ||||
|     api_tokens=api_tokens_list,  | ||||
|     ssh_public_keys=ssh_keys_list, | ||||
|     generated_api_token=token, | ||||
|   ) | ||||
|  | ||||
| def get_vms(): | ||||
|   return get_model().list_vms_for_account(session["account"]) | ||||
|   if 'user_vms' not in g: | ||||
|     g.user_vms = get_model().list_vms_for_account(session["account"]) | ||||
|   return g.user_vms | ||||
|  | ||||
| def get_payments(): | ||||
|   return get_model().list_payments_for_account(session["account"]) | ||||
|   if 'user_payments' not in g: | ||||
|     g.user_payments = get_model().list_payments_for_account(session["account"]) | ||||
|   return g.user_payments | ||||
|  | ||||
|  | ||||
| average_number_of_days_in_a_month = 30.44 | ||||
| @ -366,7 +398,6 @@ def get_vm_months_float(vm, as_of): | ||||
|   return days / average_number_of_days_in_a_month | ||||
|  | ||||
| def get_account_balance(vms, payments, as_of): | ||||
|  | ||||
|   vm_cost_dollars = 0.0 | ||||
|   for vm in vms: | ||||
|     vm_months = get_vm_months_float(vm, as_of) | ||||
| @ -379,7 +410,6 @@ def get_account_balance(vms, payments, as_of): | ||||
| @bp.route("/account-balance") | ||||
| @account_required | ||||
| def account_balance(): | ||||
|  | ||||
|   payment_sessions = get_model().list_payment_sessions_for_account(session['account']) | ||||
|   for payment_session in payment_sessions: | ||||
|     if payment_session['type'] == 'btcpay': | ||||
| @ -405,7 +435,7 @@ def account_balance(): | ||||
|  | ||||
|   vms_billed = list() | ||||
|  | ||||
|   for vm in vms: | ||||
|   for vm in get_vms(): | ||||
|     vm_months = get_vm_months_float(vm, datetime.utcnow()) | ||||
|     vms_billed.append(dict( | ||||
|       id=vm["id"],  | ||||
|  | ||||
| @ -33,7 +33,7 @@ def init_app(app, is_running_server): | ||||
|     result = re.search(r"^\d+_(up|down)", filename) | ||||
|     if not result: | ||||
|       app.logger.error(f"schemaVersion {filename} must match ^\\d+_(up|down). exiting.") | ||||
|       exit(1) | ||||
|       continue | ||||
|     key = result.group() | ||||
|     with open(join(schemaMigrationsPath, filename), 'rb') as file: | ||||
|       schemaMigrations[key] = file.read().decode("utf8") | ||||
| @ -43,7 +43,7 @@ def init_app(app, is_running_server): | ||||
|   hasSchemaVersionTable = False | ||||
|   actionWasTaken = False | ||||
|   schemaVersion = 0 | ||||
|   desiredSchemaVersion = 18 | ||||
|   desiredSchemaVersion = 19 | ||||
|  | ||||
|   cursor = connection.cursor() | ||||
|  | ||||
| @ -128,4 +128,3 @@ def close_db(e=None): | ||||
|   if db_model is not None: | ||||
|     db_model.cursor.close() | ||||
|     current_app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db_model.connection) | ||||
|  | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
|  | ||||
| import re | ||||
|  | ||||
| # I was never able to get this type hinting to work correctly  | ||||
| # from psycopg2.extensions import connection as Psycopg2Connection, cursor as Psycopg2Cursor | ||||
| import hashlib | ||||
| from nanoid import generate | ||||
| from flask import current_app | ||||
| from typing import List | ||||
| @ -17,7 +17,6 @@ class DBModel: | ||||
|     self.cursor = cursor | ||||
|  | ||||
|  | ||||
|  | ||||
|   #     ------    LOGIN     ---------      | ||||
|  | ||||
|  | ||||
| @ -44,6 +43,16 @@ class DBModel: | ||||
|  | ||||
|     return (token, ignoreCaseMatches) | ||||
|  | ||||
|   def authenticate_token(self, token): | ||||
|     m = hashlib.md5() | ||||
|     m.update(token.encode('utf-8')) | ||||
|     hash_token = m.hexdigest() | ||||
|     self.cursor.execute("SELECT email FROM api_tokens WHERE token = %s", (hash_token, )) | ||||
|     result = self.cursor.fetchall() | ||||
|     if len(result) == 1: | ||||
|       return result[0] | ||||
|     return None | ||||
|      | ||||
|   def consume_token(self, token): | ||||
|     self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, )) | ||||
|     row = self.cursor.fetchone() | ||||
| @ -132,6 +141,32 @@ class DBModel: | ||||
|     self.cursor.execute( "DELETE FROM ssh_public_keys where email = %s AND name = %s", (email, name) ) | ||||
|     self.connection.commit() | ||||
|  | ||||
|   def list_api_tokens(self, email): | ||||
|     self.cursor.execute( | ||||
|       "SELECT id, token, name, created FROM api_tokens WHERE email = %s",  | ||||
|       (email, ) | ||||
|     ) | ||||
|     return list(map( | ||||
|       lambda x: dict(id=x[0], token=x[1], name=x[2], created=x[3]),  | ||||
|       self.cursor.fetchall() | ||||
|     )) | ||||
|  | ||||
|   def generate_api_token(self, email, name): | ||||
|     token = generate() | ||||
|     m = hashlib.md5() | ||||
|     m.update(token.encode('utf-8')) | ||||
|     hash_token = m.hexdigest() | ||||
|     self.cursor.execute( | ||||
|       "INSERT INTO api_tokens (email, name, token) VALUES (%s, %s, %s)",  | ||||
|       (email, name, hash_token) | ||||
|     ) | ||||
|     self.connection.commit() | ||||
|     return token | ||||
|  | ||||
|   def delete_api_token(self, email, id_): | ||||
|     self.cursor.execute( "DELETE FROM api_tokens where email = %s AND id = %s", (email, id_)) | ||||
|     self.connection.commit() | ||||
|  | ||||
|   def list_vms_for_account(self, email): | ||||
|     self.cursor.execute("""  | ||||
|       SELECT vms.id, vms.public_ipv4, vms.public_ipv6, vms.size, vms.os, vms.created, vms.deleted, vm_sizes.dollars_per_month | ||||
| @ -479,8 +514,3 @@ class DBModel: | ||||
|     #cursor.close() | ||||
|  | ||||
|     return to_return | ||||
|        | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -44,6 +44,7 @@ class MockHub(VirtualizationInterface): | ||||
|     validate_capsul_id(id) | ||||
|     current_app.logger.info(f"mock create: {id} for {email}") | ||||
|     sleep(1) | ||||
|  | ||||
|     get_model().create_vm( | ||||
|       email=email,  | ||||
|       id=id,  | ||||
|  | ||||
							
								
								
									
										49
									
								
								capsulflask/publicapi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								capsulflask/publicapi.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| import datetime | ||||
|  | ||||
| from flask import Blueprint | ||||
| from flask import current_app | ||||
| from flask import jsonify | ||||
| from flask import request | ||||
| from flask import session | ||||
| from nanoid import generate | ||||
|  | ||||
| from capsulflask.auth import account_required | ||||
| from capsulflask.db import get_model | ||||
|  | ||||
| bp = Blueprint("publicapi", __name__, url_prefix="/api") | ||||
|  | ||||
| @bp.route("/capsul/create", methods=["POST"]) | ||||
| @account_required | ||||
| def capsul_create(): | ||||
|     email = session["account"] | ||||
|      | ||||
|     from .console import _create, get_account_balance, get_payments, get_vms | ||||
|  | ||||
|     vm_sizes = get_model().vm_sizes_dict() | ||||
|     operating_systems = get_model().operating_systems_dict() | ||||
|     public_keys_for_account = get_model().list_ssh_public_keys_for_account(session["account"]) | ||||
|     account_balance = get_account_balance(get_vms(), get_payments(), datetime.datetime.utcnow()) | ||||
|     capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(512*1024*1024) | ||||
|  | ||||
|     affordable_vm_sizes = dict() | ||||
|     for key, vm_size in vm_sizes.items(): | ||||
|       # if a user deposits $7.50 and then creates an f1-s vm which costs 7.50 a month,  | ||||
|       # then they have to delete the vm and re-create it, they will not be able to, they will have to pay again. | ||||
|       # so for UX it makes a lot of sense to give a small margin of 25 cents for usability sake | ||||
|       if vm_size["dollars_per_month"] <= account_balance+0.25: | ||||
|         affordable_vm_sizes[key] = vm_size | ||||
|  | ||||
|     request.json['ssh_authorized_key_count'] = 1 | ||||
|  | ||||
|     id, errors = _create( | ||||
|       vm_sizes,  | ||||
|       operating_systems, | ||||
|       public_keys_for_account, | ||||
|       affordable_vm_sizes, | ||||
|       request.json) | ||||
|  | ||||
|     if id is not None: | ||||
|         return jsonify( | ||||
|             id=id, | ||||
|         ) | ||||
|     return jsonify(errors=errors) | ||||
							
								
								
									
										2
									
								
								capsulflask/schema_migrations/19_down_api_tokens.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								capsulflask/schema_migrations/19_down_api_tokens.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| DROP TABLE api_keys; | ||||
| UPDATE schemaversion SET version = 18; | ||||
							
								
								
									
										9
									
								
								capsulflask/schema_migrations/19_up_api_tokens.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								capsulflask/schema_migrations/19_up_api_tokens.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| CREATE TABLE api_tokens ( | ||||
|   id                 SERIAL PRIMARY KEY, | ||||
|   email              TEXT REFERENCES accounts(email) ON DELETE RESTRICT, | ||||
|   name               TEXT NOT NULL, | ||||
|   created            TIMESTAMP NOT NULL DEFAULT NOW(), | ||||
|   token              TEXT NOT NULL | ||||
| ); | ||||
|  | ||||
| UPDATE schemaversion SET version = 19; | ||||
| @ -31,7 +31,7 @@ | ||||
|  | ||||
|     {% if session["account"] %}  | ||||
|       <a href="/console">Capsuls</a> | ||||
|       <a href="/console/ssh">SSH Public Keys</a> | ||||
|       <a href="/console/keys">SSH & API Keys</a> | ||||
|       <a href="/console/account-balance">Account Balance</a> | ||||
|     {% endif %} | ||||
|  | ||||
|  | ||||
| @ -101,7 +101,7 @@ | ||||
|       </div> | ||||
|       <div class="row justify-start"> | ||||
|         <label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label> | ||||
|         <a id="ssh_authorized_keys" href="/console/ssh">{{ vm['ssh_authorized_keys'] }}</a> | ||||
|         <a id="ssh_authorized_keys" href="/console/keys">{{ vm['ssh_authorized_keys'] }}</a> | ||||
|       </div> | ||||
|        | ||||
|     </div> | ||||
|  | ||||
| @ -31,7 +31,7 @@ | ||||
|     <p>(At least one month of funding is required)</p> | ||||
|   {% elif no_ssh_public_keys %} | ||||
|     <p>You don't have any ssh public keys yet.</p>  | ||||
|     <p>You must <a href="/console/ssh">upload one</a> before you can create a Capsul.</p> | ||||
|     <p>You must <a href="/console/keys">upload one</a> before you can create a Capsul.</p> | ||||
|   {% elif not capacity_avaliable %} | ||||
|     <p>Host(s) at capacity. No capsuls can be created at this time. sorry. </p>  | ||||
|   {% else %} | ||||
|  | ||||
| @ -1,17 +1,18 @@ | ||||
| {% extends 'base.html' %} | ||||
| 
 | ||||
| {% block title %}SSH Public Keys{% endblock %} | ||||
| {% block title %}SSH & API Keys{% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| <div class="row third-margin"> | ||||
|   <h1>SSH PUBLIC KEYS</h1> | ||||
| </div> | ||||
| <div class="row third-margin"><div> | ||||
|   {% if has_ssh_public_keys %} <hr/> {% endif %} | ||||
|     {% if ssh_public_keys|length > 0 %} <hr/> {% endif %} | ||||
|    | ||||
|   {% for ssh_public_key in ssh_public_keys %} | ||||
|     <form method="post"> | ||||
|       <input type="hidden" name="method" value="DELETE"></input>  | ||||
|       <input type="hidden" name="action" value="delete_ssh_key"></input> | ||||
|       <input type="hidden" name="name" value="{{ ssh_public_key['name'] }}"></input>  | ||||
|       <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> | ||||
|       <div class="row"> | ||||
| @ -22,13 +23,14 @@ | ||||
|     </form> | ||||
|   {% endfor %} | ||||
| 
 | ||||
|   {% if has_ssh_public_keys %} <hr/> {% endif %} | ||||
|   {% if ssh_public_keys|length > 0 %} <hr/> {% endif %} | ||||
| 
 | ||||
|   <div class="third-margin"> | ||||
|     <h1>UPLOAD A NEW SSH PUBLIC KEY</h1> | ||||
|   </div> | ||||
|   <form method="post"> | ||||
|     <input type="hidden" name="method" value="POST"></input> | ||||
|     <input type="hidden" name="action" value="upload_ssh_key"></input> | ||||
|     <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> | ||||
|     <div class="row justify-start"> | ||||
|       <label class="align" for="content">File Contents</label> | ||||
| @ -54,6 +56,51 @@ | ||||
|     </div> | ||||
|   </form> | ||||
| </div></div> | ||||
| <hr/> | ||||
| <div class="row third-margin"> | ||||
|   <h1>API KEYS</h1> | ||||
| </div> | ||||
| <div class="row third-margin"><div> | ||||
|   {% if generated_api_token %} | ||||
|     <hr/> | ||||
|     Generated key: | ||||
|     <span class="code">{{ generated_api_token }}</span> | ||||
|   {% endif %} | ||||
|   {% if api_tokens|length >0 %} <hr/>{% endif %} | ||||
|   {% for api_token in api_tokens %} | ||||
|     <form method="post"> | ||||
|       <input type="hidden" name="method" value="DELETE"></input>  | ||||
|       <input type="hidden" name="action" value="delete_api_token"></input> | ||||
|       <input type="hidden" name="id" value="{{ api_token['id'] }}"></input>  | ||||
|       <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> | ||||
|       <div class="row"> | ||||
|         <span class="code">{{ api_token['name'] }}</span> | ||||
|         created {{ api_token['created'].strftime("%b %d %Y") }} | ||||
|         <input type="submit" value="Delete"> | ||||
|       </div> | ||||
|     </form> | ||||
|   {% endfor %} | ||||
|   {% if api_tokens|length >0 %} <hr/>{% endif %} | ||||
| 
 | ||||
|   <div class="third-margin"> | ||||
|     <h1>GENERATE A NEW API KEY</h1> | ||||
|   </div> | ||||
|   <form method="post"> | ||||
|     <input type="hidden" name="method" value="POST"></input> | ||||
|     <input type="hidden" name="action" value="generate_api_token"></input> | ||||
|     <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> | ||||
|     <div class="smalltext"> | ||||
|       <p>Generate a new API key, to integrate with other systems.</p> | ||||
|     </div> | ||||
|     <div class="row justify-start"> | ||||
|       <label class="align" for="name">Key Name</label> | ||||
|       <input type="text" id="name" name="name"></input> (defaults to creation time) | ||||
|     </div> | ||||
|     <div class="row justify-end"> | ||||
|       <input type="submit" value="Generate"> | ||||
|     </div> | ||||
|   </form> | ||||
| </div></div> | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block pagesource %}/templates/ssh-public-keys.html{% endblock %} | ||||
| @ -1,23 +0,0 @@ | ||||
| from flask import url_for, session | ||||
|  | ||||
| from capsulflask.db import get_model | ||||
| from capsulflask.tests_base import BaseTestCase | ||||
|  | ||||
|  | ||||
| class LoginTests(BaseTestCase): | ||||
|     render_templates = False | ||||
|  | ||||
|     def test_login_request(self): | ||||
|         with self.client as client: | ||||
|             response = client.get(url_for("auth.login")) | ||||
|             self.assert_200(response) | ||||
|  | ||||
|             # FIXME test generated login link | ||||
|  | ||||
|     def test_login_magiclink(self): | ||||
|         token, ignoreCaseMatches = get_model().login('test@example.com') | ||||
|  | ||||
|         with self.client as client: | ||||
|             response = client.get(url_for("auth.magiclink", token=token)) | ||||
|             self.assertRedirects(response, url_for("console.index")) | ||||
|             self.assertEqual(session['account'], 'test@example.com') | ||||
| @ -1,190 +0,0 @@ | ||||
| from copy import deepcopy | ||||
|  | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from flask import url_for | ||||
|  | ||||
| from capsulflask.db import get_model | ||||
| from capsulflask.tests_base import BaseTestCase | ||||
| from capsulflask.hub_model import MockHub | ||||
|  | ||||
|  | ||||
| class ConsoleTests(BaseTestCase): | ||||
|     capsul_data = { | ||||
|         "size": "f1-xs", | ||||
|         "os": "debian10", | ||||
|         "ssh_authorized_key_count": 1, | ||||
|         "ssh_key_0": "key" | ||||
|     } | ||||
|  | ||||
|     ssh_key_data = { | ||||
|         "name": "key2", | ||||
|         "method": "POST", | ||||
|         "content": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDntq1t8Ddsa2q4p+PM7W4CLYYmxakokRRVLlf7AQlsTJFPsgBe9u0zuoOaKDMkBr0dlnuLm4Eub1Mj+BrdqAokto0YDiAnxUKRuYQKuHySKK8bLkisi2k47jGBDikx/jihgiuFTawo1mYsJJepC7PPwZGsoCImJEgq1L+ug0p3Zrj3QkUx4h25MpCSs2yvfgWjDyN8hEC76O42P+4ETezYrzrd1Kj26hdzHRnrxygvIUOtfau+5ydlaz8xQBEPrEY6/+pKDuwtXg1pBL7GmoUxBXVfHQSgq5s9jIJH+G0CR0ZoHMB25Ln4X/bsCQbLOu21+IGYKSDVM5TIMLtkKUkERQMVWvnpOp1LZKir4dC0m7SW74wpA8+2b1IsURIr9ARYGJpCEv1Q1Wz/X3yTf6Mfey7992MjUc9HcgjgU01/+kYomoXHprzolk+22Gjfgo3a4dRIoTY82GO8kkUKiaWHvDkkVURCY5dpteLA05sk3Z9aRMYsNXPLeOOPfzTlDA0=" | ||||
|     } | ||||
|  | ||||
|     def test_index(self): | ||||
|         self._login('test@example.com') | ||||
|         with self.client as client: | ||||
|             response = client.get(url_for("console.index")) | ||||
|             self.assert_200(response) | ||||
|  | ||||
|     def test_create_loads(self): | ||||
|         self._login('test@example.com') | ||||
|         with self.client as client: | ||||
|             response = client.get(url_for("console.create")) | ||||
|             self.assert_200(response) | ||||
|  | ||||
|     def test_create_fails_credit(self): | ||||
|         with self.client as client: | ||||
|             client.get(url_for("console.create")) | ||||
|             csrf_token = self.get_context_variable('csrf_token') | ||||
|  | ||||
|             data = self.capsul_data | ||||
|             data['csrf-token'] = csrf_token | ||||
|             client.post(url_for("console.create"), data=data) | ||||
|  | ||||
|             capacity_message = \ | ||||
|                 'Your account must have enough credit to run an f1-xs for 1 month before you will be allowed to create it' | ||||
|             self.assert_message_flashed(capacity_message, category='message') | ||||
|  | ||||
|             self.assertEqual( | ||||
|                 len(get_model().list_vms_for_account('test@example.com')), | ||||
|                 0 | ||||
|             ) | ||||
|  | ||||
|  | ||||
|     def test_create_fails_capacity(self): | ||||
|         with self.client as client: | ||||
|             client.get(url_for("console.create")) | ||||
|             csrf_token = self.get_context_variable('csrf_token') | ||||
|  | ||||
|             data = self.capsul_data | ||||
|             data['csrf-token'] = csrf_token | ||||
|                  | ||||
|             get_model().create_payment_session('fake', 'test', 'test@example.com', 20) | ||||
|             get_model().consume_payment_session('fake', 'test', 20) | ||||
|  | ||||
|             with patch.object(MockHub, 'capacity_avaliable', return_value=False) as mock_method: | ||||
|                 response = client.post(url_for("console.create"), data=data) | ||||
|  | ||||
|             mock_method.assert_called() | ||||
|  | ||||
|             capacity_message = \ | ||||
|                 '\n        host(s) at capacity. no capsuls can be created at this time. sorry. \n      ' | ||||
|             self.assert_message_flashed(capacity_message, category='message') | ||||
|  | ||||
|             self.assertEqual( | ||||
|                 len(get_model().list_vms_for_account('test@example.com')), | ||||
|                 0 | ||||
|             ) | ||||
|  | ||||
|     def test_create_fails_invalid(self): | ||||
|         with self.client as client: | ||||
|             client.get(url_for("console.create")) | ||||
|             csrf_token = self.get_context_variable('csrf_token') | ||||
|  | ||||
|             data = deepcopy(self.capsul_data) | ||||
|             data['csrf-token'] = csrf_token | ||||
|             data['os'] = '' | ||||
|             client.post(url_for("console.create"), data=data) | ||||
|  | ||||
|             self.assert_message_flashed( | ||||
|                 'OS is required',  | ||||
|                 category='message' | ||||
|             ) | ||||
|  | ||||
|             self.assertEqual( | ||||
|                 len(get_model().list_vms_for_account('test@example.com')), | ||||
|                 0 | ||||
|             ) | ||||
|  | ||||
|     def test_create_succeeds(self): | ||||
|         with self.client as client: | ||||
|             client.get(url_for("console.create")) | ||||
|             csrf_token = self.get_context_variable('csrf_token') | ||||
|  | ||||
|             data = deepcopy(self.capsul_data) | ||||
|             data['csrf-token'] = csrf_token | ||||
|  | ||||
|             get_model().create_payment_session('fake', 'test', 'test@example.com', 20) | ||||
|             get_model().consume_payment_session('fake', 'test', 20) | ||||
|  | ||||
|             response = client.post(url_for("console.create"), data=data) | ||||
|  | ||||
|             vms = get_model().list_vms_for_account('test@example.com') | ||||
|             self.assertEqual( | ||||
|                 len(vms), | ||||
|                 1  | ||||
|             ) | ||||
|              | ||||
|             vm_id = vms[0]['id'] | ||||
|              | ||||
|             self.assertRedirects( | ||||
|                 response,  | ||||
|                 url_for("console.index") + f'?created={vm_id}' | ||||
|             ) | ||||
|  | ||||
|     def test_keys_loads(self): | ||||
|         self._login('test@example.com') | ||||
|         with self.client as client: | ||||
|             response = client.get(url_for("console.ssh_public_keys")) | ||||
|             self.assert_200(response) | ||||
|             keys = self.get_context_variable('ssh_public_keys') | ||||
|             self.assertEqual(keys[0]['name'], 'key') | ||||
|  | ||||
|     def test_keys_add_fails_invalid(self): | ||||
|         self._login('test@example.com') | ||||
|         with self.client as client: | ||||
|             client.get(url_for("console.ssh_public_keys")) | ||||
|             csrf_token = self.get_context_variable('csrf_token') | ||||
|  | ||||
|             data = self.ssh_key_data | ||||
|             data['csrf-token'] = csrf_token | ||||
|  | ||||
|             data_invalid_content = data | ||||
|             data_invalid_content['content'] = 'foo' | ||||
|             client.post( | ||||
|                 url_for("console.ssh_public_keys"), | ||||
|                 data=data_invalid_content | ||||
|             ) | ||||
|  | ||||
|             self.assert_message_flashed( | ||||
|                 'Content must match "^(ssh|ecdsa)-[0-9A-Za-z+/_=@:. -]+$"', | ||||
|                 category='message' | ||||
|             ) | ||||
|  | ||||
|             data_missing_content = data | ||||
|             data_missing_content['content'] = '' | ||||
|             client.post(url_for("console.ssh_public_keys"), data=data_missing_content) | ||||
|  | ||||
|             self.assert_message_flashed( | ||||
|                 'Content is required', category='message' | ||||
|             ) | ||||
|  | ||||
|     def test_keys_add_fails_duplicate(self): | ||||
|         self._login('test@example.com') | ||||
|         with self.client as client: | ||||
|             client.get(url_for("console.ssh_public_keys")) | ||||
|             csrf_token = self.get_context_variable('csrf_token') | ||||
|  | ||||
|             data = self.ssh_key_data | ||||
|             data['csrf-token'] = csrf_token | ||||
|             data['name'] = 'key' | ||||
|             client.post(url_for("console.ssh_public_keys"), data=data) | ||||
|  | ||||
|             self.assert_message_flashed( | ||||
|                 'A key with that name already exists', | ||||
|                 category='message' | ||||
|             ) | ||||
|  | ||||
|     def setUp(self): | ||||
|         self._login('test@example.com') | ||||
|         get_model().create_ssh_public_key('test@example.com', 'key', 'foo')  | ||||
|  | ||||
|     def tearDown(self): | ||||
|         get_model().cursor.execute("DELETE FROM ssh_public_keys") | ||||
|         get_model().cursor.execute("DELETE FROM login_tokens") | ||||
|         get_model().cursor.execute("DELETE FROM vms") | ||||
|         get_model().cursor.execute("DELETE FROM payments") | ||||
|         get_model().cursor.connection.commit() | ||||
| @ -1,14 +0,0 @@ | ||||
| from capsulflask.tests_base import BaseTestCase | ||||
|  | ||||
|  | ||||
| class LandingTests(BaseTestCase): | ||||
|     #: Do not render templates, we're only testing logic here. | ||||
|     render_templates = False | ||||
|  | ||||
|     def test_landing(self): | ||||
|         pages = ['/', 'pricing', 'faq', 'about-ssh', 'changelog', 'support'] | ||||
|  | ||||
|         with self.client as client: | ||||
|             for page in pages: | ||||
|                 response = client.get(page) | ||||
|                 self.assert_200(response) | ||||
| @ -1,28 +0,0 @@ | ||||
| import os | ||||
| from nanoid import generate | ||||
|  | ||||
| from flask_testing import TestCase | ||||
|  | ||||
| from capsulflask import create_app | ||||
| from capsulflask.db import get_model | ||||
|  | ||||
| class BaseTestCase(TestCase): | ||||
|     def create_app(self): | ||||
|         # Use default connection paramaters | ||||
|         os.environ['POSTGRES_CONNECTION_PARAMETERS'] = "host=localhost port=5432 user=postgres password=dev dbname=capsulflask_test" | ||||
|         os.environ['TESTING'] = '1' | ||||
|         os.environ['SPOKE_MODEL'] = 'mock' | ||||
|         os.environ['HUB_MODEL'] = 'mock' | ||||
|         return create_app() | ||||
|  | ||||
|     def setUp(self): | ||||
|         pass | ||||
|  | ||||
|     def tearDown(self): | ||||
|         pass | ||||
|  | ||||
|     def _login(self, user_email): | ||||
|         get_model().login(user_email) | ||||
|         with self.client.session_transaction() as session: | ||||
|             session['account'] = user_email | ||||
|             session['csrf-token'] = generate() | ||||
| @ -1,32 +0,0 @@ | ||||
| ## automated testing  | ||||
|  | ||||
| Automated tests could make it safer to contribute code, easier to review new code, and much easier to refactor, or upgrade Python dependencies. | ||||
|  | ||||
| To run tests:  | ||||
| 1. create a Postgres database called `capsulflask_test` | ||||
|   - e.g.: `docker exec -it d1702306f409 psql -U postgres createdb -O postgres capsulflask_test;` | ||||
|     - (`d1702306f409` is the docker container ID of the postgres container) | ||||
| 2. run `python -m unittest` | ||||
|  | ||||
| ### Architecture | ||||
|  | ||||
| I tried to make the absolute minimal changes to be able to override settings in tests – possible alternative approaches include accepting an argument to create_app() to define which env file to load, or adding conditional logic to create_app() to pre-load specific settings before running load_dotenv() – but allowing env vars to override dotenv vars seemed cleanest. (Thanks @forest for improving on this approach) | ||||
|  | ||||
| ### Creating test databases | ||||
|  | ||||
| One outstanding question is how to initialise/reinitialise the test database. | ||||
|  | ||||
| Currently, the tests rely on the existence of a capsulflask_test database on localhost, accessible by the postgres user with password dev. | ||||
|  | ||||
| I create this manually using: | ||||
| `docker exec -it d1702306f409 psql -U postgres createdb -O postgres capsulflask_test;` | ||||
|  | ||||
| where `d1702306f409` is the docker container ID of the postgres container.  | ||||
|  | ||||
| In between test runs, you can either drop and recreate that database, or manually clear data using: | ||||
|  | ||||
| `docker exec -it d1702306f409 psql -U postgres capsulflask_test -c "DELETE FROM vms; DELETE FROM login_tokens; DELETE FROM ssh_public_keys; DELETE FROM api_tokens; DELETE FROM accounts;` | ||||
|  | ||||
| ### Test coverage | ||||
|  | ||||
| This tests the "landing" (public) pages, login, capsul index and creation. I didn't add automated coverage reporting yet, unclear if that seems useful. | ||||
		Reference in New Issue
	
	Block a user
	