diff --git a/.prod.env b/.prod.env index 8f52df39..b6b3dc2a 100644 --- a/.prod.env +++ b/.prod.env @@ -61,6 +61,12 @@ SECRET_KEY=fixme #BEARER_TOKEN_EXPIRATION=3600 * 12 # in seconds +#SECURITY_BEARER_SALT=NODEFAULT +SECURITY_BEARER_SALT=fixme + +#SECURITY_EMAIL_SALT=NODEFAULT +SECURITY_EMAIL_SALT=fixme + #SECURITY_PASSWORD_SALT=NODEFAULT SECURITY_PASSWORD_SALT=fixme diff --git a/server/.test.env b/server/.test.env index d83920b8..908db5b9 100644 --- a/server/.test.env +++ b/server/.test.env @@ -20,3 +20,6 @@ GLOBAL_WORKSPACE='mergin' GLOBAL_STORAGE=104857600 COLLECT_STATISTICS=0 GEODIFF_WORKING_DIR=/tmp/geodiff +SECURITY_BEARER_SALT='bearer' +SECURITY_EMAIL_SALT='email' +SECURITY_PASSWORD_SALT='password' diff --git a/server/Pipfile b/server/Pipfile index de5f40b2..62622bfc 100644 --- a/server/Pipfile +++ b/server/Pipfile @@ -39,7 +39,7 @@ urllib3 = "==2.2.2" shapely = "==2.0.6" psycogreen = "==1.0.2" importlib-metadata = "==8.4.0" # https://github.com/pallets/flask/issues/4502 -typing_extensions= "==4.12.2" +typing_extensions = "==4.12.2" # requirements for development on windows colorama = "==0.4.5" diff --git a/server/Pipfile.lock b/server/Pipfile.lock index d642bf5b..be32d083 100644 --- a/server/Pipfile.lock +++ b/server/Pipfile.lock @@ -18,11 +18,11 @@ "default": { "alembic": { "hashes": [ - "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25", - "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b" + "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", + "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" ], "markers": "python_version >= '3.8'", - "version": "==1.14.0" + "version": "==1.14.1" }, "amqp": { "hashes": [ @@ -42,11 +42,11 @@ }, "attrs": { "hashes": [ - "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", - "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" + "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", + "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a" ], - "markers": "python_version >= '3.7'", - "version": "==24.2.0" + "markers": "python_version >= '3.8'", + "version": "==25.1.0" }, "bcrypt": { "hashes": [ @@ -79,6 +79,7 @@ "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.2.0" }, "billiard": { @@ -111,15 +112,16 @@ "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==5.4.0" }, "certifi": { "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" ], "markers": "python_version >= '3.6'", - "version": "==2024.8.30" + "version": "==2025.1.31" }, "chardet": { "hashes": [ @@ -131,122 +133,109 @@ }, "charset-normalizer": { "hashes": [ - "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", - "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", - "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", - "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", - "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", - "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", - "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", - "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", - "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", - "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", - "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", - "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", - "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", - "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", - "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", - "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", - "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", - "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", - "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", - "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", - "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", - "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", - "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", - "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", - "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", - "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", - "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", - "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", - "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", - "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", - "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", - "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", - "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", - "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", - "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", - "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", - "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", - "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", - "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", - "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", - "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", - "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", - "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", - "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", - "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", - "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", - "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", - "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", - "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", - "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", - "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", - "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", - "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", - "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", - "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", - "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", - "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", - "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", - "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", - "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", - "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", - "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", - "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", - "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", - "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", - "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", - "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", - "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", - "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", - "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", - "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", - "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", - "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", - "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", - "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", - "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", - "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", - "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", - "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", - "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", - "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", - "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", - "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", - "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", - "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", - "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", - "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", - "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", - "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", - "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", - "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", - "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", - "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", - "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", - "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", - "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", - "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", - "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", - "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", - "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", - "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", - "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", - "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", - "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", - "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.4.0" + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.1" }, "click": { "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" ], "markers": "python_version >= '3.7'", - "version": "==8.1.7" + "version": "==8.1.8" }, "click-didyoumean": { "hashes": [ @@ -284,6 +273,7 @@ "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.5" }, "connexion": { @@ -294,7 +284,7 @@ "sha256:99aa5781e70a7b94f8ffae8cf89f309d49cdb811bbd65a8e2f2546f3b19a01e6", "sha256:f343717241b4c4802a694c38fee66fb1693c897fe4ea5a957fa9b3b07caf6394" ], - "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==2.14.1" }, "distro": { @@ -326,6 +316,7 @@ "sha256:edee9b0a7ff26621bd5a8c10ff484ae28737a2410d99b0bb9a6850c7fb977aa0" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.2.5" }, "flask-login": { @@ -334,6 +325,7 @@ "sha256:c0a7baa9fdc448cdd3dd6f0939df72eec5177b2f7abe6cb82fc934d29caac9c3" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.6.2" }, "flask-mail": { @@ -342,6 +334,7 @@ "sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==0.10.0" }, "flask-marshmallow": { @@ -366,6 +359,7 @@ "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.5.1" }, "flask-wtf": { @@ -374,6 +368,7 @@ "sha256:9d733658c80be551ce7d5bc13c7a7ac0d80df509be1e23827c847d9520f4359a" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==1.0.1" }, "gevent": { @@ -506,8 +501,8 @@ "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" ], - "index": "pypi", - "version": "==19.9" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==19.9.0" }, "idna": { "hashes": [ @@ -523,6 +518,7 @@ "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==8.4.0" }, "inflection": { @@ -539,15 +535,16 @@ "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==2.2.0" }, "jinja2": { "hashes": [ - "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", - "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", + "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" ], "markers": "python_version >= '3.7'", - "version": "==3.1.4" + "version": "==3.1.5" }, "jsons": { "hashes": [ @@ -555,6 +552,7 @@ "sha256:f07f8919316f72a3843c7ca6cc6c900513089f10092626934d1bfe4b5cf15401" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==1.6.3" }, "jsonschema": { @@ -583,11 +581,11 @@ }, "mako": { "hashes": [ - "sha256:9ec3a1583713479fae654f83ed9fa8c9a4c16b7bb0daba0e6bbebff50c0d983d", - "sha256:a91198468092a2f1a0de86ca92690fb0cfc43ca90ee17e15d93662b4c04b241a" + "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", + "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" ], "markers": "python_version >= '3.8'", - "version": "==1.3.6" + "version": "==1.3.9" }, "markupsafe": { "hashes": [ @@ -658,11 +656,11 @@ }, "marshmallow": { "hashes": [ - "sha256:3a8dfda6edd8dcdbf216c0ede1d1e78d230a6dc9c5a088f58c4083b974a0d468", - "sha256:fece2eb2c941180ea1b7fcbd4a83c51bfdd50093fdd3ad2585ee5e1df2508491" + "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", + "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6" ], "markers": "python_version >= '3.9'", - "version": "==3.23.1" + "version": "==3.26.1" }, "marshmallow-sqlalchemy": { "hashes": [ @@ -670,68 +668,69 @@ "sha256:cce261148e4c6ec4ee275f3d29352933380a1afa2fd3933f5e9ecd02fdc16ade" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==1.1.0" }, "numpy": { "hashes": [ - "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", - "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", - "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", - "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", - "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", - "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", - "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", - "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", - "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", - "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", - "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", - "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", - "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", - "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", - "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", - "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", - "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", - "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", - "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", - "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", - "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", - "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", - "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", - "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", - "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", - "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", - "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", - "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", - "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", - "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", - "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", - "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", - "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", - "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", - "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", - "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", - "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", - "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", - "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", - "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", - "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", - "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", - "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", - "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", - "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", - "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", - "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", - "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", - "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", - "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", - "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", - "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", - "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", - "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", - "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4" + "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f", + "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0", + "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd", + "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2", + "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4", + "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648", + "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be", + "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb", + "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160", + "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd", + "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a", + "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84", + "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e", + "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748", + "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825", + "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60", + "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957", + "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715", + "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317", + "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e", + "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283", + "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278", + "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9", + "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de", + "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369", + "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb", + "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189", + "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014", + "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323", + "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e", + "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49", + "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50", + "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d", + "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37", + "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39", + "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576", + "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a", + "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba", + "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7", + "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826", + "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467", + "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495", + "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc", + "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391", + "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0", + "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97", + "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c", + "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac", + "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369", + "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8", + "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2", + "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff", + "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a", + "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df", + "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f" ], "markers": "python_version >= '3.10'", - "version": "==2.1.3" + "version": "==2.2.2" }, "packaging": { "hashes": [ @@ -747,15 +746,16 @@ "sha256:cc593caa6299b22b37f228148257997e2fa850eea2daf7e4cc9205cef6908dee" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.2.0" }, "prompt-toolkit": { "hashes": [ - "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", - "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e" + "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", + "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198" ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.48" + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.50" }, "psycogreen": { "hashes": [ @@ -840,6 +840,7 @@ "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.9.9" }, "pygeodiff": { @@ -887,6 +888,7 @@ "sha256:fd1e7e59671fe80e7f46b20c1ac0e7b7f3683b07c05f021dc6d1160575a451c4" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.0.6" }, "python-dateutil": { @@ -895,6 +897,7 @@ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "python-decouple": { @@ -911,6 +914,7 @@ "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==0.20.0" }, "pytz": { @@ -986,15 +990,16 @@ "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==5.0.1" }, "referencing": { "hashes": [ - "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", - "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" + "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", + "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0" ], - "markers": "python_version >= '3.8'", - "version": "==0.35.1" + "markers": "python_version >= '3.9'", + "version": "==0.36.2" }, "requests": { "hashes": [ @@ -1010,6 +1015,7 @@ "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.0.0" }, "result": { @@ -1018,103 +1024,116 @@ "sha256:f4563ff615b1147822d13eb363fbda202511fcbf281b3cf7acf0723ca7cb612b" ], "index": "pypi", - "version": "==0.5" + "version": "==0.5.0" }, "rpds-py": { "hashes": [ - "sha256:031819f906bb146561af051c7cef4ba2003d28cff07efacef59da973ff7969ba", - "sha256:0626238a43152918f9e72ede9a3b6ccc9e299adc8ade0d67c5e142d564c9a83d", - "sha256:085ed25baac88953d4283e5b5bd094b155075bb40d07c29c4f073e10623f9f2e", - "sha256:0a9e0759e7be10109645a9fddaaad0619d58c9bf30a3f248a2ea57a7c417173a", - "sha256:0c025820b78817db6a76413fff6866790786c38f95ea3f3d3c93dbb73b632202", - "sha256:1ff2eba7f6c0cb523d7e9cff0903f2fe1feff8f0b2ceb6bd71c0e20a4dcee271", - "sha256:20cc1ed0bcc86d8e1a7e968cce15be45178fd16e2ff656a243145e0b439bd250", - "sha256:241e6c125568493f553c3d0fdbb38c74babf54b45cef86439d4cd97ff8feb34d", - "sha256:2c51d99c30091f72a3c5d126fad26236c3f75716b8b5e5cf8effb18889ced928", - "sha256:2d6129137f43f7fa02d41542ffff4871d4aefa724a5fe38e2c31a4e0fd343fb0", - "sha256:30b912c965b2aa76ba5168fd610087bad7fcde47f0a8367ee8f1876086ee6d1d", - "sha256:30bdc973f10d28e0337f71d202ff29345320f8bc49a31c90e6c257e1ccef4333", - "sha256:320c808df533695326610a1b6a0a6e98f033e49de55d7dc36a13c8a30cfa756e", - "sha256:32eb88c30b6a4f0605508023b7141d043a79b14acb3b969aa0b4f99b25bc7d4a", - "sha256:3b766a9f57663396e4f34f5140b3595b233a7b146e94777b97a8413a1da1be18", - "sha256:3b929c2bb6e29ab31f12a1117c39f7e6d6450419ab7464a4ea9b0b417174f044", - "sha256:3e30a69a706e8ea20444b98a49f386c17b26f860aa9245329bab0851ed100677", - "sha256:3e53861b29a13d5b70116ea4230b5f0f3547b2c222c5daa090eb7c9c82d7f664", - "sha256:40c91c6e34cf016fa8e6b59d75e3dbe354830777fcfd74c58b279dceb7975b75", - "sha256:4991ca61656e3160cdaca4851151fd3f4a92e9eba5c7a530ab030d6aee96ec89", - "sha256:4ab2c2a26d2f69cdf833174f4d9d86118edc781ad9a8fa13970b527bf8236027", - "sha256:4e8921a259f54bfbc755c5bbd60c82bb2339ae0324163f32868f63f0ebb873d9", - "sha256:4eb2de8a147ffe0626bfdc275fc6563aa7bf4b6db59cf0d44f0ccd6ca625a24e", - "sha256:5145282a7cd2ac16ea0dc46b82167754d5e103a05614b724457cffe614f25bd8", - "sha256:520ed8b99b0bf86a176271f6fe23024323862ac674b1ce5b02a72bfeff3fff44", - "sha256:52c041802a6efa625ea18027a0723676a778869481d16803481ef6cc02ea8cb3", - "sha256:5555db3e618a77034954b9dc547eae94166391a98eb867905ec8fcbce1308d95", - "sha256:58a0e345be4b18e6b8501d3b0aa540dad90caeed814c515e5206bb2ec26736fd", - "sha256:590ef88db231c9c1eece44dcfefd7515d8bf0d986d64d0caf06a81998a9e8cab", - "sha256:5afb5efde74c54724e1a01118c6e5c15e54e642c42a1ba588ab1f03544ac8c7a", - "sha256:688c93b77e468d72579351a84b95f976bd7b3e84aa6686be6497045ba84be560", - "sha256:6b4ef7725386dc0762857097f6b7266a6cdd62bfd209664da6712cb26acef035", - "sha256:6bc0e697d4d79ab1aacbf20ee5f0df80359ecf55db33ff41481cf3e24f206919", - "sha256:6dcc4949be728ede49e6244eabd04064336012b37f5c2200e8ec8eb2988b209c", - "sha256:6f54e7106f0001244a5f4cf810ba8d3f9c542e2730821b16e969d6887b664266", - "sha256:808f1ac7cf3b44f81c9475475ceb221f982ef548e44e024ad5f9e7060649540e", - "sha256:8404b3717da03cbf773a1d275d01fec84ea007754ed380f63dfc24fb76ce4592", - "sha256:878f6fea96621fda5303a2867887686d7a198d9e0f8a40be100a63f5d60c88c9", - "sha256:8a7ff941004d74d55a47f916afc38494bd1cfd4b53c482b77c03147c91ac0ac3", - "sha256:95a5bad1ac8a5c77b4e658671642e4af3707f095d2b78a1fdd08af0dfb647624", - "sha256:97ef67d9bbc3e15584c2f3c74bcf064af36336c10d2e21a2131e123ce0f924c9", - "sha256:98486337f7b4f3c324ab402e83453e25bb844f44418c066623db88e4c56b7c7b", - "sha256:98e4fe5db40db87ce1c65031463a760ec7906ab230ad2249b4572c2fc3ef1f9f", - "sha256:998a8080c4495e4f72132f3d66ff91f5997d799e86cec6ee05342f8f3cda7dca", - "sha256:9afe42102b40007f588666bc7de82451e10c6788f6f70984629db193849dced1", - "sha256:9e20da3957bdf7824afdd4b6eeb29510e83e026473e04952dca565170cd1ecc8", - "sha256:a017f813f24b9df929674d0332a374d40d7f0162b326562daae8066b502d0590", - "sha256:a429b99337062877d7875e4ff1a51fe788424d522bd64a8c0a20ef3021fdb6ed", - "sha256:a58ce66847711c4aa2ecfcfaff04cb0327f907fead8945ffc47d9407f41ff952", - "sha256:a78d8b634c9df7f8d175451cfeac3810a702ccb85f98ec95797fa98b942cea11", - "sha256:a89a8ce9e4e75aeb7fa5d8ad0f3fecdee813802592f4f46a15754dcb2fd6b061", - "sha256:a8eeec67590e94189f434c6d11c426892e396ae59e4801d17a93ac96b8c02a6c", - "sha256:aaeb25ccfb9b9014a10eaf70904ebf3f79faaa8e60e99e19eef9f478651b9b74", - "sha256:ad116dda078d0bc4886cb7840e19811562acdc7a8e296ea6ec37e70326c1b41c", - "sha256:af04ac89c738e0f0f1b913918024c3eab6e3ace989518ea838807177d38a2e94", - "sha256:af4a644bf890f56e41e74be7d34e9511e4954894d544ec6b8efe1e21a1a8da6c", - "sha256:b21747f79f360e790525e6f6438c7569ddbfb1b3197b9e65043f25c3c9b489d8", - "sha256:b229ce052ddf1a01c67d68166c19cb004fb3612424921b81c46e7ea7ccf7c3bf", - "sha256:b4de1da871b5c0fd5537b26a6fc6814c3cc05cabe0c941db6e9044ffbb12f04a", - "sha256:b80b4690bbff51a034bfde9c9f6bf9357f0a8c61f548942b80f7b66356508bf5", - "sha256:b876f2bc27ab5954e2fd88890c071bd0ed18b9c50f6ec3de3c50a5ece612f7a6", - "sha256:b8f107395f2f1d151181880b69a2869c69e87ec079c49c0016ab96860b6acbe5", - "sha256:b9b76e2afd585803c53c5b29e992ecd183f68285b62fe2668383a18e74abe7a3", - "sha256:c2b2f71c6ad6c2e4fc9ed9401080badd1469fa9889657ec3abea42a3d6b2e1ed", - "sha256:c3761f62fcfccf0864cc4665b6e7c3f0c626f0380b41b8bd1ce322103fa3ef87", - "sha256:c38dbf31c57032667dd5a2f0568ccde66e868e8f78d5a0d27dcc56d70f3fcd3b", - "sha256:ca9989d5d9b1b300bc18e1801c67b9f6d2c66b8fd9621b36072ed1df2c977f72", - "sha256:cbd7504a10b0955ea287114f003b7ad62330c9e65ba012c6223dba646f6ffd05", - "sha256:d167e4dbbdac48bd58893c7e446684ad5d425b407f9336e04ab52e8b9194e2ed", - "sha256:d2132377f9deef0c4db89e65e8bb28644ff75a18df5293e132a8d67748397b9f", - "sha256:da52d62a96e61c1c444f3998c434e8b263c384f6d68aca8274d2e08d1906325c", - "sha256:daa8efac2a1273eed2354397a51216ae1e198ecbce9036fba4e7610b308b6153", - "sha256:dc5695c321e518d9f03b7ea6abb5ea3af4567766f9852ad1560f501b17588c7b", - "sha256:de552f4a1916e520f2703ec474d2b4d3f86d41f353e7680b597512ffe7eac5d0", - "sha256:de609a6f1b682f70bb7163da745ee815d8f230d97276db049ab447767466a09d", - "sha256:e12bb09678f38b7597b8346983d2323a6482dcd59e423d9448108c1be37cac9d", - "sha256:e168afe6bf6ab7ab46c8c375606298784ecbe3ba31c0980b7dcbb9631dcba97e", - "sha256:e78868e98f34f34a88e23ee9ccaeeec460e4eaf6db16d51d7a9b883e5e785a5e", - "sha256:e860f065cc4ea6f256d6f411aba4b1251255366e48e972f8a347cf88077b24fd", - "sha256:ea3a6ac4d74820c98fcc9da4a57847ad2cc36475a8bd9683f32ab6d47a2bd682", - "sha256:ebf64e281a06c904a7636781d2e973d1f0926a5b8b480ac658dc0f556e7779f4", - "sha256:ed6378c9d66d0de903763e7706383d60c33829581f0adff47b6535f1802fa6db", - "sha256:ee1e4fc267b437bb89990b2f2abf6c25765b89b72dd4a11e21934df449e0c976", - "sha256:ee4eafd77cc98d355a0d02f263efc0d3ae3ce4a7c24740010a8b4012bbb24937", - "sha256:efec946f331349dfc4ae9d0e034c263ddde19414fe5128580f512619abed05f1", - "sha256:f414da5c51bf350e4b7960644617c130140423882305f7574b6cf65a3081cecb", - "sha256:f71009b0d5e94c0e86533c0b27ed7cacc1239cb51c178fd239c3cfefefb0400a", - "sha256:f983e4c2f603c95dde63df633eec42955508eefd8d0f0e6d236d31a044c882d7", - "sha256:faa5e8496c530f9c71f2b4e1c49758b06e5f4055e17144906245c99fa6d45356", - "sha256:fed5dfefdf384d6fe975cc026886aece4f292feaf69d0eeb716cfd3c5a4dd8be" + "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518", + "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059", + "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61", + "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5", + "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9", + "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543", + "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2", + "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a", + "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d", + "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56", + "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d", + "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd", + "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b", + "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4", + "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99", + "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d", + "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd", + "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe", + "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1", + "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e", + "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f", + "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3", + "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca", + "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d", + "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e", + "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc", + "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea", + "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38", + "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b", + "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c", + "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff", + "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723", + "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e", + "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493", + "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6", + "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83", + "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091", + "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1", + "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627", + "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1", + "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728", + "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16", + "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c", + "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45", + "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7", + "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a", + "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730", + "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967", + "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25", + "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24", + "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055", + "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d", + "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0", + "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e", + "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7", + "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c", + "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f", + "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd", + "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652", + "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8", + "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11", + "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333", + "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96", + "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64", + "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b", + "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e", + "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c", + "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9", + "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec", + "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb", + "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37", + "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad", + "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9", + "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c", + "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf", + "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4", + "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f", + "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d", + "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09", + "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", + "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566", + "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74", + "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338", + "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15", + "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c", + "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648", + "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84", + "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3", + "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123", + "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520", + "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831", + "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e", + "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf", + "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b", + "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2", + "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3", + "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130", + "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b", + "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de", + "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5", + "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d", + "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00", + "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e" ], "markers": "python_version >= '3.9'", - "version": "==0.21.0" + "version": "==0.22.3" }, "safe": { "hashes": [ @@ -1130,15 +1149,16 @@ "sha256:a6860e300f6807e76f21854163bdb9db16afc74eadf34bd6a9947d3fdfcd725a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.18.1" }, "setuptools": { "hashes": [ - "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", - "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d" + "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", + "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3" ], "markers": "python_version >= '3.9'", - "version": "==75.6.0" + "version": "==75.8.0" }, "shapely": { "hashes": [ @@ -1186,15 +1206,16 @@ "sha256:fbb7bf02a7542dba55129062570211cfb0defa05386409b3e306c39612e7fbcc" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.0.6" }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "version": "==1.17.0" }, "sqlalchemy": { "hashes": [ @@ -1244,6 +1265,7 @@ "sha256:fb8e15dfa47f5de11ab073e12aadd6b502cfb7ac4bafd18bd18cfd1c7d13dbbc" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.4.53" }, "swagger-ui-bundle": { @@ -1297,6 +1319,7 @@ "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==4.12.2" }, "typish": { @@ -1307,11 +1330,11 @@ }, "tzdata": { "hashes": [ - "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", - "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd" + "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", + "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639" ], "markers": "python_version >= '2'", - "version": "==2024.2" + "version": "==2025.1" }, "urllib3": { "hashes": [ @@ -1319,6 +1342,7 @@ "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==2.2.2" }, "vine": { @@ -1360,7 +1384,7 @@ "sha256:bf831c042829c8cdbad74c27575098d541d039b1faa74c771545ecac916f2c07", "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9" ], - "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.1.2" }, "wtforms-json": { @@ -1452,197 +1476,185 @@ "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.0" }, "certifi": { "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" ], "markers": "python_version >= '3.6'", - "version": "==2024.8.30" + "version": "==2025.1.31" }, "charset-normalizer": { "hashes": [ - "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", - "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", - "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", - "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", - "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", - "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", - "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", - "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", - "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", - "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", - "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", - "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", - "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", - "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", - "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", - "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", - "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", - "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", - "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", - "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", - "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", - "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", - "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", - "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", - "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", - "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", - "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", - "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", - "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", - "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", - "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", - "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", - "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", - "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", - "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", - "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", - "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", - "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", - "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", - "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", - "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", - "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", - "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", - "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", - "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", - "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", - "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", - "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", - "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", - "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", - "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", - "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", - "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", - "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", - "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", - "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", - "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", - "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", - "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", - "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", - "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", - "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", - "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", - "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", - "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", - "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", - "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", - "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", - "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", - "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", - "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", - "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", - "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", - "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", - "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", - "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", - "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", - "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", - "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", - "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", - "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", - "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", - "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", - "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", - "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", - "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", - "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", - "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", - "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", - "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", - "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", - "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", - "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", - "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", - "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", - "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", - "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", - "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", - "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", - "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", - "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", - "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", - "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", - "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", - "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.4.0" + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.1" }, "coverage": { "extras": [ "toml" ], "hashes": [ - "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", - "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", - "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", - "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638", - "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", - "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc", - "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed", - "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", - "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d", - "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", - "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c", - "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", - "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", - "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", - "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", - "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee", - "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e", - "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e", - "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", - "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", - "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", - "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076", - "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", - "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", - "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", - "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e", - "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce", - "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", - "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", - "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", - "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf", - "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6", - "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", - "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", - "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4", - "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", - "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", - "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", - "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", - "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", - "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea", - "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", - "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", - "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", - "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", - "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50", - "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779", - "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", - "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", - "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", - "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", - "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", - "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", - "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", - "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", - "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331", - "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", - "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0", - "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", - "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92", - "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a", - "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9" + "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", + "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f", + "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", + "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", + "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", + "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", + "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", + "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", + "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", + "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", + "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", + "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", + "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", + "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", + "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", + "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59", + "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", + "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18", + "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", + "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", + "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", + "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", + "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", + "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", + "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90", + "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", + "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a", + "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", + "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", + "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", + "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", + "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", + "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", + "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d", + "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", + "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", + "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", + "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", + "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", + "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", + "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", + "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", + "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", + "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4", + "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25", + "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", + "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", + "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", + "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", + "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315", + "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", + "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", + "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27", + "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", + "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", + "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", + "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", + "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", + "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", + "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", + "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", + "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f" ], "markers": "python_version >= '3.9'", - "version": "==7.6.8" + "version": "==7.6.10" }, "dill": { "hashes": [ @@ -1722,6 +1734,7 @@ "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3" ], "index": "pypi", + "markers": "python_full_version >= '3.8.0'", "version": "==3.2.6" }, "pysqlite3-binary": { @@ -1743,6 +1756,7 @@ "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==8.3.2" }, "pytest-cov": { @@ -1751,6 +1765,7 @@ "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==5.0.0" }, "pytest-dotenv": { @@ -1767,6 +1782,7 @@ "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==0.20.0" }, "requests": { @@ -1783,6 +1799,7 @@ "sha256:b82502eb5f09a0289d8e209e7bad71ef3978334f56d09b444253d5ad67bf5253" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.21.0" }, "tomli": { @@ -1837,6 +1854,7 @@ "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==4.12.2" }, "urllib3": { @@ -1845,6 +1863,7 @@ "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==2.2.2" } } diff --git a/server/application.py b/server/application.py index 2a4220b6..2a73f2ef 100644 --- a/server/application.py +++ b/server/application.py @@ -26,7 +26,7 @@ from mergin.sync.tasks import remove_temp_files, remove_projects_backups from mergin.celery import celery, configure_celery from mergin.stats.config import Configuration -from mergin.stats.tasks import send_statistics +from mergin.stats.tasks import save_statistics, send_statistics from mergin.stats.app import register as register_stats Configuration.SERVER_TYPE = "ce" @@ -65,14 +65,14 @@ def setup_periodic_tasks(sender, **kwargs): remove_projects_backups, name="remove old project backups", ) + sender.add_periodic_task( + crontab(hour="*/12", minute=0), + save_statistics, + name="Save usage statistics to database", + ) if Configuration.COLLECT_STATISTICS: sender.add_periodic_task( crontab(hour=randint(0, 5), minute=randint(0, 60)), send_statistics, name="send usage statistics", ) - - -# send report after start -if Configuration.COLLECT_STATISTICS: - send_statistics.delay() diff --git a/server/mergin/.env b/server/mergin/.env index db417d2e..0ff3dc42 100644 --- a/server/mergin/.env +++ b/server/mergin/.env @@ -1,6 +1,8 @@ GEODIFF_LOGGER_LEVEL="2" # only for dev - should be overwritten in production SECRET_KEY='top-secret' +SECURITY_BEARER_SALT='top-secret' +SECURITY_EMAIL_SALT='top-secret' SECURITY_PASSWORD_SALT='top-secret' MAIL_DEFAULT_SENDER='' FLASK_DEBUG=0 diff --git a/server/mergin/app.py b/server/mergin/app.py index 86c72d74..0f23e2ac 100644 --- a/server/mergin/app.py +++ b/server/mergin/app.py @@ -139,15 +139,6 @@ def create_simple_app() -> Flask: if Configuration.GEVENT_WORKER: flask_app.wsgi_app = GeventTimeoutMiddleware(flask_app.wsgi_app) - @flask_app.cli.command() - def init_db(): - """Re-creates application database""" - print("Database initialization ...") - db.drop_all(bind=None) - db.create_all(bind=None) - db.session.commit() - print("Done. Tables created.") - add_commands(flask_app) return flask_app @@ -211,6 +202,7 @@ def load_user_from_header(header_val): # pylint: disable=W0613,W0612 try: data = decode_token( app.app.config["SECRET_KEY"], + app.app.config["SECURITY_BEARER_SALT"], header_val, app.app.config["BEARER_TOKEN_EXPIRATION"], ) diff --git a/server/mergin/auth/app.py b/server/mergin/auth/app.py index cfba8d98..7d3d8e29 100644 --- a/server/mergin/auth/app.py +++ b/server/mergin/auth/app.py @@ -80,17 +80,15 @@ def authenticate(login, password): return user -def generate_confirmation_token(app, email): +def generate_confirmation_token(app, email, salt): serializer = URLSafeTimedSerializer(app.config["SECRET_KEY"]) - return serializer.dumps(email, salt=app.config["SECURITY_PASSWORD_SALT"]) + return serializer.dumps(email, salt=salt) -def confirm_token(token, expiration=3600 * 24 * 3): +def confirm_token(token, salt, expiration=3600): serializer = URLSafeTimedSerializer(current_app.config["SECRET_KEY"]) try: - email = serializer.loads( - token, salt=current_app.config["SECURITY_PASSWORD_SALT"], max_age=expiration - ) + email = serializer.loads(token, salt=salt, max_age=expiration) except: return return email @@ -103,7 +101,12 @@ def send_confirmation_email(app, user, url, template, header, **kwargs): """ from ..celery import send_email_async - token = generate_confirmation_token(app, user.email) + salt = ( + app.config["SECURITY_EMAIL_SALT"] + if url == "confirm-email" + else app.config["SECURITY_PASSWORD_SALT"] + ) + token = generate_confirmation_token(app, user.email, salt) confirm_url = f"{url}/{token}" html = render_template( template, subject=header, confirm_url=confirm_url, user=user, **kwargs diff --git a/server/mergin/auth/bearer.py b/server/mergin/auth/bearer.py index 117f86d3..1c54a054 100644 --- a/server/mergin/auth/bearer.py +++ b/server/mergin/auth/bearer.py @@ -7,8 +7,7 @@ from flask.sessions import TaggedJSONSerializer -def decode_token(secret_key, token, max_age=None): - salt = "bearer-session" +def decode_token(secret_key, salt, token, max_age=None): serializer = TaggedJSONSerializer() signer_kwargs = {"key_derivation": "hmac", "digest_method": hashlib.sha1} s = URLSafeTimedSerializer( @@ -17,8 +16,7 @@ def decode_token(secret_key, token, max_age=None): return s.loads(token, max_age=max_age) -def encode_token(secret_key, data): - salt = "bearer-session" +def encode_token(secret_key, salt, data): serializer = TaggedJSONSerializer() signer_kwargs = {"key_derivation": "hmac", "digest_method": hashlib.sha1} s = URLSafeTimedSerializer( diff --git a/server/mergin/auth/config.py b/server/mergin/auth/config.py index 4484014e..07b5a05d 100644 --- a/server/mergin/auth/config.py +++ b/server/mergin/auth/config.py @@ -6,6 +6,8 @@ class Configuration(object): + SECURITY_BEARER_SALT = config("SECURITY_BEARER_SALT") + SECURITY_EMAIL_SALT = config("SECURITY_EMAIL_SALT") SECURITY_PASSWORD_SALT = config("SECURITY_PASSWORD_SALT") BEARER_TOKEN_EXPIRATION = config( "BEARER_TOKEN_EXPIRATION", default=3600 * 12, cast=int diff --git a/server/mergin/auth/controller.py b/server/mergin/auth/controller.py index 106d30bd..86fa5610 100644 --- a/server/mergin/auth/controller.py +++ b/server/mergin/auth/controller.py @@ -39,6 +39,9 @@ from ..sync.utils import files_size +EMAIL_CONFIRMATION_EXPIRATION = 12 * 3600 + + # public endpoints def user_profile(user, return_all=True): """Return user profile in json format @@ -143,7 +146,11 @@ def login_public(): # noqa: E501 "email": user.email, "expire": str(expire), } - token = encode_token(current_app.config["SECRET_KEY"], token_data) + token = encode_token( + current_app.config["SECRET_KEY"], + current_app.config["SECURITY_BEARER_SALT"], + token_data, + ) data = user_profile(user) data["session"] = {"token": token, "expire": expire} @@ -297,7 +304,7 @@ def password_reset(): # pylint: disable=W0613,W0612 def confirm_new_password(token): # pylint: disable=W0613,W0612 - email = confirm_token(token) + email = confirm_token(token, salt=current_app.config["SECURITY_PASSWORD_SALT"]) if not email: abort(400, "Invalid token") @@ -315,7 +322,11 @@ def confirm_new_password(token): # pylint: disable=W0613,W0612 def confirm_email(token): # pylint: disable=W0613,W0612 - email = confirm_token(token) + email = confirm_token( + token, + expiration=EMAIL_CONFIRMATION_EXPIRATION, + salt=current_app.config["SECURITY_EMAIL_SALT"], + ) if not email: abort(400, "Invalid token") @@ -375,7 +386,9 @@ def register_user(): # pylint: disable=W0613,W0612 if form.validate(): user = User.create(form.username.data, form.email.data, form.password.data) user_created.send(user, source="admin") - token = generate_confirmation_token(current_app, user.email) + token = generate_confirmation_token( + current_app, user.email, current_app.config["SECURITY_EMAIL_SALT"] + ) confirm_url = f"confirm-email/{token}" html = render_template( "email/user_created.html", diff --git a/server/mergin/auth/forms.py b/server/mergin/auth/forms.py index 30a615fa..a5def163 100644 --- a/server/mergin/auth/forms.py +++ b/server/mergin/auth/forms.py @@ -20,13 +20,26 @@ def username_validation(form, field): - from ..sync.utils import is_name_allowed + from ..sync.utils import ( + has_valid_characters, + has_valid_first_character, + check_filename, + is_reserved_word, + ) - if field.data and (not is_name_allowed(field.data) or "@" in field.data): - raise ValidationError( - f"Please don't start username with . and " - f"use only alphanumeric or these -._! characters in {field.name}." - ) + errors = ( + [ + has_valid_characters(field.data), + has_valid_first_character(field.data), + is_reserved_word(field.data), + check_filename(field.data), + ] + if field.data + else [] + ) + for error in errors: + if error: + raise ValidationError(error) class PasswordValidator: diff --git a/server/mergin/commands.py b/server/mergin/commands.py index e1b80875..47c0600f 100644 --- a/server/mergin/commands.py +++ b/server/mergin/commands.py @@ -1,95 +1,211 @@ import click from flask import Flask -from sqlalchemy import or_, func +import random +import string +from datetime import datetime, timezone -from .config import Configuration +def _echo_title(title): + click.echo("") + click.echo(f"# {title}") + click.echo() + + +def _echo_error(msg): + click.secho("Error: ", fg="red", nl=False, bold=True) + click.secho(msg, fg="bright_red") def add_commands(app: Flask): from .app import db + from mergin.auth.models import UserProfile - @app.cli.group() - def server(): - """Server management commands.""" - pass + def _check_celery(): + from celery import current_app - @server.command() - @click.option("--email", required=True) - def send_check_email(email: str): # pylint: disable=W0612 + ping_celery = current_app.control.inspect().ping() + if not ping_celery: + _echo_error( + "Celery process not running properly. Configure celery worker and celery beat. This breaks also email sending from the system.", + ) + return + click.secho("Celery is running properly", fg="green") + return True + + def _send_statistics(): + from .stats.tasks import send_statistics, save_statistics + + _echo_title("Sending statistics.") + # save rows to MerginStatistics table + save_statistics.delay() + + if not app.config.get("COLLECT_STATISTICS"): + click.secho( + "Statistics sending is disabled.", + ) + return + + if not _check_celery(): + return + send_statistics.delay() + click.secho("Statistics sent.", fg="green") + + def _send_email(email: str): """Send check email to specified email address.""" from .celery import send_email_async + _echo_title(f"Sending check email to specified email address {email}.") if app.config["MAIL_SUPPRESS_SEND"]: - click.echo( - click.style( - "Sending emails is disabled. Please set MAIL_SUPPRESS_SEND=False to enable sending emails.", - fg="red", - ) + _echo_error( + "Sending emails is disabled. Please set MAIL_SUPPRESS_SEND=False to enable sending emails." ) return - if not app.config["MAIL_DEFAULT_SENDER"]: - click.echo( - click.style( - "No default sender set. Please set MAIL_DEFAULT_SENDER environment variable", - fg="red", - ) + default_sender = app.config.get("MAIL_DEFAULT_SENDER") + if not default_sender: + _echo_error( + "No default sender set. Please set MAIL_DEFAULT_SENDER environment variable", ) return email_data = { "subject": "Mergin Maps server check", "html": "Awesome, your email configuration of Mergin Maps server is working.", "recipients": [email], - "sender": app.config["MAIL_DEFAULT_SENDER"], + "sender": default_sender, } - click.echo( - f"Sending email to specified email address {email}. Check your inbox." - ) try: + is_celery_running = _check_celery() + if not is_celery_running: + return send_email_async.delay(**email_data) except Exception as e: - click.echo( - click.style( - f"Error sending email: {e}", - fg="red", - ) + _echo_error( + f"Error sending email: {e}", ) - @server.command() - def check(): # pylint: disable=W0612 - """Check server configuration. Define email to send testing email.""" - from celery import current_app + def _check_server(): # pylint: disable=W0612 + """Check server configuration.""" - click.echo(f"Mergin Maps server version: {app.config['VERSION']}") + _echo_title("Server health check") + edition_map = { + "ce": "Community Edition", + "ee": "Enterprise Edition", + } + edition = edition_map.get(app.config["SERVER_TYPE"]) + if edition: + click.echo(f"Mergin Maps edition: {edition}") + click.echo(f"Mergin Maps version: {app.config['VERSION']}") base_url = app.config["MERGIN_BASE_URL"] if not base_url: - click.echo( - click.style( - "No base URL set. Please set MERGIN_BASE_URL environment variable", - fg="red", - ), + _echo_error( + "No base URL set. Please set MERGIN_BASE_URL environment variable", + ) + else: + click.secho(f"Base URL of server is {base_url}", fg="green") + + contact_email = app.config["CONTACT_EMAIL"] + if not contact_email: + _echo_error( + "No contact email set. Please set CONTACT_EMAIL environment variable", ) else: - click.echo(f"Base URL of server is {base_url}") + click.secho(f"Base URL of server is {base_url}", fg="green") tables = db.engine.table_names() if not tables: - click.echo( - click.style( - "Database not initialized. Run flask init-db command", fg="red" - ) - ) + _echo_error("Database not initialized. Run flask init-db command") else: - click.echo("Database initialized properly") + click.secho("Database initialized properly", fg="green") - ping_celery = current_app.control.inspect().ping() - if not ping_celery: - click.echo( - click.style( - "Celery not running. Configure celery worker and celery beat", - fg="red", - ) + _check_celery() + + def _init_db(): + """Create database tables.""" + from .stats.models import MerginInfo + + _echo_title("Database initialization") + with click.progressbar( + label="Creating database", length=4, show_eta=False + ) as progress_bar: + progress_bar.update(0) + db.drop_all(bind=None) + progress_bar.update(1) + db.create_all(bind=None) + progress_bar.update(2) + db.session.commit() + progress_bar.update(3) + info = MerginInfo.query.first() + if not info and app.config.get("COLLECT_STATISTICS"): + # create new info with random service id + service_id = app.config.get("SERVICE_ID", None) + info = MerginInfo(service_id) + db.session.add(info) + db.session.commit() + progress_bar.update(4) + + click.secho("Tables created.", fg="green") + + @app.cli.command() + def init_db(): + """Re-create database tables.""" + _init_db() + + @app.cli.command() + @click.option("--email", "-e", required=True, envvar="CONTACT_EMAIL") + @click.option( + "--recreate", + "-r", + help="Recreate database and admin user.", + is_flag=True, + required=False, + ) + def init(email: str, recreate: bool): + """Initialize database if does not exist or -r is provided. Perform check of server configuration. Send statistics, respecting your setup.""" + + from .auth.models import User + + tables = db.engine.table_names() + if recreate and tables: + click.confirm( + "Are you sure you want to recreate database and admin user? This will remove all data!", + default=False, + abort=True, ) - else: - click.echo("Celery running properly") + + if not tables or recreate: + _init_db() + + _echo_title("Creating admin user. Copy generated password.") + username = User.generate_username(email) + password_chars = string.ascii_letters + string.digits + password = "".join(random.choice(password_chars) for i in range(12)) + user = User(username=username, passwd=password, email=email, is_admin=True) + user.profile = UserProfile() + user.active = True + db.session.add(user) + db.session.commit() + click.secho( + "Admin user created. Please save generated password.", fg="green" + ) + click.secho(f"Email: {email}") + click.secho(f"Username: {username}") + click.secho(f"Password: {password}") + _check_server() + _send_email(email) + _send_statistics() + + @app.cli.group() + def server(): + """Server management commands.""" + pass + + @server.command() + @click.option("--email", required=True) + def send_check_email(email: str): # pylint: disable=W0612 + """Send check email to specified email address.""" + _send_email(email) + + @server.command() + def check(): + """Check server configuration.""" + _check_server() diff --git a/server/mergin/stats/api.yaml b/server/mergin/stats/api.yaml index dd67d34b..382da567 100644 --- a/server/mergin/stats/api.yaml +++ b/server/mergin/stats/api.yaml @@ -22,6 +22,37 @@ paths: "404": $ref: "#/components/responses/NotFoundResp" x-openapi-router-controller: mergin.stats.controller + /app/admin/report: + get: + summary: Download statistics for server + operationId: download_report + x-openapi-router-controller: mergin.stats.controller + parameters: + - name: date_from + in: query + description: Start date for statistics (YYYY-MM-DD) + required: true + schema: + type: string + format: date + - name: date_to + in: query + description: End date for statistics (YYYY-MM-DD) + required: true + schema: + type: string + format: date + responses: + "200": + description: CSV file with statistics + content: + text/csv: + schema: + type: string + "400": + $ref: "#/components/responses/BadStatusResp" + "404": + $ref: "#/components/responses/NotFoundResp" components: responses: UnauthorizedError: diff --git a/server/mergin/stats/controller.py b/server/mergin/stats/controller.py index 6fe255aa..4606f1e5 100644 --- a/server/mergin/stats/controller.py +++ b/server/mergin/stats/controller.py @@ -2,11 +2,29 @@ # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial +from dataclasses import asdict import requests -from flask import abort, current_app +from flask import abort, current_app, make_response +from datetime import datetime, time +from csv import DictWriter + +from mergin.auth.app import auth_required +from mergin.stats.models import MerginStatistics, ServerCallhomeData from .config import Configuration -from ..app import parse_version_string +from ..app import parse_version_string, db + + +class CsvTextBuilder(object): + """ + Mock csv writer that writes to text buffer + """ + + def __init__(self): + self.data = [] + + def write(self, row): + self.data.append(row) def get_latest_version(): @@ -29,3 +47,44 @@ def get_latest_version(): data = {**data, **parsed_version} return data, 200 + + +@auth_required(permissions=["admin"]) +def download_report(date_from: str, date_to: str): + """Download statistics from server instance""" + try: + # try to validate dates to prevent unhandled date formats + # add start of the day time and end of the day time to prevent bad filtering in db + parsed_from = datetime.combine( + datetime.strptime(date_from, "%Y-%m-%d"), time.min + ) + parsed_to = datetime.combine(datetime.strptime(date_to, "%Y-%m-%d"), time.max) + except ValueError: + abort(400, "Invalid date format") + + stats = ( + db.session.query(MerginStatistics.created_at, MerginStatistics.data) + .filter(MerginStatistics.created_at.between(parsed_from, parsed_to)) + .order_by(MerginStatistics.created_at.desc()) + .all() + ) + created_column = "created_at" + data = [ + { + **stat.data, + "created_at": datetime.isoformat(stat.created_at), + } + for stat in stats + ] + columns = list(ServerCallhomeData.__dataclass_fields__.keys()) + [created_column] + # get columns for data, this is usefull when we will update data json format (removing columns, adding new ones) + + builder = CsvTextBuilder() + writer = DictWriter(builder, fieldnames=columns, extrasaction="ignore") + writer.writeheader() + writer.writerows(data) + csv_data = "".join(builder.data) + response = make_response(csv_data) + response.headers["Content-Disposition"] = f"attachment; filename=usage-report.csv" + response.mimetype = "text/csv" + return response diff --git a/server/mergin/stats/models.py b/server/mergin/stats/models.py index 320e0137..3c32e887 100644 --- a/server/mergin/stats/models.py +++ b/server/mergin/stats/models.py @@ -2,12 +2,30 @@ # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial +from dataclasses import dataclass +from typing import Optional import uuid -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.postgresql import UUID, JSONB +from datetime import datetime, timezone from ..app import db +@dataclass +class ServerCallhomeData: + service_uuid: Optional[str] + url: Optional[str] + contact_email: Optional[str] + licence: Optional[str] + projects_count: Optional[int] + users_count: Optional[int] + workspaces_count: Optional[int] + last_change: Optional[str] + server_version: Optional[str] + monthly_contributors: Optional[int] + editors: Optional[int] + + class MerginInfo(db.Model): """Information about deployment""" @@ -19,3 +37,14 @@ def __init__(self, service_id: str = None): self.service_id = uuid.UUID(service_id) else: self.service_id = uuid.uuid4() + + +class MerginStatistics(db.Model): + """Information about deployment""" + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + created_at = db.Column( + db.DateTime, index=True, nullable=False, default=datetime.utcnow + ) + # data with statistics + data = db.Column(JSONB, nullable=False) diff --git a/server/mergin/stats/tasks.py b/server/mergin/stats/tasks.py index da7a9cb6..9812340d 100644 --- a/server/mergin/stats/tasks.py +++ b/server/mergin/stats/tasks.py @@ -2,20 +2,57 @@ # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial +from dataclasses import asdict import requests -import datetime +from datetime import datetime, timedelta, timezone import json import logging from flask import current_app from sqlalchemy.sql.operators import is_ -from .models import MerginInfo +from .models import MerginInfo, MerginStatistics, ServerCallhomeData from ..celery import celery from ..app import db from ..auth.models import User from ..sync.models import Project +def get_callhome_data(info: MerginInfo | None = None) -> ServerCallhomeData: + """ + Get data about server to send to callhome service + """ + last_change_item = ( + db.session.query(Project.updated).order_by(Project.updated.desc()).first() + ) + service_uuid = str(info.service_id) if info else None + data = ServerCallhomeData( + service_uuid=service_uuid, + url=current_app.config["MERGIN_BASE_URL"], + contact_email=current_app.config["CONTACT_EMAIL"], + licence=current_app.config["SERVER_TYPE"], + projects_count=Project.query.filter(Project.removed_at.is_(None)).count(), + users_count=User.query.filter( + is_(User.username.ilike("deleted_%"), False) + ).count(), + workspaces_count=current_app.ws_handler.workspace_count(), + last_change=str(last_change_item.updated) + "Z" if last_change_item else "", + server_version=current_app.config["VERSION"], + monthly_contributors=current_app.ws_handler.monthly_contributors_count(), + editors=current_app.ws_handler.server_editors_count(), + ) + return data + + +@celery.task(ignore_result=True) +def save_statistics(): + """Save statistics about usage.""" + info = MerginInfo.query.first() + data = get_callhome_data(info) + stat = MerginStatistics(data=data) + db.session.add(stat) + db.session.commit() + + @celery.task(ignore_result=True) def send_statistics(): """Send statistics about usage.""" @@ -45,32 +82,12 @@ def send_statistics(): db.session.add(info) db.session.commit() - if ( - info.last_reported - and datetime.datetime.utcnow() - < info.last_reported + datetime.timedelta(hours=12) + if info.last_reported and datetime.utcnow() < info.last_reported + timedelta( + hours=12 ): return - last_change_item = ( - db.session.query(Project.updated).order_by(Project.updated.desc()).first() - ) - - data = { - "service_uuid": str(info.service_id), - "url": current_app.config["MERGIN_BASE_URL"], - "contact_email": current_app.config["CONTACT_EMAIL"], - "licence": current_app.config["SERVER_TYPE"], - "projects_count": Project.query.filter(Project.removed_at.is_(None)).count(), - "users_count": User.query.filter( - is_(User.username.ilike("deleted_%"), False) - ).count(), - "workspaces_count": current_app.ws_handler.workspace_count(), - "last_change": str(last_change_item.updated) + "Z" if last_change_item else "", - "server_version": current_app.config["VERSION"], - "monthly_contributors": current_app.ws_handler.monthly_contributors_count(), - "editors": current_app.ws_handler.server_editors_count(), - } + data = asdict(get_callhome_data(info)) try: resp = requests.post( @@ -78,7 +95,7 @@ def send_statistics(): data=json.dumps(data), ) if resp.ok: - info.last_reported = datetime.datetime.utcnow() + info.last_reported = datetime.utcnow() db.session.commit() else: logging.warning("Statistics error: " + str(resp.text)) diff --git a/server/mergin/sync/forms.py b/server/mergin/sync/forms.py index 275380b1..d2c2ad0a 100644 --- a/server/mergin/sync/forms.py +++ b/server/mergin/sync/forms.py @@ -6,6 +6,29 @@ from wtforms.validators import DataRequired from flask_wtf import FlaskForm +from mergin.sync.utils import ( + is_reserved_word, + has_valid_characters, + check_filename, + has_valid_first_character, +) + + +def project_name_validation(name: str) -> str | None: + """Check whether project name is valid""" + if not name.strip(): + return "Project name cannot be empty" + errors = [ + has_valid_characters(name), + has_valid_first_character(name), + is_reserved_word(name), + check_filename(name), + ] + for error in errors: + if error: + return error + return + class IntegerListField(Field): def _value(self): diff --git a/server/mergin/sync/permissions.py b/server/mergin/sync/permissions.py index 4e3901d5..4305a15f 100644 --- a/server/mergin/sync/permissions.py +++ b/server/mergin/sync/permissions.py @@ -192,7 +192,7 @@ def is_active_workspace(workspace): return workspace.is_active or is_admin -def require_project(ws, project_name, permission): +def require_project(ws, project_name, permission) -> Project: workspace = current_app.ws_handler.get_by_name(ws) if not workspace: abort(404) diff --git a/server/mergin/sync/public_api_controller.py b/server/mergin/sync/public_api_controller.py index 6a39e441..9f4eca3d 100644 --- a/server/mergin/sync/public_api_controller.py +++ b/server/mergin/sync/public_api_controller.py @@ -25,15 +25,16 @@ ) from pygeodiff import GeoDiffLibError from flask_login import current_user -from sqlalchemy import and_, desc, asc, text, func, select +from sqlalchemy import and_, desc, asc from sqlalchemy.exc import IntegrityError from binaryornot.check import is_binary from gevent import sleep import base64 -from sqlalchemy.orm import load_only from werkzeug.exceptions import HTTPException +from mergin.sync.forms import project_name_validation + from .interfaces import WorkspaceRole from ..app import db from ..auth import auth_required @@ -83,9 +84,9 @@ is_valid_uuid, gpkg_wkb_to_wkt, is_versioned_file, - is_name_allowed, get_project_path, get_device_id, + is_valid_path, ) from .errors import StorageLimitHit from ..utils import format_time_delta @@ -179,10 +180,11 @@ def add_project(namespace): # noqa: E501 """ request.json["name"] = request.json["name"].strip() - if not is_name_allowed(request.json["name"]): + validation_error = project_name_validation(request.json["name"]) + if validation_error: abort( 400, - "Please don't start project name with . and use only alphanumeric or these -._! characters in project name.", + validation_error, ) if request.is_json: @@ -805,10 +807,13 @@ def project_push(namespace, project_name): abort(400, "Another process is running. Please try later.") upload_changes = ChangesSchema(context={"version": version + 1}).load(changes) - # check if same file is not already uploaded + for item in upload_changes.added: + # check if same file is not already uploaded if not all(ele.path != item.path for ele in project.files): abort(400, f"File {item.path} has been already uploaded") + if not is_valid_path(item.path): + abort(400, f"File {item.path} contains invalid characters.") # changes' files must be unique changes_files = [ @@ -1194,6 +1199,8 @@ def clone_project(namespace, project_name): # noqa: E501 dest_ns = request.json.get("namespace", cp_workspace_name).strip() dest_project = request.json.get("project", cloned_project.name).strip() ws = current_app.ws_handler.get_by_name(dest_ns) + if not dest_project: + abort(400, "Project name cannot be empty") if not ws: if dest_ns == current_user.username: abort( @@ -1204,12 +1211,9 @@ def clone_project(namespace, project_name): # noqa: E501 abort(404, "Workspace does not exist") if not ws.user_has_permissions(current_user, "admin"): abort(403, "You do not have permissions for this workspace") - - if not is_name_allowed(dest_project): - abort( - 400, - "Please don't start project name with . and use only alphanumeric or these -._! characters in project name.", - ) + validation = project_name_validation(dest_project) + if validation and dest_project != cloned_project.name: + abort(400, validation) _project = Project.query.filter_by(name=dest_project, workspace_id=ws.id).first() if _project: diff --git a/server/mergin/sync/public_api_v2.yaml b/server/mergin/sync/public_api_v2.yaml index 3d47ca4e..04dbce61 100644 --- a/server/mergin/sync/public_api_v2.yaml +++ b/server/mergin/sync/public_api_v2.yaml @@ -66,8 +66,6 @@ paths: example: InvalidProjectName detail: type: string - enum: - - "Entered project name is invalid" example: "Entered project name is invalid" "401": $ref: "#/components/responses/Unauthorized" @@ -267,7 +265,7 @@ components: example: writer Role: allOf: - - $ref: '#/components/schemas/ProjectRole' + - $ref: "#/components/schemas/ProjectRole" nullable: false description: combination of workspace role and project role ProjectMember: @@ -284,8 +282,8 @@ components: format: email example: john.doe@example.com workspace_role: - $ref: '#/components/schemas/WorkspaceRole' + $ref: "#/components/schemas/WorkspaceRole" project_role: - $ref: '#/components/schemas/ProjectRole' + $ref: "#/components/schemas/ProjectRole" role: - $ref: '#/components/schemas/Role' + $ref: "#/components/schemas/Role" diff --git a/server/mergin/sync/public_api_v2_controller.py b/server/mergin/sync/public_api_v2_controller.py index d5d36b11..e95029e1 100644 --- a/server/mergin/sync/public_api_v2_controller.py +++ b/server/mergin/sync/public_api_v2_controller.py @@ -7,6 +7,8 @@ from flask import abort, jsonify from flask_login import current_user +from mergin.sync.forms import project_name_validation + from .schemas import ProjectMemberSchema from .workspace import WorkspaceRole from ..app import db @@ -15,7 +17,6 @@ from .models import Project, ProjectRole, ProjectMember from .permissions import ProjectPermissions, require_project_by_uuid from .private_api_controller import project_access_granted -from .utils import is_name_allowed @auth_required @@ -45,13 +46,11 @@ def delete_project_now(id): def update_project(id): """Rename project""" project = require_project_by_uuid(id, ProjectPermissions.Update) - new_name = request.json["name"] - - if not is_name_allowed(new_name): + new_name = request.json["name"].strip() + validation_error = project_name_validation(new_name) + if validation_error: return ( - jsonify( - code="InvalidProjectName", detail="Entered project name is invalid" - ), + jsonify(code="InvalidProjectName", detail=validation_error), 400, ) new_name_exists = Project.query.filter_by( diff --git a/server/mergin/sync/utils.py b/server/mergin/sync/utils.py index b4120acc..f844d6b6 100644 --- a/server/mergin/sync/utils.py +++ b/server/mergin/sync/utils.py @@ -15,6 +15,12 @@ from flask import Request from typing import Optional from sqlalchemy import text +from pathvalidate import ( + validate_filename, + ValidationError, + is_valid_filepath, + is_valid_filename, +) def generate_checksum(file, chunk_size=4096): @@ -261,22 +267,36 @@ def convert_byte(size_bytes, unit): return size_bytes -def is_name_allowed(string): - """Check if string is just has whitelisted character +def is_reserved_word(name: str) -> str | None: + """Check if name is reserved in system""" + reserved = r"^support$|^helpdesk$|^merginmaps$|^lutraconsulting$|^mergin$|^lutra$|^input$|^admin$|^sales$" + if re.match(reserved, name) is not None: + return "The provided value is invalid." + return None - :param string: string to be checked. - :type string: str - :return: boolean of has just whitelisted character - :rtype: bool - """ - return ( - re.match( - r".*[\@\#\$\%\^\&\*\(\)\{\}\[\]\?\'\"`,;\:\+\=\~\\\/\|\<\>].*|^[\s^\.].*$|^CON$|^PRN$|^AUX$|^NUL$|^COM\d$|^LPT\d|^support$|^helpdesk$|^merginmaps$|^lutraconsulting$|^mergin$|^lutra$|^input$|^admin$|^sales$|^$", - string, - ) - is None - ) +def has_valid_characters(name: str) -> str | None: + """Check if name contains only valid characters""" + if re.match(r"^[\w\s\-\.]+$", name) is None: + return "Please use only alphanumeric or the following -_. characters." + return None + + +def has_valid_first_character(name: str) -> str | None: + """Check if name contains only valid characters in first position""" + if re.match(r"^[\s^\.].*$", name) is not None: + return f"Value can not start with space or dot." + return None + + +def check_filename(name: str) -> str | None: + """Check if name contains only valid characters for filename""" + error = None + try: + validate_filename(name) + except ValidationError: + error = "The provided value is invalid." + return error def workspace_names(workspaces): @@ -337,3 +357,14 @@ def files_size(): """ ) return db.session.execute(files_size).scalar() + + +def is_valid_path(filepath: str) -> bool: + """Check filepath and filename for invalid characters, absolute path or path traversal""" + return ( + not len(re.split(r"\.[/\\]", filepath)) > 1 # ./ or .\ + and is_valid_filepath(filepath) # invalid characters in filepath, absolute path + and is_valid_filename( + os.path.basename(filepath) + ) # invalid characters in filename, reserved filenames + ) diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index 280626de..18b50ffb 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import os +import time import pytest import json from flask import url_for @@ -12,7 +13,7 @@ from unittest.mock import patch from mergin.tests import test_workspace - +from ..auth.app import generate_confirmation_token, confirm_token from ..auth.models import User, UserProfile, LoginHistory from ..auth.tasks import anonymize_removed_users from ..app import db @@ -152,26 +153,20 @@ def test_user_register(client, username, email, pwd, expected): def test_confirm_email(app, client): - serializer = URLSafeTimedSerializer(app.config["SECRET_KEY"]) - token = serializer.dumps( - "mergin@mergin.com", salt=app.config["SECURITY_PASSWORD_SALT"] - ) - resp = client.post(url_for("/.mergin_auth_controller_confirm_email", token=token)) - assert resp.status_code == 200 - user = User.query.filter_by(username="mergin").first() - # tests with old registered user + token = generate_confirmation_token( + app, user.email, app.config["SECURITY_EMAIL_SALT"] + ) user.verified_email = False - user.registration_date = datetime.utcnow() - timedelta(days=1) db.session.commit() - resp = client.post(url_for("/.mergin_auth_controller_confirm_email", token=token)) - assert resp.status_code == 200 - # try again with freshly registered user - user.verified_email = False - user.registration_date = datetime.utcnow() - db.session.add(user) - db.session.commit() + # verify token can't be used in different context + resp = client.post( + url_for("/.mergin_auth_controller_confirm_new_password", token=token), + json={"password": "ilovemergin#0", "confirm": "ilovemergin#0"}, + ) + assert resp.status_code == 400 + resp = client.post(url_for("/.mergin_auth_controller_confirm_email", token=token)) assert resp.status_code == 200 @@ -187,21 +182,35 @@ def test_confirm_email(app, client): resp = client.post( url_for( "/.mergin_auth_controller_confirm_email", - token=serializer.dumps( - "tests@mergin.com", salt=app.config["SECURITY_PASSWORD_SALT"] + token=generate_confirmation_token( + app, "tests@mergin.com", app.config["SECURITY_EMAIL_SALT"] ), ) ) assert resp.status_code == 404 + # test expired token + token = generate_confirmation_token( + app, user.email, app.config["SECURITY_EMAIL_SALT"] + ) + time.sleep(2) + assert not confirm_token( + token=token, expiration=1, salt=app.config["SECURITY_EMAIL_SALT"] + ) + def test_confirm_password(app, client): - serializer = URLSafeTimedSerializer(app.config["SECRET_KEY"]) - token = serializer.dumps( - "mergin@mergin.com", salt=app.config["SECURITY_PASSWORD_SALT"] + user = User.query.filter_by(username="mergin").first() + token = generate_confirmation_token( + app, user.email, app.config["SECURITY_PASSWORD_SALT"] ) form_data = {"password": "ilovemergin#0", "confirm": "ilovemergin#0"} + + # verify token can't be used in different context + resp = client.post(url_for("/.mergin_auth_controller_confirm_email", token=token)) + assert resp.status_code == 400 + resp = client.post( url_for("/.mergin_auth_controller_confirm_new_password", token=token), data=json.dumps(form_data), @@ -221,8 +230,8 @@ def test_confirm_password(app, client): resp = client.post( url_for( "/.mergin_auth_controller_confirm_new_password", - token=serializer.dumps( - "tests@mergin.com", salt=app.config["SECURITY_PASSWORD_SALT"] + token=generate_confirmation_token( + app, "tests@mergin.com", app.config["SECURITY_PASSWORD_SALT"] ), ), data=json.dumps(form_data), @@ -240,8 +249,8 @@ def test_confirm_password(app, client): resp = client.post( url_for( "/.mergin_auth_controller_confirm_new_password", - token=serializer.dumps( - "tests@mergin.com", salt=app.config["SECURITY_PASSWORD_SALT"] + token=generate_confirmation_token( + app, "tests@mergin.com", app.config["SECURITY_PASSWORD_SALT"] ), ) ) diff --git a/server/mergin/tests/test_project_controller.py b/server/mergin/tests/test_project_controller.py index 76c94d89..9d8671e8 100644 --- a/server/mergin/tests/test_project_controller.py +++ b/server/mergin/tests/test_project_controller.py @@ -36,8 +36,7 @@ ProjectFilePath, ) from ..sync.files import ChangesSchema -from ..sync.permissions import projects_query -from ..sync.schemas import ProjectListSchema, ProjectSchema +from ..sync.schemas import ProjectListSchema from ..sync.utils import generate_checksum, is_versioned_file from ..auth.models import User, UserProfile @@ -386,6 +385,7 @@ def test_get_projects_by_names(client): ({"name": "foo/bar", "template": test_project}, 400), # invalid project name ({"name": "ba%r", "template": test_project}, 400), # invalid project name ({"name": "bar*", "template": test_project}, 400), # invalid project name + ({"name": " ", "template": test_project}, 400), # empty ({"name": "support", "template": test_project}, 400), # forbidden project name ({"name": test_project}, 409), ] @@ -1486,8 +1486,7 @@ def test_whole_push_process(client): "テスト.txt", "£¥§.txt", "name_1_1.txt", - "name\\1\\1.txt", - "+?%@&.qgs", + "+%@&.qgs", ] # prepare dummy files os.mkdir(test_dir) @@ -2513,3 +2512,35 @@ def test_signals(client): ) as push_finished_mock: upload_file_to_project(project, "test.txt", client) push_finished_mock.assert_called_once() + + +def test_upload_validation(client): + """Test filepath and filename validation during file upload""" + push_start_url = url_for( + f"/v1.mergin_sync_public_api_controller_project_push", + namespace=test_workspace_name, + project_name=test_project, + ) + filename = "image.png" + with open(os.path.join(TMP_DIR, filename), "w") as f: + f.write("Hello, Mergin!") + changes = { + "added": [file_info(TMP_DIR, filename, chunk_size=CHUNK_SIZE)], + "updated": [], + "removed": [], + } + # Manipulate the path by prepending ../../ + manipulated_path = "../../image.png" + changes["added"][0]["path"] = manipulated_path + # Block script upload in push_start because of the invalid path + resp = client.post( + push_start_url, + data=json.dumps( + {"version": "v1", "changes": changes}, cls=DateTimeEncoder + ).encode("utf-8"), + headers=json_headers, + ) + assert resp.status_code == 400 + assert ( + resp.json["detail"] == f"File {manipulated_path} contains invalid characters." + ) diff --git a/server/mergin/tests/test_public_api_v2.py b/server/mergin/tests/test_public_api_v2.py index a17d1696..2d88d652 100644 --- a/server/mergin/tests/test_public_api_v2.py +++ b/server/mergin/tests/test_public_api_v2.py @@ -64,7 +64,7 @@ def test_rename_project(client): assert response.status_code == 400 assert response.json["code"] == "InvalidProjectName" response = client.patch( - f"v2/projects/{project.id}", json={"name": " new_project_name"} + f"v2/projects/{project.id}", json={"name": ".new_project_name"} ) assert response.status_code == 400 assert response.json["code"] == "InvalidProjectName" diff --git a/server/mergin/tests/test_statistics.py b/server/mergin/tests/test_statistics.py index b73cb042..a87ba08c 100644 --- a/server/mergin/tests/test_statistics.py +++ b/server/mergin/tests/test_statistics.py @@ -2,6 +2,9 @@ # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial +import csv +from dataclasses import asdict +from datetime import timedelta, timezone, datetime import json from unittest.mock import patch import requests @@ -11,8 +14,8 @@ from mergin.sync.models import Project, ProjectRole from ..app import db -from ..stats.tasks import send_statistics -from ..stats.models import MerginInfo +from ..stats.tasks import get_callhome_data, save_statistics, send_statistics +from ..stats.models import MerginInfo, MerginStatistics, ServerCallhomeData from .utils import Response, add_user, create_project, create_workspace @@ -152,3 +155,66 @@ def test_server_updates(client): mock.side_effect = requests.exceptions.RequestException("Some failure") resp = client.get(url) assert resp.status_code == 400 + + +def test_save_statistics(app, client): + """Test save statistics celery job""" + info = MerginInfo.query.first() + app.config["CONTACT_EMAIL"] = "test@example.com" + assert MerginStatistics.query.count() == 0 + save_statistics.s().apply() + assert MerginStatistics.query.count() == 1 + stats = MerginStatistics.query.order_by(MerginStatistics.created_at.desc()).first() + stats_json_data = get_callhome_data(info) + assert stats.created_at + assert stats.data == asdict(stats_json_data) + + +def test_download_report(app, client): + """Test download report endpoint""" + url = "/app/admin/report" + resp = client.get(url) + resp.status_code == 400 + + # bad date format + resp = client.get(f"{url}?date_from=2021-01-01T00:00:00&date_to=2021-01-01") + assert resp.status_code == 400 + + app.config["CONTACT_EMAIL"] = "test@example.com" + save_statistics.s().apply() + resp = client.get( + f"{url}?date_from=2021-01-01&date_to={datetime.now(timezone.utc).strftime('%Y-%m-%d')}" + ) + assert resp.status_code == 200 + assert resp.mimetype == "text/csv" + lines = resp.data.splitlines() + assert len(lines) == 2 + + stat = MerginStatistics.query.first() + keys = list(asdict(ServerCallhomeData(**stat.data)).keys()) + ["created_at"] + assert lines[0].decode("UTF-8") == ",".join(keys) + + # test same day + stat.created_at = datetime(2021, 1, 1, tzinfo=timezone.utc) + db.session.commit() + + resp = client.get( + f"{url}?date_from=2021-01-01&date_to={datetime.now(timezone.utc).strftime('%Y-%m-%d')}" + ) + assert resp.status_code == 200 + assert resp.mimetype == "text/csv" + lines = resp.data.splitlines() + assert len(lines) == 2 + + # empty response + stat.created_at = datetime(2020, 1, 1, tzinfo=timezone.utc) + db.session.commit() + resp = client.get( + f"{url}?date_from=2021-01-01&date_to={datetime.now(timezone.utc).strftime('%Y-%m-%d')}" + ) + assert resp.status_code == 200 + assert resp.mimetype == "text/csv" + lines = resp.data.splitlines() + empty_file = f"{','.join(keys)}\r\n" + assert resp.data.decode("UTF-8") == empty_file + assert len(lines) == 1 diff --git a/server/mergin/tests/test_utils.py b/server/mergin/tests/test_utils.py index 6141e573..2a70a29e 100644 --- a/server/mergin/tests/test_utils.py +++ b/server/mergin/tests/test_utils.py @@ -11,7 +11,15 @@ from unittest.mock import MagicMock from ..app import db -from ..sync.utils import parse_gpkgb_header_size, gpkg_wkb_to_wkt, is_name_allowed +from ..sync.utils import ( + parse_gpkgb_header_size, + gpkg_wkb_to_wkt, + is_reserved_word, + has_valid_characters, + has_valid_first_character, + check_filename, + is_valid_path, +) from ..auth.models import LoginHistory, User from . import json_headers from .utils import login @@ -137,13 +145,15 @@ def test_is_name_allowed(): ("Pro123ject", True), ("123PROJECT", True), ("PROJECT", True), - ("project ", True), + # Not valid filename + ("project ", False), ("pro ject", True), ("proj-ect", True), ("-project", True), ("proj_ect", True), ("proj.ect", True), - ("proj!ect", True), + # We are removing ! from valids + ("proj!ect", False), (" project", False), (".project", False), ("proj~ect", False), @@ -182,14 +192,15 @@ def test_is_name_allowed(): ("NUL", False), ("NULL", True), ("PRN", False), - ("LPT0", False), + # is not reserved word + ("LPT0", True), ("lpt0", True), ("LPT1", False), - ("lpt1", True), + ("lpt1", False), ("COM1", False), - ("com1", True), + ("com1", False), ("AUX", False), - ("AuX", True), + ("AuX", False), ("projAUXect", True), ("CONproject", True), ("projectCON", True), @@ -204,7 +215,41 @@ def test_is_name_allowed(): ("sales", False), ("", False), (" ", False), + ("😄guy", False), + ("会社", True), ] for t in test_cases: - assert is_name_allowed(t[0]) == t[1] + name = t[0] + expected = t[1] + assert ( + not ( + has_valid_characters(name) + and has_valid_first_character(name) + and check_filename(name) + and is_reserved_word(name) + ) + == expected + ) + + +filepaths = [ + ("/home/user/mm/project/survey.gpkg", False), + ("C:\Documents\Summer2018.pdf", False), + ("\Desktop\Summer2019.pdf", False), + ("../image.png", False), + ("./image.png", False), + ("assets/photos/im?age.png", False), + ("assets/photos/CON.png", False), + ("assets/photos/CONfile.png", True), + ("image.png", True), + ("images/image.png", True), + ("media/photos/image.png", True), + ("med..ia/pho.tos/ima...ge.png", True), + ("med/../ia/pho.tos/ima...ge.png", False), +] + + +@pytest.mark.parametrize("filepath,allow", filepaths) +def test_is_valid_path(client, filepath, allow): + assert is_valid_path(filepath) == allow diff --git a/server/mergin/version.py b/server/mergin/version.py index 9852ba0d..a366d530 100644 --- a/server/mergin/version.py +++ b/server/mergin/version.py @@ -4,4 +4,4 @@ def get_version(): - return "2024.5.3" + return "2025.2.0" diff --git a/server/migrations/community/ba5051218de4_add_mergin_statistics_table.py b/server/migrations/community/ba5051218de4_add_mergin_statistics_table.py new file mode 100644 index 00000000..56a3778f --- /dev/null +++ b/server/migrations/community/ba5051218de4_add_mergin_statistics_table.py @@ -0,0 +1,43 @@ +"""add mergin statistics table + +Revision ID: ba5051218de4 +Revises: d02961c7416c +Create Date: 2025-01-24 12:41:23.714579 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "ba5051218de4" +down_revision = "d02961c7416c" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "mergin_statistics", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column( + "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + sa.Column("data", postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_mergin_statistics")), + ) + op.create_index( + op.f("ix_mergin_statistics_created_at"), + "mergin_statistics", + ["created_at"], + unique=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + op.drop_index( + op.f("ix_mergin_statistics_created_at"), table_name="mergin_statistics" + ) + op.drop_table("mergin_statistics") diff --git a/server/setup.py b/server/setup.py index 236fa702..70673c1c 100644 --- a/server/setup.py +++ b/server/setup.py @@ -6,7 +6,7 @@ setup( name="mergin", - version="2024.5.3", + version="2025.2.0", url="https://github.com/MerginMaps/mergin", license="AGPL-3.0-only", author="Lutra Consulting Limited", diff --git a/web-app/packages/admin-lib/components.d.ts b/web-app/packages/admin-lib/components.d.ts index 89889c60..bf16d936 100644 --- a/web-app/packages/admin-lib/components.d.ts +++ b/web-app/packages/admin-lib/components.d.ts @@ -9,6 +9,7 @@ declare module 'vue' { export interface GlobalComponents { PAvatar: typeof import('primevue/avatar')['default'] PButton: typeof import('primevue/button')['default'] + PCalendar: typeof import('primevue/calendar')['default'] PColumn: typeof import('primevue/column')['default'] PDataTable: typeof import('primevue/datatable')['default'] PDivider: typeof import('primevue/divider')['default'] diff --git a/web-app/packages/admin-lib/src/modules/admin/adminApi.ts b/web-app/packages/admin-lib/src/modules/admin/adminApi.ts index 6380ef6c..08114297 100644 --- a/web-app/packages/admin-lib/src/modules/admin/adminApi.ts +++ b/web-app/packages/admin-lib/src/modules/admin/adminApi.ts @@ -18,7 +18,8 @@ import { CreateUserData, PaginatedAdminProjectsResponse, PaginatedAdminProjectsParams, - ServerUsageResponse + ServerUsageResponse, + DownloadReportParams } from '@/modules/admin/types' export const AdminApi = { @@ -99,6 +100,23 @@ export const AdminApi = { }, async getServerUsage(): Promise> { - return AdminModule.httpService.get('/app/admin/usage', ) + return AdminModule.httpService.get('/app/admin/usage') + }, + + /** + * Create url for csv file with server statistics + */ + contructDownloadStatisticsUrl(): string { + return AdminModule.httpService.absUrl('/app/admin/report') + }, + + async downloadStatistics( + url: string, + params: DownloadReportParams + ): Promise> { + return AdminModule.httpService.get(url, { + responseType: 'blob', + params + }) } } diff --git a/web-app/packages/admin-lib/src/modules/admin/components/ReportDownloadDialog.vue b/web-app/packages/admin-lib/src/modules/admin/components/ReportDownloadDialog.vue new file mode 100644 index 00000000..6ba72cc8 --- /dev/null +++ b/web-app/packages/admin-lib/src/modules/admin/components/ReportDownloadDialog.vue @@ -0,0 +1,86 @@ + + + diff --git a/web-app/packages/admin-lib/src/modules/admin/store.ts b/web-app/packages/admin-lib/src/modules/admin/store.ts index 7b9281ff..1f76d4b1 100644 --- a/web-app/packages/admin-lib/src/modules/admin/store.ts +++ b/web-app/packages/admin-lib/src/modules/admin/store.ts @@ -13,6 +13,7 @@ import { useNotificationStore, UserResponse } from '@mergin/lib' +import FileSaver from 'file-saver' import { defineStore, getActivePinia } from 'pinia' import Cookies from 'universal-cookie' @@ -22,10 +23,12 @@ import { AdminApi } from '@/modules/admin/adminApi' import { LatestServerVersionResponse, PaginatedAdminProjectsParams, - PaginatedAdminProjectsResponse, ServerUsageResponse, + PaginatedAdminProjectsResponse, + ServerUsageResponse, UpdateUserPayload, UsersResponse } from '@/modules/admin/types' +import axios from 'axios' export interface AdminState { loading: boolean @@ -325,6 +328,43 @@ export const useAdminStore = defineStore('adminModule', { } catch (e) { notificationStore.error({ text: errorUtils.getErrorMessage(e) }) } + }, + + async downloadReport(payload: { from: Date; to: Date }) { + const notificationStore = useNotificationStore() + + try { + const { from, to } = payload + const url = AdminApi.contructDownloadStatisticsUrl() + const date = new Date() + // we need to sanitize hours from custom range in Calendar picker, because it's 00:00:00 by default. If you convert it to UTC -> it could be previous day, which is not what you selected. + from.setHours(date.getHours(), date.getMinutes(), date.getSeconds()) + to.setHours(date.getHours(), date.getMinutes(), date.getSeconds()) + // toISOString converts date to iso format and UTC zone + const date_from = payload.from.toISOString().split('T')[0] + const date_to = payload.to.toISOString().split('T')[0] + const resp = await AdminApi.downloadStatistics(url, { + date_from, + date_to + }) + const fileName = + resp.headers['content-disposition'].split('filename=')[1] + const extension = fileName.split('.')[1] + new FileSaver.saveAs( + resp.data, + `usage-report-${date.toISOString().split('T')[0]}.${extension}` + ) + } catch (e) { + // parse error details from blob + if (axios.isAxiosError(e)) { + let resp + const blob = new Blob([e.response.data], { type: 'text/plain' }) + blob.text().then((text) => { + resp = JSON.parse(text) + notificationStore.error({ text: resp.detail }) + }) + } + } } } }) diff --git a/web-app/packages/admin-lib/src/modules/admin/types.ts b/web-app/packages/admin-lib/src/modules/admin/types.ts index 2c3bc54d..e443b0a7 100644 --- a/web-app/packages/admin-lib/src/modules/admin/types.ts +++ b/web-app/packages/admin-lib/src/modules/admin/types.ts @@ -80,4 +80,9 @@ export interface ServerUsage { editors: number } +export interface DownloadReportParams { + date_from: string + date_to: string +} + /* eslint-enable camelcase */ diff --git a/web-app/packages/admin-lib/src/modules/admin/views/OverviewView.vue b/web-app/packages/admin-lib/src/modules/admin/views/OverviewView.vue index 03c87bb9..d90d4747 100644 --- a/web-app/packages/admin-lib/src/modules/admin/views/OverviewView.vue +++ b/web-app/packages/admin-lib/src/modules/admin/views/OverviewView.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
- +