Compare commits
2 Commits
0d4fbef13d
...
50aff05614
| Author | SHA1 | Date |
|---|---|---|
|
|
50aff05614 | |
|
|
19d264a03b |
|
|
@ -8,13 +8,16 @@ RUN echo Creating dev user with UID $DEV_USER_UID && \
|
|||
USER dev
|
||||
ENV PATH="/home/dev/.local/bin:${PATH}"
|
||||
|
||||
# Don't write bytecode cache files
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Install pipenv to generate requirements.txt from Pipfile
|
||||
RUN pip install pipenv
|
||||
|
||||
# Generate requirements.txt and install dependencies with pip
|
||||
WORKDIR /app
|
||||
COPY Pipfile Pipfile.lock ./
|
||||
RUN pipenv lock --requirements --dev > tmp_requirements.txt && \
|
||||
RUN pipenv requirements --dev > tmp_requirements.txt && \
|
||||
pip install -r tmp_requirements.txt
|
||||
|
||||
# Set default command
|
||||
|
|
|
|||
3
Pipfile
3
Pipfile
|
|
@ -6,12 +6,13 @@ name = "pypi"
|
|||
[packages]
|
||||
gunicorn = "~=20.1"
|
||||
werkzeug = "~=2.0"
|
||||
flask = "~=2.0"
|
||||
flask = "~=2.2"
|
||||
pyyaml = "*"
|
||||
sqlalchemy = "~=1.4"
|
||||
pymysql = "*"
|
||||
alembic = "~=1.7"
|
||||
python-dateutil = "*"
|
||||
validataclass = "~=0.6"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "f63a254c353bc5c8e6cbe2a920f8402ba0da5bec775bfff91f64fa485e9bf95c"
|
||||
"sha256": "84613c292abfb9f596faadd525137db1053dfe39d650638455c11ac684a91487"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
|
@ -18,88 +18,87 @@
|
|||
"default": {
|
||||
"alembic": {
|
||||
"hashes": [
|
||||
"sha256:29be0856ec7591c39f4e1cb10f198045d890e6e2274cf8da80cb5e721a09642b",
|
||||
"sha256:4961248173ead7ce8a21efb3de378f13b8398e6630fab0eb258dc74a8af24c58"
|
||||
"sha256:0a024d7f2de88d738d7395ff866997314c837be6104e90c5724350313dee4da4",
|
||||
"sha256:cd0b5e45b14b706426b833f06369b9a6d5ee03f826ec3238723ce8caaf6e5ffa"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.7"
|
||||
"version": "==1.8.1"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
|
||||
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
|
||||
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
|
||||
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.2"
|
||||
"version": "==8.1.3"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:8a4cf32d904cf5621db9f0c9fbcd7efabf3003f22a04e4d0ce790c7137ec5264",
|
||||
"sha256:a8c9bd3e558ec99646d177a9739c41df1ded0629480b4c8d2975412f3c9519c8"
|
||||
"sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b",
|
||||
"sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.1"
|
||||
"version": "==2.2.2"
|
||||
},
|
||||
"greenlet": {
|
||||
"hashes": [
|
||||
"sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3",
|
||||
"sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711",
|
||||
"sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd",
|
||||
"sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073",
|
||||
"sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708",
|
||||
"sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67",
|
||||
"sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23",
|
||||
"sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1",
|
||||
"sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08",
|
||||
"sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd",
|
||||
"sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2",
|
||||
"sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa",
|
||||
"sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8",
|
||||
"sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40",
|
||||
"sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab",
|
||||
"sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6",
|
||||
"sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc",
|
||||
"sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b",
|
||||
"sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e",
|
||||
"sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963",
|
||||
"sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3",
|
||||
"sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d",
|
||||
"sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d",
|
||||
"sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe",
|
||||
"sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28",
|
||||
"sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3",
|
||||
"sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e",
|
||||
"sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c",
|
||||
"sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d",
|
||||
"sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0",
|
||||
"sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497",
|
||||
"sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee",
|
||||
"sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713",
|
||||
"sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58",
|
||||
"sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a",
|
||||
"sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06",
|
||||
"sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88",
|
||||
"sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965",
|
||||
"sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f",
|
||||
"sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4",
|
||||
"sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5",
|
||||
"sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c",
|
||||
"sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a",
|
||||
"sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1",
|
||||
"sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43",
|
||||
"sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627",
|
||||
"sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b",
|
||||
"sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168",
|
||||
"sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d",
|
||||
"sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5",
|
||||
"sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478",
|
||||
"sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf",
|
||||
"sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce",
|
||||
"sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c",
|
||||
"sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"
|
||||
"sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4",
|
||||
"sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc",
|
||||
"sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8",
|
||||
"sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202",
|
||||
"sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380",
|
||||
"sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08",
|
||||
"sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7",
|
||||
"sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268",
|
||||
"sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e",
|
||||
"sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809",
|
||||
"sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403",
|
||||
"sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080",
|
||||
"sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76",
|
||||
"sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0",
|
||||
"sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2",
|
||||
"sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e",
|
||||
"sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1",
|
||||
"sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd",
|
||||
"sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c",
|
||||
"sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe",
|
||||
"sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96",
|
||||
"sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20",
|
||||
"sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600",
|
||||
"sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed",
|
||||
"sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2",
|
||||
"sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26",
|
||||
"sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a",
|
||||
"sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32",
|
||||
"sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79",
|
||||
"sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b",
|
||||
"sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa",
|
||||
"sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57",
|
||||
"sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e",
|
||||
"sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba",
|
||||
"sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700",
|
||||
"sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318",
|
||||
"sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382",
|
||||
"sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905",
|
||||
"sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e",
|
||||
"sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455",
|
||||
"sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910",
|
||||
"sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2",
|
||||
"sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3",
|
||||
"sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5",
|
||||
"sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41",
|
||||
"sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d",
|
||||
"sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9",
|
||||
"sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47",
|
||||
"sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49",
|
||||
"sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477",
|
||||
"sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33",
|
||||
"sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25",
|
||||
"sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90",
|
||||
"sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932"
|
||||
],
|
||||
"markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
|
||||
"version": "==1.1.2"
|
||||
"version": "==1.1.3"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
|
|
@ -119,19 +118,19 @@
|
|||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119",
|
||||
"sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"
|
||||
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
|
||||
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.1.1"
|
||||
"version": "==3.1.2"
|
||||
},
|
||||
"mako": {
|
||||
"hashes": [
|
||||
"sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba",
|
||||
"sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"
|
||||
"sha256:3724869b363ba630a272a5f89f68c070352137b8fd1757650017b7e06fda163f",
|
||||
"sha256:8efcb8004681b5f71d09c983ad5a9e6f5c40601a6ec469148753292abc0da534"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.2.0"
|
||||
"version": "==1.2.2"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
|
|
@ -197,6 +196,7 @@
|
|||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf",
|
||||
"sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
|
||||
"sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
|
||||
"sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
|
||||
|
|
@ -208,26 +208,32 @@
|
|||
"sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
|
||||
"sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
|
||||
"sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
|
||||
"sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782",
|
||||
"sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
|
||||
"sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
|
||||
"sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
|
||||
"sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
|
||||
"sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
|
||||
"sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1",
|
||||
"sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
|
||||
"sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
|
||||
"sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
|
||||
"sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
|
||||
"sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
|
||||
"sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
|
||||
"sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d",
|
||||
"sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
|
||||
"sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
|
||||
"sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7",
|
||||
"sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
|
||||
"sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
|
||||
"sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
|
||||
"sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358",
|
||||
"sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
|
||||
"sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
|
||||
"sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
|
||||
"sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
|
||||
"sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f",
|
||||
"sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
|
||||
"sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
|
||||
],
|
||||
|
|
@ -236,11 +242,11 @@
|
|||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8",
|
||||
"sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592"
|
||||
"sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82",
|
||||
"sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==62.1.0"
|
||||
"version": "==65.3.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
|
|
@ -252,53 +258,66 @@
|
|||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:093b3109c2747d5dc0fa4314b1caf4c7ca336d5c8c831e3cfbec06a7e861e1e6",
|
||||
"sha256:186cb3bd77abf2ddcf722f755659559bfb157647b3fd3f32ea1c70e8311e8f6b",
|
||||
"sha256:1b4eac3933c335d7f375639885765722534bb4e52e51cdc01a667eea822af9b6",
|
||||
"sha256:1ff9f84b2098ef1b96255a80981ee10f4b5d49b6cfeeccf9632c2078cd86052e",
|
||||
"sha256:28aa2ef06c904729620cc735262192e622db9136c26d8587f71f29ec7715628a",
|
||||
"sha256:28b17ebbaee6587013be2f78dc4f6e95115e1ec8dd7647c4e7be048da749e48b",
|
||||
"sha256:2c6c411d8c59afba95abccd2b418f30ade674186660a2d310d364843049fb2c1",
|
||||
"sha256:2ffc813b01dc6473990f5e575f210ca5ac2f5465ace3908b78ffd6d20058aab5",
|
||||
"sha256:48036698f20080462e981b18d77d574631a3d1fc2c33b416c6df299ec1d10b99",
|
||||
"sha256:48f0eb5bcc87a9b2a95b345ed18d6400daaa86ca414f6840961ed85c342af8f4",
|
||||
"sha256:4ba2c1f368bcf8551cdaa27eac525022471015633d5bdafbc4297e0511f62f51",
|
||||
"sha256:53c7469b86a60fe2babca4f70111357e6e3d5150373bc85eb3b914356983e89a",
|
||||
"sha256:6204d06bfa85f87625e1831ca663f9dba91ac8aec24b8c65d02fb25cbaf4b4d7",
|
||||
"sha256:63c82c9e8ccc2fb4bfd87c24ffbac320f70b7c93b78f206c1f9c441fa3013a5f",
|
||||
"sha256:70e571ae9ee0ff36ed37e2b2765445d54981e4d600eccdf6fe3838bc2538d157",
|
||||
"sha256:95411abc0e36d18f54fa5e24d42960ea3f144fb16caaa5a8c2e492b5424cc82c",
|
||||
"sha256:9837133b89ad017e50a02a3b46419869cf4e9aa02743e911b2a9e25fa6b05403",
|
||||
"sha256:9bec63b1e20ef69484f530fb4b4837e050450637ff9acd6dccc7003c5013abf8",
|
||||
"sha256:9d8edfb09ed2b865485530c13e269833dab62ab2d582fde21026c9039d4d0e62",
|
||||
"sha256:9dac1924611698f8fe5b2e58601156c01da2b6c0758ba519003013a78280cf4d",
|
||||
"sha256:9e1a72197529ea00357640f21d92ffc7024e156ef9ac36edf271c8335facbc1a",
|
||||
"sha256:9e7094cf04e6042c4210a185fa7b9b8b3b789dd6d1de7b4f19452290838e48bd",
|
||||
"sha256:a4efb70a62cbbbc052c67dc66b5448b0053b509732184af3e7859d05fdf6223c",
|
||||
"sha256:a5dbdbb39c1b100df4d182c78949158073ca46ba2850c64fe02ffb1eb5b70903",
|
||||
"sha256:aeea6ace30603ca9a8869853bb4a04c7446856d7789e36694cd887967b7621f6",
|
||||
"sha256:b2489e70bfa2356f2d421106794507daccf6cc8711753c442fc97272437fc606",
|
||||
"sha256:babd63fb7cb6b0440abb6d16aca2be63342a6eea3dc7b613bb7a9357dc36920f",
|
||||
"sha256:c6fb6b9ed1d0be7fa2c90be8ad2442c14cbf84eb0709dd1afeeff1e511550041",
|
||||
"sha256:cfd8e4c64c30a5219032e64404d468c425bdbc13b397da906fc9bee6591fc0dd",
|
||||
"sha256:d17316100fcd0b6371ac9211351cb976fd0c2e12a859c1a57965e3ef7f3ed2bc",
|
||||
"sha256:d38a49aa75a5759d0d118e26701d70c70a37b896379115f8386e91b0444bfa70",
|
||||
"sha256:da25e75ba9f3fabc271673b6b413ca234994e6d3453424bea36bb5549c5bbaec",
|
||||
"sha256:e255a8dd5572b0c66d6ee53597d36157ad6cf3bc1114f61c54a65189f996ab03",
|
||||
"sha256:e8b09e2d90267717d850f2e2323919ea32004f55c40e5d53b41267e382446044",
|
||||
"sha256:ecc81336b46e31ae9c9bdfa220082079914e31a476d088d3337ecf531d861228",
|
||||
"sha256:effadcda9a129cc56408dd5b2ea20ee9edcea24bd58e6a1489fa27672d733182"
|
||||
"sha256:0002e829142b2af00b4eaa26c51728f3ea68235f232a2e72a9508a3116bd6ed0",
|
||||
"sha256:0005bd73026cd239fc1e8ccdf54db58b6193be9a02b3f0c5983808f84862c767",
|
||||
"sha256:0292f70d1797e3c54e862e6f30ae474014648bc9c723e14a2fda730adb0a9791",
|
||||
"sha256:036d8472356e1d5f096c5e0e1a7e0f9182140ada3602f8fff6b7329e9e7cfbcd",
|
||||
"sha256:05f0de3a1dc3810a776275763764bb0015a02ae0f698a794646ebc5fb06fad33",
|
||||
"sha256:0990932f7cca97fece8017414f57fdd80db506a045869d7ddf2dda1d7cf69ecc",
|
||||
"sha256:13e397a9371ecd25573a7b90bd037db604331cf403f5318038c46ee44908c44d",
|
||||
"sha256:14576238a5f89bcf504c5f0a388d0ca78df61fb42cb2af0efe239dc965d4f5c9",
|
||||
"sha256:199a73c31ac8ea59937cc0bf3dfc04392e81afe2ec8a74f26f489d268867846c",
|
||||
"sha256:2082a2d2fca363a3ce21cfa3d068c5a1ce4bf720cf6497fb3a9fc643a8ee4ddd",
|
||||
"sha256:22ff16cedab5b16a0db79f1bc99e46a6ddececb60c396562e50aab58ddb2871c",
|
||||
"sha256:2307495d9e0ea00d0c726be97a5b96615035854972cc538f6e7eaed23a35886c",
|
||||
"sha256:2ad2b727fc41c7f8757098903f85fafb4bf587ca6605f82d9bf5604bd9c7cded",
|
||||
"sha256:2d6495f84c4fd11584f34e62f9feec81bf373787b3942270487074e35cbe5330",
|
||||
"sha256:361f6b5e3f659e3c56ea3518cf85fbdae1b9e788ade0219a67eeaaea8a4e4d2a",
|
||||
"sha256:3e2ef592ac3693c65210f8b53d0edcf9f4405925adcfc031ff495e8d18169682",
|
||||
"sha256:4676d51c9f6f6226ae8f26dc83ec291c088fe7633269757d333978df78d931ab",
|
||||
"sha256:4ba7e122510bbc07258dc42be6ed45997efdf38129bde3e3f12649be70683546",
|
||||
"sha256:5102fb9ee2c258a2218281adcb3e1918b793c51d6c2b4666ce38c35101bb940e",
|
||||
"sha256:5323252be2bd261e0aa3f33cb3a64c45d76829989fa3ce90652838397d84197d",
|
||||
"sha256:58bb65b3274b0c8a02cea9f91d6f44d0da79abc993b33bdedbfec98c8440175a",
|
||||
"sha256:59bdc291165b6119fc6cdbc287c36f7f2859e6051dd923bdf47b4c55fd2f8bd0",
|
||||
"sha256:5facb7fd6fa8a7353bbe88b95695e555338fb038ad19ceb29c82d94f62775a05",
|
||||
"sha256:639e1ae8d48b3c86ffe59c0daa9a02e2bfe17ca3d2b41611b30a0073937d4497",
|
||||
"sha256:8eb8897367a21b578b26f5713833836f886817ee2ffba1177d446fa3f77e67c8",
|
||||
"sha256:90484a2b00baedad361402c257895b13faa3f01780f18f4a104a2f5c413e4536",
|
||||
"sha256:9c56e19780cd1344fcd362fd6265a15f48aa8d365996a37fab1495cae8fcd97d",
|
||||
"sha256:b67fc780cfe2b306180e56daaa411dd3186bf979d50a6a7c2a5b5036575cbdbb",
|
||||
"sha256:c0dcf127bb99458a9d211e6e1f0f3edb96c874dd12f2503d4d8e4f1fd103790b",
|
||||
"sha256:c23d64a0b28fc78c96289ffbd0d9d1abd48d267269b27f2d34e430ea73ce4b26",
|
||||
"sha256:ccfd238f766a5bb5ee5545a62dd03f316ac67966a6a658efb63eeff8158a4bbf",
|
||||
"sha256:cd767cf5d7252b1c88fcfb58426a32d7bd14a7e4942497e15b68ff5d822b41ad",
|
||||
"sha256:ce8feaa52c1640de9541eeaaa8b5fb632d9d66249c947bb0d89dd01f87c7c288",
|
||||
"sha256:d2e054aed4645f9b755db85bc69fc4ed2c9020c19c8027976f66576b906a74f1",
|
||||
"sha256:e16c2be5cb19e2c08da7bd3a87fed2a0d4e90065ee553a940c4fc1a0fb1ab72b",
|
||||
"sha256:e4b12e3d88a8fffd0b4ca559f6d4957ed91bd4c0613a4e13846ab8729dc5c251",
|
||||
"sha256:e570cfc40a29d6ad46c9aeaddbdcee687880940a3a327f2c668dd0e4ef0a441d",
|
||||
"sha256:eb30cf008850c0a26b72bd1b9be6730830165ce049d239cfdccd906f2685f892",
|
||||
"sha256:f37fa70d95658763254941ddd30ecb23fc4ec0c5a788a7c21034fc2305dab7cc",
|
||||
"sha256:f5ebeeec5c14533221eb30bad716bc1fd32f509196318fb9caa7002c4a364e4c",
|
||||
"sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.35"
|
||||
"version": "==1.4.41"
|
||||
},
|
||||
"validataclass": {
|
||||
"hashes": [
|
||||
"sha256:154021e42be7168aa6465e3c8d05b7add3ac5a78a5c1b02f17efdf886c345d9e",
|
||||
"sha256:db198754479aac855a62a4195f2ba6d0d0893cac10a949946566169ca92d4895"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.6.2"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:3c5493ece8268fecdcdc9c0b112211acd006354723b280d643ec732b6d4063d6",
|
||||
"sha256:f8e89a20aeabbe8a893c24a461d3ee5dad2123b05cc6abd73ceed01d39c3ae74"
|
||||
"sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f",
|
||||
"sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.1"
|
||||
"version": "==2.2.2"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dev": {
|
||||
"api_host": "http://localhost:5000"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
### Fetch all tasks
|
||||
GET {{api_host}}/api/tasks
|
||||
|
||||
|
||||
### Fetch one task
|
||||
GET {{api_host}}/api/tasks/1
|
||||
|
||||
|
||||
### Create new task
|
||||
POST {{api_host}}/api/tasks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "Some test task"
|
||||
}
|
||||
|
||||
|
||||
### Update task
|
||||
PATCH {{api_host}}/api/tasks/1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"description": "Update!"
|
||||
}
|
||||
|
||||
|
||||
### Delete task
|
||||
DELETE {{api_host}}/api/tasks/10
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
FLASK_APP=tofu_api.app
|
||||
FLASK_ENV=development
|
||||
FLASK_DEBUG=true
|
||||
FLASK_CONFIG_FILE=config.dev.yml
|
||||
|
||||
# Show SQLAlchemy 2.0 deprecation warnings
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from tofu_api.common.rest import BaseBlueprint
|
||||
from .tasks import TaskApiBlueprint
|
||||
from .tasks import TaskBlueprint
|
||||
|
||||
|
||||
class TofuApiBlueprint(BaseBlueprint):
|
||||
|
|
@ -12,4 +12,4 @@ class TofuApiBlueprint(BaseBlueprint):
|
|||
url_prefix = '/api'
|
||||
|
||||
def init_blueprint(self) -> None:
|
||||
self.register_blueprint(TaskApiBlueprint(self.app))
|
||||
self.register_blueprint(TaskBlueprint(self.app))
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
from .task_api import TaskApiBlueprint
|
||||
from .task_blueprint import TaskBlueprint
|
||||
from .task_handler import TaskHandler
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
from flask import jsonify
|
||||
from flask.views import MethodView
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from tofu_api.common.rest import BaseBlueprint
|
||||
from tofu_api.models import Task
|
||||
|
||||
|
||||
class TaskApiBlueprint(BaseBlueprint):
|
||||
"""
|
||||
Blueprint for the tasks REST API.
|
||||
"""
|
||||
|
||||
# Blueprint settings
|
||||
name = 'rest_api_tasks'
|
||||
import_name = __name__
|
||||
url_prefix = '/tasks'
|
||||
|
||||
def init_blueprint(self) -> None:
|
||||
"""
|
||||
Register URL rules.
|
||||
"""
|
||||
db_session = self.app.dependencies.get_db_session()
|
||||
|
||||
self.add_url_rule(
|
||||
'',
|
||||
view_func=self.create_view_func(TaskCollectionView, db_session=db_session),
|
||||
methods=['GET', 'POST'],
|
||||
)
|
||||
self.add_url_rule(
|
||||
'/<int:task_id>',
|
||||
view_func=self.create_view_func(TaskItemView, db_session=db_session),
|
||||
methods=['GET', 'PATCH', 'DELETE'],
|
||||
)
|
||||
|
||||
|
||||
class TaskBaseView(MethodView):
|
||||
"""
|
||||
Base class for view classes for the `/tasks` endpoint.
|
||||
"""
|
||||
|
||||
# TODO: Use a handler class instead of accessing the database session directly
|
||||
db_session: Session
|
||||
|
||||
def __init__(self, *, db_session: Session):
|
||||
self.db_session = db_session
|
||||
|
||||
|
||||
class TaskCollectionView(TaskBaseView):
|
||||
"""
|
||||
View class for `/tasks` endpoint.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Get list of all tasks.
|
||||
"""
|
||||
task_list = self.db_session.query(Task).all()
|
||||
return jsonify({
|
||||
'count': len(task_list),
|
||||
'items': [task.to_dict() for task in task_list],
|
||||
}), 200
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
Create a new task.
|
||||
"""
|
||||
# TODO: Parse request data and create real data
|
||||
new_task = Task(
|
||||
title='Do stuff'
|
||||
)
|
||||
self.db_session.add(new_task)
|
||||
self.db_session.commit()
|
||||
return jsonify(new_task.to_dict()), 201
|
||||
|
||||
|
||||
class TaskItemView(TaskBaseView):
|
||||
"""
|
||||
View class for `/tasks/<int:task_id>` endpoint.
|
||||
"""
|
||||
|
||||
def get(self, task_id: int):
|
||||
"""
|
||||
Get a single task by ID.
|
||||
"""
|
||||
task = self.db_session.query(Task).get(task_id)
|
||||
if task is None:
|
||||
raise NotFound(f'Task with ID {task_id} not found!')
|
||||
return jsonify(task.to_dict()), 200
|
||||
|
||||
def patch(self, task_id: int):
|
||||
"""
|
||||
Update a single task by ID.
|
||||
"""
|
||||
# TODO: Implement
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, task_id: int):
|
||||
"""
|
||||
Delete a single task by ID.
|
||||
"""
|
||||
# TODO: Implement
|
||||
raise NotImplementedError
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
from tofu_api.common.rest import BaseBlueprint
|
||||
from .task_views import TaskCollectionView, TaskItemView
|
||||
|
||||
|
||||
class TaskBlueprint(BaseBlueprint):
|
||||
"""
|
||||
Blueprint for the tasks REST API.
|
||||
"""
|
||||
|
||||
# Blueprint settings
|
||||
name = 'rest_api_tasks'
|
||||
import_name = __name__
|
||||
url_prefix = '/tasks'
|
||||
|
||||
def init_blueprint(self) -> None:
|
||||
"""
|
||||
Register URL rules.
|
||||
"""
|
||||
task_handler = self.app.dependencies.get_task_handler()
|
||||
|
||||
self.add_url_rule(
|
||||
'',
|
||||
view_func=TaskCollectionView.as_view(task_handler=task_handler),
|
||||
methods=['GET', 'POST'],
|
||||
)
|
||||
self.add_url_rule(
|
||||
'/<int:task_id>',
|
||||
view_func=TaskItemView.as_view(task_handler=task_handler),
|
||||
methods=['GET', 'PATCH', 'DELETE'],
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from tofu_api.models import Task
|
||||
from tofu_api.repositories import TaskRepository
|
||||
from .validators import TaskCreateData, TaskUpdateData
|
||||
|
||||
|
||||
class TaskHandler:
|
||||
"""
|
||||
Handles operations on tasks.
|
||||
"""
|
||||
|
||||
task_repository: TaskRepository
|
||||
|
||||
def __init__(self, *, task_repository: TaskRepository):
|
||||
self.task_repository = task_repository
|
||||
|
||||
def fetch_task(self, task_id: int) -> Task:
|
||||
"""
|
||||
Fetches a single task by its ID from the database.
|
||||
Raises an ObjectNotFoundError if the task was not found.
|
||||
"""
|
||||
return self.task_repository.fetch_by_id(task_id)
|
||||
|
||||
def fetch_all_tasks(self) -> list[Task]:
|
||||
"""
|
||||
Fetches a list of all tasks.
|
||||
"""
|
||||
return self.task_repository.fetch_all()
|
||||
|
||||
def create_task(self, create_data: TaskCreateData) -> Task:
|
||||
"""
|
||||
Creates a new task, saves it to the database and returns it.
|
||||
"""
|
||||
task = Task()
|
||||
task.update_from(create_data)
|
||||
self.task_repository.save_resource(task)
|
||||
return task
|
||||
|
||||
def update_task(self, task: Task, update_data: TaskUpdateData) -> Task:
|
||||
"""
|
||||
Updates a Task object with new data.
|
||||
"""
|
||||
task.update_from(update_data)
|
||||
self.task_repository.save_resource(task)
|
||||
return task
|
||||
|
||||
def delete_task(self, task: Task) -> None:
|
||||
"""
|
||||
Deletes a task from the database.
|
||||
"""
|
||||
self.task_repository.delete_resource(task)
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
from flask import jsonify
|
||||
from validataclass.validators import DataclassValidator
|
||||
|
||||
from tofu_api.common.rest import BaseMethodView
|
||||
from .task_handler import TaskHandler
|
||||
from .validators import TaskCreateData, TaskUpdateData
|
||||
|
||||
|
||||
class TaskBaseView(BaseMethodView):
|
||||
"""
|
||||
Base class for view classes for the `/tasks` endpoint.
|
||||
"""
|
||||
|
||||
task_handler: TaskHandler
|
||||
|
||||
def __init__(self, *, task_handler: TaskHandler):
|
||||
self.task_handler = task_handler
|
||||
|
||||
|
||||
class TaskCollectionView(TaskBaseView):
|
||||
"""
|
||||
View class for `/api/tasks` endpoint.
|
||||
"""
|
||||
|
||||
# Validators
|
||||
task_create_validator = DataclassValidator(TaskCreateData)
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Get list of all tasks.
|
||||
"""
|
||||
task_list = self.task_handler.fetch_all_tasks()
|
||||
return jsonify({
|
||||
'items': [task.to_dict() for task in task_list],
|
||||
'total_count': len(task_list),
|
||||
}), 200
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
Create a new task.
|
||||
"""
|
||||
# Parse request data
|
||||
create_data: TaskCreateData = self.validate_request_data(self.task_create_validator)
|
||||
|
||||
# Create new task
|
||||
new_task = self.task_handler.create_task(create_data)
|
||||
|
||||
# Return new task as JSON
|
||||
return jsonify(new_task.to_dict()), 201
|
||||
|
||||
|
||||
class TaskItemView(TaskBaseView):
|
||||
"""
|
||||
View class for `/api/tasks/<int:task_id>` endpoint.
|
||||
"""
|
||||
|
||||
# Validators
|
||||
task_update_validator = DataclassValidator(TaskUpdateData)
|
||||
|
||||
def get(self, task_id: int):
|
||||
"""
|
||||
Get a single task by ID.
|
||||
"""
|
||||
task = self.task_handler.fetch_task(task_id)
|
||||
return jsonify(task.to_dict()), 200
|
||||
|
||||
def patch(self, task_id: int):
|
||||
"""
|
||||
Update a single task by ID.
|
||||
"""
|
||||
# Parse request data
|
||||
update_data: TaskUpdateData = self.validate_request_data(self.task_update_validator)
|
||||
|
||||
# Fetch task and update
|
||||
task = self.task_handler.fetch_task(task_id)
|
||||
task = self.task_handler.update_task(task, update_data)
|
||||
|
||||
# Return updated task as JSON
|
||||
return jsonify(task.to_dict()), 200
|
||||
|
||||
def delete(self, task_id: int):
|
||||
"""
|
||||
Delete a single task by ID.
|
||||
"""
|
||||
# Fetch task and delete
|
||||
task = self.task_handler.fetch_task(task_id)
|
||||
self.task_handler.delete_task(task)
|
||||
|
||||
return self.empty_response()
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from validataclass.dataclasses import Default, DefaultUnset, ValidataclassMixin, validataclass
|
||||
from validataclass.helpers import OptionalUnset
|
||||
from validataclass.validators import StringValidator
|
||||
|
||||
|
||||
@validataclass
|
||||
class TaskCreateData(ValidataclassMixin):
|
||||
"""
|
||||
Dataclass for "create task" request data.
|
||||
"""
|
||||
|
||||
title: str = StringValidator(min_length=1, max_length=200)
|
||||
description: str = StringValidator(max_length=2000), Default('')
|
||||
|
||||
|
||||
@validataclass
|
||||
class TaskUpdateData(TaskCreateData):
|
||||
"""
|
||||
Dataclass for "update task" request data.
|
||||
"""
|
||||
|
||||
title: OptionalUnset[str] = DefaultUnset
|
||||
description: OptionalUnset[str] = DefaultUnset
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
|
@ -6,11 +7,12 @@ from flask import Flask
|
|||
|
||||
from tofu_api.api import TofuApiBlueprint
|
||||
from tofu_api.common.config import Config
|
||||
from tofu_api.common.json import JSONEncoder
|
||||
from tofu_api.common.json import JSONProvider
|
||||
from tofu_api.common.rest import RestApiErrorHandler
|
||||
from tofu_api.dependencies import Dependencies
|
||||
|
||||
# Enable deprecation warnings in dev environment
|
||||
if not sys.warnoptions and os.getenv('FLASK_ENV') == 'development':
|
||||
if not sys.warnoptions and os.getenv('FLASK_DEBUG'):
|
||||
warnings.filterwarnings('default', module='tofu_api.*')
|
||||
|
||||
|
||||
|
|
@ -20,7 +22,7 @@ class App(Flask):
|
|||
"""
|
||||
# Override Flask classes
|
||||
config_class = Config
|
||||
json_encoder = JSONEncoder
|
||||
json_provider_class = JSONProvider
|
||||
|
||||
# Set type hint for config
|
||||
config: Config
|
||||
|
|
@ -42,6 +44,10 @@ class App(Flask):
|
|||
# Load app configuration from YAML file
|
||||
self.config.from_yaml(os.getenv('FLASK_CONFIG_FILE', default='config.yml'))
|
||||
|
||||
# Configure logging and error handling
|
||||
self.configure_logging()
|
||||
self.configure_error_handling()
|
||||
|
||||
# Initialize DI container
|
||||
self.dependencies = Dependencies()
|
||||
|
||||
|
|
@ -51,6 +57,24 @@ class App(Flask):
|
|||
# Register blueprints
|
||||
self.register_blueprint(TofuApiBlueprint(self))
|
||||
|
||||
def configure_logging(self) -> None:
|
||||
"""
|
||||
Configures the logging system.
|
||||
"""
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if self.debug else logging.INFO,
|
||||
format='{asctime}.{msecs:03.0f} {levelname:>8} [{name}] {message}',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
style='{',
|
||||
)
|
||||
|
||||
def configure_error_handling(self) -> None:
|
||||
"""
|
||||
Registers error handlers to the app.
|
||||
"""
|
||||
error_handler = RestApiErrorHandler(debug_mode=self.debug)
|
||||
error_handler.register_error_handlers(self)
|
||||
|
||||
def init_database(self) -> None:
|
||||
"""
|
||||
Initialize database connection and models.
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from .base import AppException
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from typing import Optional
|
||||
|
||||
|
||||
class AppException(Exception):
|
||||
"""
|
||||
Base class for application specific exceptions that can also be used as API error responses.
|
||||
"""
|
||||
code: str = 'unspecified_error'
|
||||
status_code: int = 400
|
||||
message: str
|
||||
|
||||
def __init__(self, message: str, *, code: Optional[str] = None, status_code: Optional[int] = None):
|
||||
if code is not None:
|
||||
self.code = code
|
||||
if status_code is not None:
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'code': self.code,
|
||||
'message': self.message,
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
from .json_encoder import JSONEncoder
|
||||
from .json_provider import JSONProvider
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from flask.json import JSONEncoder as _FlaskJSONEncoder
|
||||
from flask.json.provider import DefaultJSONProvider
|
||||
|
||||
__all__ = [
|
||||
'JSONEncoder',
|
||||
'JSONProvider',
|
||||
]
|
||||
|
||||
|
||||
class JSONEncoder(_FlaskJSONEncoder):
|
||||
class JSONProvider(DefaultJSONProvider):
|
||||
"""
|
||||
Custom JSON encoder built on top of the Flask JSONEncoder class.
|
||||
Custom JSON provider.
|
||||
"""
|
||||
|
||||
def default(self, obj: Any) -> Any:
|
||||
|
|
@ -25,5 +25,5 @@ class JSONEncoder(_FlaskJSONEncoder):
|
|||
if hasattr(obj, 'to_dict'):
|
||||
return obj.to_dict()
|
||||
|
||||
# Fallback to the Flask JSONEncoder
|
||||
# Fallback to the default JSON provider
|
||||
return super().default(obj)
|
||||
|
|
@ -1 +1,3 @@
|
|||
from .base_blueprint import BaseBlueprint
|
||||
from .base_method_view import BaseMethodView
|
||||
from .error_handler import RestApiErrorHandler
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Type, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from flask import Blueprint
|
||||
from flask.views import View
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tofu_api.app import App
|
||||
|
||||
__all__ = [
|
||||
'BaseBlueprint',
|
||||
]
|
||||
|
||||
|
||||
class BaseBlueprint(Blueprint, ABC):
|
||||
"""
|
||||
|
|
@ -62,11 +57,3 @@ class BaseBlueprint(Blueprint, ABC):
|
|||
Register child blueprints and URL rules.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def create_view_func(view_cls: Type[View], *args, **kwargs) -> Callable:
|
||||
"""
|
||||
Helper function to create a view function from a `View` class using `view_cls.as_view()`.
|
||||
All arguments are passed to the constructor of the view class.
|
||||
"""
|
||||
return view_cls.as_view(view_cls.__name__, *args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
from typing import Any
|
||||
|
||||
import flask
|
||||
from flask.typing import RouteCallable
|
||||
from flask.views import MethodView
|
||||
from validataclass.validators import Validator
|
||||
|
||||
|
||||
class BaseMethodView(MethodView):
|
||||
"""
|
||||
Base class for REST API views.
|
||||
"""
|
||||
|
||||
@property
|
||||
def request(self) -> flask.Request:
|
||||
return flask.request
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, *args, **kwargs) -> RouteCallable:
|
||||
return super().as_view(cls.__name__, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def empty_response(code: int = 204) -> tuple[str, int]:
|
||||
return '', code
|
||||
|
||||
def validate_request_data(self, validator: Validator) -> Any:
|
||||
"""
|
||||
Parses request data as JSON and validates it using a validataclass validator.
|
||||
"""
|
||||
# TODO error handling: wrong content type; empty body; invalid json; validation errors
|
||||
parsed_json = self.request.json
|
||||
return validator.validate(parsed_json)
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import logging
|
||||
from typing import Union
|
||||
|
||||
from flask import Flask, Response, jsonify
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from werkzeug.http import HTTP_STATUS_CODES
|
||||
|
||||
from tofu_api.common import string_utils
|
||||
from tofu_api.common.exceptions import AppException
|
||||
from .exceptions import InternalServerError
|
||||
|
||||
T_Response = Union[Response, tuple[Response, int]]
|
||||
|
||||
|
||||
class RestApiErrorHandler:
|
||||
"""
|
||||
Error handler class for REST API errors.
|
||||
"""
|
||||
|
||||
# Dependencies
|
||||
logger: logging.Logger
|
||||
|
||||
# Options
|
||||
debug_mode: bool = False
|
||||
|
||||
# Lookup table for HTTP status codes to API error codes
|
||||
_http_to_api_error_codes: dict[int, str]
|
||||
|
||||
def __init__(self, *, debug_mode: bool = False):
|
||||
self.logger = logging.getLogger(type(self).__name__)
|
||||
self.debug_mode = debug_mode
|
||||
|
||||
# Generate lookup table for HTTP status codes
|
||||
self._http_to_api_error_codes = {
|
||||
http_status: string_utils.str_to_snake_case(name)
|
||||
for http_status, name in HTTP_STATUS_CODES.items()
|
||||
}
|
||||
|
||||
def register_error_handlers(self, app: Flask) -> None:
|
||||
"""
|
||||
Registers error handlers for different types of exceptions to the app.
|
||||
"""
|
||||
app.register_error_handler(AppException, self.handle_app_exception)
|
||||
app.register_error_handler(HTTPException, self.handle_http_exception)
|
||||
app.register_error_handler(Exception, self.handle_generic_exception)
|
||||
|
||||
@staticmethod
|
||||
def handle_app_exception(exception: AppException) -> T_Response:
|
||||
"""
|
||||
Handles exceptions of type `AppException` that were not handled by any more specific handler.
|
||||
"""
|
||||
return jsonify(exception.to_dict()), exception.status_code
|
||||
|
||||
def handle_http_exception(self, exception: HTTPException) -> T_Response:
|
||||
"""
|
||||
Handles exceptions of type `HTTPException`, i.e. any werkzeug HTTP exceptions.
|
||||
"""
|
||||
if exception.code >= 500:
|
||||
self.logger.exception('HTTP exception with status code %s: %s', exception.code, type(exception).__name__)
|
||||
|
||||
return jsonify({
|
||||
'code': self._http_to_api_error_codes.get(exception.code, 'unknown_http_error'),
|
||||
'message': exception.description,
|
||||
}), exception.code
|
||||
|
||||
def handle_generic_exception(self, exception: Exception) -> T_Response:
|
||||
"""
|
||||
Fallback handler for any exceptions not handled by any other handler.
|
||||
"""
|
||||
self.logger.exception('Uncaught exception: %s', type(exception).__name__)
|
||||
|
||||
wrapped_exception = InternalServerError('There was an uncaught error on the server.', inner_exception=exception)
|
||||
return jsonify(wrapped_exception.to_dict(debug=self.debug_mode)), wrapped_exception.status_code
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
__all__ = [
|
||||
'InternalServerError'
|
||||
]
|
||||
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
from tofu_api.common.exceptions import AppException
|
||||
|
||||
|
||||
class InternalServerError(AppException):
|
||||
"""
|
||||
Wrapper exception for any uncaught exception.
|
||||
"""
|
||||
status_code = 500
|
||||
code = 'internal_server_error'
|
||||
inner_exception: Optional[Exception] = None
|
||||
|
||||
def __init__(self, message: str, *, inner_exception: Optional[Exception] = None):
|
||||
super().__init__(message)
|
||||
self.inner_exception = inner_exception
|
||||
|
||||
def to_dict(self, *, debug: bool = False) -> dict:
|
||||
data = super().to_dict()
|
||||
if debug:
|
||||
data['_debug'] = {
|
||||
'exception': str(self.inner_exception),
|
||||
'traceback': traceback.format_exception(self.inner_exception),
|
||||
}
|
||||
return data
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
__all__ = [
|
||||
'SNAKE_CASE_CHARACTERS',
|
||||
'is_snake_case',
|
||||
'str_to_snake_case',
|
||||
]
|
||||
|
||||
import string
|
||||
|
||||
SNAKE_CASE_CHARACTERS = string.ascii_lowercase + string.digits + '_'
|
||||
|
||||
|
||||
def is_snake_case(input_str: str) -> bool:
|
||||
"""
|
||||
Returns True if the input string only consists of snake case characters (lowercase letters, digits, underscore).
|
||||
"""
|
||||
return all(c in SNAKE_CASE_CHARACTERS for c in input_str)
|
||||
|
||||
|
||||
def str_to_snake_case(input_str: str) -> str:
|
||||
"""
|
||||
Converts any string to a snake case string: Whitespaces are replaced with an underscore, uppercase letters are
|
||||
converted to lowercase, and any non-alphanumeric character is removed.
|
||||
"""
|
||||
# First, lowercase string and replace any consecutive whitespaces with a single underscore
|
||||
almost_snake_case = '_'.join(input_str.lower().split())
|
||||
|
||||
# Now, remove all characters that are neither letters, digits, nor underscores
|
||||
return ''.join(filter(lambda c: c in SNAKE_CASE_CHARACTERS, almost_snake_case))
|
||||
|
|
@ -1,6 +1,26 @@
|
|||
from typing import TypeVar
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from tofu_api.api.tasks import TaskHandler
|
||||
from tofu_api.common.database import SQLAlchemy
|
||||
from tofu_api.repositories import TaskRepository
|
||||
|
||||
T_Dep_Callable = TypeVar('T_Dep_Callable')
|
||||
|
||||
|
||||
def cache_dependency(func: T_Dep_Callable) -> T_Dep_Callable:
|
||||
"""
|
||||
Decorator to be used in `Dependencies` to cache dependencies inside the Dependencies instance.
|
||||
"""
|
||||
dep_name = func.__name__
|
||||
|
||||
def wrapped_func(self: 'Dependencies'):
|
||||
if dep_name not in self._dependency_cache:
|
||||
self._dependency_cache[dep_name] = func(self)
|
||||
return self._dependency_cache.get(dep_name)
|
||||
|
||||
return wrapped_func
|
||||
|
||||
|
||||
class Dependencies:
|
||||
|
|
@ -15,10 +35,22 @@ class Dependencies:
|
|||
|
||||
# Database dependencies
|
||||
|
||||
@cache_dependency
|
||||
def get_sqlalchemy(self) -> SQLAlchemy:
|
||||
if SQLAlchemy not in self._dependency_cache:
|
||||
self._dependency_cache[SQLAlchemy] = SQLAlchemy()
|
||||
return self._dependency_cache[SQLAlchemy]
|
||||
return SQLAlchemy()
|
||||
|
||||
# No caching necessary here
|
||||
def get_db_session(self) -> Session:
|
||||
return self.get_sqlalchemy().session
|
||||
|
||||
# Repository classes
|
||||
|
||||
@cache_dependency
|
||||
def get_task_repository(self) -> TaskRepository:
|
||||
return TaskRepository(session=self.get_db_session())
|
||||
|
||||
# API Handler classes
|
||||
|
||||
@cache_dependency
|
||||
def get_task_handler(self) -> TaskHandler:
|
||||
return TaskHandler(task_repository=self.get_task_repository())
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
from typing import Any, Iterable, Optional
|
||||
|
||||
from sqlalchemy import Column, Integer, inspect
|
||||
from sqlalchemy.orm import InstanceState, as_declarative
|
||||
|
||||
from tofu_api.common.database import Col, MetaData
|
||||
|
||||
__all__ = [
|
||||
'BaseModel',
|
||||
]
|
||||
|
||||
from typing import Any, Iterable, Optional, Union
|
||||
|
||||
@as_declarative(name='BaseModel', metadata=MetaData())
|
||||
from sqlalchemy import Column, Integer, inspect
|
||||
from sqlalchemy.orm import InstanceState, as_declarative
|
||||
from validataclass.dataclasses import ValidataclassMixin
|
||||
|
||||
from tofu_api.common.database import Col, MetaData
|
||||
|
||||
|
||||
@as_declarative(metadata=MetaData())
|
||||
class BaseModel:
|
||||
"""
|
||||
Declarative base class for database models.
|
||||
|
|
@ -46,9 +47,9 @@ class BaseModel:
|
|||
"""
|
||||
Return the object's data as a dictionary.
|
||||
|
||||
By default, the dictionary will contain all table columns (with their column name as key) defined in the model. This can be
|
||||
overridden by setting the `fields` and/or `exclude` parameters, in which case only fields that are listed in `fields` will be
|
||||
included in the dictionary, except for fields listed in `exclude`.
|
||||
By default, the dictionary will contain all table columns (with their column name as key) defined in the model.
|
||||
This can be overridden by setting the `fields` and/or `exclude` parameters, in which case only fields that are
|
||||
listed in `fields` will be included in the dictionary, except for fields listed in `exclude`.
|
||||
"""
|
||||
# Determine fields to include in dictionary (starting will all table columns)
|
||||
included_fields = set(column.name for column in self.__table__.columns)
|
||||
|
|
@ -60,3 +61,15 @@ class BaseModel:
|
|||
return {
|
||||
field: getattr(self, field) for field in included_fields
|
||||
}
|
||||
|
||||
def update_from(self, data: Union[dict, ValidataclassMixin]) -> None:
|
||||
"""
|
||||
Updates the object with data from either a dictionary or a validataclass object (requires the ValidataclassMixin).
|
||||
"""
|
||||
if isinstance(data, ValidataclassMixin):
|
||||
data = data.to_dict()
|
||||
|
||||
# TODO: Is it a good idea to just iterate over data and setattr? Or should we check __table__.columns?
|
||||
for key, value in data.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
from .base_repository import BaseRepository
|
||||
from .task_repository import TaskRepository
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
__all__ = [
|
||||
'BaseRepository',
|
||||
'T_Model',
|
||||
]
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, Optional, Type, TypeVar
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from tofu_api.models import BaseModel
|
||||
from .exceptions import ObjectNotFoundException
|
||||
|
||||
T_Model = TypeVar('T_Model', bound=BaseModel)
|
||||
|
||||
|
||||
class BaseRepository(Generic[T_Model], ABC):
|
||||
"""
|
||||
Base class for repositories.
|
||||
"""
|
||||
|
||||
# Database session
|
||||
session: Session
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def model_cls(self) -> Type[T_Model]:
|
||||
"""
|
||||
Set this to the model class.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(self, *, session: Session):
|
||||
self.session = session
|
||||
|
||||
@staticmethod
|
||||
def _or_raise(resource: Optional[T_Model], exception_msg: Optional[str] = None) -> T_Model:
|
||||
if resource is None:
|
||||
raise ObjectNotFoundException(exception_msg)
|
||||
return resource
|
||||
|
||||
def fetch_by_id(self, resource_id: int) -> T_Model:
|
||||
"""
|
||||
Fetches a resource by ID.
|
||||
|
||||
Raises an ObjectNotFoundException if no resource with the ID was found.
|
||||
"""
|
||||
resource = self.session.get(self.model_cls, resource_id)
|
||||
return self._or_raise(resource, f'Resource with ID {resource_id} was not found.')
|
||||
|
||||
def fetch_all(self) -> list[T_Model]:
|
||||
"""
|
||||
Fetches all resources of the repository type.
|
||||
"""
|
||||
return self.session.scalars(
|
||||
select(self.model_cls)
|
||||
).all()
|
||||
|
||||
def commit_session(self) -> None:
|
||||
"""
|
||||
Commits the current database session.
|
||||
"""
|
||||
self.commit_session()
|
||||
|
||||
def rollback_session(self) -> None:
|
||||
"""
|
||||
Rolls back the current database session.
|
||||
"""
|
||||
self.rollback_session()
|
||||
|
||||
def save_resource(self, *resources: T_Model, commit: bool = True) -> None:
|
||||
"""
|
||||
Saves one or multiple resources to the database by adding them to the session and committing the session.
|
||||
Set `commit` to False to skip committing (the session will still be flushed, though).
|
||||
"""
|
||||
for resource in resources:
|
||||
self.session.add(resource)
|
||||
self.session.flush()
|
||||
|
||||
if commit:
|
||||
self.session.commit()
|
||||
|
||||
def delete_resource(self, *resources: T_Model, commit: bool = True) -> None:
|
||||
"""
|
||||
Deletes one or multiple resources from the database.
|
||||
Set `commit` to False to skip committing (the session will still be flushed, though).
|
||||
"""
|
||||
for resource in resources:
|
||||
self.session.delete(resource)
|
||||
self.session.flush()
|
||||
|
||||
if commit:
|
||||
self.session.commit()
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from tofu_api.common.exceptions import AppException
|
||||
|
||||
|
||||
class ObjectNotFoundException(AppException):
|
||||
"""
|
||||
Exception raised when a database object was not found, i.e. does not exist or is inaccessible for the user.
|
||||
"""
|
||||
status_code = 404
|
||||
code = 'not_found'
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from tofu_api.models import Task
|
||||
from .base_repository import BaseRepository
|
||||
|
||||
|
||||
class TaskRepository(BaseRepository[Task]):
|
||||
"""
|
||||
Repository for tasks.
|
||||
"""
|
||||
model_cls = Task
|
||||
Loading…
Reference in New Issue