First version
This commit is contained in:
parent
03b1d71018
commit
c05154cad9
22 changed files with 6784 additions and 0 deletions
19
Pipfile
Normal file
19
Pipfile
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[[source]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
flake8 = "*"
|
||||||
|
epc = "*"
|
||||||
|
importmagic = "*"
|
||||||
|
ipython = "*"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
flask = "*"
|
||||||
|
flask-sqlalchemy = "*"
|
||||||
|
base58 = "*"
|
||||||
|
flask-wtf = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.8"
|
312
Pipfile.lock
generated
Normal file
312
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "5dc5b4443b620da8c4ee4052dea107005a6edec09fe78eae470daecc8d2fc570"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.8"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"base58": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:365c9561d9babac1b5f18ee797508cd54937a724b6e419a130abad69cec5ca79",
|
||||||
|
"sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.0.1"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||||
|
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||||
|
],
|
||||||
|
"version": "==7.1.2"
|
||||||
|
},
|
||||||
|
"flask": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
|
||||||
|
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.1.2"
|
||||||
|
},
|
||||||
|
"flask-sqlalchemy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e",
|
||||||
|
"sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.4.4"
|
||||||
|
},
|
||||||
|
"flask-wtf": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2",
|
||||||
|
"sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.14.3"
|
||||||
|
},
|
||||||
|
"itsdangerous": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||||
|
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||||
|
],
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||||
|
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||||
|
],
|
||||||
|
"version": "==2.11.2"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||||
|
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||||
|
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||||
|
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||||
|
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||||
|
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||||
|
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||||
|
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||||
|
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||||
|
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||||
|
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||||
|
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
|
||||||
|
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||||
|
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||||
|
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||||
|
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||||
|
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||||
|
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||||
|
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||||
|
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||||
|
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||||
|
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||||
|
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||||
|
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||||
|
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||||
|
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||||
|
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||||
|
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||||
|
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||||
|
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||||
|
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||||
|
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||||
|
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||||
|
],
|
||||||
|
"version": "==1.1.1"
|
||||||
|
},
|
||||||
|
"sqlalchemy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6",
|
||||||
|
"sha256:0157c269701d88f5faf1fa0e4560e4d814f210c01a5b55df3cab95e9346a8bcc",
|
||||||
|
"sha256:0a92745bb1ebbcb3985ed7bda379b94627f0edbc6c82e9e4bac4fb5647ae609a",
|
||||||
|
"sha256:0cca1844ba870e81c03633a99aa3dc62256fb96323431a5dec7d4e503c26372d",
|
||||||
|
"sha256:166917a729b9226decff29416f212c516227c2eb8a9c9f920d69ced24e30109f",
|
||||||
|
"sha256:1f5f369202912be72fdf9a8f25067a5ece31a2b38507bb869306f173336348da",
|
||||||
|
"sha256:2909dffe5c9a615b7e6c92d1ac2d31e3026dc436440a4f750f4749d114d88ceb",
|
||||||
|
"sha256:2b5dafed97f778e9901b79cc01b88d39c605e0545b4541f2551a2fd785adc15b",
|
||||||
|
"sha256:2e9bd5b23bba8ae8ce4219c9333974ff5e103c857d9ff0e4b73dc4cb244c7d86",
|
||||||
|
"sha256:3aa6d45e149a16aa1f0c46816397e12313d5e37f22205c26e06975e150ffcf2a",
|
||||||
|
"sha256:4bdbdb8ca577c6c366d15791747c1de6ab14529115a2eb52774240c412a7b403",
|
||||||
|
"sha256:53fd857c6c8ffc0aa6a5a3a2619f6a74247e42ec9e46b836a8ffa4abe7aab327",
|
||||||
|
"sha256:5cdfe54c1e37279dc70d92815464b77cd8ee30725adc9350f06074f91dbfeed2",
|
||||||
|
"sha256:5d92c18458a4aa27497a986038d5d797b5279268a2de303cd00910658e8d149c",
|
||||||
|
"sha256:632b32183c0cb0053194a4085c304bc2320e5299f77e3024556fa2aa395c2a8b",
|
||||||
|
"sha256:7c735c7a6db8ee9554a3935e741cf288f7dcbe8706320251eb38c412e6a4281d",
|
||||||
|
"sha256:7cd40cb4bc50d9e87b3540b23df6e6b24821ba7e1f305c1492b0806c33dbdbec",
|
||||||
|
"sha256:84f0ac4a09971536b38cc5d515d6add7926a7e13baa25135a1dbb6afa351a376",
|
||||||
|
"sha256:8dcbf377529a9af167cbfc5b8acec0fadd7c2357fc282a1494c222d3abfc9629",
|
||||||
|
"sha256:950f0e17ffba7a7ceb0dd056567bc5ade22a11a75920b0e8298865dc28c0eff6",
|
||||||
|
"sha256:9e379674728f43a0cd95c423ac0e95262500f9bfd81d33b999daa8ea1756d162",
|
||||||
|
"sha256:b15002b9788ffe84e42baffc334739d3b68008a973d65fad0a410ca5d0531980",
|
||||||
|
"sha256:b6f036ecc017ec2e2cc2a40615b41850dc7aaaea6a932628c0afc73ab98ba3fb",
|
||||||
|
"sha256:bad73f9888d30f9e1d57ac8829f8a12091bdee4949b91db279569774a866a18e",
|
||||||
|
"sha256:bbc58fca72ce45a64bb02b87f73df58e29848b693869e58bd890b2ddbb42d83b",
|
||||||
|
"sha256:bca4d367a725694dae3dfdc86cf1d1622b9f414e70bd19651f5ac4fb3aa96d61",
|
||||||
|
"sha256:be41d5de7a8e241864189b7530ca4aaf56a5204332caa70555c2d96379e18079",
|
||||||
|
"sha256:bf53d8dddfc3e53a5bda65f7f4aa40fae306843641e3e8e701c18a5609471edf",
|
||||||
|
"sha256:c092fe282de83d48e64d306b4bce03114859cdbfe19bf8a978a78a0d44ddadb1",
|
||||||
|
"sha256:c3ab23ee9674336654bf9cac30eb75ac6acb9150dc4b1391bec533a7a4126471",
|
||||||
|
"sha256:ce64a44c867d128ab8e675f587aae7f61bd2db836a3c4ba522d884cd7c298a77",
|
||||||
|
"sha256:d05cef4a164b44ffda58200efcb22355350979e000828479971ebca49b82ddb1",
|
||||||
|
"sha256:d2f25c7f410338d31666d7ddedfa67570900e248b940d186b48461bd4e5569a1",
|
||||||
|
"sha256:d3b709d64b5cf064972b3763b47139e4a0dc4ae28a36437757f7663f67b99710",
|
||||||
|
"sha256:e32e3455db14602b6117f0f422f46bc297a3853ae2c322ecd1e2c4c04daf6ed5",
|
||||||
|
"sha256:ed53209b5f0f383acb49a927179fa51a6e2259878e164273ebc6815f3a752465",
|
||||||
|
"sha256:f605f348f4e6a2ba00acb3399c71d213b92f27f2383fc4abebf7a37368c12142",
|
||||||
|
"sha256:fcdb3755a7c355bc29df1b5e6fb8226d5c8b90551d202d69d0076a8a5649d68b"
|
||||||
|
],
|
||||||
|
"version": "==1.3.20"
|
||||||
|
},
|
||||||
|
"werkzeug": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
|
||||||
|
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
|
||||||
|
],
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"wtforms": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c",
|
||||||
|
"sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
|
||||||
|
],
|
||||||
|
"version": "==2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"backcall": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e",
|
||||||
|
"sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"
|
||||||
|
],
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"decorator": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
|
||||||
|
"sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"
|
||||||
|
],
|
||||||
|
"version": "==4.4.2"
|
||||||
|
},
|
||||||
|
"epc": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a14d2ea74817955a20eb00812e3a4630a132897eb4d976420240f1152c0d7d25"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.0.5"
|
||||||
|
},
|
||||||
|
"flake8": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
|
||||||
|
"sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.8.4"
|
||||||
|
},
|
||||||
|
"importmagic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3f7757a5b74c9a291e20e12023bb3bf71bc2fa3adfb15a08570648ab83eaf8d8"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.1.7"
|
||||||
|
},
|
||||||
|
"ipython": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8",
|
||||||
|
"sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==7.18.1"
|
||||||
|
},
|
||||||
|
"ipython-genutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
|
||||||
|
"sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
|
||||||
|
],
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"jedi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20",
|
||||||
|
"sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"
|
||||||
|
],
|
||||||
|
"version": "==0.17.2"
|
||||||
|
},
|
||||||
|
"mccabe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||||
|
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||||
|
],
|
||||||
|
"version": "==0.6.1"
|
||||||
|
},
|
||||||
|
"parso": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea",
|
||||||
|
"sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"
|
||||||
|
],
|
||||||
|
"version": "==0.7.1"
|
||||||
|
},
|
||||||
|
"pexpect": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937",
|
||||||
|
"sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"
|
||||||
|
],
|
||||||
|
"markers": "sys_platform != 'win32'",
|
||||||
|
"version": "==4.8.0"
|
||||||
|
},
|
||||||
|
"pickleshare": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
|
||||||
|
"sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
|
||||||
|
],
|
||||||
|
"version": "==0.7.5"
|
||||||
|
},
|
||||||
|
"prompt-toolkit": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c",
|
||||||
|
"sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"
|
||||||
|
],
|
||||||
|
"version": "==3.0.8"
|
||||||
|
},
|
||||||
|
"ptyprocess": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
|
||||||
|
"sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
|
||||||
|
],
|
||||||
|
"version": "==0.6.0"
|
||||||
|
},
|
||||||
|
"pycodestyle": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
||||||
|
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
|
||||||
|
],
|
||||||
|
"version": "==2.6.0"
|
||||||
|
},
|
||||||
|
"pyflakes": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
|
||||||
|
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
|
||||||
|
],
|
||||||
|
"version": "==2.2.0"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0",
|
||||||
|
"sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"
|
||||||
|
],
|
||||||
|
"version": "==2.7.2"
|
||||||
|
},
|
||||||
|
"sexpdata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1ac827a616c5e87ebb60fd6686fb86f8a166938c645f4089d92de3ffbdd494e0"
|
||||||
|
],
|
||||||
|
"version": "==0.0.3"
|
||||||
|
},
|
||||||
|
"traitlets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396",
|
||||||
|
"sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"
|
||||||
|
],
|
||||||
|
"version": "==5.0.5"
|
||||||
|
},
|
||||||
|
"wcwidth": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
|
||||||
|
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
|
||||||
|
],
|
||||||
|
"version": "==0.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
TODO.org
Normal file
19
TODO.org
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#+TODO: TODO NEXT HOLD | DONE CANCELLED
|
||||||
|
|
||||||
|
* URL Shortener [22%]
|
||||||
|
** DONE Landing Page Endpoint
|
||||||
|
CLOSED: [2020-10-25 Sun 04:50]
|
||||||
|
** DONE Snip Endpoint
|
||||||
|
CLOSED: [2020-10-25 Sun 05:22]
|
||||||
|
** TODO Shorten URL
|
||||||
|
** TODO Save Shortened URLs
|
||||||
|
** TODO Unsnip Shortened URLs
|
||||||
|
** TODO Landing Page Template
|
||||||
|
** TODO Rediret
|
||||||
|
** TODO Stats Endpoint
|
||||||
|
** TODO Store Stats
|
||||||
|
** TODO Stats Template
|
||||||
|
** TODO Externalize config
|
||||||
|
** TODO Deploy script
|
||||||
|
** TODO Improvement: Pronouncable short urls
|
||||||
|
with markov chain?
|
5859
package-lock.json
generated
Normal file
5859
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
package.json
Normal file
32
package.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "snip",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A tiny url shortener",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --config webpack.dev.js",
|
||||||
|
"watch": "webpack --watch --config webpack.dev.js",
|
||||||
|
"publish": "webpack --config webpack.prod.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@git.friedl.net:incubator/snip.git"
|
||||||
|
},
|
||||||
|
"author": "Armin Friedl",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
|
"css-loader": "^4.2.2",
|
||||||
|
"gulp": "^4.0.2",
|
||||||
|
"sass": "^1.26.10",
|
||||||
|
"sass-loader": "^10.0.2",
|
||||||
|
"style-loader": "^1.2.1",
|
||||||
|
"webpack": "^4.44.1",
|
||||||
|
"webpack-cli": "^3.3.12",
|
||||||
|
"webpack-merge": "^5.1.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"loglevel": "^1.7.0",
|
||||||
|
"reset-css": "^5.0.1"
|
||||||
|
}
|
||||||
|
}
|
88
snip.svg
Normal file
88
snip.svg
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 122.88 122.88"
|
||||||
|
style="enable-background:new 0 0 122.88 122.88"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="snip.svg"
|
||||||
|
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"><metadata
|
||||||
|
id="metadata13"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs11"><filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter1676"><feFlood
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(92,153,246)"
|
||||||
|
result="flood"
|
||||||
|
id="feFlood1666" /><feComposite
|
||||||
|
in="flood"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
operator="in"
|
||||||
|
result="composite1"
|
||||||
|
id="feComposite1668" /><feGaussianBlur
|
||||||
|
in="composite1"
|
||||||
|
stdDeviation="2.77556e-17"
|
||||||
|
result="blur"
|
||||||
|
id="feGaussianBlur1670" /><feOffset
|
||||||
|
dx="1"
|
||||||
|
dy="1"
|
||||||
|
result="offset"
|
||||||
|
id="feOffset1672" /><feComposite
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="offset"
|
||||||
|
operator="over"
|
||||||
|
result="composite2"
|
||||||
|
id="feComposite1674" /></filter></defs><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1892"
|
||||||
|
inkscape:window-height="1052"
|
||||||
|
id="namedview9"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:snap-object-midpoints="true"
|
||||||
|
inkscape:snap-center="true"
|
||||||
|
inkscape:zoom="4.8279834"
|
||||||
|
inkscape:cx="11.579107"
|
||||||
|
inkscape:cy="35.3816"
|
||||||
|
inkscape:window-x="28"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" /><style
|
||||||
|
type="text/css"
|
||||||
|
id="style2">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:label="Layer 2"><ellipse
|
||||||
|
style="fill:#abccff;fill-opacity:1;stroke-width:1.05806"
|
||||||
|
id="path844"
|
||||||
|
cx="61.351963"
|
||||||
|
cy="61.581459"
|
||||||
|
rx="60.941147"
|
||||||
|
ry="60.66304" /></g><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
style="display:inline"><path
|
||||||
|
class="st0"
|
||||||
|
d="m 74.09,14.2 6.58,6.58 c 8.27,8.27 7.699178,22.304751 -0.628516,30.516652 L 62.321965,68.769785 C 55.801965,75.299785 48.11,75.79 40.2,72.02 L 69.86,41.55 c 3.008641,-3.090806 3.05,-8.03 0,-11.07 l -5.97,-5.97 c -3.05,-3.05 -8.02,-3.05 -11.07,0 L 22.15,55.17 c -3.37,-7.78 -1.9,-17.2 4.43,-23.53 L 44.02,14.2 c 8.27,-8.27 21.8,-8.27 30.07,0 z m -33.89,87.89 6.58,6.58 c 8.27,8.27 21.8,8.27 30.08,0 L 94.29,91.24 C 100.82,84.72 102.18,74.91 98.41,67 L 67.55,97.86 c -3.05,3.05 -8.03,3.05 -11.07,0 l -5.97,-5.97 c -3.05,-3.05 -3.05,-8.03 0,-11.08 L 81.18,50.15 C 73.39,46.78 63.950912,48.00765 57.610912,54.34765 L 40.2,72.02 c -8.27,8.27 -8.27,21.8 0,30.07 z"
|
||||||
|
id="path4"
|
||||||
|
sodipodi:nodetypes="sssccscsccssssscccscsccccs"
|
||||||
|
style="opacity:1;mix-blend-mode:hard-light;fill:#ffac76;fill-opacity:1;stroke:#000000;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0;paint-order:normal;filter:url(#filter1676)" /></g></svg>
|
After Width: | Height: | Size: 3.8 KiB |
13
snip/__init__.py
Normal file
13
snip/__init__.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
config.configure("local")
|
||||||
|
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
from . import api
|
||||||
|
from . import views
|
47
snip/api.py
Normal file
47
snip/api.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
from flask import request
|
||||||
|
from flask import jsonify
|
||||||
|
|
||||||
|
from . import app
|
||||||
|
from . import snipper
|
||||||
|
|
||||||
|
|
||||||
|
class ClientError(Exception):
|
||||||
|
def __init__(self, message, status_code=400, payload=None):
|
||||||
|
Exception.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.status_code = status_code
|
||||||
|
self.payload = payload
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
rv = dict(self.payload or ())
|
||||||
|
rv['message'] = self.message
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(ClientError)
|
||||||
|
def handle_invalid_usage(error):
|
||||||
|
response = jsonify(error.to_dict())
|
||||||
|
response.status_code = error.status_code
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/snip", methods=['POST'])
|
||||||
|
def snip():
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
url = data.get('url')
|
||||||
|
reusable = data.get('reusable', False)
|
||||||
|
if not url:
|
||||||
|
raise ClientError("Cannot shorten empty URL", 400)
|
||||||
|
|
||||||
|
app.logger.info(f"Snipping url {url}")
|
||||||
|
snip = snipper.snip(url, reusable)
|
||||||
|
return {"url": url, "snip": snip}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/unsnip/<snip>", methods=['GET'])
|
||||||
|
def unsnip(snip):
|
||||||
|
url = snipper.unsnip(snip)
|
||||||
|
if not url:
|
||||||
|
raise ClientError(f"Snip {snip} not found", 404)
|
||||||
|
|
||||||
|
return {"url": url, "snip": snip}
|
17
snip/config.local.py
Normal file
17
snip/config.local.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# FLASK
|
||||||
|
FLASK_DEBUG = True
|
||||||
|
FLASK_ENV = "development"
|
||||||
|
|
||||||
|
# elisp:
|
||||||
|
# (->> (let (res) (dotimes (n 64 res) (setq res (cons (random (math-pow 2 8)) res))))
|
||||||
|
# (--map (format "\\x%s" it))
|
||||||
|
# (--reduce (concat acc it))
|
||||||
|
# (kill-new))
|
||||||
|
SECRET_KEY = b'\x90\xd6\x07\xa9\x84\x41\x15\x7c\x7b\x96\xd5\xb7\xa2\x52\xc3'\
|
||||||
|
b'\x33\x3e\x40\xdf\x0a\x64\xdc\x27\x2b\x4a\x48\x7a\x88\xf6\x4c\xce\x9a'\
|
||||||
|
b'\x5c\x5f\xc5\xc8\xa8\xa3\xbc\x28\xc0\x10\x6d\x54\xbb\x10\x58\x86\x9c'\
|
||||||
|
b'\x68\x1b\xcc\xad\x68\xd0\xf3\x66\x65\xb5\xf4\x70\x9d\x0e\x3d'
|
||||||
|
|
||||||
|
# SQL ALCHEMY
|
||||||
|
SQLALCHEMY_DATABASE_URI = "sqlite:////tmp/test.db"
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
53
snip/config.py
Normal file
53
snip/config.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from . import app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
default_env = "local"
|
||||||
|
|
||||||
|
config_vars = [
|
||||||
|
"SQLALCHEMY_DATABASE_URI",
|
||||||
|
"SQLALCHEMY_TRACK_MODIFICATIONS",
|
||||||
|
"SECRET_KEY",
|
||||||
|
"DEBUG",
|
||||||
|
"ENV"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def find_env():
|
||||||
|
env = (os.environ.get("SNIP_ENV")
|
||||||
|
or os.environ.get("FLASK_ENV")
|
||||||
|
or default_env)
|
||||||
|
|
||||||
|
if not env:
|
||||||
|
raise Exception("Could not determine environment")
|
||||||
|
|
||||||
|
logger.info(f"Using environment {env}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_config_file(env):
|
||||||
|
config_file = f"config.{env}.py"
|
||||||
|
logger.info(f"Config file {config_file}")
|
||||||
|
return config_file
|
||||||
|
|
||||||
|
|
||||||
|
def validate(config):
|
||||||
|
key = config.get("SECRET_KEY")
|
||||||
|
if isinstance(key, str):
|
||||||
|
key = key.encode("utf-8")
|
||||||
|
if not isinstance(key, bytes):
|
||||||
|
raise Exception("Secret key cannot be cast to bytes")
|
||||||
|
if len(key) < 64: # for blake2b hashing in snipper
|
||||||
|
raise Exception("Secret key must be 64 bytes long")
|
||||||
|
|
||||||
|
logger.info("Configuration is valid")
|
||||||
|
|
||||||
|
|
||||||
|
def configure(env=None):
|
||||||
|
logger.info("Configuring snip")
|
||||||
|
if not env:
|
||||||
|
env = find_env()
|
||||||
|
app.config.from_pyfile(find_config_file(env))
|
||||||
|
validate(app.config)
|
11
snip/forms.py
Normal file
11
snip/forms.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
|
||||||
|
from wtforms import StringField
|
||||||
|
from wtforms import validators
|
||||||
|
|
||||||
|
class SnipForm(FlaskForm):
|
||||||
|
# The URL validator from wtforms is rather simple and only used as a first
|
||||||
|
# line of defense here. The URL is later validated again by urlvalidator.py
|
||||||
|
# in snipper.py which can lead to a UrlValidationError.
|
||||||
|
url = StringField('url', validators=[validators.InputRequired(),
|
||||||
|
validators.URL(require_tld=False, message="Not a valid URL")])
|
11
snip/models.py
Normal file
11
snip/models.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Snip(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
url = db.Column(db.String(512), nullable=False)
|
||||||
|
snip = db.Column(db.String(16), unique=True, nullable=False, index=True)
|
||||||
|
reusable = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Snip .url={self.url}, .snip={self.snip}"
|
49
snip/snipper.py
Normal file
49
snip/snipper.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
from . import db
|
||||||
|
from .urlvalidator import URLValidator
|
||||||
|
from .models import Snip
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import secrets
|
||||||
|
import base58
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def snip(url: str, reusable=False) -> str:
|
||||||
|
url_validator = URLValidator()
|
||||||
|
url_validator(url)
|
||||||
|
|
||||||
|
if reusable:
|
||||||
|
log.debug("Snipping is marked reusable. Looking for existing reusable snips.")
|
||||||
|
reusable_snip = Snip.query.filter(Snip.url == url,
|
||||||
|
Snip.reusable == True).first()
|
||||||
|
if reusable_snip:
|
||||||
|
log.debug(f"Found reusable snip {reusable_snip} for URL {url}")
|
||||||
|
return reusable_snip.snip
|
||||||
|
else:
|
||||||
|
log.debug(f"No reusable snips exist. Generating new snip.")
|
||||||
|
|
||||||
|
snip = gen_snip()
|
||||||
|
log.debug(f"Generated snip: {snip}")
|
||||||
|
|
||||||
|
while Snip.query.filter(Snip.snip == snip).first():
|
||||||
|
log.info(f"Snip {snip} for {url} already exists. Generating new snip.")
|
||||||
|
snip = gen_snip()
|
||||||
|
log.debug(f"Generated new snip: {snip}")
|
||||||
|
|
||||||
|
log.info(f"Using snip {snip} for URL {url}")
|
||||||
|
db.session.add(Snip(url=url, snip=snip, reusable=reusable))
|
||||||
|
db.session.commit()
|
||||||
|
return snip
|
||||||
|
|
||||||
|
def gen_snip():
|
||||||
|
""" Generate a random snip """
|
||||||
|
rand = secrets.token_bytes(5)
|
||||||
|
snip = str(base58.b58encode(rand), 'ascii')
|
||||||
|
return snip
|
||||||
|
|
||||||
|
def unsnip(snip: str):
|
||||||
|
snip_dao = Snip.query.filter(Snip.snip == snip).first()
|
||||||
|
if snip_dao:
|
||||||
|
return snip_dao.url
|
||||||
|
return None
|
BIN
snip/static/favicon.ico
Normal file
BIN
snip/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
27
snip/templates/index.html
Normal file
27
snip/templates/index.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Snip!</title>
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<form method="post" action="{{ url_for('submit_snip') }}">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{{ form.url }}
|
||||||
|
<button type="submit">Snip!</button>
|
||||||
|
</form>
|
||||||
|
{% if form.url.errors %}
|
||||||
|
<ul class=errors>
|
||||||
|
{% for error in form.url.errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='dist/snip.bundle.js') }}"></script>
|
||||||
|
</html>
|
1
snip/templates/index.js
Normal file
1
snip/templates/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
console.log("Hello World");
|
16
snip/templates/success.html
Normal file
16
snip/templates/success.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Snip!</title>
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<a href=" {{ url_for('snip_to', snip=snip) }}">test</a>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='dist/snip.bundle.js') }}"></script>
|
||||||
|
</html>
|
136
snip/urlvalidator.py
Normal file
136
snip/urlvalidator.py
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
"""
|
||||||
|
Url validator broken out from Django
|
||||||
|
https://github.com/django/django/blob/83fbaa92311dd96e330496a0e443ea71b9c183e2/django/core/validators.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import ipaddress
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
|
class UrlValidationError(Exception):
|
||||||
|
def __init__(self, message, code, params):
|
||||||
|
self.message = message
|
||||||
|
self.code = code
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
class RegexValidator:
|
||||||
|
regex = ''
|
||||||
|
message = 'Enter a valid value.'
|
||||||
|
code = 'invalid'
|
||||||
|
inverse_match = False
|
||||||
|
flags = 0
|
||||||
|
|
||||||
|
def __init__(self, regex=None, message=None, code=None, inverse_match=None, flags=None):
|
||||||
|
if regex is not None:
|
||||||
|
self.regex = regex
|
||||||
|
if message is not None:
|
||||||
|
self.message = message
|
||||||
|
if code is not None:
|
||||||
|
self.code = code
|
||||||
|
if inverse_match is not None:
|
||||||
|
self.inverse_match = inverse_match
|
||||||
|
if flags is not None:
|
||||||
|
self.flags = flags
|
||||||
|
if self.flags and not isinstance(self.regex, str):
|
||||||
|
raise TypeError("If the flags are set, regex must be a regular expression string.")
|
||||||
|
|
||||||
|
self.regex = re.compile(self.regex, self.flags)
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
"""
|
||||||
|
Validate that the input contains (or does *not* contain, if
|
||||||
|
inverse_match is True) a match for the regular expression.
|
||||||
|
"""
|
||||||
|
regex_matches = self.regex.search(str(value))
|
||||||
|
invalid_input = regex_matches if self.inverse_match else not regex_matches
|
||||||
|
if invalid_input:
|
||||||
|
raise UrlValidationError(self.message, code=self.code, params={'value': value})
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
isinstance(other, RegexValidator) and
|
||||||
|
self.regex.pattern == other.regex.pattern and
|
||||||
|
self.regex.flags == other.regex.flags and
|
||||||
|
(self.message == other.message) and
|
||||||
|
(self.code == other.code) and
|
||||||
|
(self.inverse_match == other.inverse_match)
|
||||||
|
)
|
||||||
|
|
||||||
|
class URLValidator(RegexValidator):
|
||||||
|
ul = '\u00a1-\uffff' # Unicode letters range (must not be a raw string).
|
||||||
|
|
||||||
|
# IP patterns
|
||||||
|
ipv4_re = r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
|
||||||
|
ipv6_re = r'\[[0-9a-f:.]+\]' # (simple regex, validated later)
|
||||||
|
|
||||||
|
# Host patterns
|
||||||
|
hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'
|
||||||
|
# Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
|
||||||
|
domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?<!-))*'
|
||||||
|
tld_re = (
|
||||||
|
r'\.' # dot
|
||||||
|
r'(?!-)' # can't start with a dash
|
||||||
|
r'(?:[a-z' + ul + '-]{2,63}' # domain label
|
||||||
|
r'|xn--[a-z0-9]{1,59})' # or punycode label
|
||||||
|
r'(?<!-)' # can't end with a dash
|
||||||
|
r'\.?' # may have a trailing dot
|
||||||
|
)
|
||||||
|
host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'
|
||||||
|
|
||||||
|
regex = re.compile(
|
||||||
|
r'^(?:[a-z0-9.+-]*)://' # scheme is validated separately
|
||||||
|
r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?' # user:pass authentication
|
||||||
|
r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
|
||||||
|
r'(?::\d{2,5})?' # port
|
||||||
|
r'(?:[/?#][^\s]*)?' # resource path
|
||||||
|
r'\Z', re.IGNORECASE)
|
||||||
|
message = 'Enter a valid URL.'
|
||||||
|
schemes = ['http', 'https', 'ftp', 'ftps']
|
||||||
|
|
||||||
|
def __init__(self, schemes=None, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
if schemes is not None:
|
||||||
|
self.schemes = schemes
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise UrlValidationError(self.message, code=self.code, params={'value': value})
|
||||||
|
# Check if the scheme is valid.
|
||||||
|
scheme = value.split('://')[0].lower()
|
||||||
|
if scheme not in self.schemes:
|
||||||
|
raise UrlValidationError(self.message, code=self.code, params={'value': value})
|
||||||
|
|
||||||
|
# Then check full URL
|
||||||
|
try:
|
||||||
|
super().__call__(value)
|
||||||
|
except UrlValidationError as e:
|
||||||
|
# Trivial case failed. Try for possible IDN domain
|
||||||
|
if value:
|
||||||
|
try:
|
||||||
|
scheme, netloc, path, query, fragment = urlsplit(value)
|
||||||
|
except ValueError: # for example, "Invalid IPv6 URL"
|
||||||
|
raise UrlValidationError(self.message, code=self.code, params={'value': value})
|
||||||
|
try:
|
||||||
|
netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
|
||||||
|
except UnicodeError: # invalid domain part
|
||||||
|
raise e
|
||||||
|
url = urlunsplit((scheme, netloc, path, query, fragment))
|
||||||
|
super().__call__(url)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# Now verify IPv6 in the netloc part
|
||||||
|
host_match = re.search(r'^\[(.+)\](?::\d{2,5})?$', urlsplit(value).netloc)
|
||||||
|
if host_match:
|
||||||
|
potential_ip = host_match[1]
|
||||||
|
try:
|
||||||
|
ipaddress.IPv6Address(potential_ip)
|
||||||
|
except ValueError:
|
||||||
|
raise UrlValidationError(self.message, code=self.code, params={'value': value})
|
||||||
|
|
||||||
|
# The maximum length of a full host name is 253 characters per RFC 1034
|
||||||
|
# section 3.1. It's defined to be 255 bytes or less, but this includes
|
||||||
|
# one byte for the length of the name and one byte for the trailing dot
|
||||||
|
# that's used to indicate absolute names in DNS.
|
||||||
|
if len(urlsplit(value).netloc) > 253:
|
||||||
|
raise UrlValidationError(self.message, code=self.code, params={'value': value})
|
25
snip/views.py
Normal file
25
snip/views.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from flask import render_template, redirect, url_for
|
||||||
|
|
||||||
|
from . import app
|
||||||
|
from .forms import SnipForm
|
||||||
|
from . import snipper
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
form = SnipForm()
|
||||||
|
return render_template('index.html', form=form)
|
||||||
|
|
||||||
|
@app.route('/snip', methods=['POST'])
|
||||||
|
def submit_snip():
|
||||||
|
form = SnipForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
snip = snipper.snip(form.url.data, reusable=False)
|
||||||
|
return render_template('success.html', snip=snip)
|
||||||
|
return render_template('index.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/<snip>')
|
||||||
|
def snip_to(snip):
|
||||||
|
url = snipper.unsnip(snip)
|
||||||
|
return redirect(url)
|
28
webpack.common.js
Normal file
28
webpack.common.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
snip: './snip/templates/index.js',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CleanWebpackPlugin()
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
filename: '[name].bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'snip', 'static', 'dist')
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.s[ac]ss$/i,
|
||||||
|
use: [
|
||||||
|
// Creates `style` nodes from JS strings
|
||||||
|
'style-loader',
|
||||||
|
// Translates CSS into CommonJS
|
||||||
|
'css-loader',
|
||||||
|
// Compiles Sass to CSS
|
||||||
|
'sass-loader',
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
};
|
14
webpack.dev.js
Normal file
14
webpack.dev.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
const common = require('./webpack.common.js');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'development',
|
||||||
|
devtool: 'eval-source-map',
|
||||||
|
plugins: [
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
LOG_LEVEL: 'trace'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
7
webpack.prod.js
Normal file
7
webpack.prod.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
const common = require('./webpack.common.js');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'production'
|
||||||
|
});
|
Loading…
Reference in a new issue