diff --git a/.github/check.sh b/.github/check.sh new file mode 100644 index 0000000..3e76a34 --- /dev/null +++ b/.github/check.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +pyclean() { + # Cleaning cache: + find . | + grep -E '(__pycache__|\.hypothesis|\.perm|\.cache|\.static|\.py[cod]$)' | + xargs rm -rf +} + +run_checks() { + echo '[Check Started]' + set -x # we want to print commands during the CI process. + + # Running linting for all python files in the project: + python -m flake8 + + # Running type checking, see https://github.com/typeddjango/django-stubs + python -m mypy leeteasy tests + + # Running tests: + python -m pytest --cov + + # Checking dependencies status: + python -m pip check + + set +x + echo '[checks completed]' +} + +# Remove any cache before the script: +pyclean + +# Clean everything up: +trap pyclean EXIT INT TERM + +# Run the CI process: +run_checks diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..257fd35 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: Test +on: + pull_request: + types: [ready_for_review] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macOS-latest] + python-version: [ '3.8', '3.9', '3.10' ] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + id: setup-python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Python version + run: python -c "import sys; print(sys.version)" + + # Install pipenv + - name: Install pipenv + run: python3 -m pip install --upgrade pipenv + + # create .venv folder + - name: create .venv folder + run: mkdir -p .venv + + # caching dependencies + - name: Caching Dependencies + uses: actions/cache@v2 + id: cache-dependencies + with: + path: .venv + key: ${{ matrix.os }}-python-${{ steps.setup-python.outputs.python-version }}-pipenv-${{ hashFiles('**/Pipfile.lock') }} + + # install dependencies + - name: Install dependencies + if: steps.cache-dependencies.outputs.cache-hit != 'true' + run: | + pipenv install --dev --verbose + pipenv install types-requests --dev + # Run bash script + - name: run Bash script + run: pipenv run bash ./.github/check.sh \ No newline at end of file diff --git a/Pipfile b/Pipfile index b02d641..21c0555 100644 --- a/Pipfile +++ b/Pipfile @@ -13,6 +13,13 @@ notify-py = "*" flake8 = "*" pytest = "*" pytest-mock = "*" +mypy = "*" +darglint = "*" +flake8-docstrings = "*" +wemake-python-styleguide = "*" +flake8-pytest-style = "*" +flake8-bandit = "*" +pytest-cov = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index cf15dca..1dfa79c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "64e11253c57c51a0d6a0ae423b649c0b13449cc0acea3bd1a5c05b903942004e" + "sha256": "50a0b9af3dae5b272919718d80645de15f074609242db3e8275ab39152f55ae6" }, "pipfile-spec": 6, "requires": { @@ -93,7 +93,7 @@ "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4.0'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", "version": "==1.26.12" } }, @@ -106,6 +106,81 @@ "markers": "python_version >= '3.5'", "version": "==22.1.0" }, + "bandit": { + "hashes": [ + "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2", + "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a" + ], + "markers": "python_version >= '3.7'", + "version": "==1.7.4" + }, + "coverage": { + "extras": [ + "toml" + ], + "hashes": [ + "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", + "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", + "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", + "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", + "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", + "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", + "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", + "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", + "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", + "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", + "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", + "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", + "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", + "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", + "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", + "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", + "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", + "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", + "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", + "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", + "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", + "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", + "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", + "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", + "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", + "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", + "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", + "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", + "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", + "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", + "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", + "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", + "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", + "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", + "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", + "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", + "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", + "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", + "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", + "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", + "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", + "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", + "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", + "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", + "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", + "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", + "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", + "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", + "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", + "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" + ], + "markers": "python_version >= '3.7'", + "version": "==6.5.0" + }, + "darglint": { + "hashes": [ + "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da", + "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d" + ], + "index": "pypi", + "version": "==1.8.1" + }, "flake8": { "hashes": [ "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", @@ -114,6 +189,54 @@ "index": "pypi", "version": "==5.0.4" }, + "flake8-bandit": { + "hashes": [ + "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e", + "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d" + ], + "index": "pypi", + "version": "==4.1.1" + }, + "flake8-docstrings": { + "hashes": [ + "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde", + "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b" + ], + "index": "pypi", + "version": "==1.6.0" + }, + "flake8-plugin-utils": { + "hashes": [ + "sha256:1fe43e3e9acf3a7c0f6b88f5338cad37044d2f156c43cb6b080b5f9da8a76f06", + "sha256:20fa2a8ca2decac50116edb42e6af0a1253ef639ad79941249b840531889c65a" + ], + "markers": "python_version >= '3.6' and python_full_version < '4.0.0'", + "version": "==1.3.2" + }, + "flake8-pytest-style": { + "hashes": [ + "sha256:5fedb371a950e9fe0e0e6bfc854be7d99151271208f34cd2cc517681ece27780", + "sha256:c1175713e9e11b78cd1a035ed0bca0d1e41d09c4af329a952750b61d4194ddac" + ], + "index": "pypi", + "version": "==1.6.0" + }, + "gitdb": { + "hashes": [ + "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd", + "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.9" + }, + "gitpython": { + "hashes": [ + "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704", + "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.27" + }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -129,6 +252,43 @@ "markers": "python_version >= '3.6'", "version": "==0.7.0" }, + "mypy": { + "hashes": [ + "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d", + "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24", + "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046", + "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e", + "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3", + "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5", + "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20", + "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda", + "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1", + "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146", + "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206", + "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746", + "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6", + "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e", + "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc", + "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a", + "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8", + "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763", + "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2", + "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947", + "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40", + "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b", + "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795", + "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c" + ], + "index": "pypi", + "version": "==0.982" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, "packaging": { "hashes": [ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", @@ -137,6 +297,14 @@ "markers": "python_version >= '3.6'", "version": "==21.3" }, + "pbr": { + "hashes": [ + "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a", + "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf" + ], + "markers": "python_version >= '2.6'", + "version": "==5.10.0" + }, "pluggy": { "hashes": [ "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", @@ -161,6 +329,14 @@ "markers": "python_version >= '3.6'", "version": "==2.9.1" }, + "pydocstyle": { + "hashes": [ + "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc", + "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4" + ], + "markers": "python_version >= '3.6'", + "version": "==6.1.1" + }, "pyflakes": { "hashes": [ "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", @@ -185,21 +361,114 @@ "index": "pypi", "version": "==7.1.3" }, + "pytest-cov": { + "hashes": [ + "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b", + "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470" + ], + "index": "pypi", + "version": "==4.0.0" + }, "pytest-mock": { "hashes": [ - "sha256:77f03f4554392558700295e05aed0b1096a20d4a60a4f3ddcde58b0c31c8fca2", - "sha256:8a9e226d6c0ef09fcf20c94eb3405c388af438a90f3e39687f84166da82d5948" + "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b", + "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f" ], "index": "pypi", - "version": "==3.8.2" + "version": "==3.10.0" + }, + "pyyaml": { + "hashes": [ + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "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" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0" + }, + "smmap": { + "hashes": [ + "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", + "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" + ], + "markers": "python_version >= '3.6'", + "version": "==5.0.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" + ], + "version": "==2.2.0" + }, + "stevedore": { + "hashes": [ + "sha256:87e4d27fe96d0d7e4fc24f0cbe3463baae4ec51e81d95fbe60d2474636e0c7d8", + "sha256:f82cc99a1ff552310d19c379827c2c64dd9f85a38bcd5559db2470161867b786" + ], + "markers": "python_version >= '3.8'", + "version": "==4.0.0" }, "tomli": { "hashes": [ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.11'", "version": "==2.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", + "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" + ], + "markers": "python_version >= '3.7'", + "version": "==4.3.0" + }, + "wemake-python-styleguide": { + "hashes": [ + "sha256:505a19d82f9c4f450c6f06bb8c74d86c99cabcc4d5e6d8ea70e90b13b049f34f", + "sha256:e1f47a2be6aa79ca8a1cfbbbffdd67bf4df32b76306f4c3dd2a620a2af78e671" + ], + "index": "pypi", + "version": "==0.0.1" } } } diff --git a/README.md b/README.md index b8e60e5..f3640e5 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ crontab -e @reboot python -m leeteasy start 14:30 & ``` -## Contribution guideline +## Contributing -If you like this project and want to improve by adding features, fixing bugs or anything, please follow -the [contributing guidelines](docs/CONTRIBUTING.md). \ No newline at end of file +We are very happy to see you here. Before sending your pull requests, make sure that you read the whole workflow and the naming conventions mentioned in the [contributing guidelines](docs/CONTRIBUTING.md). + +If you have any doubts regarding the contributing guidelines, please feel free to [state it clearly in an issue](https://github.com/sudiptob2/leet-easy/issues/new/choose). All the best! \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index fc297a9..d56ad5c 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,11 +1,55 @@ +Given below is the general workflow we expect you to follow while making contributions to this project. + + +### Workflow +1. Go to the issues tab and find an issue you would like to work on. + + 1.1. Clarify any doubts in the comments section of the issue. + +2. Fork the project + +3. Create a branch and make small changes on it. + +4. Create a **draft PR** + +5. Then make other changes and push to the remote branch you created. In this way, the maintainers will be able to provide early reviews and comments for your commits which will save time later on. + + +6. Once the above steps are done, you can change the PR status from **draft to active** + + +7. Once the PR is approved, make sure to update and sync your branch + +8. Wait for the maintainers to merge your contribution + +9. Congratulations! You made your first contribution to Leet Easy + +
+ ### Fixing a bug, or adding a new feature This section generally defines how you can make code contribution. Please follow the below instructions to make code contributions. -Code, PR, commit message format, etc convension I follow when I code. Please follow the links below to get details. +Please follow the links given below to see the code, PR, commit message, etc. conventions which we follow. -- [Git branch naming convension](BRANCH-NAMING.md) +- [Git branch naming convention](BRANCH-NAMING.md) - [Conventional Commits](https://www.conventionalcommits.org/) for commit messages, and [Commit message format](https://gist.github.com/digitaljhelms/3761873) for what to write - [Linking a PR to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) + +
+ +### Examples + +#### Branch Names: `{branch_type}/{issue-tracker-id-}issue-one-liner` +1. feature/2234-infinite-scroll +2. documentation/3344-linux-installation +3. test/2222-unit-tests + +#### Commits +1. [#1234] feature: Submit button added. +2. [#1232] fix: Infinite scroll fixed +3. [#333] test: Add unit tests for xyz feature + +Keep contributing. We're eager to see your contributions! \ No newline at end of file diff --git a/leeteasy/__main__.py b/leeteasy/__main__.py index eefdac0..3f7e7d3 100644 --- a/leeteasy/__main__.py +++ b/leeteasy/__main__.py @@ -5,6 +5,7 @@ import click import schedule +from leeteasy.constant import Constant from leeteasy.services.notification_service import Notifier from leeteasy.utils.validatiors import TimeValidator @@ -14,13 +15,13 @@ '-d', '--difficulty', type=click.Choice(['Medium', 'Hard'], case_sensitive=False), - help='Additional problem difficulty for notification.' + help='Additional problem difficulty for notification.', ) @click.option( - "--sleep_duration", - default=3600, - type=click.IntRange(1, 3600, clamp=True), - help='Sleep duration in seconds.' + '--sleep_duration', + default=Constant.DEFAULT_SLEEP, + type=click.IntRange(1, Constant.DEFAULT_SLEEP, clamp=True), + help='Sleep duration in seconds.', ) @click.argument('time') def execute_start(time, difficulty, sleep_duration) -> None: @@ -33,21 +34,20 @@ def execute_start(time, difficulty, sleep_duration) -> None: Notifier.target_difficulty.append(difficulty) schedule.every().day.at(time).do(Notifier.notify) - while True: + while True: # NOQA: WPS457 schedule.run_pending() clock.sleep(sleep_duration) @click.command('stop') def execute_stop() -> None: - """Stops leeteasy process""" + """Stop leeteasy process.""" os.system('pkill -9 -f leeteasy') @click.group('leeteasy') def execute_root(): - """v0.4.0 | supported version strings: 0.7.2""" - pass + """Group child command.""" execute_root.add_command(execute_start) @@ -55,8 +55,11 @@ def execute_root(): if __name__ == '__main__': if platform != 'win32': - import pwd + import pwd # NOQA: WPS433 - os.environ[ - 'DBUS_SESSION_BUS_ADDRESS'] = f'unix:path=/run/user/{pwd.getpwuid(os.getuid()).pw_uid}/bus' # NOQA: E501 + bus_addr = 'unix:path=/run/user/{0}/bus'.format( + pwd.getpwuid(os.getuid()).pw_uid, + ) + + os.environ['DBUS_SESSION_BUS_ADDRESS'] = bus_addr execute_root() diff --git a/leeteasy/constant.py b/leeteasy/constant.py index c4931ce..7aa8d09 100644 --- a/leeteasy/constant.py +++ b/leeteasy/constant.py @@ -34,3 +34,6 @@ class Constant: # http call retries HTTP_CALL_RETRIES = 3 + + # default sleep duration + DEFAULT_SLEEP = 3600 diff --git a/leeteasy/models/challenge.py b/leeteasy/models/challenge.py index 4546589..d83fb42 100644 --- a/leeteasy/models/challenge.py +++ b/leeteasy/models/challenge.py @@ -1,39 +1,44 @@ -from typing import List +from typing import Dict, List, Optional class Challenge: """Singleton Model class for daily challenge.""" title: str = '' - raw_tags: List[dict] = None + raw_tags: List[Dict[str, str]] = [] ac_rate: float = 0 - difficulty: str = None - question_id: int = None + difficulty: str = '' + question_id: int = 0 title_slug: str = '' - date: str = None + date: str = '' def __new__(cls): - if not hasattr(cls, 'instance'): - cls.instance = super(Challenge, cls).__new__(cls) + """Override default class creation logic.""" + if not hasattr(cls, 'instance'): # NOQA : WPS421 + cls.instance = super(Challenge, cls).__new__(cls) # NOQA: WPS608 return cls.instance @property def problem_link(self) -> str: - """Returns the link of the problem.""" + """Return the link of the problem.""" return 'https://leetcode.com/problems/{0}/'.format( self.title_slug, ) @property - def tags(self) -> List[str]: - """Returns the link of the problem.""" + def tags(self) -> List[Optional[str]]: + """Return the link of the problem.""" tags = [] for tag in self.raw_tags: tags.append(tag.get('name')) return tags def __str__(self): - """Returns the string rep of the class.""" - return f"Title: {self.title}\nAcceptance Rate: {self.ac_rate}" \ - f"\nDifficulty: {self.difficulty}\n" + \ - f"id: {self.question_id}\nTags: {self.tags}" + """Return the string rep of the class.""" + return 'Title: {0}\nAcceptance: {1}\nDifficulty: {2}\nID: {3}\nTags: {4}\n'.format( + self.title, + self.ac_rate, + self.difficulty, + self.question_id, + self.tags, + ) diff --git a/leeteasy/services/notification_service.py b/leeteasy/services/notification_service.py index e6d99b3..ccdbe0a 100644 --- a/leeteasy/services/notification_service.py +++ b/leeteasy/services/notification_service.py @@ -2,6 +2,7 @@ from notifypy import Notify +from leeteasy.models.challenge import Challenge from leeteasy.services.request_handler import RequestHandler from leeteasy.services.request_parser import RequestParser @@ -11,11 +12,11 @@ class Notifier: target_difficulty = ['Easy'] app_name = 'LeetEasy' - challenge = None + challenge: Challenge @classmethod def prepare_notification(cls): - """Prepares notification msg and triggers notification.""" + """Prepare notification msg and triggers notification.""" challenge_info = RequestHandler.get_challenge_info() cls.challenge = RequestParser.parse(challenge_info) if cls.challenge.difficulty in cls.target_difficulty: @@ -27,14 +28,16 @@ def prepare_notification(cls): @classmethod def notify(cls): """Send desktop notification.""" - app_name_with_subtitle = f'{cls.app_name} - Easy Problem Notification' + app_name_with_subtitle = '{0} - Easy Problem Notification'.format(cls.app_name) icon_path = Path(__file__).parent.parent / 'assets/leetcoin.png' notification = Notify( default_notification_application_name=app_name_with_subtitle, default_notification_icon=icon_path, ) notification.message = cls.prepare_notification() - notification.title = f'{cls.app_name} - {cls.challenge.difficulty} ' \ - f'Problem Alert \U0001F514' + notification.title = '{0} - {1} Problem Alert \U0001F514'.format( + cls.app_name, + cls.challenge.difficulty, + ) if notification.message: notification.send() diff --git a/leeteasy/services/request_handler.py b/leeteasy/services/request_handler.py index 39991a1..d576fb1 100644 --- a/leeteasy/services/request_handler.py +++ b/leeteasy/services/request_handler.py @@ -1,5 +1,5 @@ import time -from typing import Union +from typing import Dict import requests @@ -9,23 +9,17 @@ class RequestHandler: """Provides services for requesting leetcode API.""" - @classmethod - def get_challenge_info(cls) -> Union[dict, None]: - """Gets daily challenge info from leetcode API.""" - url = Constant.LEETCODE_API_ENDPOINT - query = Constant.DAILY_CODING_CHALLENGE_QUERY - max_retries = Constant.HTTP_CALL_RETRIES # Change HTTP_CALL_RETRIES for more retries + url = Constant.LEETCODE_API_ENDPOINT + query = Constant.DAILY_CODING_CHALLENGE_QUERY + max_retries = Constant.HTTP_CALL_RETRIES - for i in range(max_retries): + @classmethod + def get_challenge_info(cls) -> Dict: + """Get daily challenge info from leetcode API.""" + for iteration in range(cls.max_retries): try: - response = requests.post(url, json={'query': query}) + response = requests.post(cls.url, json={'query': cls.query}) return response.json().get('data').get('activeDailyCodingChallengeQuestion') except Exception: - """ - On first hit sleep 10 minutes. - On second hit sleep 20 minutes. - On third hit sleep 30 minutes. - """ - time.sleep(((i+1)*10)*60) - raise SystemExit(f"""Connection to leetcode failed and max retries have been exceeded. - Total attempts made: {max_retries}. Try again""") + time.sleep(((iteration + 1) * 10) * 60) + raise SystemExit('Could not connect to the leetcode server.') diff --git a/leeteasy/services/request_parser.py b/leeteasy/services/request_parser.py index ff45217..eb2ba1c 100644 --- a/leeteasy/services/request_parser.py +++ b/leeteasy/services/request_parser.py @@ -1,3 +1,5 @@ +from typing import Dict + from leeteasy.models.challenge import Challenge @@ -5,19 +7,20 @@ class RequestParser: """Parse responses of leetcode API.""" @classmethod - def parse(cls, challenge_info: dict) -> Challenge: + def parse(cls, challenge_info: Dict) -> Challenge: """Parse API data ans update challenge model.""" - return RequestParser._parse_challenge_info(challenge_info) + return cls._parse_challenge_info(challenge_info) @classmethod def _parse_challenge_info(cls, challenge_info) -> Challenge: """Parse and update challenge model.""" + question = challenge_info.get('question') challenge = Challenge() - challenge.title = challenge_info.get('question').get('title') - challenge.ac_rate = challenge_info.get('question').get('acRate') - challenge.difficulty = challenge_info.get('question').get('difficulty') - challenge.question_id = challenge_info.get('question').get('frontendQuestionId') + challenge.title = question.get('title') + challenge.ac_rate = question.get('acRate') + challenge.difficulty = question.get('difficulty') + challenge.question_id = question.get('frontendQuestionId') challenge.date = challenge_info.get('date') - challenge.title_slug = challenge_info.get('question').get('titleSlug') - challenge.raw_tags = challenge_info.get('question').get('topicTags') + challenge.title_slug = question.get('titleSlug') + challenge.raw_tags = question.get('topicTags') return challenge diff --git a/leeteasy/utils/validatiors.py b/leeteasy/utils/validatiors.py index 0f4910f..f83ed9e 100644 --- a/leeteasy/utils/validatiors.py +++ b/leeteasy/utils/validatiors.py @@ -8,7 +8,7 @@ class TimeValidator: @classmethod def validate(cls, time: str): - """Validates the given string is in valid time format.""" + """Validate the given string is in valid time format.""" try: return datetime.strptime(time, cls.time_format).time() except ValueError: diff --git a/setup.cfg b/setup.cfg index 071d2ad..b23433f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,61 @@ exclude = docs.* [flake8] +# flake8 configuration: +# https://flake8.pycqa.org/en/latest/user/configuration.html max-line-length = 99 +max-complexity = 6 +format = wemake +show-source = True +statistics = True +doctests = True +enable-extensions = G +# Excluding some directories: +exclude = .git,__pycache__,.venv,.eggs,*.egg, tests + +# darglint configuration: +# https://github.com/terrencepreilly/darglint +strictness = long +docstring-style = numpy + +# Disable some checks: +ignore = + D100, + S605, + S607, + WPS115, + WPS306, + D104, + WPS201, # Found module with too many imports + WPS229, # Found too long try body length + + + +[mypy] +# Mypy configuration: +# https://mypy.readthedocs.io/en/latest/config_file.html +allow_redefinition = False +check_untyped_defs = True +disallow_untyped_decorators = True +disallow_any_explicit = True +;disallow_any_generics = True +;disallow_untyped_calls = True +ignore_errors = False +ignore_missing_imports = True +implicit_reexport = False +local_partial_types = True +# strict_optional = True +strict_equality = True +no_implicit_optional = True +warn_unused_ignores = True +warn_redundant_casts = True +warn_unused_configs = True +warn_unreachable = True +warn_no_return = True +show_error_codes = True + + +[options.entry_points] console_scripts = leeteasy = leeteasy.__main__:main \ No newline at end of file diff --git a/tests/test_services/test_request_handler.py b/tests/test_services/test_request_handler.py deleted file mode 100644 index 4d6880e..0000000 --- a/tests/test_services/test_request_handler.py +++ /dev/null @@ -1,15 +0,0 @@ -from leeteasy.services.request_handler import RequestHandler - - -class TestRequestHandler: - """Class for testing request handlers.""" - - def test_get_challenge_info(self, mocker, fake_challenge_data): - """Tests get_challenge_info method.""" - - def fake_get_challenge_info(): - RequestHandler.challenge_info = fake_challenge_data - - mocker.patch.object(RequestHandler, 'get_challenge_info', fake_get_challenge_info) - RequestHandler.get_challenge_info() - assert type(RequestHandler.challenge_info) == dict diff --git a/tests/test_services/test_request_parser.py b/tests/test_services/test_request_parser.py index 537e581..8d15276 100644 --- a/tests/test_services/test_request_parser.py +++ b/tests/test_services/test_request_parser.py @@ -9,14 +9,14 @@ def test_parse(self, mocker, fake_challenge_data): """Tests parse method.""" def fake_get_challenge_info(): - RequestHandler.challenge_info = fake_challenge_data + return fake_challenge_data mocker.patch.object( RequestHandler, 'get_challenge_info', fake_get_challenge_info, ) - challenge = RequestParser.parse() + challenge = RequestParser.parse(RequestHandler.get_challenge_info()) assert challenge.title == fake_challenge_data.get('question').get('title') assert challenge.difficulty == fake_challenge_data.get('question').get('difficulty')