diff --git a/.env.bkp b/.env.bkp new file mode 100644 index 0000000..06aa685 --- /dev/null +++ b/.env.bkp @@ -0,0 +1,18 @@ +ENV=dev +CLIENT_ID=6zakmdaqzkjqga9fmmsq922uq3tgw0 +CLIENT_SECRET=qsgqic56t5mrip34lz5mcbr9o09ns3 +TWITTER_API_KEY=z3mn1LTX4MA0qZtmDVn7OqRCQ +TWITTER_API_SECRET=3Lj5nZyCQkxnMxJO1SCdLK7klmpeEAh9y1hwtmUQyuJFJE29PS +TWITTER_ACCESS_TOKEN=24740510-xNhELDLkbuQBqc1ksO4DZbqOiuRmoXgoJpLxFtuzO +TWITTER_ACCESS_SECRET=O5Ji4yGaX8YWb8TKw0zt50TN5uVCsQFgS6pe6kIAQgMxh +GITHUB_TOKEN=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 +PRIVATE_KEY=/etc/letsencrypt/live/brstreamers.dev/privkey.pem +CERT=/etc/letsencrypt/live/brstreamers.dev/cert.pem +API_TOKEN=1q2w3e4 +DB=a +DB_NAME=postgres +DB_USER=postgres +DB_PASS=mamaco123! +DB_HOST=104.198.13.23 +DB_PORT=5432 +REDIS_HOST=104.198.13.23 \ No newline at end of file diff --git a/.env.example b/.env.example index 991d87a..8c3a5d1 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,18 @@ -ENV=dev -CLIENT_ID= #client ID for your Twitch App -CLIENT_SECRET= #client Secret for your Twitch App -PRIVATE_KEY= #private key (prod env only) -CERT= #cert (prod env only) -API_TOKEN= #token for private api (you choose here) -DB= #sqlite db location \ No newline at end of file +ENV= +CLIENT_ID= +CLIENT_SECRET= +TWITTER_API_KEY= +TWITTER_API_SECRET= +TWITTER_ACCESS_TOKEN= +TWITTER_ACCESS_SECRET= +GITHUB_TOKEN= +PRIVATE_KEY= +CERT= +API_TOKEN= +DB=a +DB_NAME= +DB_USER= +DB_PASS= +DB_HOST= +DB_PORT= +REDIS_HOST= \ No newline at end of file diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..0a3d8ae --- /dev/null +++ b/.env.test @@ -0,0 +1,18 @@ +ENV=a +CLIENT_ID=a +CLIENT_SECRET=a +TWITTER_API_KEY=a +TWITTER_API_SECRET=a +TWITTER_ACCESS_TOKEN=a +TWITTER_ACCESS_SECRET=a +GITHUB_TOKEN=a +PRIVATE_KEY=a +CERT=a +API_TOKEN=a +DB=a +DB_NAME=a +DB_USER=a +DB_PASS=a +DB_HOST=a +DB_PORT=a +REDIS_HOST=a \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54b3649..6f014e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: Continuous Integration Tests on: [push] jobs: - execute_black_linter_check: + lint-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -12,6 +12,25 @@ jobs: with: python-version: 3.8 - name: Install dependencies - run: pip install black + run: pip install --no-cache-dir --upgrade -r requirements.txt - name: Execute the black linter run: black -l 100 -S . --check + - name: Execute Tests + run: pytest --envfile .env.test --cov=. test + build: + runs-on: ubuntu-latest + needs: [lint-and-test] + steps: + - uses: actions/checkout@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build the brstreamers-bot Docker image + run: | + docker build -t ghcr.io/br-dev-streamers/brdevstreamers:latest . + docker push ghcr.io/br-dev-streamers/brdevstreamers:latest diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 8b4440a..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: CI - -on: - push: - branches: [ master ] - workflow_dispatch: - -jobs: - - build: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - - name: Login to ghcr - if: github.event_name != 'pull_request' - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v2 - with: - context: ./ - file: ./Dockerfile - builder: ${{ steps.buildx.outputs.name }} - push: true - tags: ghcr.io/flaviojmendes/brdevstreamers:latest - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/Pipfile b/Pipfile index c796845..33dbef2 100644 --- a/Pipfile +++ b/Pipfile @@ -58,6 +58,12 @@ PyJWT = "==2.3.0" PyNaCl = "==1.5.0" PyYAML = "==6.0" fastapi-cache2 = "*" +pytest = "*" +asyncmock = "*" +pytest-cov = "*" +black = "*" +pytest-dotenv = "*" +coverage-badge = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 47e541e..977b176 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4cfe2d19d9add39387cc3b062afcd9256b869443f69e2ac0308827dd23bd7c0b" + "sha256": "b4c2e23acef63282a36509edb061d56e40e4d085ab3e19b46af9dcbba7d703e1" }, "pipfile-spec": 6, "requires": { @@ -134,6 +134,14 @@ "index": "pypi", "version": "==4.0.2" }, + "asyncmock": { + "hashes": [ + "sha256:c251889d542e98fe5f7ece2b5b8643b7d62b50a5657d34a4cbce8a1d5170d750", + "sha256:fd8bc4e7813251a8959d1140924ccba3adbbc7af885dba7047c67f73c0b664b1" + ], + "index": "pypi", + "version": "==0.4.2" + }, "asynctest": { "hashes": [ "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676", @@ -142,6 +150,14 @@ "index": "pypi", "version": "==0.13.0" }, + "atomicwrites": { + "hashes": [ + "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", + "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" + ], + "markers": "sys_platform == 'win32'", + "version": "==1.4.0" + }, "attrs": { "hashes": [ "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", @@ -150,6 +166,35 @@ "index": "pypi", "version": "==21.4.0" }, + "black": { + "hashes": [ + "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2", + "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71", + "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6", + "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5", + "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912", + "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866", + "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d", + "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0", + "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321", + "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8", + "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd", + "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3", + "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba", + "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0", + "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5", + "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a", + "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28", + "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c", + "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1", + "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab", + "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f", + "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61", + "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3" + ], + "index": "pypi", + "version": "==22.1.0" + }, "brotli": { "hashes": [ "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d", @@ -313,6 +358,63 @@ "index": "pypi", "version": "==0.4.4" }, + "coverage": { + "extras": [ + "toml" + ], + "hashes": [ + "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", + "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d", + "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf", + "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7", + "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6", + "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4", + "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059", + "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39", + "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536", + "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac", + "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c", + "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903", + "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d", + "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05", + "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684", + "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1", + "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f", + "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7", + "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca", + "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad", + "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca", + "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d", + "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92", + "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4", + "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf", + "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6", + "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1", + "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4", + "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359", + "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3", + "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620", + "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512", + "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69", + "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2", + "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518", + "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0", + "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa", + "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4", + "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e", + "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1", + "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2" + ], + "version": "==6.3.2" + }, + "coverage-badge": { + "hashes": [ + "sha256:c824a106503e981c02821e7d32f008fb3984b2338aa8c3800ec9357e33345b78", + "sha256:e365d56e5202e923d1b237f82defd628a02d1d645a147f867ac85c58c81d7997" + ], + "index": "pypi", + "version": "==1.1.0" + }, "cryptography": { "hashes": [ "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3", @@ -490,6 +592,20 @@ "index": "pypi", "version": "==4.11.1" }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "mock": { + "hashes": [ + "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", + "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc" + ], + "version": "==4.0.3" + }, "multidict": { "hashes": [ "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60", @@ -555,6 +671,13 @@ "index": "pypi", "version": "==6.0.2" }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, "numpy": { "hashes": [ "sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb", @@ -591,6 +714,20 @@ "index": "pypi", "version": "==1.21.5" }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "version": "==21.3" + }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, "peewee": { "hashes": [ "sha256:69c1b88dc89b184231cc1ce6df241075aca5cec43e89749cc4a63108f9ceea47" @@ -624,6 +761,20 @@ ], "version": "==2.1.2" }, + "platformdirs": { + "hashes": [ + "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d", + "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227" + ], + "version": "==2.5.1" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "version": "==1.0.0" + }, "psycopg2": { "hashes": [ "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c", @@ -641,6 +792,13 @@ "index": "pypi", "version": "==2.9.3" }, + "py": { + "hashes": [ + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + ], + "version": "==1.11.0" + }, "pyasn1": { "hashes": [ "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", @@ -722,6 +880,37 @@ "index": "pypi", "version": "==1.5.0" }, + "pyparsing": { + "hashes": [ + "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", + "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" + ], + "version": "==3.0.7" + }, + "pytest": { + "hashes": [ + "sha256:b555252a95bbb2a37a97b5ac2eb050c436f7989993565f5e0c9128fcaacadd0e", + "sha256:f1089d218cfcc63a212c42896f1b7fbf096874d045e1988186861a1a87d27b47" + ], + "index": "pypi", + "version": "==7.1.0" + }, + "pytest-cov": { + "hashes": [ + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" + ], + "index": "pypi", + "version": "==3.0.0" + }, + "pytest-dotenv": { + "hashes": [ + "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732", + "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f" + ], + "index": "pypi", + "version": "==0.5.2" + }, "python-dateutil": { "hashes": [ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", @@ -835,6 +1024,13 @@ "index": "pypi", "version": "==0.17.1" }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "version": "==2.0.1" + }, "twitchapi": { "hashes": [ "sha256:f0ee5388911154375170a83df9a18e8a698fe382cea5d94a3e33ad27a7ce9133" @@ -842,6 +1038,36 @@ "index": "pypi", "version": "==2.5.2" }, + "typed-ast": { + "hashes": [ + "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e", + "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344", + "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266", + "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a", + "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd", + "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d", + "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837", + "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098", + "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e", + "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27", + "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b", + "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596", + "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76", + "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30", + "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4", + "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78", + "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca", + "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985", + "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb", + "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88", + "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7", + "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5", + "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e", + "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7" + ], + "markers": "python_version < '3.8' and implementation_name == 'cpython'", + "version": "==1.5.2" + }, "typing-extensions": { "hashes": [ "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", diff --git a/README.md b/README.md index c8ad5fc..0e98f3a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![Coverage](./coverage.svg) + # Br Dev Streamers - Backend ![Logo](./logo.svg) diff --git a/brdevstreamers (1).db b/brdevstreamers (1).db deleted file mode 100644 index 5c23535..0000000 Binary files a/brdevstreamers (1).db and /dev/null differ diff --git a/controller/__init__.py b/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controller/private_api.py b/controller/private_api.py index 9bab0ec..03aae48 100644 --- a/controller/private_api.py +++ b/controller/private_api.py @@ -10,7 +10,12 @@ from model.user_interaction_model import UserInteraction from model.user_model import User -from persistence.user_dao import create_user_model, delete_user, get_user_by_login, update_user_model +from persistence.user_dao import ( + create_user_model, + delete_user, + get_user_by_login, + update_user_model, +) from persistence.user_interaction_dao import get_user_interactions_by_user_login from service.stats_service import compute_stat @@ -32,56 +37,55 @@ ) -AUTH0_DOMAIN = 'zapperson.us.auth0.com' +AUTH0_DOMAIN = "zapperson.us.auth0.com" API_AUDIENCE = "BrStreamersApi" ALGORITHMS = ["RS256"] def decode_jwt(token: str): - token = token.split(" ")[1] - jsonurl = urlopen("https://"+AUTH0_DOMAIN+"/.well-known/jwks.json") - jwks = json.loads(jsonurl.read()) - unverified_header = jwt.get_unverified_header(token) - rsa_key = {} - for key in jwks["keys"]: - if key["kid"] == unverified_header["kid"]: - rsa_key = { - "kty": key["kty"], - "kid": key["kid"], - "use": key["use"], - "n": key["n"], - "e": key["e"] - } - if rsa_key: - try: - payload = jwt.decode( - token, - rsa_key, - algorithms=ALGORITHMS, - audience=API_AUDIENCE, - issuer="https://"+AUTH0_DOMAIN+"/" - ) - except jwt.ExpiredSignatureError: - raise HTTPException(status_code=401, detail="token_expired") - except jwt.JWTClaimsError: - raise HTTPException(status_code=404, detail="invalid_claims") - - except Exception: - raise HTTPException(status_code=401, detail="invalid_header") - if payload is not None: - return payload - raise HTTPException(status_code=401, detail="invalid_header") + token = token.split(" ")[1] + jsonurl = urlopen("https://" + AUTH0_DOMAIN + "/.well-known/jwks.json") + jwks = json.loads(jsonurl.read()) + unverified_header = jwt.get_unverified_header(token) + rsa_key = {} + for key in jwks["keys"]: + if key["kid"] == unverified_header["kid"]: + rsa_key = { + "kty": key["kty"], + "kid": key["kid"], + "use": key["use"], + "n": key["n"], + "e": key["e"], + } + if rsa_key: + try: + payload = jwt.decode( + token, + rsa_key, + algorithms=ALGORITHMS, + audience=API_AUDIENCE, + issuer="https://" + AUTH0_DOMAIN + "/", + ) + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=401, detail="token_expired") + except jwt.JWTClaimsError: + raise HTTPException(status_code=404, detail="invalid_claims") + + except Exception: + raise HTTPException(status_code=401, detail="invalid_header") + if payload is not None: + return payload + raise HTTPException(status_code=401, detail="invalid_header") @app_private.middleware("http") async def verify_user_agent(request: Request, call_next): - token = request.headers['Authorization'] + token = request.headers["Authorization"] payload = decode_jwt(token) response = await call_next(request) return response - @app_private.get("/users") async def get_users(): try: @@ -92,71 +96,71 @@ async def get_users(): @app_private.get("/user/{user_login}") -async def user(user_login: str, Authorization = Header(...)): +async def user(user_login: str, Authorization=Header(...)): try: token = decode_jwt(Authorization) - nickname = token['https://brstreamers.dev/nickname'] - if(nickname == user_login): + nickname = token["https://brstreamers.dev/nickname"] + if nickname == user_login: streamer = get_user_by_login(user_login) return streamer - + raise HTTPException(status_code=403, detail="Unauthorized") except Exception as e: raise HTTPException(status_code=404, detail=f"Streamer not found {e}") @app_private.post("/user") -async def save_user(user: UpdateUserViewModel, Authorization = Header(...)): +async def save_user(user: UpdateUserViewModel, Authorization=Header(...)): token = decode_jwt(Authorization) - nickname = token['https://brstreamers.dev/nickname'] + nickname = token["https://brstreamers.dev/nickname"] - if(nickname == user.user_login): + if nickname == user.user_login: return create_user_model(user) - raise HTTPException(status_code=403, detail="Unauthorized") - + raise HTTPException(status_code=403, detail="Unauthorized") + @app_private.put("/user") -async def update_user(user: UpdateUserViewModel, Authorization = Header(...)): +async def update_user(user: UpdateUserViewModel, Authorization=Header(...)): token = decode_jwt(Authorization) - nickname = token['https://brstreamers.dev/nickname'] + nickname = token["https://brstreamers.dev/nickname"] - if(nickname == user.user_login): + if nickname == user.user_login: res = update_user_model(user) return res - raise HTTPException(status_code=403, detail="Unauthorized") + raise HTTPException(status_code=403, detail="Unauthorized") + @app_private.delete("/user/{user_login}") -async def delete_streamer(user_login, Authorization = Header(...)): +async def delete_streamer(user_login, Authorization=Header(...)): token = decode_jwt(Authorization) - nickname = token['https://brstreamers.dev/nickname'] + nickname = token["https://brstreamers.dev/nickname"] try: - if(nickname == user_login): + if nickname == user_login: user = delete_user(user_login) return user - raise HTTPException(status_code=403, detail="Unauthorized") + raise HTTPException(status_code=403, detail="Unauthorized") except: raise HTTPException(status_code=404, detail="Streamer not found") @app_private.post("/userinteraction") -async def stats(stat: UserInteractionViewModel, Authorization = Header(...)): +async def stats(stat: UserInteractionViewModel, Authorization=Header(...)): token = decode_jwt(Authorization) - nickname = token['https://brstreamers.dev/nickname'] - if(nickname == stat.user_login): + nickname = token["https://brstreamers.dev/nickname"] + if nickname == stat.user_login: return compute_stat(stat) - raise HTTPException(status_code=403, detail="Unauthorized") + raise HTTPException(status_code=403, detail="Unauthorized") - @app_private.get("/userinteraction/{user_login}") -async def stats(user_login, Authorization = Header(...)): +async def stats(user_login, Authorization=Header(...)): token = decode_jwt(Authorization) - nickname = token['https://brstreamers.dev/nickname'] - if(nickname == user_login): + nickname = token["https://brstreamers.dev/nickname"] + if nickname == user_login: user_interactions = get_user_interactions_by_user_login(user_login) data = [] for interaction in user_interactions: data.append(interaction.__data__) return data - raise HTTPException(status_code=403, detail="Unauthorized") \ No newline at end of file + raise HTTPException(status_code=403, detail="Unauthorized") diff --git a/controller/public_api.py b/controller/public_api.py index c3d2aef..4cb571e 100644 --- a/controller/public_api.py +++ b/controller/public_api.py @@ -1,3 +1,4 @@ +import os from typing import List from fastapi import FastAPI @@ -5,19 +6,21 @@ from fastapi_cache.decorator import cache from service.stats_service import compute_stat, get_stats, get_stats_summary -from service.twitch_service import get_streamers, get_tags, get_vods +from service.twitch_service import TwitchService from view_model.stats_viewmodel import StatsViewModel from view_model.stream_viewmodel import StreamViewModel from view_model.tag_viewmodel import TagViewModel from view_model.vod_viewmodel import VodViewModel +from twitchAPI.twitch import Twitch origins = ["*"] app_public = FastAPI(openapi_prefix="/public") +twitch = Twitch(os.environ["CLIENT_ID"], os.environ["CLIENT_SECRET"]) app_public.add_middleware( CORSMiddleware, - allow_origins=origins, + allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -26,14 +29,16 @@ @app_public.get("/streams", response_model=List[StreamViewModel]) @cache(expire=60) -async def root(): - return get_streamers() +async def streams(): + twitch_service = TwitchService(twitch) + return twitch_service.get_streamers() @app_public.get("/vods", response_model=List[VodViewModel]) @cache(expire=60) async def vods(): - return get_vods() + twitch_service = TwitchService(twitch) + return twitch_service.get_vods() @app_public.get("/stats", response_model=List[StatsViewModel]) @@ -43,8 +48,10 @@ async def stats(): @app_public.get("/tags", response_model=List[TagViewModel]) async def tags(): - return get_tags() + twitch_service = TwitchService(twitch) + return twitch_service.get_tags() + @app_public.get("/stats/summary") async def stats_summary(): - return get_stats_summary() \ No newline at end of file + return get_stats_summary() diff --git a/coverage.svg b/coverage.svg new file mode 100644 index 0000000..fa9907f --- /dev/null +++ b/coverage.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + coverage + coverage + 24% + 24% + + diff --git a/main.py b/main.py index 343e4ce..39459a4 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ - +import os import aioredis import uvicorn -from dotenv import dotenv_values +from dotenv import load_dotenv from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware @@ -12,7 +12,7 @@ from controller.public_api import app_public from model.initializer import init_db -config = dotenv_values(".env") +load_dotenv() init_db() origins = ["*"] @@ -31,9 +31,12 @@ allow_headers=["*"], ) + @app.on_event("startup") async def startup(): - redis = aioredis.from_url(f"redis://{config['REDIS_HOST']}", encoding="utf8", decode_responses=True) + redis = aioredis.from_url( + f"redis://{os.environ['REDIS_HOST']}", encoding="utf8", decode_responses=True + ) FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache") @@ -44,18 +47,15 @@ async def shutdown(): app.add_middleware(GZipMiddleware) -if __name__ == '__main__': - if(config["ENV"] == 'prod'): - uvicorn.run("main:app", - host="0.0.0.0", - port=8000, - reload=True, - ssl_keyfile=config["PRIVATE_KEY"], - ssl_certfile=config["CERT"] - ) +if __name__ == "__main__": + if os.environ["ENV"] == "prod": + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8000, + reload=True, + ssl_keyfile=os.environ["PRIVATE_KEY"], + ssl_certfile=os.environ["CERT"], + ) else: - uvicorn.run("main:app", - host="0.0.0.0", - port=8000, - reload=True - ) + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/model/__init__.py b/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/initializer.py b/model/initializer.py index 8dca34f..e3602e1 100644 --- a/model/initializer.py +++ b/model/initializer.py @@ -1,13 +1,16 @@ -from dotenv import dotenv_values +import os from model.reward_model import Reward from model.user_interaction_model import UserInteraction from model.user_model import User from peewee import PostgresqlDatabase -config = dotenv_values(".env") - -db = PostgresqlDatabase(config['DB_NAME'], user=config['DB_USER'], - password=config['DB_PASS'], host=config['DB_HOST'], port=config['DB_PORT']) +db = PostgresqlDatabase( + os.environ["DB_NAME"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASS"], + host=os.environ["DB_HOST"], + port=os.environ["DB_PORT"], +) def init_db(): diff --git a/model/reward_model.py b/model/reward_model.py index 7261488..a4aac86 100644 --- a/model/reward_model.py +++ b/model/reward_model.py @@ -1,15 +1,20 @@ -from dotenv import dotenv_values +import os from peewee import PostgresqlDatabase, CharField, IntegerField, Model -config = dotenv_values(".env") -db = PostgresqlDatabase(config['DB_NAME'], user=config['DB_USER'], - password=config['DB_PASS'], host=config['DB_HOST'], port=config['DB_PORT']) +db = PostgresqlDatabase( + os.environ["DB_NAME"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASS"], + host=os.environ["DB_HOST"], + port=os.environ["DB_PORT"], +) + class Reward(Model): - id= CharField(null=False) + id = CharField(null=False) description = CharField(null=False) price = IntegerField(null=False) quantity_available = IntegerField(null=False) class Meta: - database = db \ No newline at end of file + database = db diff --git a/model/user_interaction_model.py b/model/user_interaction_model.py index 83d5771..b90176b 100644 --- a/model/user_interaction_model.py +++ b/model/user_interaction_model.py @@ -1,18 +1,22 @@ - -from dotenv import dotenv_values +import os from peewee import PostgresqlDatabase, CharField, DateField, Model -config = dotenv_values(".env") -db = PostgresqlDatabase(config['DB_NAME'], user=config['DB_USER'], - password=config['DB_PASS'], host=config['DB_HOST'], port=config['DB_PORT']) +db = PostgresqlDatabase( + os.environ["DB_NAME"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASS"], + host=os.environ["DB_HOST"], + port=os.environ["DB_PORT"], +) + class UserInteraction(Model): - user_login= CharField(null=False) + user_login = CharField(null=False) target_user = CharField(null=True) date = DateField(null=False) type = CharField(null=False) interaction_fingerprint = CharField(null=False) class Meta: - database = db \ No newline at end of file + database = db diff --git a/model/user_model.py b/model/user_model.py index a3c8c31..b6cd211 100644 --- a/model/user_model.py +++ b/model/user_model.py @@ -1,10 +1,14 @@ -from dotenv import dotenv_values +import os from peewee import PostgresqlDatabase, CharField, Model -config = dotenv_values(".env") +db = PostgresqlDatabase( + os.environ["DB_NAME"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASS"], + host=os.environ["DB_HOST"], + port=os.environ["DB_PORT"], +) -db = PostgresqlDatabase(config['DB_NAME'], user=config['DB_USER'], - password=config['DB_PASS'], host=config['DB_HOST'], port=config['DB_PORT']) class User(Model): user_login = CharField(unique=True) @@ -17,4 +21,4 @@ class User(Model): bio = CharField(unique=False, null=True) class Meta: - database = db \ No newline at end of file + database = db diff --git a/persistence/__init__.py b/persistence/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/persistence/user_dao.py b/persistence/user_dao.py index b6775c6..de0f28b 100644 --- a/persistence/user_dao.py +++ b/persistence/user_dao.py @@ -1,11 +1,15 @@ - -from dotenv import dotenv_values -config = dotenv_values(".env") +import os from model.user_model import User from peewee import * -db = PostgresqlDatabase(config['DB_NAME'], user=config['DB_USER'], - password=config['DB_PASS'], host=config['DB_HOST'], port=config['DB_PORT']) +db = PostgresqlDatabase( + os.environ["DB_NAME"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASS"], + host=os.environ["DB_HOST"], + port=os.environ["DB_PORT"], +) + def exception_handler(func): def inner_function(*args, **kwargs): @@ -13,14 +17,17 @@ def inner_function(*args, **kwargs): return func(*args, **kwargs) except Exception as e: print(f"Error executing {func.__name__}. Error: {e}") + return inner_function + @exception_handler def get_users_by_name(users): users = User.select().where(User.user_login << users).execute() db.close() return users + @exception_handler def get_users(): users = User.select().execute() @@ -38,34 +45,39 @@ def get_user_by_login(user_login): @exception_handler def create_user_model(user): res = User.create( - user_login=user.user_login, - email=user.email, - bio=user.bio, - discord = user.discord, - instagram = user.instagram, - linkedin = user.linkedin, - github = user.github, - twitter = user.twitter) + user_login=user.user_login, + email=user.email, + bio=user.bio, + discord=user.discord, + instagram=user.instagram, + linkedin=user.linkedin, + github=user.github, + twitter=user.twitter, + ) return res @exception_handler def update_user_model(user): - res = (User - .update({User.instagram: user.instagram, - User.linkedin: user.linkedin, - User.github: user.github, - User.twitter: user.twitter, - User.discord: user.discord, - User.bio: user.bio - }) + res = ( + User.update( + { + User.instagram: user.instagram, + User.linkedin: user.linkedin, + User.github: user.github, + User.twitter: user.twitter, + User.discord: user.discord, + User.bio: user.bio, + } + ) .where(User.user_login == user.user_login) - .execute()) + .execute() + ) return res @exception_handler def delete_user(user_login): res = User.delete().where(User.user_login == user_login).execute() - return res \ No newline at end of file + return res diff --git a/persistence/user_interaction_dao.py b/persistence/user_interaction_dao.py index ba3e504..978d0ac 100644 --- a/persistence/user_interaction_dao.py +++ b/persistence/user_interaction_dao.py @@ -1,11 +1,16 @@ +import os from peewee import * from model.user_interaction_model import UserInteraction from model.user_model import User -from dotenv import dotenv_values -config = dotenv_values(".env") -db = PostgresqlDatabase(config['DB_NAME'], user=config['DB_USER'], - password=config['DB_PASS'], host=config['DB_HOST'], port=config['DB_PORT']) + +db = PostgresqlDatabase( + os.environ["DB_NAME"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASS"], + host=os.environ["DB_HOST"], + port=os.environ["DB_PORT"], +) def exception_handler(func): @@ -14,11 +19,14 @@ def inner_function(*args, **kwargs): return func(*args, **kwargs) except Exception as e: print(f"Error executing {func.__name__}. Error: {e}") + return inner_function @exception_handler def get_user_interactions_by_user_login(user_login): - user_interactions = UserInteraction.select().where(UserInteraction.user_login == user_login).execute() + user_interactions = ( + UserInteraction.select().where(UserInteraction.user_login == user_login).execute() + ) db.close() - return user_interactions \ No newline at end of file + return user_interactions diff --git a/poc.py b/poc.py index eb5a572..f99d79c 100644 --- a/poc.py +++ b/poc.py @@ -4,9 +4,9 @@ config = dotenv_values(".env") -twitch = Twitch(config['CLIENT_ID'], config['CLIENT_SECRET']) +twitch = Twitch(os.environ["CLIENT_ID"], os.environ["CLIENT_SECRET"]) -users = twitch.get_users(user_ids=['227168488']) +users = twitch.get_users(user_ids=["227168488"]) print(users) diff --git a/requirements.txt b/requirements.txt index 2a17532..6e528b4 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/service/__init__.py b/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/stats_service.py b/service/stats_service.py index 5b42090..ef2c60c 100644 --- a/service/stats_service.py +++ b/service/stats_service.py @@ -1,23 +1,29 @@ +import os from typing import List -from dotenv import dotenv_values from peewee import * from model.user_interaction_model import UserInteraction from view_model.stats_viewmodel import StatsViewModel -config = dotenv_values(".env") -db = PostgresqlDatabase(config['DB_NAME'], user=config['DB_USER'], - password=config['DB_PASS'], host=config['DB_HOST'], port=config['DB_PORT']) +db = PostgresqlDatabase( + os.environ["DB_NAME"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASS"], + host=os.environ["DB_HOST"], + port=os.environ["DB_PORT"], +) + def get_stats() -> List[StatsViewModel]: cursor = db.execute_sql( - "SELECT distinct s.target_user, " + - "(SELECT count(*) FROM userinteraction WHERE target_user = s.target_user AND type = 'STREAM_CLICK')," + - "(SELECT count(*) FROM userinteraction WHERE target_user = s.target_user AND type = 'VOD_CLICK')," + - "(SELECT count(*) FROM userinteraction WHERE target_user = s.target_user AND type = 'PREVIEW_CLICK')" + - "FROM userinteraction s ORDER BY s.target_user") + "SELECT distinct s.target_user, " + + "(SELECT count(*) FROM userinteraction WHERE target_user = s.target_user AND type = 'STREAM_CLICK')," + + "(SELECT count(*) FROM userinteraction WHERE target_user = s.target_user AND type = 'VOD_CLICK')," + + "(SELECT count(*) FROM userinteraction WHERE target_user = s.target_user AND type = 'PREVIEW_CLICK')" + + "FROM userinteraction s ORDER BY s.target_user" + ) stats: List[StatsViewModel] = [] @@ -29,19 +35,33 @@ def get_stats() -> List[StatsViewModel]: stat.preview_clicks = row[3] stats.append(stat) return stats - + + def get_stats_summary(): - streams = UserInteraction.select().where(UserInteraction.type == 'STREAM_CLICK').count() - vods = UserInteraction.select().where(UserInteraction.type == 'VOD_CLICK').count() - previews = UserInteraction.select().where(UserInteraction.type == 'PREVIEW').count() + streams = UserInteraction.select().where(UserInteraction.type == "STREAM_CLICK").count() + vods = UserInteraction.select().where(UserInteraction.type == "VOD_CLICK").count() + previews = UserInteraction.select().where(UserInteraction.type == "PREVIEW").count() stats_summary = {"streams": streams, "vods": vods, "previews": previews} return stats_summary + def compute_stat(stat: UserInteraction): - db_stat = UserInteraction.select().where(UserInteraction.target_user == stat.target_user, - UserInteraction.type == stat.type, - UserInteraction.date == stat.date, - UserInteraction.interaction_fingerprint == stat.interaction_fingerprint).count() + db_stat = ( + UserInteraction.select() + .where( + UserInteraction.target_user == stat.target_user, + UserInteraction.type == stat.type, + UserInteraction.date == stat.date, + UserInteraction.interaction_fingerprint == stat.interaction_fingerprint, + ) + .count() + ) if db_stat == 0: - return UserInteraction.create(user_login=stat.user_login, date=stat.date, target_user=stat.target_user, type=stat.type, interaction_fingerprint=stat.interaction_fingerprint) - return None \ No newline at end of file + return UserInteraction.create( + user_login=stat.user_login, + date=stat.date, + target_user=stat.target_user, + type=stat.type, + interaction_fingerprint=stat.interaction_fingerprint, + ) + return None diff --git a/service/twitch_service.py b/service/twitch_service.py index 2d26a68..5719368 100644 --- a/service/twitch_service.py +++ b/service/twitch_service.py @@ -1,7 +1,8 @@ +import json +import os from random import shuffle from typing import List -from dotenv import dotenv_values from twitchAPI.twitch import Twitch from twitchAPI.types import TimePeriod @@ -11,120 +12,115 @@ from view_model.tag_viewmodel import TagViewModel from view_model.vod_viewmodel import VodViewModel -config = dotenv_values(".env") -twitch = Twitch(config['CLIENT_ID'], config['CLIENT_SECRET']) - - -def get_streamers() -> List[StreamViewModel]: - streams = twitch.get_streams(language="pt", game_id='1469308723') - - streams_model: List[StreamViewModel] = [] - stream_users = [] - for s in streams['data']: - stream = StreamViewModel() - stream.id = s['id'] - stream.user_id = s['user_id'] - stream.user_name = s['user_name'] - stream.user_login = s['user_login'] - stream.title = s['title'] - stream.viewer_count = s['viewer_count'] - stream.started_at = s['started_at'] - stream.thumbnail_url = s['thumbnail_url'] - - stream.tags = s['tag_ids'] - - streamer = get_streamer(s['user_id']) - stream.profile_image_url = streamer['profile_image_url'] - stream.description = streamer['description'][:100] + '...' - - stream_users.append(s['user_login']) - streams_model.append(stream) - - try: - streamers = get_users_by_name(stream_users) - for s in streamers: - for stream in streams_model: - if(stream.user_login == s.user_login): - stream.github_url = s.github - stream.twitter_url = s.twitter - stream.instagram_url = s.instagram - stream.linkedin_url = s.linkedin - stream.discord_url = s.discord - stream.bio = s.bio - break - except Exception as e: - print(e) - shuffle(streams_model) - return streams_model - - -def get_streamer(id): - return twitch.get_users(user_ids=[id])['data'][0] - - -def get_vods() -> List[VodViewModel]: - vods = twitch.get_videos( - language="pt", game_id='1469308723', period=TimePeriod.DAY) - vods_model: List[VodViewModel] = [] - vod_users = [] - - for s in vods['data']: - if is_long_enough(s['duration']): - stream = VodViewModel() - stream.id = s['id'] - stream.user_id = s['user_id'] - stream.user_name = s['user_name'] - stream.user_login = s['user_login'] - stream.title = s['title'] - stream.viewer_count = s['view_count'] - stream.started_at = s['published_at'] - stream.thumbnail_url = s['thumbnail_url'] - stream.stream_id = s['id'] - stream.duration = s['duration'] - - streamer = get_streamer(s['user_id']) - stream.profile_image_url = streamer['profile_image_url'] - stream.description = streamer['description'][:100] + '...' - - vod_users.append(s['user_login']) - vods_model.append(stream) - try: - streamers = get_users_by_name(vod_users) - for s in streamers: - for stream in vods_model: - if(stream.user_login == s.user_login): - stream.github_url = s.github - stream.twitter_url = s.twitter - stream.instagram_url = s.instagram - stream.linkedin_url = s.linkedin - stream.discord_url = s.discord - stream.bio = s.bio - break - except Exception as e: - print(e) - return vods_model - - -def is_long_enough(duration): - return 'h' in duration - - -def get_tags() -> List[TagViewModel]: - streams = twitch.get_streams(language="pt", game_id='1469308723') - tag_ids = get_tag_list_from_streams(streams) - tags = twitch.get_all_stream_tags(tag_ids=tag_ids) - tags_dict = {} - for tag in tags['data']: - tag_model = TagViewModel( - id=tag['tag_id'], name=tag['localization_names']['pt-br']) - tags_dict[tag['tag_id']] = tag_model - return list(tags_dict.values()) - - -def get_tag_list_from_streams(streams): - tag_ids = [] - for s in streams['data']: - for tag in s['tag_ids']: - tag_ids.append(tag) - return tag_ids +class TwitchService: + + config, twitch = None, None + + def __init__(self, twitch): + self.twitch = twitch + + def get_streamers(self) -> List[StreamViewModel]: + streams = self.twitch.get_streams(language="pt", game_id="1469308723") + streams_model: List[StreamViewModel] = [] + stream_users = [] + for s in streams["data"]: + stream = StreamViewModel() + stream.id = s["id"] + stream.user_id = s["user_id"] + stream.user_name = s["user_name"] + stream.user_login = s["user_login"] + stream.title = s["title"] + stream.viewer_count = s["viewer_count"] + stream.started_at = s["started_at"] + stream.thumbnail_url = s["thumbnail_url"] + + stream.tags = s["tag_ids"] + + streamer = self.get_streamer(s["user_id"]) + stream.profile_image_url = streamer["profile_image_url"] + stream.description = streamer["description"][:100] + "..." + + stream_users.append(s["user_login"]) + streams_model.append(stream) + + try: + streamers = get_users_by_name(stream_users) + for s in streamers: + for stream in streams_model: + if stream.user_login == s.user_login: + stream.github_url = s.github + stream.twitter_url = s.twitter + stream.instagram_url = s.instagram + stream.linkedin_url = s.linkedin + stream.discord_url = s.discord + stream.bio = s.bio + break + except Exception as e: + print(e) + shuffle(streams_model) + return streams_model + + def get_streamer(self, id): + return self.twitch.get_users(user_ids=[id])["data"][0] + + def get_vods(self) -> List[VodViewModel]: + vods = self.twitch.get_videos(language="pt", game_id="1469308723", period=TimePeriod.DAY) + vods_model: List[VodViewModel] = [] + vod_users = [] + + for s in vods["data"]: + if self.is_long_enough(s["duration"]): + stream = VodViewModel() + stream.id = s["id"] + stream.user_id = s["user_id"] + stream.user_name = s["user_name"] + stream.user_login = s["user_login"] + stream.title = s["title"] + stream.viewer_count = s["view_count"] + stream.started_at = s["published_at"] + stream.thumbnail_url = s["thumbnail_url"] + stream.stream_id = s["id"] + stream.duration = s["duration"] + + streamer = self.get_streamer(s["user_id"]) + stream.profile_image_url = streamer["profile_image_url"] + stream.description = streamer["description"][:100] + "..." + + vod_users.append(s["user_login"]) + vods_model.append(stream) + try: + streamers = get_users_by_name(vod_users) + for s in streamers: + for stream in vods_model: + if stream.user_login == s.user_login: + stream.github_url = s.github + stream.twitter_url = s.twitter + stream.instagram_url = s.instagram + stream.linkedin_url = s.linkedin + stream.discord_url = s.discord + stream.bio = s.bio + break + except Exception as e: + print(e) + return vods_model + + def is_long_enough(self, duration): + return "h" in duration + + def get_tags(self) -> List[TagViewModel]: + streams = self.twitch.get_streams(language="pt", game_id="1469308723") + tag_ids = self.get_tag_list_from_streams(streams) + tags = self.twitch.get_all_stream_tags(tag_ids=tag_ids) + tags_dict = {} + for tag in tags["data"]: + tag_model = TagViewModel(id=tag["tag_id"], name=tag["localization_names"]["pt-br"]) + tags_dict[tag["tag_id"]] = tag_model + return list(tags_dict.values()) + + def get_tag_list_from_streams(self, streams): + tag_ids = [] + for s in streams["data"]: + for tag in s["tag_ids"]: + tag_ids.append(tag) + return tag_ids diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/service/__init__.py b/test/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/service/test_twitch_service.py b/test/service/test_twitch_service.py new file mode 100644 index 0000000..0a25186 --- /dev/null +++ b/test/service/test_twitch_service.py @@ -0,0 +1,56 @@ +import unittest +from unittest.mock import MagicMock, Mock, patch +from dotenv import load_dotenv +import pytest +from twitchAPI.twitch import Twitch + +from service.twitch_service import TwitchService + + +class TestTwitchService(unittest.TestCase): + def __init__(self, methodName: str = ...) -> None: + super().__init__(methodName) + + def mock_twitch(self): + twitch = Mock() + response = { + "data": [ + { + "id": "44960190524", + "user_id": "166681140", + "user_login": "marcobrunodev", + "user_name": "MarcoBrunoDev", + "game_id": "1469308723", + "game_name": "Software and Game Development", + "type": "live", + "title": "#28 Pet Snoar | Pet Runner | !Alura", + "viewer_count": 158, + "started_at": "2022-03-14T11:00:48Z", + "language": "pt", + "thumbnail_url": "https://static-cdn.jtvnw.net/previews-ttv/live_user_marcobrunodev-{width}x{height}.jpg", + "tag_ids": [ + "39ee8140-901a-4762-bfca-8260dea1310f", + "a106f013-6e26-4f27-9a4b-01e9d76084e2", + "6e23d976-33ec-47e8-b22b-3727acd41862", + "f588bd74-e496-4d11-9169-3597f38a5d25", + "6f86127d-6051-4a38-94bb-f7b475dde109", + "c23ce252-cf78-4b98-8c11-8769801aaf3a", + ], + "is_mature": 'false', + } + ], + "pagination": { + "cursor": "eyJiIjp7IkN1cnNvciI6ImV5SnpJam94TlRndU1URXdOVEl3TlRreE9UZzFNRGdzSW1RaU9tWmhiSE5sTENKMElqcDBjblZsZlE9PSJ9LCJhIjp7IkN1cnNvciI6IiJ9fQ" + }, + } + twitch.get_streams = MagicMock(return_value=response) + + streamer = {"data": [{"profile_image_url": '', 'description': ''}]} + twitch.get_users = MagicMock(return_value=streamer) + return twitch + + def test_get_streamers(self): + twitch_service = TwitchService(self.mock_twitch()) + streamers = twitch_service.get_streamers() + self.assertEqual(len(streamers), 1) + self.assertEqual(streamers[0].user_login, "marcobrunodev") diff --git a/view_model/__init__.py b/view_model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/view_model/stream_viewmodel.py b/view_model/stream_viewmodel.py index bcf890c..f770491 100644 --- a/view_model/stream_viewmodel.py +++ b/view_model/stream_viewmodel.py @@ -13,4 +13,4 @@ class StreamViewModel(UserOutViewModel): thumbnail_url: Optional[str] profile_image_url: Optional[str] description: Optional[str] - tags: Optional[List[str]] \ No newline at end of file + tags: Optional[List[str]] diff --git a/view_model/tag_viewmodel.py b/view_model/tag_viewmodel.py index 3cd9378..51bb13a 100644 --- a/view_model/tag_viewmodel.py +++ b/view_model/tag_viewmodel.py @@ -4,5 +4,5 @@ class TagViewModel(BaseModel): - name:Optional[str] - id:Optional[str] \ No newline at end of file + name: Optional[str] + id: Optional[str] diff --git a/view_model/user_interaction_viewmodel.py b/view_model/user_interaction_viewmodel.py index b60b75f..eb5e468 100644 --- a/view_model/user_interaction_viewmodel.py +++ b/view_model/user_interaction_viewmodel.py @@ -6,8 +6,8 @@ class UserInteractionViewModel(BaseModel): - user_login:str - target_user:Optional[str] + user_login: str + target_user: Optional[str] date: datetime type: str - interaction_fingerprint: str \ No newline at end of file + interaction_fingerprint: str diff --git a/view_model/user_viewmodel.py b/view_model/user_viewmodel.py index 6286f40..05fdcb6 100644 --- a/view_model/user_viewmodel.py +++ b/view_model/user_viewmodel.py @@ -13,6 +13,7 @@ class UpdateUserViewModel(BaseModel): github: Optional[str] twitter: Optional[str] + class UserOutViewModel(BaseModel): user_login: Optional[str] email: Optional[str] @@ -21,4 +22,4 @@ class UserOutViewModel(BaseModel): twitter_url: Optional[str] instagram_url: Optional[str] linkedin_url: Optional[str] - discord_url: Optional[str] \ No newline at end of file + discord_url: Optional[str] diff --git a/view_model/vod_viewmodel.py b/view_model/vod_viewmodel.py index 5610bd2..1c162c7 100644 --- a/view_model/vod_viewmodel.py +++ b/view_model/vod_viewmodel.py @@ -14,4 +14,4 @@ class VodViewModel(UserOutViewModel): profile_image_url: Optional[str] description: Optional[str] stream_id: Optional[str] - duration: Optional[str] \ No newline at end of file + duration: Optional[str]